const compiled = new Map<string, RegExp>()

/**
 * @param value The pattern to evaluate
 */
function getPattern(value: string): RegExp {
  if (compiled.has(value)) {
    return compiled.get(value) as RegExp
  }
  const result = new RegExp(`^${value}$`)
  compiled.set(value, result)
  return result
}

/**
 * @param uri The path value of the current URL.
 * @param pattern The pattern to evaluate
 */
function testRoute(uri: string, pattern: string): boolean {
  return getPattern(pattern).test(uri)
}

/**
 * @param pattern The pattern to evaluate
 * @param uri The path value of the current URL.
 */
function parseParams(pattern: string, uri: string): Record<string, string | string[]> | undefined {
  const r = getPattern(pattern)
  const match = r.exec(uri)
  if (!match) {
    return undefined
  }
  const { groups } = match
  const result: Record<string, string | string[]> = {}
  if (groups) {
    Object.keys(groups).forEach((key) => {
      let value = groups[key] as string | undefined
      if (value === undefined) {
        return
      }
      if (value[0] === '/') {
        value = value.substring(1)
      }
      if (value.includes('/')) {
        result[key] = value.split('/').map((i) => decodeURIComponent(i))
      } else {
        result[key] = decodeURIComponent(value)
      }
    })
  }
  return result
}

/**
 * Reads params from the route for given patterns.
 * @param patterns The list of patterns to consider
 * @param route The current route
 * @returns A list of read parameters or undefined when there's no matching route or params.
 *
 * @example
 *
 * ```typescript
 * const patterns = ['/my/path/:id']
 * const route = '/my/path/1234'
 * const params = readRouteParams(patterns, route)
 * // -> { id: '1234' }
 * ```
 */
export function readRouteParams(patterns: string[], route: string): Record<string, string | string[]> | undefined {
  for (const pattern of patterns) {
    if (!testRoute(route, pattern)) {
      continue
    }
    const params = parseParams(pattern, route)
    if (params) {
      return params
    }
  }
  return undefined
}

/**
 * Reads the current route as everything that is past the `#`
 * @param defaultRoute The route that would be considered a default route for the activity.
 * @returns The current route.
 */
export function getRoute(defaultRoute = ''): string {
  const url = new URL(window.location.href)
  let path = url.hash.replace('#', '')
  if (!path) {
    path = defaultRoute
  }
  if (!path.startsWith('/')) {
    path = `/${path}`
  }
  return path
}

function createHash(...route: string[]): string {
  const atoms: string[] = []
  for (const atom of route) {
    if (atom.includes('/')) {
      atoms.push(...atom.split('/').filter((i) => !!i))
    } else {
      atoms.push(atom)
    }
  }
  let hash = atoms.map(encodeURIComponent).join('/')
  if (!hash.startsWith('/')) {
    hash = `/${hash}`
  }
  return hash
}

export function buildRoute(...route: string[]): string {
  const url = new URL(window.location.href)
  url.hash = createHash(...route)
  return url.toString()
}

/**
 * Navigates to an internal route.
 *
 * @param route Optional route params to add to the hash part of the url.
 */
export function navigateScreen(...route: string[]): void {
  window.location.href = buildRoute(...route)
}

/**
 * Navigates to another application.
 *
 * @param htmlFile The relative location of the target HTML file.
 * @param route Optional route params to add to the has part of the url.
 */
export function navigateApp(htmlFile: string, ...route: string[]): void {
  const url = new URL(htmlFile, window.location.href)
  url.hash = createHash(...route)
  window.location.href = url.toString()
}

/**
 * Navigates to another application.
 *
 * @param htmlFile The relative location of the target HTML file.
 * @param route Optional route params to add to the has part of the url.
 */
export function openApp(htmlFile: string, ...route: string[]): void {
  const url = new URL(htmlFile, window.location.href)
  url.hash = createHash(...route)
  window.open(url.toString(), '_blank')
}
