/*!
 * Wunderbaum - common
 * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
 * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
 */

import {
  ApplyCommandType,
  NavigationType,
  SourceListType,
  SourceObjectType,
  IconMapType,
  MatcherCallback,
} from "./types";
import * as util from "./util";
import { WunderbaumNode } from "./wb_node";

export const DEFAULT_DEBUGLEVEL = 4; // Replaced by rollup script
/**
 * Fixed height of a row in pixel. Must match the SCSS variable `$row-outer-height`.
 */
export const DEFAULT_ROW_HEIGHT = 22;
/**
 * Fixed width of node icons in pixel. Must match the SCSS variable `$icon-outer-width`.
 */
export const ICON_WIDTH = 20;
/**
 * Adjust the width of the title span, so overflow ellipsis work.
 * (2 x `$col-padding-x` + 3px rounding errors).
 */
export const TITLE_SPAN_PAD_Y = 7;
/** Render row markup for N nodes above and below the visible viewport. */
export const RENDER_MAX_PREFETCH = 5;
/** Skip rendering new rows when we have at least N nodes rendeed above and below the viewport. */
export const RENDER_MIN_PREFETCH = 5;
/** Minimum column width if not set otherwise. */
export const DEFAULT_MIN_COL_WIDTH = 4;
/** Regular expression to detect if a string describes an image URL (in contrast
 * to a class name). Strings are considered image urls if they contain '.' or '/'.
 */
export const TEST_IMG = new RegExp(/\.|\//);
// export const RECURSIVE_REQUEST_ERROR = "$recursive_request";
// export const INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid";

/**
 * Default node icons for icon libraries
 *
 *  - 'bootstrap': {@link https://icons.getbootstrap.com}
 *  - 'fontawesome6' {@link https://fontawesome.com/icons}
 *
 */
export const iconMaps: { [key: string]: IconMapType } = {
  bootstrap: {
    error: "bi bi-exclamation-triangle",
    // loading: "bi bi-hourglass-split wb-busy",
    loading: "bi bi-chevron-right wb-busy",
    // loading: "bi bi-arrow-repeat wb-spin",
    // loading: '<div class="spinner-border spinner-border-sm" role="status"> <span class="visually-hidden">Loading...</span> </div>',
    // noData: "bi bi-search",
    noData: "bi bi-question-circle",
    expanderExpanded: "bi bi-chevron-down",
    // expanderExpanded: "bi bi-dash-square",
    expanderCollapsed: "bi bi-chevron-right",
    // expanderCollapsed: "bi bi-plus-square",
    expanderLazy: "bi bi-chevron-right wb-helper-lazy-expander",
    // expanderLazy: "bi bi-chevron-bar-right",
    checkChecked: "bi bi-check-square",
    checkUnchecked: "bi bi-square",
    checkUnknown: "bi bi-dash-square-dotted",
    radioChecked: "bi bi-circle-fill",
    radioUnchecked: "bi bi-circle",
    radioUnknown: "bi bi-record-circle",
    folder: "bi bi-folder2",
    folderOpen: "bi bi-folder2-open",
    folderLazy: "bi bi-folder-symlink",
    doc: "bi bi-file-earmark",
    colSortable: "bi bi-chevron-expand",
    // colSortable: "bi bi-arrow-down-up",
    // colSortAsc: "bi bi-chevron-down",
    // colSortDesc: "bi bi-chevron-up",
    colSortAsc: "bi bi-arrow-down",
    colSortDesc: "bi bi-arrow-up",
    colFilter: "bi bi-filter-circle",
    colFilterActive: "bi bi-filter-circle-fill wb-helper-invalid",
    colMenu: "bi bi-three-dots-vertical",
  },
  fontawesome6: {
    error: "fa-solid fa-triangle-exclamation",
    loading: "fa-solid fa-chevron-right fa-beat",
    noData: "fa-solid fa-circle-question",
    expanderExpanded: "fa-solid fa-chevron-down",
    expanderCollapsed: "fa-solid fa-chevron-right",
    expanderLazy: "fa-solid fa-chevron-right wb-helper-lazy-expander",
    checkChecked: "fa-regular fa-square-check",
    checkUnchecked: "fa-regular fa-square",
    checkUnknown: "fa-regular fa-square-minus",
    radioChecked: "fa-solid fa-circle",
    radioUnchecked: "fa-regular fa-circle",
    radioUnknown: "fa-regular fa-circle-question",
    folder: "fa-solid fa-folder-closed",
    folderOpen: "fa-regular fa-folder-open",
    folderLazy: "fa-solid fa-folder-plus",
    doc: "fa-regular fa-file",
    colSortable: "fa-solid fa-fw fa-sort",
    colSortAsc: "fa-solid fa-fw fa-sort-up",
    colSortDesc: "fa-solid fa-fw fa-sort-down",
    colFilter: "fa-solid fa-fw fa-filter",
    colFilterActive: "fa-solid fa-fw fa-filter wb-helper-invalid",
    colMenu: "fa-solid fa-fw fa-ellipsis-v",
  },
};

export const KEY_NODATA = "__not_found__";

/** Define which keys are handled by embedded <input> control, and should
 * *not* be passed to tree navigation handler in cell-edit mode.
 */
export const INPUT_KEYS: { [key: string]: Array<string> } = {
  text: ["left", "right", "home", "end", "backspace"],
  number: ["up", "down", "left", "right", "home", "end", "backspace"],
  checkbox: [],
  link: [],
  radiobutton: ["up", "down"],
  "select-one": ["up", "down"],
  "select-multiple": ["up", "down"],
};

/** Dict keys that are evaluated by source loader (others are added to `tree.data` instead). */
export const RESERVED_TREE_SOURCE_KEYS: Set<string> = new Set([
  "_format", // reserved for future use
  "_keyMap", // Used for compressed data format
  "_positional", // Used for compressed data format
  "_typeList", // Used for compressed data format @deprecated
  "_valueMap", // Used for compressed data format
  "_version", // reserved for future use
  "children",
  "columns",
  "types",
]);

// /** Key codes that trigger grid navigation, even when inside an input element. */
// export const INPUT_BREAKOUT_KEYS: Set<string> = new Set([
//   // "ArrowDown",
//   // "ArrowUp",
//   "Enter",
//   "Escape",
// ]);

/** Map `KeyEvent.key` to navigation action. */
export const KEY_TO_NAVIGATION_MAP: { [key: string]: NavigationType } = {
  ArrowDown: "down",
  ArrowLeft: "left",
  ArrowRight: "right",
  ArrowUp: "up",
  Backspace: "parent",
  End: "lastCol",
  Home: "firstCol",
  "Control+End": "last",
  "Control+Home": "first",
  "Meta+ArrowDown": "last", // macOs
  "Meta+ArrowUp": "first", // macOs
  PageDown: "pageDown",
  PageUp: "pageUp",
};

/** Map `KeyEvent.key` to navigation action. */
export const KEY_TO_COMMAND_MAP: { [key: string]: ApplyCommandType } = {
  " ": "toggleSelect",
  "+": "expand",
  Add: "expand",
  ArrowDown: "down",
  ArrowLeft: "left",
  ArrowRight: "right",
  ArrowUp: "up",
  Backspace: "parent",
  "/": "collapseAll",
  Divide: "collapseAll",
  End: "lastCol",
  Home: "firstCol",
  "Control+End": "last",
  "Control+Home": "first",
  "Meta+ArrowDown": "last", // macOs
  "Meta+ArrowUp": "first", // macOs
  "*": "expandAll",
  Multiply: "expandAll",
  PageDown: "pageDown",
  PageUp: "pageUp",
  "-": "collapse",
  Subtract: "collapse",
};

/** Return a callback that returns true if the node title matches the string
 * or regular expression.
 * @see {@link WunderbaumNode.findAll}
 */
export function makeNodeTitleMatcher(match: string | RegExp): MatcherCallback {
  if (match instanceof RegExp) {
    return function (node: WunderbaumNode) {
      return (<RegExp>match).test(node.title);
    };
  }
  util.assert(
    typeof match === "string",
    `Expected a string or RegExp: ${match}`
  );

  // s = escapeRegex(s.toLowerCase());
  return function (node: WunderbaumNode) {
    return node.title === match;
    // console.log("match " + node, node.title.toLowerCase().indexOf(match))
    // return node.title.toLowerCase().indexOf(match) >= 0;
  };
}

/** Return a callback that returns true if the node title starts with a string (case-insensitive). */
export function makeNodeTitleStartMatcher(s: string): MatcherCallback {
  s = util.escapeRegex(s);
  const reMatch = new RegExp("^" + s, "i");
  return function (node: WunderbaumNode) {
    return reMatch.test(node.title);
  };
}

/** Compare two nodes by title (case-insensitive). */
export function nodeTitleSorter(a: WunderbaumNode, b: WunderbaumNode): number {
  const x = a.title.toLowerCase();
  const y = b.title.toLowerCase();

  return x === y ? 0 : x > y ? 1 : -1;
}

/**
 * Convert 'flat' to 'nested' format.
 *
 * Flat node entry format:
 *    [PARENT_IDX, {KEY_VALUE_ARGS}]
 * or, if N _positional re defined:
 *    [PARENT_IDX, POSITIONAL_ARG_1, POSITIONAL_ARG_2, ..., POSITIONAL_ARG_N]
 * Even if _positional additional are defined, KEY_VALUE_ARGS can be appended:
 *    [PARENT_IDX, POSITIONAL_ARG_1, ..., {KEY_VALUE_ARGS}]
 *
 * 1. Parent-referencing list is converted to a list of nested dicts with
 *    optional `children` properties.
 * 2. `[POSITIONAL_ARGS]` are added as dict attributes.
 */
function unflattenSource(source: SourceObjectType): void {
  const { _format, _keyMap = {}, _positional = [], children } = source;
  const _positionalCount = _positional.length;

  if (_format !== "flat") {
    throw new Error(`Expected source._format: "flat", but got ${_format}`);
  }
  if (_positionalCount && _positional.includes("children")) {
    throw new Error(
      `source._positional must not include "children": ${_positional}`
    );
  }
  let longToShort = _keyMap;
  if (_keyMap.t) {
    // Inverse keyMap was used (pre 0.7.0)
    // TODO: raise Error on final 1.x release
    const msg = `source._keyMap maps from long to short since v0.7.0. Flip key/value!`;
    console.warn(msg); // eslint-disable-line no-console
    longToShort = {};
    for (const [key, value] of Object.entries(_keyMap)) {
      longToShort[value] = key;
    }
  }
  const positionalShort = _positional.map((e: string) => longToShort[e] ?? e);
  const newChildren: SourceListType = [];
  const keyToNodeMap: { [key: string]: number } = {};
  const indexToNodeMap: { [key: number]: any } = {};
  const keyAttrName = longToShort["key"] ?? "key";
  const childrenAttrName = longToShort["children"] ?? "children";

  for (const [index, nodeTuple] of children.entries()) {
    // Node entry format:
    //   [PARENT_ID, [POSITIONAL_ARGS]]
    // or
    //   [PARENT_ID, POSITIONAL_ARG_1, POSITIONAL_ARG_2, ..., {KEY_VALUE_ARGS}]
    let kwargs;
    const [parentId, ...args] = <any>nodeTuple;
    if (args.length === _positionalCount) {
      kwargs = {};
    } else if (args.length === _positionalCount + 1) {
      kwargs = args.pop();
      if (typeof kwargs !== "object") {
        throw new Error(
          `unflattenSource: Expected dict as last tuple element: ${nodeTuple}`
        );
      }
    } else {
      throw new Error(`unflattenSource: unexpected tuple length: ${nodeTuple}`);
    }

    // Free up some memory as we go
    nodeTuple[1] = null;
    if (nodeTuple[2] != null) {
      nodeTuple[2] = null;
    }
    // We keep `kwargs` as our new node definition. Then we add all positional
    // values to this object:
    args.forEach((val: string, positionalIdx: number) => {
      kwargs[positionalShort[positionalIdx]] = val;
    });
    args.length = 0;

    // Find the parent node. `null` means 'toplevel'. PARENT_ID may be the numeric
    // index of the source.children list. If PARENT_ID is a string, we search
    // a parent with node.key of this value.
    indexToNodeMap[index] = kwargs;
    const key = kwargs[keyAttrName];
    if (key != null) {
      keyToNodeMap[key] = kwargs;
    }
    let parentNode = null;
    if (parentId === null) {
      // top-level node
    } else if (typeof parentId === "number") {
      parentNode = indexToNodeMap[parentId];
      if (parentNode === undefined) {
        throw new Error(
          `unflattenSource: Could not find parent node by index: ${parentId}.`
        );
      }
    } else {
      parentNode = keyToNodeMap[parentId];
      if (parentNode === undefined) {
        throw new Error(
          `unflattenSource: Could not find parent node by key: ${parentId}`
        );
      }
    }
    if (parentNode) {
      parentNode[childrenAttrName] ??= [];
      parentNode[childrenAttrName].push(kwargs);
    } else {
      newChildren.push(kwargs);
    }
  }
  source.children = newChildren;
}

/**
 * Decompresses the source data by
 * - converting from 'flat' to 'nested' format
 * - expanding short alias names to long names (if defined in _keyMap)
 * - resolving value indexes to value strings (if defined in _valueMap)
 *
 * @param source - The source object to be decompressed.
 * @returns void
 */
export function decompressSourceData(source: SourceObjectType): void {
  let { _format, _version = 1, _keyMap, _valueMap } = source;

  util.assert(_version === 1, `Expected file version 1 instead of ${_version}`);

  let longToShort = _keyMap;
  let shortToLong: { [key: string]: string } = {};

  if (longToShort) {
    for (const [key, value] of Object.entries(longToShort)) {
      shortToLong[value] = key;
    }
  }

  // Fallback for old format (pre 0.7.0, using _keyMap in reverse direction)
  // TODO: raise Error on final 1.x release
  if (longToShort && longToShort.t) {
    const msg = `source._keyMap maps from long to short since v0.7.0. Flip key/value!`;
    console.warn(msg); // eslint-disable-line no-console
    [longToShort, shortToLong] = [shortToLong, longToShort];
  }

  // Fallback for old format (pre 0.7.0, using _typeList instead of _valueMap)
  // TODO: raise Error on final 1.x release
  if ((<any>source)._typeList != null) {
    const msg = `source._typeList is deprecated since v0.7.0: use source._valueMap: {"type": [...]} instead.`;
    if (_valueMap != null) {
      throw new Error(msg);
    } else {
      console.warn(msg); // eslint-disable-line no-console
      _valueMap = { type: (<any>source)._typeList };
      delete (<any>source)._typeList;
    }
  }

  if (_format === "flat") {
    unflattenSource(source);
  }
  delete source._format;
  delete source._version;
  delete source._keyMap;
  delete source._valueMap;
  delete source._positional;

  function _iter(childList: SourceListType) {
    for (const node of childList) {
      // Iterate over a list of names, because we modify inside the loop
      // (for ... of ... does not allow this)
      Object.getOwnPropertyNames(node).forEach((propName) => {
        const value: any = node[propName];

        // Replace short names with long names if defined in _keyMap
        let longName = propName;
        if (_keyMap && shortToLong[propName] != null) {
          longName = shortToLong[propName];
          if (longName !== propName) {
            node[longName] = value;
            delete node[propName];
          }
        }
        // Replace type index with type name if defined in _valueMap
        if (
          _valueMap &&
          typeof value === "number" &&
          _valueMap[longName] != null
        ) {
          const newValue = _valueMap[longName][value];
          if (newValue == null) {
            throw new Error(
              `Expected valueMap[${longName}][${value}] entry in [${_valueMap[longName]}]`
            );
          }
          node[longName] = newValue;
        }
      });

      // Recursion
      if (node.children) {
        _iter(node.children);
      }
    }
  }
  if (_keyMap || _valueMap) {
    _iter(source.children);
  }
}
