import * as qs from 'querystring';
import { parse as parseToTokens, tokensToRegexp, tokensToFunction } from 'path-to-regexp';

import type * as ptr from 'path-to-regexp';
import type { Page, RouteParams } from '../types';

const PATH_CONFIG = {
  delimiter: '/',
  end: false,
  strict: false,
  sensitive: false
};

type CacheRecordPath = [ptr.PathFunction, Array<ptr.Token>];
type CacheRecordConvert = {
  regexp: RegExp,
  keys: Array<ptr.Token>
};

type CachePath = Map<Page, CacheRecordPath>;
type CacheConvert = Map<Page, CacheRecordConvert>;

export declare type MatchedPath = {
  uri: string,
  exact: boolean,
  params: RouteParams
};

export declare type ParsedPath = {
  url: string,
  params: RouteParams
};

const cachedPaths: CachePath = new Map();
const cachedConverts: CacheConvert = new Map();

const cacheLimit = 512;
let cacheCount = 0;

export function parsePage(page: Page): CacheRecordPath {
  const cached = cachedPaths.get(page);

  if (cached) {
    return cached;
  }

  const tokens = parseToTokens(page, PATH_CONFIG);
  const generator = tokensToFunction(tokens, PATH_CONFIG);

  if (cacheCount < cacheLimit) {
    cachedPaths.set(page, [generator, tokens]);
    cacheCount++;
  }

  return [generator, tokens];
}

export function buildPage(page: Page, params: RouteParams = {}) {
  if (page === '/') {
    return page;
  }

  const [generatePath, tokens] = parsePage(page);
  const path = generatePath(params);

  const query = { ...params };
  tokens.forEach((token) => {
    if (typeof token === 'object') {
      delete query[token.name];
    }
  });

  return path + '?' + qs.stringify(query);
}

export function convertPage(page: Page): CacheRecordConvert {
  const cached = cachedConverts.get(page);

  if (cached) {
    return cached;
  }

  const tokens = parseToTokens(page, PATH_CONFIG);
  const keys: ptr.Key[] = [];
  const regexp = tokensToRegexp(tokens, keys, PATH_CONFIG);
  const result = { regexp, keys };

  if (cacheCount < cacheLimit) {
    cachedConverts.set(page, result);
    cacheCount++;
  }

  return result;
}

export function matchPage(page: Page, path: string): MatchedPath {
  const { regexp, keys } = convertPage(page);

  const match = regexp.exec(path);

  if (!match) {
    return null;
  }

  const [url, ...values] = match;

  const params = keys.reduce((acc, key, index) => {
    if (typeof key === 'object') {
      acc[key.name] = values[index];
    }
    return acc;
  }, {});

  const uri = page === '/' && url === '' ? '/' : url;
  const exact = path === url;

  return {
    uri,
    exact,
    params
  };
}

export function parsePath(path: string): ParsedPath {
  if (!path.includes('?')) {
    return {
      url: path,
      params: {}
    };
  }

  const [url, query] = path.split('?', 2);
  if (!query) {
    return {
      url,
      params: {}
    };
  }

  return {
    url,
    params: qs.parse(query)
  };
}

export function preventScrollRestoration() {
  if (window.history.scrollRestoration) {
    window.history.scrollRestoration = 'manual';
  }
}
