// NOTE: kept (intentionally) near-identical to packages/tldts-icann/src/suffix-trie.ts.
// They are separate copies rather than a shared helper because the lookup is
// only fast when the typed arrays are module-scope monomorphic globals —
// closing over them (a shared factory) measured ~20% slower. The ICANN build
// also specializes (constant mask, no isIcann/isPrivate). Keep the two in sync.
import {
  fastPathLookup,
  IPublicSuffix,
  ISuffixLookupOptions,
} from 'tldts-core';
import {
  edgeChild,
  edgeLength,
  edgeStart,
  exceptionsRoot,
  labelText,
  nodeFlags,
  rulesRoot,
} from './data/trie';

const enum RULE_TYPE {
  ICANN = 1,
  PRIVATE = 2,
}

// `edgeOffset` (where each label starts in `labelText`), `edgeHash` (djb2 of
// each label) and `wildcardEdge` (each node's '*' edge, or -1) are derived once
// at load instead of being shipped: the bundle then carries only the
// compressible `labelText` + structure, while the lookup binary-searches
// integer hashes. The cost is a single ~1ms pass at first import — cheaper than
// the object trie it replaces. Kept at module scope (not captured in a closure)
// so V8 treats the typed arrays as fast monomorphic globals.
const numberOfNodes = nodeFlags.length;
const numberOfEdges = edgeLength.length;
const edgeOffset = new Uint32Array(numberOfEdges);
const edgeHash = new Uint32Array(numberOfEdges);
const wildcardEdge = new Int32Array(numberOfNodes).fill(-1);
for (let node = 0, offset = 0; node < numberOfNodes; node += 1) {
  for (let edge = edgeStart[node]!; edge < edgeStart[node + 1]!; edge += 1) {
    edgeOffset[edge] = offset;
    const end = offset + edgeLength[edge]!;
    let hash = 5381;
    for (let i = end - 1; i >= offset; i -= 1) {
      hash = (hash * 33) ^ labelText.charCodeAt(i);
    }
    edgeHash[edge] = hash >>> 0;
    if (
      edgeLength[edge] === 1 &&
      labelText.charCodeAt(offset) === 42 /* '*' */
    ) {
      wildcardEdge[node] = edge;
    }
    offset = end;
  }
}

// Result of the last `walk`, kept in module scope to avoid allocating a match
// object. Safe because lookups are synchronous and read right after `walk`.
let matchNode = -1;
let matchStart = 0;
let matchEnd = 0;

/**
 * True if edge `edge`'s label equals `hostname[start, start + length)`.
 */
function labelEquals(
  edge: number,
  hostname: string,
  start: number,
  length: number,
): boolean {
  if (edgeLength[edge] !== length) {
    return false;
  }
  const offset = edgeOffset[edge]!;
  for (let i = 0; i < length; i += 1) {
    if (labelText.charCodeAt(offset + i) !== hostname.charCodeAt(start + i)) {
      return false;
    }
  }
  return true;
}

/**
 * Find the child edge of `node` whose label is `hostname[start, start + length)`.
 * Edges are sorted by hash, so binary-search the hash then verify the label
 * (scanning the rare run of equal hashes). Returns the edge index or -1.
 */
function findEdge(
  node: number,
  hash: number,
  hostname: string,
  start: number,
  length: number,
): number {
  let lo = edgeStart[node]!;
  let hi = edgeStart[node + 1]!;
  while (lo < hi) {
    const mid = (lo + hi) >>> 1;
    const value = edgeHash[mid]!;
    if (value < hash) {
      lo = mid + 1;
    } else if (value > hash) {
      hi = mid;
    } else {
      for (let e = mid; e >= lo && edgeHash[e] === hash; e -= 1) {
        if (labelEquals(e, hostname, start, length)) return e;
      }
      for (let e = mid + 1; e < hi && edgeHash[e] === hash; e += 1) {
        if (labelEquals(e, hostname, start, length)) return e;
      }
      return -1;
    }
  }
  return -1;
}

/**
 * Walk `hostname`'s labels right-to-left from `root`, recording the deepest
 * node whose flag passes `allowedMask` (with the label boundaries of that match
 * in `matchStart`/`matchEnd`). Returns whether any match was found.
 */
function walk(hostname: string, root: number, allowedMask: number): boolean {
  let node = root;
  let end = hostname.length;
  let hash = 5381;
  matchNode = -1;
  for (let i = hostname.length - 1; i >= 0; i -= 1) {
    const code = hostname.charCodeAt(i);
    if (code === 46 /* '.' */) {
      const start = i + 1;
      let edge = findEdge(node, hash >>> 0, hostname, start, end - start);
      if (edge === -1) {
        edge = wildcardEdge[node]!;
      }
      if (edge === -1) {
        return matchNode !== -1;
      }
      node = edgeChild[edge]!;
      if ((nodeFlags[node]! & allowedMask) !== 0) {
        matchNode = node;
        matchStart = start;
        matchEnd = end;
      }
      end = i;
      hash = 5381;
    } else {
      hash = (hash * 33) ^ code;
    }
  }

  // Left-most label: hostname[0, end). Same find/descend/record as the loop —
  // duplicated rather than folded into the loop (via `i >= -1`) because that
  // extra per-character branch measured slightly slower on the hot path.
  let edge = findEdge(node, hash >>> 0, hostname, 0, end);
  if (edge === -1) {
    edge = wildcardEdge[node]!;
  }
  if (edge !== -1) {
    node = edgeChild[edge]!;
    if ((nodeFlags[node]! & allowedMask) !== 0) {
      matchNode = node;
      matchStart = 0;
      matchEnd = end;
    }
  }
  return matchNode !== -1;
}

/**
 * Check if `hostname` has a valid public suffix in the trie.
 */
export default function suffixLookup(
  hostname: string,
  options: ISuffixLookupOptions,
  out: IPublicSuffix,
): void {
  if (fastPathLookup(hostname, options, out)) {
    return;
  }

  const allowedMask =
    (options.allowPrivateDomains ? RULE_TYPE.PRIVATE : 0) |
    (options.allowIcannDomains ? RULE_TYPE.ICANN : 0);

  // Exceptions have priority and strip their own left-most label (e.g. the
  // rule '!www.ck' makes the suffix of 'www.ck' be 'ck').
  if (walk(hostname, exceptionsRoot, allowedMask)) {
    out.isIcann = (nodeFlags[matchNode]! & RULE_TYPE.ICANN) !== 0;
    out.isPrivate = (nodeFlags[matchNode]! & RULE_TYPE.PRIVATE) !== 0;
    out.publicSuffix = hostname.slice(matchEnd + 1);
    return;
  }

  if (walk(hostname, rulesRoot, allowedMask)) {
    out.isIcann = (nodeFlags[matchNode]! & RULE_TYPE.ICANN) !== 0;
    out.isPrivate = (nodeFlags[matchNode]! & RULE_TYPE.PRIVATE) !== 0;
    out.publicSuffix = hostname.slice(matchStart);
    return;
  }

  // No match: the prevailing '*' rule makes the right-most label the suffix.
  out.isIcann = false;
  out.isPrivate = false;
  const lastDot = hostname.lastIndexOf('.');
  out.publicSuffix = lastDot === -1 ? hostname : hostname.slice(lastDot + 1);
}
