import { bold, rgb8 } from "https://deno.land/std@0.171.0/fmt/colors.ts";
import { sprintf } from "https://deno.land/std@0.171.0/fmt/printf.ts";

// deno-fmt-ignore
const colors = [
   20,  21,  26,  27,  32,  33,  38,  39,  40,  41,  42,  43,  44,  45,  56,  57,  62,  63,  68,
   69,  74,  75,  76,  77,  78,  79,  80,  81,  92,  93,  98,  99, 112, 113, 128, 129, 134, 135,
  148, 149, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 178, 179, 184,
  185, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 214, 215, 220, 221,
];

function getNextColorByNamespace(namespace: string) {
  let hash = 0;
  for (let i = 0; i < namespace.length; i++) {
    hash = (hash << 5) - hash + namespace.charCodeAt(i);
    hash |= 0; // Convert to 32bit integer
  }
  return colors[Math.abs(hash) % colors.length];
}

const MS = 1;
const S = 1000;
const M = 60000;
const H = 3600000;

function timeAgo(since: Date | number, at: Date | number = new Date()) {
  const _since = typeof since === "number" ? since : since.getTime();
  const _at = typeof at === "number" ? at : at.getTime();
  const from = Math.min(_since, _at);
  const to = Math.max(_since, _at);
  let r = to - from;
  if (r / H >= 1) return `${Math.round(r / H)}h`;
  if (r / M >= 1) return `${Math.round(r / M)}m`;
  if (r / S >= 1) return `${Math.round(r / S)}s`;
  if (r / MS >= 1) return `${Math.round(r / MS)}ms`;
  return `${Math.round((r - Math.floor(r)) * 1000)}μs`;
}

export type Debugger = ((...args: unknown[]) => void) & {
  enabled: boolean;
};
type Validator =
  & ((str: string, passIfNotMatching?: boolean, debug?: boolean) => boolean)
  & {
    rule: string;
  };

const SPACE = /^[\s,]$/;
const CHAR = /^\w$/;

function _evaluateNamespaceList(str: string, prefix = ""): string[] {
  const evaluated: string[] = [];

  let buffer: string | undefined = undefined;
  let buffer2: string | undefined = undefined;
  let capture = 0;
  let joinPrefix = false;

  let isPrefixAllow = true;
  {
    const firstChar = prefix.charAt(0);
    if (firstChar === "-") {
      isPrefixAllow = false;
      prefix = prefix.substring(1);
    } else if (firstChar === "+") {
      prefix = prefix.substring(1);
    }
  }

  let isAllow = isPrefixAllow;
  for (let i = 0; i < str.length; i++) {
    const char = str.charAt(i);
    const isSpace = SPACE.test(char);

    if (char === "(") {
      if (capture > 0) buffer2 = (buffer2 || "") + "(";
      capture++;
      continue;
    }

    if (char === ")") {
      capture--;
      if (capture === 0) {
        if (joinPrefix) {
          evaluated.push((isAllow ? "" : "-") + prefix + (buffer || ""));
        }
        evaluated.push(
          ..._evaluateNamespaceList(
            buffer2!,
            (isAllow ? "" : "-") + prefix + (buffer || ""),
          ),
        );
        isAllow = isPrefixAllow;
        buffer2 = undefined;
        buffer = undefined;
      } else if (capture < 0) return evaluated;
      else buffer2 += ")";
      continue;
    }

    if (capture) {
      buffer2 = (buffer2 || "") + char;
      continue;
    }

    if (isSpace && buffer) {
      evaluated.push((isAllow ? "" : "-") + prefix + buffer);
      isAllow = isPrefixAllow;
      buffer = undefined;
      continue;
    }

    if (!buffer && char === "&") {
      joinPrefix = true;
      continue;
    }

    if (isSpace && !buffer) continue;

    if (!buffer && char === "-") {
      isAllow = false;
      continue;
    }
    if (!buffer && char === "+") {
      isAllow = true;
      continue;
    }

    if (!buffer) buffer = "";
    buffer += char;
  }

  if (buffer) {
    evaluated.push((isAllow ? "" : "-") + prefix + buffer);
    isAllow = isPrefixAllow;
    buffer = undefined;
  }

  return evaluated;
}

function namespaceToTest(namespace: string): Validator {
  const original = namespace;
  let allow = true;
  {
    const first = namespace.charAt(0);
    if (first === "-") {
      allow = false;
      namespace = namespace.substring(1);
    } else if (first === "+") {
      namespace = namespace.substring(1);
    }
    namespace = namespace.replace(/:+/g, ":").replace(/\*\*+/g, "**");
    let newNs = "";
    for (let i = 0; i < namespace.length; i++) {
      const char = namespace[i];
      const next = namespace[i + 1];
      if (char === "*") {
        if (next === "*") {
          i++;
          newNs += ".*";
          continue;
        }
        newNs += "[^:]*";
        continue;
      }
      if (CHAR.test(char)) {
        newNs += char;
        continue;
      }
      newNs += `[\\${char}]`;
    }
    namespace = newNs;
  }
  const regex = new RegExp(`^${namespace}$`, "i");
  const validator = ((str, passIfNotMatching) => {
    const test = regex.test(str);
    return test ? (allow ? true : false) : passIfNotMatching ?? false;
  }) as Validator;
  validator.rule = original;
  return validator;
}

function evaluateNamespaceList(str: string) {
  const list = _evaluateNamespaceList(str);
  return list.map((str) => namespaceToTest(str));
}

function isNamespaceAllowed(validators: Validator[], ns: string) {
  return validators.reduce((p, validate) => validate(ns, p), false);
}

// ---

export const rules: Validator[] = [];
const debuggers = new Map<string, Debugger>();

function addRule(validator: Validator) {
  const index = rules.findIndex((v) => v.rule === validator.rule);
  if (index !== -1) return;
  rules.push(validator);
}

function removeRule(validator: Validator) {
  const index = rules.findIndex((v) => v.rule === validator.rule);
  if (index === -1) return;
  rules.splice(index, 1);
}

function hasRule(validator: Validator) {
  return rules.some((v) => v.rule === validator.rule);
}

function reEvaluate() {
  for (const [namespace, d] of debuggers) {
    d.enabled = isNamespaceAllowed(rules, namespace);
  }
}

// ---

export function removeRules(rules: string) {
  for (const validator of evaluateNamespaceList(rules)) {
    removeRule(validator);
  }
  reEvaluate();
}

export function addRules(rules: string) {
  for (const validator of evaluateNamespaceList(rules)) {
    addRule(validator);
  }
  reEvaluate();
}

export function hasSomeRule(rules: string) {
  for (const validator of evaluateNamespaceList(rules)) {
    if (hasRule(validator)) return true;
  }
}

export function hasEveryRule(rules: string) {
  for (const validator of evaluateNamespaceList(rules)) {
    if (!hasRule(validator)) return false;
  }
  return true;
}

function _formatTty(
  namespace: string,
  at: Date | number,
  data: string,
  color: number,
  lastCall?: Date | number,
) {
  const start = "  " + namespace + " ";
  let msg = start;
  msg += data.replace(/\r?\n/g, (eol) => eol + start);
  msg += " " + rgb8(lastCall ? "+" + timeAgo(lastCall, at) : "+0μs", color);
  return msg;
}

function _formatNoTty(namespace: string, at: Date, data: string) {
  return sprintf("%s %s %s", at.toISOString(), namespace, data);
}

declare namespace performance {
  export function now(): number;
}

const nowTty = () => performance.now();
const nowNoTty = () => new Date();

const now = Deno.isatty(Deno.stdout.rid) ? nowTty : nowNoTty;

function _format(
  namespace: string,
  at: Date | number,
  data: string,
  color: number,
  lastCall?: Date | number,
) {
  return Deno.isatty(Deno.stdout.rid)
    ? _formatTty(namespace, at, data, color, lastCall)
    // deno-lint-ignore no-explicit-any
    : _formatNoTty(namespace, at as any, data);
}

export function debug(
  namespace: string,
  // deno-lint-ignore no-explicit-any
  logger: (message: string) => any = console.log,
) {
  let d = debuggers.get(namespace);
  if (d) return d;
  let lastCall: Date | number | undefined = undefined;
  const tty = Deno.isatty(Deno.stdout.rid);
  const color = getNextColorByNamespace(namespace);
  const displayName = tty ? bold(rgb8(namespace, color)) : namespace;
  d = ((formatter, ...args) => {
    if (!d!.enabled) return;
    const at = now();
    if (typeof formatter !== "string") {
      args.unshift(formatter);
      formatter = "%J";
    }
    const data = sprintf(formatter as string, ...args);
    const message = _format(displayName, at, data, color, lastCall);
    lastCall = at;
    logger(message);
  }) as Debugger;
  d.enabled = isNamespaceAllowed(rules, namespace);
  debuggers.set(namespace, d);
  return d!;
}

try {
  const rules = Deno.env.get("DEBUG") || "";
  if (rules) addRules(rules);
} catch {
  // ignore
}

export default debug;
