import { invariant } from './invariant'
import { trimPathRight } from './path'
import { createSieveCache } from './sieve-cache'
import { last } from './utils'
import type { SieveCache } from './sieve-cache'
import type { InterpolationSegment, RouteInterpolation } from './path'

export const SEGMENT_TYPE_PATHNAME = 0
export const SEGMENT_TYPE_PARAM = 1
export const SEGMENT_TYPE_WILDCARD = 2
export const SEGMENT_TYPE_OPTIONAL_PARAM = 3
const SEGMENT_TYPE_INDEX = 4
const SEGMENT_TYPE_PATHLESS = 5 // only used in matching to represent pathless routes that need to carry more information

/**
 * All the kinds of segments that can be present in a route path.
 */
export type SegmentKind =
  | typeof SEGMENT_TYPE_PATHNAME
  | typeof SEGMENT_TYPE_PARAM
  | typeof SEGMENT_TYPE_WILDCARD
  | typeof SEGMENT_TYPE_OPTIONAL_PARAM

/**
 * All the kinds of segments that can be present in the segment tree.
 */
type ExtendedSegmentKind =
  | SegmentKind
  | typeof SEGMENT_TYPE_INDEX
  | typeof SEGMENT_TYPE_PATHLESS

export type DynamicPathSegment = [
  kind: Exclude<SegmentKind, typeof SEGMENT_TYPE_PATHNAME>,
  key: string,
  prefix: string,
  /** Undefined marks a bare splat, which discards the remaining template. */
  suffix: string | undefined,
]

export function getParamNames(data: RouteInterpolation): Array<string> {
  const cached = data.names
  if (cached) {
    return cached
  }
  const keys: Array<string> = []
  for (const segment of data) {
    if (typeof segment !== 'string') {
      keys.push(segment[1 /* key */])
    }
  }
  return (data.names = keys)
}

/** Parse one segment for matching and retain the same record for interpolation. */
export function parseSegment(
  /** The full path string containing the segment. */
  path: string,
  /** The starting index of the segment within the path. */
  start: number,
  /** The next slash, or the length of the path. */
  end: number,
): InterpolationSegment {
  const part = path.substring(start, end)
  if (part.charCodeAt(0) === 36) {
    return part.length === 1
      ? [SEGMENT_TYPE_WILDCARD, '_splat', '', undefined]
      : [SEGMENT_TYPE_PARAM, part.substring(1), '', '']
  }
  const open = part.indexOf('{')
  if (open >= 0) {
    const close = part.indexOf('}', open)
    const optional = part.charCodeAt(open + 1) === 45
    const nameStart = open + (optional ? 3 : 2)
    if (
      close >= 0 &&
      part.charCodeAt(nameStart - 1) === 36 &&
      (!optional || nameStart < close)
    ) {
      const key = part.substring(nameStart, close)
      return [
        optional
          ? SEGMENT_TYPE_OPTIONAL_PARAM
          : key
            ? SEGMENT_TYPE_PARAM
            : SEGMENT_TYPE_WILDCARD,
        key || '_splat',
        part.substring(0, open),
        path.substring(start + close + 1, key ? end : path.length),
      ]
    }
  }
  return part
}

type ParsedRoute<T extends RouteLike> = [
  node: AnySegmentNode<T>,
  cursor: number,
  segments: RouteInterpolation | undefined,
]

/** Compile a template, optionally attaching its new segments to the matching trie. */
export function parseSegments<TRouteLike extends RouteLike>(
  defaultCaseSensitive: boolean,
  route: TRouteLike,
  start: number,
): RouteInterpolation
export function parseSegments<TRouteLike extends RouteLike>(
  defaultCaseSensitive: boolean,
  route: TRouteLike,
  start: number,
  node: AnySegmentNode<TRouteLike>,
  dynamicListsToSort?: Array<Array<DynamicSegmentNode<TRouteLike>>>,
  parentInterpolation?: RouteInterpolation,
): ParsedRoute<TRouteLike>
export function parseSegments<TRouteLike extends RouteLike>(
  defaultCaseSensitive: boolean,
  route: TRouteLike,
  start: number,
  node?: AnySegmentNode<TRouteLike>,
  /** Each dynamic sibling list is recorded once, when it first needs sorting. */
  dynamicListsToSort?: Array<Array<DynamicSegmentNode<TRouteLike>>>,
  parentInterpolation?: RouteInterpolation,
): RouteInterpolation | ParsedRoute<TRouteLike> {
  let cursor = start
  const path = route.fullPath ?? route.from
  const options = route.options
  const length = path.length
  const literalEnd = path.endsWith('/') ? length - 1 : length
  const caseSensitive = options?.caseSensitive ?? defaultCaseSensitive
  const parseParams = options?.params?.parse ?? options?.parseParams
  let interpolation: Array<InterpolationSegment> | undefined
  let literalStart = parentInterpolation ? start - 1 : 0
  if (!node || path.includes('$')) {
    interpolation = parentInterpolation?.slice() ?? []
    const tail = last(interpolation)
    if (
      tail &&
      typeof tail !== 'string' &&
      tail[0 /* kind */] === SEGMENT_TYPE_WILDCARD
    ) {
      // A parent's wildcard consumes the child's template too.
      // `start` follows that entire template, including its trailing slash.
      interpolation[interpolation.length - 1] = [
        tail[0 /* kind */],
        tail[1 /* key */],
        tail[2 /* prefix */],
        tail[3 /* suffix */] === undefined
          ? undefined
          : tail[3 /* suffix */] +
            path.substring(
              start - (path[start - 2] === '/' ? 2 : 1),
              literalEnd,
            ),
      ]
      literalStart = length
    }
  }
  while (cursor < length) {
    const start = cursor
    const next = path.indexOf('/', start)
    let end = next === -1 ? length : next
    const segment = parseSegment(path, start, end)
    cursor = end + 1
    let nextNode: AnySegmentNode<TRouteLike>
    if (typeof segment === 'string') {
      if (!node) {
        continue
      }
      let name = segment
      let staticChildren: Map<string, StaticSegmentNode<TRouteLike>>
      if (caseSensitive) {
        staticChildren = node.static ??= new Map()
      } else {
        name = segment.toLowerCase()
        staticChildren = node.staticInsensitive ??= new Map()
      }
      const existingNode = staticChildren.get(name)
      if (existingNode) {
        nextNode = existingNode
      } else {
        const next = createSegmentNode(node)
        nextNode = next
        staticChildren.set(name, next)
      }
    } else {
      const kind = segment[0 /* kind */]
      let prefix = segment[2 /* prefix */]
      let suffix = segment[3 /* suffix */] ?? ''
      if (kind === SEGMENT_TYPE_WILDCARD) {
        end = length
        cursor = end + 1
      }
      if (interpolation && literalStart < end) {
        // Retain original spelling before matcher case folding.
        if (literalStart < start - 1) {
          interpolation.push(path.substring(literalStart, start - 1))
        }
        segment[2 /* prefix */] = '/' + prefix
        if (
          kind === SEGMENT_TYPE_WILDCARD &&
          segment[3 /* suffix */] !== undefined &&
          literalEnd < length
        ) {
          segment[3 /* suffix */] = suffix.slice(0, -1)
        }
        interpolation.push(segment)
        literalStart = end
      }
      if (!node) {
        continue
      }
      const actuallyCaseSensitive = caseSensitive && !!(prefix || suffix)
      if (!caseSensitive) {
        prefix = prefix.toLowerCase()
        suffix = suffix.toLowerCase()
      }
      const siblings =
        kind === SEGMENT_TYPE_PARAM
          ? (node.dynamic ??= [])
          : kind === SEGMENT_TYPE_OPTIONAL_PARAM
            ? (node.optional ??= [])
            : (node.wildcard ??= [])
      const existingNode =
        // Keep wildcard aliases as separate match candidates, even when
        // they have the same shape and no parser.
        kind !== SEGMENT_TYPE_WILDCARD &&
        !parseParams &&
        siblings.find(
          (s) =>
            !s.parse &&
            s.caseSensitive === actuallyCaseSensitive &&
            s.prefix === prefix &&
            s.suffix === suffix,
        )
      if (existingNode) {
        nextNode = existingNode
      } else {
        const next = createSegmentNode(
          node,
          kind,
          actuallyCaseSensitive,
          prefix,
          suffix,
        )
        nextNode = next
        siblings.push(next)
        if (siblings.length === 2) {
          dynamicListsToSort?.push(siblings)
        }
      }
    }
    node = nextNode
  }

  if (interpolation && literalStart < literalEnd) {
    interpolation.push(path.substring(literalStart, literalEnd))
  }
  // Discard push()'s spare capacity before retaining this array on both owners.
  const segmentData = interpolation?.slice()
  if (!node) {
    return segmentData!
  }

  // create pathless node
  if (
    parseParams &&
    route.children &&
    !route.isRoot &&
    route.id &&
    route.id.charCodeAt(route.id.lastIndexOf('/') + 1) === 95 /* '_' */
  ) {
    const pathlessNode = createSegmentNode(node, SEGMENT_TYPE_PATHLESS)
    ;(node.pathless ??= []).push(pathlessNode)
    node = pathlessNode
  }

  const isLeaf = (route.path || !route.children) && !route.isRoot
  // create index node
  if (isLeaf && literalEnd < length) {
    const indexNode = createSegmentNode(node, SEGMENT_TYPE_INDEX)
    node.index = indexNode
    node = indexNode
  }

  node.parse = parseParams ?? null
  node.priority = options?.params?.priority ?? 0
  // A shared candidate keeps the original names of its first terminal route.
  if (!node.route) {
    node.data = segmentData
    if (isLeaf) {
      node.route = route
    }
  }
  return [node, cursor, segmentData]
}

function sortDynamic(
  a: {
    prefix: string
    suffix: string
    caseSensitive: boolean
    parse: null | ((params: Record<string, string>) => unknown)
    priority: number
  },
  b: {
    prefix: string
    suffix: string
    caseSensitive: boolean
    parse: null | ((params: Record<string, string>) => unknown)
    priority: number
  },
) {
  if (a.parse && !b.parse) return -1
  if (!a.parse && b.parse) return 1
  if (a.parse && b.parse && (a.priority || b.priority))
    return b.priority - a.priority
  if (a.prefix && b.prefix && a.prefix !== b.prefix) {
    if (a.prefix.startsWith(b.prefix)) return -1
    if (b.prefix.startsWith(a.prefix)) return 1
  }
  if (a.suffix && b.suffix && a.suffix !== b.suffix) {
    if (a.suffix.endsWith(b.suffix)) return -1
    if (b.suffix.endsWith(a.suffix)) return 1
  }
  if (a.prefix && !b.prefix) return -1
  if (!a.prefix && b.prefix) return 1
  if (a.suffix && !b.suffix) return -1
  if (!a.suffix && b.suffix) return 1
  if (a.caseSensitive && !b.caseSensitive) return -1
  if (!a.caseSensitive && b.caseSensitive) return 1

  // Equal specificity preserves route declaration order through stable sort.
  return 0
}

function createSegmentNode<T extends RouteLike>(
  parent?: AnySegmentNode<T>,
  kind?: StaticSegmentNode<T>['kind'],
): StaticSegmentNode<T>
function createSegmentNode<T extends RouteLike>(
  parent: AnySegmentNode<T>,
  kind:
    | typeof SEGMENT_TYPE_PARAM
    | typeof SEGMENT_TYPE_WILDCARD
    | typeof SEGMENT_TYPE_OPTIONAL_PARAM,
  caseSensitive: boolean,
  prefix: string,
  suffix: string,
): DynamicSegmentNode<T>
function createSegmentNode<T extends RouteLike>(
  parent?: AnySegmentNode<T>,
  kind: ExtendedSegmentKind = SEGMENT_TYPE_PATHNAME,
  caseSensitive?: boolean,
  prefix?: string,
  suffix?: string,
): SegmentNode<T> {
  return {
    kind,
    depth: parent ? parent.depth + 1 : 0,
    pathless: null,
    index: null,
    static: null,
    staticInsensitive: null,
    dynamic: null,
    optional: null,
    wildcard: null,
    route: null,
    data: undefined,
    parent,
    parse: null,
    priority: 0,
    caseSensitive,
    prefix,
    suffix,
  }
}

type StaticSegmentNode<T extends RouteLike> = SegmentNode<T> & {
  kind:
    | typeof SEGMENT_TYPE_PATHNAME
    | typeof SEGMENT_TYPE_PATHLESS
    | typeof SEGMENT_TYPE_INDEX
}

type DynamicSegmentNode<T extends RouteLike> = SegmentNode<T> & {
  kind:
    | typeof SEGMENT_TYPE_PARAM
    | typeof SEGMENT_TYPE_WILDCARD
    | typeof SEGMENT_TYPE_OPTIONAL_PARAM
  prefix: string
  suffix: string
  caseSensitive: boolean
}

type AnySegmentNode<T extends RouteLike> =
  | StaticSegmentNode<T>
  | DynamicSegmentNode<T>

type SegmentNode<T extends RouteLike> = {
  kind: ExtendedSegmentKind
  prefix?: string
  suffix?: string
  caseSensitive?: boolean

  pathless: Array<StaticSegmentNode<T>> | null

  /** Exact index segment (highest priority) */
  index: StaticSegmentNode<T> | null

  /** Static segments (2nd priority) */
  static: Map<string, StaticSegmentNode<T>> | null

  /** Case insensitive static segments (3rd highest priority) */
  staticInsensitive: Map<string, StaticSegmentNode<T>> | null

  /** Dynamic segments ($param) */
  dynamic: Array<DynamicSegmentNode<T>> | null

  /** Optional dynamic segments ({-$param}) */
  optional: Array<DynamicSegmentNode<T>> | null

  /** Wildcard segments ($ - lowest priority) */
  wildcard: Array<DynamicSegmentNode<T>> | null

  /** Terminal route (if this path can end here) */
  route: T | null

  /** Original template data for this candidate or parse gate. */
  data: RouteInterpolation | undefined

  parent: AnySegmentNode<T> | undefined

  depth: number

  /** route.options.params.parse function, set on the last node of the route */
  parse: null | ((params: Record<string, string>) => unknown)

  /** route.options.params.priority ?? 0 */
  priority: number
}

type RouteLike = {
  _interpolation?: RouteInterpolation
  id?: string
  path?: string // relative path from the parent,
  children?: Array<RouteLike> // child routes,
  parentRoute?: RouteLike // parent route,
  isRoot?: boolean
  options?: {
    caseSensitive?: boolean
    parseParams?: (params: Record<string, string>) => unknown
    params?: {
      parse?: (params: Record<string, string>) => unknown
      priority?: number
    }
  }
} &
  // router tree
  (| { fullPath: string; from?: never } // full path from the root
    // flat route masks list
    | { fullPath?: never; from: string } // full path from the root
  )

export type ProcessedTree<
  TTree extends Extract<RouteLike, { fullPath: string }>,
  TFlat extends Extract<RouteLike, { from: string }>,
  TSingle extends Extract<RouteLike, { from: string }>,
> = {
  /** a representation of the `routeTree` as a segment tree */
  segmentTree: AnySegmentNode<TTree>
  /** a mini route tree generated from the flat `routeMasks` list */
  masksTree: AnySegmentNode<TFlat> | null
  /** @deprecated keep until v2 so that `router.matchRoute` can keep not caring about the actual route tree */
  singleCache: SieveCache<string, AnySegmentNode<TSingle>>
  /** a cache of route matches from the `segmentTree` */
  matchCache: SieveCache<string, RouteMatch<TTree> | null>
  /** a cache of route matches from the `masksTree` */
  flatCache: SieveCache<string, ReturnType<typeof findMatch<TFlat>>> | null
}

export function processRouteMasks<
  TRouteLike extends Extract<RouteLike, { from: string }>,
>(
  routeList: Array<TRouteLike>,
  processedTree: ProcessedTree<any, TRouteLike, any>,
) {
  const segmentTree = createSegmentNode<TRouteLike>()
  const dynamicListsToSort: Array<Array<DynamicSegmentNode<TRouteLike>>> = []
  function visit(
    route: TRouteLike,
    start: number,
    parentNode: AnySegmentNode<TRouteLike>,
    parentInterpolation?: RouteInterpolation,
  ) {
    const [node, cursor, segments] = parseSegments(
      false,
      route,
      start,
      parentNode,
      dynamicListsToSort,
      parentInterpolation,
    )
    if (route.children) {
      for (const child of route.children) {
        visit(child as TRouteLike, cursor, node, segments)
      }
    }
  }
  for (const route of routeList) {
    visit(route, 1, segmentTree)
  }
  for (const nodes of dynamicListsToSort) {
    nodes.sort(sortDynamic)
  }
  processedTree.masksTree = segmentTree
  processedTree.flatCache = createSieveCache<
    string,
    ReturnType<typeof findMatch<TRouteLike>>
  >(1000)
}

/**
 * Take an arbitrary list of routes, create a tree from them (if it hasn't been created already), and match a path against it.
 */
export function findFlatMatch<T extends Extract<RouteLike, { from: string }>>(
  /** The path to match. */
  path: string,
  /** The `processedTree` returned by the initial `processRouteTree` call. */
  processedTree: ProcessedTree<any, T, any>,
) {
  path ||= '/'
  const cached = processedTree.flatCache!.get(path)
  if (cached !== undefined) return cached
  const result = findMatch(path, processedTree.masksTree!)
  processedTree.flatCache!.set(path, result)
  return result
}

/**
 * @deprecated keep until v2 so that `router.matchRoute` can keep not caring about the actual route tree
 */
export function findSingleMatch(
  from: string,
  caseSensitive: boolean,
  fuzzy: boolean,
  path: string,
  processedTree: ProcessedTree<any, any, { from: string }>,
) {
  from ||= '/'
  path ||= '/'
  const key = caseSensitive ? `case\0${from}` : from
  let tree = processedTree.singleCache.get(key)
  if (!tree) {
    // single flat routes (router.matchRoute) are not eagerly processed,
    // if we haven't seen this route before, process it now
    tree = createSegmentNode<{ from: string }>()
    parseSegments(caseSensitive, { from }, 1, tree)
    processedTree.singleCache.set(key, tree)
  }
  return findMatch(path, tree, fuzzy)
}

type RouteMatch<T extends Extract<RouteLike, { fullPath: string }>> = {
  route: T
  rawParams: Record<string, string>
  branch: ReadonlyArray<T>
}

export function findRouteMatch<
  T extends Extract<RouteLike, { fullPath: string }>,
>(
  /** The path to match against the route tree. */
  path: string,
  /** The `processedTree` returned by the initial `processRouteTree` call. */
  processedTree: ProcessedTree<T, any, any>,
  /** If `true`, allows fuzzy matching (partial matches), i.e. which node in the tree would have been an exact match if the `path` had been shorter? */
  fuzzy = false,
): RouteMatch<T> | null {
  const key = fuzzy ? path : `nofuzz\0${path}` // the main use for `findRouteMatch` is fuzzy:true, so we optimize for that case
  const cached = processedTree.matchCache.get(key)
  if (cached !== undefined) return cached
  path ||= '/'
  let result: RouteMatch<T> | null

  try {
    result = findMatch(
      path,
      processedTree.segmentTree,
      fuzzy,
    ) as RouteMatch<T> | null
  } catch (err) {
    if (err instanceof URIError) {
      result = null
    } else {
      throw err
    }
  }

  if (result) result.branch = buildRouteBranch(result.route)
  processedTree.matchCache.set(key, result)
  return result
}

export interface ProcessRouteTreeResult<
  TRouteLike extends Extract<RouteLike, { fullPath: string }> & { id: string },
> {
  /** Should be considered a black box, needs to be provided to all matching functions in this module. */
  processedTree: ProcessedTree<TRouteLike, any, any>
  /** A lookup map of routes by their unique IDs. */
  routesById: Record<string, TRouteLike>
  /** A lookup map of routes by their trimmed full paths. */
  routesByPath: Record<string, TRouteLike>
}

/**
 * Processes a route tree into a segment trie for efficient path matching.
 * Also builds lookup maps for routes by ID and by trimmed full path.
 */
export function processRouteTree<
  TRouteLike extends Extract<RouteLike, { fullPath: string }> & {
    id: string
    init: (originalIndex: number) => void
  },
>(
  /** The root of the route tree to process. */
  routeTree: TRouteLike,
  /** Whether matching should be case sensitive by default (overridden by individual route options). */
  caseSensitive: boolean = false,
): ProcessRouteTreeResult<TRouteLike> {
  const segmentTree = createSegmentNode<TRouteLike>()
  const dynamicListsToSort: Array<Array<DynamicSegmentNode<TRouteLike>>> = []
  const routesById = {} as Record<string, TRouteLike>
  const routesByPath = {} as Record<string, TRouteLike>
  let index = 0
  function visit(
    route: TRouteLike,
    start: number,
    parentNode: AnySegmentNode<TRouteLike>,
    parentInterpolation?: RouteInterpolation,
  ) {
    route.init(index)

    if (route.id in routesById) {
      if (process.env.NODE_ENV !== 'production') {
        throw new Error(
          `Invariant failed: Duplicate routes found with id: ${String(route.id)}`,
        )
      }

      invariant()
    }

    routesById[route.id] = route
    if (index !== 0 && route.path) {
      const trimmedFullPath = trimPathRight(route.fullPath)
      if (!routesByPath[trimmedFullPath] || route.fullPath.endsWith('/')) {
        routesByPath[trimmedFullPath] = route
      }
    }
    index++

    const [node, cursor, segments] = parseSegments(
      caseSensitive,
      route,
      start,
      parentNode,
      dynamicListsToSort,
      parentInterpolation,
    )
    route._interpolation = segments
    if (route.children) {
      for (const child of route.children) {
        visit(child as TRouteLike, cursor, node, segments)
      }
    }
  }
  visit(routeTree, 1, segmentTree)
  for (const nodes of dynamicListsToSort) {
    nodes.sort(sortDynamic)
  }
  const processedTree: ProcessedTree<TRouteLike, any, any> = {
    segmentTree,
    singleCache: createSieveCache<string, AnySegmentNode<any>>(1000),
    matchCache: createSieveCache<string, RouteMatch<TRouteLike> | null>(1000),
    flatCache: null,
    masksTree: null,
  }
  return {
    processedTree,
    routesById,
    routesByPath,
  }
}

function findMatch<T extends RouteLike>(
  path: string,
  segmentTree: AnySegmentNode<T>,
  fuzzy = false,
): {
  route: T
  /**
   * The raw (unparsed) params extracted from the path.
   * This will be the exhaustive list of all params defined in the route's path.
   */
  rawParams: Record<string, string>
} | null {
  const parts = path.split('/')
  const leaf = getNodeMatch(path, parts, segmentTree, fuzzy)
  if (!leaf) return null
  const [rawParams] = extractParams(path, parts, leaf)
  return {
    route: leaf.node.route!,
    rawParams,
  }
}

type ParamExtractionState = {
  part: number
  node: number
  path: number
  param: number
}

/**
 * This function is "resumable":
 * - the `leaf` input can contain `extract` and `rawParams` properties from a previous `extractParams` call
 * - the returned `state` can be passed back as `extract` in a future call to continue extracting params from where we left off
 *
 * Inputs are *not* mutated.
 */
function extractParams<T extends RouteLike>(
  path: string,
  parts: Array<string>,
  leaf: {
    node: AnySegmentNode<T>
    skipped: number
    extract?: ParamExtractionState
    rawParams?: Record<string, string>
  },
): [rawParams: Record<string, string>, state: ParamExtractionState] {
  const list = buildBranch(leaf.node)
  const names = leaf.node.data && getParamNames(leaf.node.data)
  const rawParams: Record<string, string> = Object.create(null)
  /** which segment of the path we're currently processing */
  let partIndex = leaf.extract?.part ?? 0
  /** which node of the route tree branch we're currently processing */
  let nodeIndex = leaf.extract?.node ?? 0
  /** index of the 1st character of the segment we're processing in the path string */
  let pathIndex = leaf.extract?.path ?? 0
  /** Next original parameter name, independent of pathless/static nodes. */
  let paramIndex = leaf.extract?.param ?? 0
  for (; nodeIndex < list.length; partIndex++, nodeIndex++, pathIndex++) {
    const node = list[nodeIndex]!
    // index nodes are terminating nodes, nothing to extract, just leave
    if (node.kind === SEGMENT_TYPE_INDEX) break
    // pathless nodes do not consume a path segment
    if (node.kind === SEGMENT_TYPE_PATHLESS) {
      partIndex--
      pathIndex--
      continue
    }
    const part = parts[partIndex]
    const currentPathIndex = pathIndex
    if (part) pathIndex += part.length
    if (
      node.kind === SEGMENT_TYPE_PARAM ||
      node.kind === SEGMENT_TYPE_OPTIONAL_PARAM
    ) {
      const name = names![paramIndex++]!
      if (
        node.kind === SEGMENT_TYPE_OPTIONAL_PARAM &&
        leaf.skipped & (1 << nodeIndex)
      ) {
        partIndex-- // stay on the same part
        pathIndex = currentPathIndex - 1 // undo pathIndex advancement; -1 to account for loop increment
        continue
      }
      const value =
        node.suffix || node.prefix
          ? part!.substring(
              node.prefix.length,
              part!.length - node.suffix.length,
            )
          : part!
      if (value || node.kind === SEGMENT_TYPE_PARAM) {
        rawParams[name] = decodeURIComponent(value)
      }
    } else if (node.kind === SEGMENT_TYPE_WILDCARD) {
      const n = node
      const value = path.substring(
        currentPathIndex + n.prefix.length,
        path.length - n.suffix.length,
      )
      const splat = decodeURIComponent(value)
      // TODO: Deprecate *
      rawParams['*'] = splat
      rawParams._splat = splat
      break
    }
  }
  if (leaf.rawParams) Object.assign(rawParams, leaf.rawParams)
  return [
    rawParams,
    {
      part: partIndex,
      node: nodeIndex,
      path: pathIndex,
      param: paramIndex,
    },
  ]
}

export function buildRouteBranch<T extends RouteLike>(route: T) {
  const list = [route]
  while (route.parentRoute) {
    route = route.parentRoute as T
    list.push(route)
  }
  list.reverse()
  return list
}

function buildBranch<T extends RouteLike>(node: AnySegmentNode<T>) {
  const list: Array<AnySegmentNode<T>> = Array(node.depth + 1)
  do {
    list[node.depth] = node
    node = node.parent!
  } while (node)
  return list
}

type MatchStackFrame<T extends RouteLike> = {
  node: AnySegmentNode<T>
  /** index of the segment of path */
  index: number
  /**
   * Bitmask of skipped optional segments.
   *
   * This is a very performant way of storing an "array of booleans", but it means beyond 32 segments we can't track skipped optionals.
   * If we really really need to support more than 32 segments we can switch to using a `BigInt` here. It's about 2x slower in worst case scenarios.
   */
  skipped: number
  /** Positional bitmasks tracking which consumed URL segments matched each segment kind. */
  statics: number
  dynamics: number
  optionals: number
  /** intermediary state for param extraction */
  extract?: ParamExtractionState
  /** intermediary params from param extraction */
  rawParams?: Record<string, string>
}

function getNodeMatch<T extends RouteLike>(
  path: string,
  parts: Array<string>,
  segmentTree: AnySegmentNode<T>,
  fuzzy: boolean,
) {
  // quick check for root index
  // this is an optimization, algorithm should work correctly without this block
  if (path === '/' && segmentTree.index)
    return { node: segmentTree.index, skipped: 0 } as Pick<
      Frame,
      'node' | 'skipped'
    >

  const trailingSlash = !last(parts)
  const pathIsIndex = trailingSlash && path !== '/'
  const partsLength = parts.length - (trailingSlash ? 1 : 0)

  type Frame = MatchStackFrame<T>

  // use a stack to explore all possible paths (params cause branching)
  // iterate "backwards" (low priority first) so that we can push() each candidate, and pop() the highest priority candidate first
  // - pros: it is depth-first, so we find full matches faster
  // - cons: we cannot short-circuit, because highest priority matches are at the end of the loop (for loop with i--) (but we have no good short-circuiting anyway)
  // other possible approaches:
  // - shift instead of pop (measure performance difference), this allows iterating "forwards" (effectively breadth-first)
  // - never remove from the stack, keep a cursor instead. Then we can push "forwards" and avoid reversing the order of candidates (effectively breadth-first)
  const stack: Array<Frame> = [
    {
      node: segmentTree,
      index: 1,
      skipped: 0,
      statics: 0,
      dynamics: 0,
      optionals: 0,
    },
  ]

  let bestFuzzy: Frame | null = null
  let bestMatch: Frame | null = null

  while (stack.length) {
    const frame = stack.pop()!
    const { node, index, skipped, statics, dynamics, optionals } = frame
    let { extract, rawParams } = frame

    // Wildcard candidates are pushed speculatively as fallbacks in case a
    // higher-priority wildcard later fails params.parse. If a better wildcard
    // has already validated and become bestMatch, lower-priority wildcard
    // fallbacks cannot win anymore and should not run params.parse.
    if (
      node.kind === SEGMENT_TYPE_WILDCARD &&
      node.route &&
      !isFrameMoreSpecific(bestMatch, frame)
    ) {
      continue
    }

    if (node.parse) {
      const result = validateParseParams(path, parts, frame)
      if (!result) continue
      rawParams = frame.rawParams
      extract = frame.extract
    }

    // In fuzzy mode, track the best partial match we've found so far
    if (
      fuzzy &&
      node.route &&
      node.kind !== SEGMENT_TYPE_INDEX &&
      isFrameMoreSpecific(bestFuzzy, frame)
    ) {
      bestFuzzy = frame
    }

    const isBeyondPath = index === partsLength
    if (isBeyondPath) {
      if (
        node.route &&
        (!pathIsIndex ||
          node.kind === SEGMENT_TYPE_INDEX ||
          node.kind === SEGMENT_TYPE_WILDCARD) &&
        isFrameMoreSpecific(bestMatch, frame)
      ) {
        bestMatch = frame
      }
      // beyond the length of the path parts, only some segment types can match
      if (!node.optional && !node.wildcard && !node.index && !node.pathless)
        continue
    }

    const part = isBeyondPath ? undefined : parts[index]!
    let lowerPart: string

    // 0. Try index match
    if (isBeyondPath && node.index) {
      const indexFrame = {
        node: node.index,
        index,
        skipped,
        statics,
        dynamics,
        optionals,
        extract,
        rawParams,
      }
      let indexValid = true
      if (node.index.parse) {
        const result = validateParseParams(path, parts, indexFrame)
        if (!result) indexValid = false
      }
      if (indexValid) {
        // perfect match, no need to continue
        // this is an optimization, algorithm should work correctly without this block
        if (
          !dynamics &&
          !optionals &&
          !skipped &&
          isPerfectStaticMatch(statics, partsLength)
        ) {
          return indexFrame
        }
        if (isFrameMoreSpecific(bestMatch, indexFrame)) {
          // index matches skip the stack because they cannot have children
          bestMatch = indexFrame
        }
      }
    }

    // 5. Try wildcard match
    if (node.wildcard) {
      for (let i = node.wildcard.length - 1; i >= 0; i--) {
        const segment = node.wildcard[i]!
        const { prefix, suffix } = segment
        if (prefix) {
          if (isBeyondPath) continue
          const casePart = segment.caseSensitive
            ? part
            : (lowerPart ??= part!.toLowerCase())
          if (!casePart!.startsWith(prefix)) continue
        }
        if (suffix) {
          if (isBeyondPath) continue
          const end = parts.slice(index).join('/')
          const suffixPart = end.slice(-suffix.length)
          const casePart = segment.caseSensitive
            ? suffixPart
            : suffixPart.toLowerCase()
          if (
            casePart !== suffix ||
            end.length - suffix.length < prefix.length
          ) {
            continue
          }
        }
        // wildcard matches consume the rest of the URL and cannot have children
        stack.push({
          node: segment,
          index: partsLength,
          skipped,
          statics,
          dynamics,
          optionals,
          extract,
          rawParams,
        })
      }
    }

    // 4. Try optional match
    if (node.optional) {
      // A skipped optional is keyed by the child node's trie depth.
      const nextSkipped = skipped | (1 << (node.depth + 1))
      for (let i = node.optional.length - 1; i >= 0; i--) {
        const segment = node.optional[i]!
        // when skipping, the node advances by 1, but the index doesn't
        stack.push({
          node: segment,
          index,
          skipped: nextSkipped,
          statics,
          dynamics,
          optionals,
          extract,
          rawParams,
        }) // enqueue skipping the optional
      }
      if (!isBeyondPath) {
        for (let i = node.optional.length - 1; i >= 0; i--) {
          const segment = node.optional[i]!
          const { prefix, suffix } = segment
          if (prefix || suffix) {
            const casePart = segment.caseSensitive
              ? part!
              : (lowerPart ??= part!.toLowerCase())
            if (prefix && !casePart.startsWith(prefix)) continue
            if (
              suffix &&
              casePart.indexOf(suffix, casePart.length - suffix.length) <
                prefix.length
            ) {
              continue
            }
          }
          stack.push({
            node: segment,
            index: index + 1,
            skipped,
            statics,
            dynamics,
            optionals: optionals + segmentScore(partsLength, index),
            extract,
            rawParams,
          })
        }
      }
    }

    // 3. Try dynamic match
    if (!isBeyondPath && node.dynamic && part) {
      for (let i = node.dynamic.length - 1; i >= 0; i--) {
        const segment = node.dynamic[i]!
        const { prefix, suffix } = segment
        if (prefix || suffix) {
          const casePart = segment.caseSensitive
            ? part
            : (lowerPart ??= part.toLowerCase())
          if (prefix && !casePart.startsWith(prefix)) continue
          if (
            suffix &&
            casePart.indexOf(suffix, casePart.length - suffix.length) <
              prefix.length
          ) {
            continue
          }
        }
        stack.push({
          node: segment,
          index: index + 1,
          skipped,
          statics,
          dynamics: dynamics + segmentScore(partsLength, index),
          optionals,
          extract,
          rawParams,
        })
      }
    }

    // 2. Try case insensitive static match
    if (!isBeyondPath && node.staticInsensitive) {
      const match = node.staticInsensitive.get(
        (lowerPart ??= part!.toLowerCase()),
      )
      if (match) {
        stack.push({
          node: match,
          index: index + 1,
          skipped,
          statics: statics + segmentScore(partsLength, index),
          dynamics,
          optionals,
          extract,
          rawParams,
        })
      }
    }

    // 1. Try static match
    if (!isBeyondPath && node.static) {
      const match = node.static.get(part!)
      if (match) {
        stack.push({
          node: match,
          index: index + 1,
          skipped,
          statics: statics + segmentScore(partsLength, index),
          dynamics,
          optionals,
          extract,
          rawParams,
        })
      }
    }

    // 0. Try pathless match
    if (node.pathless) {
      for (let i = node.pathless.length - 1; i >= 0; i--) {
        const segment = node.pathless[i]!
        stack.push({
          node: segment,
          index,
          skipped,
          statics,
          dynamics,
          optionals,
          extract,
          rawParams,
        })
      }
    }
  }

  if (bestMatch) return bestMatch

  if (fuzzy && bestFuzzy) {
    let sliceIndex = bestFuzzy.index
    for (let i = 0; i < bestFuzzy.index; i++) {
      sliceIndex += parts[i]!.length
    }
    const splat = sliceIndex === path.length ? '/' : path.slice(sliceIndex)
    bestFuzzy.rawParams ??= Object.create(null)
    bestFuzzy.rawParams!['**'] = decodeURIComponent(splat)
    return bestFuzzy
  }

  return null
}

function segmentScore(partsLength: number, index: number): number {
  // The specificity scores are bitmasks over consumed URL segments. Earlier
  // URL segments should dominate later ones when comparing scores, so the
  // first real segment gets the highest bit and the last gets bit 0. Since
  // `parts[0]` is the empty string before the leading slash, real URL segments
  // are [1, partsLength), making this segment's bit `partsLength - index - 1`.
  return 2 ** (partsLength - index - 1)
}

function isPerfectStaticMatch(statics: number, partsLength: number): boolean {
  return statics === 2 ** (partsLength - 1) - 1
}

function validateParseParams<T extends RouteLike>(
  path: string,
  parts: Array<string>,
  frame: MatchStackFrame<T>,
) {
  let rawParams: Record<string, string>
  let state: ParamExtractionState

  try {
    ;[rawParams, state] = extractParams(path, parts, frame)
  } catch {
    return null
  }

  frame.rawParams = rawParams
  frame.extract = state

  if (!frame.node.parse) return true

  try {
    if (frame.node.parse(rawParams) === false) return null
  } catch {
    // Thrown parse errors should be surfaced on the selected match by
    // extractStrictParams, not used as fallback route selection.
  }

  return true
}

function isFrameMoreSpecific(
  // the stack frame previously saved as "best match"
  prev: MatchStackFrame<any> | null,
  // the candidate stack frame
  next: MatchStackFrame<any>,
): boolean {
  if (!prev) return true
  return (
    next.statics > prev.statics ||
    (next.statics === prev.statics &&
      (next.dynamics > prev.dynamics ||
        (next.dynamics === prev.dynamics &&
          (next.optionals > prev.optionals ||
            (next.optionals === prev.optionals &&
              ((next.node.kind === SEGMENT_TYPE_INDEX) >
                (prev.node.kind === SEGMENT_TYPE_INDEX) ||
                ((next.node.kind === SEGMENT_TYPE_INDEX) ===
                  (prev.node.kind === SEGMENT_TYPE_INDEX) &&
                  next.node.depth > prev.node.depth)))))))
  )
}
