import {
  CursorSelection,
  CursorTransformer,
  CursorTransformerMeta,
} from "../types/cursor";

export class UCursor<Type extends Record<string, any>> {
  _children: {
    key: keyof Type;
    each: boolean;
    cursor: UCursor<Type[keyof Type] | Type[keyof Type][number]>;
  }[] = [];
  _identationSize: number | null = null;
  _identationChar: string | null = null;
  _block: [string, string] | null = null;
  _prefix: string = "";
  _suffix: string = "";
  _move: number = 0;
  _selector: RegExp | null = null;
  _parent: UCursor<{ [key: string]: Type }> | null = null;
  _transformers: {
    key: keyof Type;
    each: boolean;
    transformer:
      | CursorTransformer<Type, any>
      | CursorTransformer<Type[keyof Type], number>;
  }[] = [];
  _template: string = "";
  _clearSelections = false;
  _eachJoin: string = "";

  constructor() {}

  $children() {
    return [...this._children];
  }

  $parent() {
    return this._parent;
  }

  $blocks(root: boolean = false) {
    let parent = this.$parent();
    let blocks = 0;

    if (!root && this._block) blocks += 1;
    if (parent) blocks += parent.$blocks();
    blocks += this._move;
    if (blocks < 0) blocks = 0;
    return blocks;
  }

  $identationChar(): string | null {
    let parent = this.$parent();
    if (parent && this._identationChar === null)
      return parent.$identationChar();
    return this._identationChar;
  }

  $identationSize(): number | null {
    let parent = this.$parent();
    if (parent && this._identationSize === null)
      return parent.$identationSize();
    return this._identationSize;
  }

  $identation() {
    let identationSize = this.$identationSize();
    let identationChar = this.$identationChar();

    return {
      root: "".padStart(
        this.$blocks(true) * (identationSize ?? 0),
        identationChar ?? ""
      ),
      children: "".padStart(
        this.$blocks(false) * (identationSize ?? 0),
        identationChar ?? ""
      ),
    };
  }

  $seletion(input: string): CursorSelection[] {
    if (!this._selector)
      return [
        {
          params: [],
          content: "",
          pos: input.length,
        },
      ];

    return Array.from(input.matchAll(this._selector)).map((match) => ({
      pos: match.index,
      content: match[0],
      params: match.slice(1),
    }));
  }

  move(amount: number) {
    this._move = amount;
    return this;
  }

  block(start: string, end: string) {
    this._block = [start, end];
    return this;
  }

  template(template: string) {
    this._template = template;
    return this;
  }

  clean(clearSelections: boolean = true) {
    this._clearSelections = clearSelections;
    return this;
  }

  prefix(prefix: string) {
    this._prefix = prefix;
    return this;
  }

  suffix(suffix: string) {
    this._suffix = suffix;
    return this;
  }

  join(str: string) {
    this._eachJoin = str;
    return this;
  }

  select(selector: RegExp) {
    this._selector = selector;
    return this;
  }

  noIdent() {
    this._identationChar = "";
    this._identationSize = 0;
    return this;
  }

  ident({ size: identation, char }: { size?: number; char?: string }) {
    if (identation) this._identationSize = identation;
    if (char) this._identationChar = char;
    return this;
  }

  parent(parent: UCursor<{ [key: string]: Type }>) {
    this._parent = parent;
    return this;
  }

  empty(write: () => string) {
    const key = "_cursorWithEmptyData";
    const nestedCursor = new UCursor<any>();
    nestedCursor.parent(this as any);
    this._children.push({ key, cursor: nestedCursor, each: false });
    nestedCursor._transformers.push({
      key: "_cursorWithEmptyData",
      transformer: write,
      each: false,
    });
    return this;
  }

  linebreak(amount: number = 1) {
    return this.empty(() => "".padStart(amount, "\n"));
  }

  expand(expanded: (cursor: UCursor<Type>) => void) {
    const expandedCursor = new UCursor<Type>();
    expandedCursor.parent(this as any);
    this._children.push({
      key: "_expandedCursor",
      cursor: expandedCursor as any,
      each: false,
    });
    expanded(expandedCursor);
    return this;
  }

  in<Key extends keyof Type>(
    key: Key,
    nested: (cursor: UCursor<Required<Type>[Key]>) => void
  ) {
    const nestedCursor = new UCursor<Type[Key]>();
    nestedCursor.parent(this as any);
    this._children.push({ key, cursor: nestedCursor as any, each: false });
    nested(nestedCursor);
    return this;
  }

  each<Key extends keyof Type>(
    key: Key,
    nested: (cursor: UCursor<Required<Type>[Key][number]>) => void
  ) {
    const nestedCursor = new UCursor<Type[Key][number]>();
    nestedCursor.parent(this as any);
    this._children.push({ key, cursor: nestedCursor as any, each: true });
    nested(nestedCursor);
    return this;
  }

  writeFrom<Key extends keyof Type>(
    key: Key,
    transformer: CursorTransformer<Type, Key>
  ) {
    this._transformers.push({ key, transformer, each: false });
    return this;
  }

  writeFromEach<Key extends keyof Type>(
    key: Key,
    transformer: CursorTransformer<Type[Key], number>
  ) {
    this._transformers.push({
      key,
      transformer: transformer as any,
      each: true,
    });
    return this;
  }

  write(transformer: CursorTransformer<{ root: Type }, "root">) {
    this._transformers.push({
      key: "_cursorRoot",
      transformer: transformer as any,
      each: false,
    });
    return this;
  }

  render(data: Type, input?: string, meta?: { noFix: boolean }) {
    input = input ?? this._template;
    let output = input;

    const selections = this.$seletion(input);
    const identation = this.$identation();

    let transformers: {
      key: keyof Type;
      each: boolean;
      transformer: CursorTransformer<any, any>;
    }[] = [];

    const lastLineIsEmpty = (str: string) =>
      str.split("\n").slice(-1)[0].length == 0;

    const applyIdentation = (str: string, identation: string) =>
      str
        .split("\n")
        .map((l) => (!!l ? identation + l : l))
        .join("\n");

    transformers.push(...this._transformers);

    for (const { key, each, transformer } of transformers) {
      if (!transformer) continue;

      const transformSelections = (
        transformerData: any,
        transformerMeta: CursorTransformerMeta
      ) => {
        for (const selection of selections) {
          if (selection.ignore) continue;

          let transformed =
            (lastLineIsEmpty(output) ? identation.root : "") +
            transformer(transformerData as any, selection, transformerMeta);

          const selectionInTransform = this._selector
            ? Array.from(transformed.matchAll(this._selector))[0] ?? null
            : null;
          const extraLength = selectionInTransform
            ? transformed.length - selection.content.length
            : transformed.length;

          output =
            output.slice(0, selection.pos) +
            transformed +
            output.slice(selection.pos + selection.content.length);

          selections
            .filter((s) => s != selection)
            .forEach((s) => {
              if (s.pos > selection.pos) s.pos += extraLength;
            });

          if (selection.content && !selectionInTransform)
            selection.ignore = true;
          else {
            const addToPos = selectionInTransform
              ? selectionInTransform.index ?? 0
              : transformed.length;
            selection.pos += addToPos;
          }
        }
      };

      const root = key === "_cursorRoot";
      const ignoreEmptyData = key === "_cursorWithEmptyData";

      const dataSrc: any = root ? data : (data || [])[key];
      if ((data === undefined || dataSrc === undefined) && !ignoreEmptyData)
        continue;
      if (each) {
        const total = Object.keys(dataSrc || []).length;
        for (const i in dataSrc) {
          const index = parseInt(i + "");
          transformSelections(dataSrc[i], {
            index,
            total,
            isLast: index == total - 1,
          });
        }
      } else transformSelections(dataSrc, {});
    }

    if (this._block)
      output +=
        (lastLineIsEmpty(output) ? identation.root : "") +
        (this._block[0] + "\n");

    for (const { key, each, cursor } of this._children) {
      const cursorIdentation = cursor.$identation();
      let cursorOutput = applyIdentation(cursor._prefix, cursorIdentation.root);
      if (each) {
        const total = Object.keys(data[key] || []).length;
        for (const i in data[key]) {
          const index = parseInt(i + "");
          cursorOutput = cursor.render(data[key][i], cursorOutput, {
            noFix: true,
          });
          if (total > 1 && index < total - 1) cursorOutput += cursor._eachJoin;
        }
      } else {
        cursorOutput = cursor.render(
          key == "_expandedCursor" ? data : data[key],
          cursorOutput,
          { noFix: true }
        );
      }
      if (cursor._suffix) {
        const identedSuffix = applyIdentation(
          cursor._suffix,
          cursorIdentation.root
        );
        cursorOutput += identedSuffix.slice(
          identedSuffix[0] == "\n" || lastLineIsEmpty(cursorOutput)
            ? 0
            : cursorIdentation.root.length
        );
      }

      output += cursorOutput;
    }

    if (this._block) output += "\n" + identation.root + this._block[1];

    if (!meta?.noFix) output = this._prefix + output + this._suffix;

    if (!this.$parent()) {
      output = this._clean(output);
    }

    return output;
  }

  _clean(output: string) {
    if (this._clearSelections && this._selector)
      output = output.replace(this._selector, "");
    this._children.forEach((child) => {
      output = child.cursor._clean(output);
    });
    return output;
  }
}
