import { unreachable } from '../../util/util.js';

let windowURL: URL | undefined = undefined;
function getWindowURL() {
  if (windowURL === undefined) {
    windowURL = new URL(window.location.toString());
  }
  return windowURL;
}

/** Parse a runner option that is always boolean-typed. False if missing or '0'. */
export function optionEnabled(
  opt: string,
  searchParams: URLSearchParams = getWindowURL().searchParams
): boolean {
  const val = searchParams.get(opt);
  return val !== null && val !== '0';
}

/** Parse a runner option that is string-typed. If the option is missing, returns `null`. */
export function optionString(
  opt: string,
  searchParams: URLSearchParams = getWindowURL().searchParams
): string | null {
  return searchParams.get(opt);
}

/** Runtime modes for running tests in different types of workers. */
export type WorkerMode = 'dedicated' | 'service' | 'shared';
/** Parse a runner option for different worker modes (as in `?worker=shared`). Null if no worker. */
export function optionWorkerMode(
  opt: string,
  searchParams: URLSearchParams = getWindowURL().searchParams
): WorkerMode | null {
  const value = searchParams.get(opt);
  if (value === null || value === '0') {
    return null;
  } else if (value === 'service') {
    return 'service';
  } else if (value === 'shared') {
    return 'shared';
  } else if (value === '' || value === '1' || value === 'dedicated') {
    return 'dedicated';
  }
  unreachable('invalid worker= option value');
}

/**
 * The possible options for the tests.
 */
export interface CTSOptions {
  worker: WorkerMode | null;
  debug: boolean;
  compatibility: boolean;
  forceFallbackAdapter: boolean;
  enforceDefaultLimits: boolean;
  blockAllFeatures: boolean;
  unrollConstEvalLoops: boolean;
  powerPreference: GPUPowerPreference | null;
  subcasesBetweenAttemptingGC: string;
  casesBetweenReplacingDevice: string;
  logToWebSocket: boolean;
}

export const kDefaultCTSOptions: Readonly<CTSOptions> = {
  worker: null,
  debug: false,
  compatibility: false,
  forceFallbackAdapter: false,
  enforceDefaultLimits: false,
  blockAllFeatures: false,
  unrollConstEvalLoops: false,
  powerPreference: null,
  subcasesBetweenAttemptingGC: '5000',
  casesBetweenReplacingDevice: 'Infinity',
  logToWebSocket: false,
};

/**
 * Extra per option info.
 */
export interface OptionInfo {
  description: string;
  parser?: (key: string, searchParams?: URLSearchParams) => boolean | string | null;
  selectValueDescriptions?: { value: string | null; description: string }[];
}

/**
 * Type for info for every option. This definition means adding an option
 * will generate a compile time error if no extra info is provided.
 */
export type OptionsInfos<Type> = Record<keyof Type, OptionInfo>;

/**
 * Options to the CTS.
 */
export const kCTSOptionsInfo: OptionsInfos<CTSOptions> = {
  worker: {
    description: 'run in a worker',
    parser: optionWorkerMode,
    selectValueDescriptions: [
      { value: null, description: 'no worker' },
      { value: 'dedicated', description: 'dedicated worker' },
      { value: 'shared', description: 'shared worker' },
      { value: 'service', description: 'service worker' },
    ],
  },
  debug: { description: 'show more info' },
  compatibility: { description: 'request adapters with featureLevel: "compatibility"' },
  forceFallbackAdapter: { description: 'pass forceFallbackAdapter: true to requestAdapter' },
  enforceDefaultLimits: {
    description: `force the adapter limits to the default limits.
Note: May fail on tests for low-power/high-performance`,
  },
  blockAllFeatures: {
    description: `block all features on adapter - except 'core-features-and-limits'.
Note: The spec requires bc or etc2+astc which means tests checking that one or other must exist will fail.
`,
  },
  unrollConstEvalLoops: { description: 'unroll const eval loops in WGSL' },
  powerPreference: {
    description: 'set default powerPreference for some tests',
    parser: optionString,
    selectValueDescriptions: [
      { value: null, description: 'default' },
      { value: 'low-power', description: 'low-power' },
      { value: 'high-performance', description: 'high-performance' },
    ],
  },
  subcasesBetweenAttemptingGC: {
    description:
      'After this many subcases, run attemptGarbageCollection(). (For custom values, edit the URL.)',
    parser: optionString,
    selectValueDescriptions: [
      { value: null, description: 'default' },
      { value: 'Infinity', description: 'Infinity' },
      { value: '5000', description: '5000' },
      { value: '50', description: '50' },
      { value: '1', description: '1' },
    ],
  },
  casesBetweenReplacingDevice: {
    description:
      'After this many cases use a device, destroy and replace it to free GPU resources. (For custom values, edit the URL.)',
    parser: optionString,
    selectValueDescriptions: [
      { value: null, description: 'default' },
      { value: 'Infinity', description: 'Infinity' },
      { value: '5000', description: '5000' },
      { value: '50', description: '50' },
      { value: '1', description: '1' },
    ],
  },
  logToWebSocket: { description: 'send some logs to ws://localhost:59497/' },
};

/**
 * Converts camel case to snake case.
 * Examples:
 *    fooBar -> foo_bar
 *    parseHTMLFile -> parse_html_file
 */
export function camelCaseToSnakeCase(id: string) {
  return id
    .replace(/(.)([A-Z][a-z]+)/g, '$1_$2')
    .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
    .toLowerCase();
}

/**
 * Creates a Options from search parameters.
 */
function getOptionsInfoFromSearchString<Type extends CTSOptions>(
  optionsInfos: OptionsInfos<Type>,
  searchString: string
): Type {
  const searchParams = new URLSearchParams(searchString);
  const optionValues: Record<string, boolean | string | null> = {};
  for (const [optionName, info] of Object.entries(optionsInfos)) {
    const parser = info.parser || optionEnabled;
    optionValues[optionName] = parser(camelCaseToSnakeCase(optionName), searchParams);
  }
  return optionValues as unknown as Type;
}

/**
 * Given a test query string in the form of `suite:foo,bar,moo&opt1=val1&opt2=val2
 * returns the query and the options.
 */
export function parseSearchParamLikeWithOptions<Type extends CTSOptions>(
  optionsInfos: OptionsInfos<Type>,
  query: string
): {
  queries: string[];
  options: Type;
} {
  const searchString = query.includes('q=') || query.startsWith('?') ? query : `q=${query}`;
  const queries = new URLSearchParams(searchString).getAll('q');
  const options = getOptionsInfoFromSearchString(optionsInfos, searchString);
  return { queries, options };
}

/**
 * Given a test query string in the form of `suite:foo,bar,moo&opt1=val1&opt2=val2
 * returns the query and the common options.
 */
export function parseSearchParamLikeWithCTSOptions(query: string) {
  return parseSearchParamLikeWithOptions(kCTSOptionsInfo, query);
}
