import * as Case from "case";
import { UModel } from "../entities/model";
import { URenderer } from "../entities/renderer";
import { $attr } from "../shortcuts/queries";
import { RenderContent, RenderPath, RenderSelection } from "../types/renderer";
import {
  _array,
  _enum,
  _ref,
  _required,
  _rootModule,
} from "../shortcuts/attributes";
import { UAttribute } from "../entities/attribute";
import { UFeature } from "../entities/feature";

const KEYS = {
  draft: "draft",
};

export default class TSDraftRenderer extends URenderer {
  private _draftPath = "src/core/draft.ts";

  constructor(options?: { draftPath?: string }) {
    super("ts@draft");
    if (options?.draftPath) this._draftPath = options.draftPath;
  }

  async select(): Promise<RenderSelection> {
    const models = this.$models();
    const features = this.$features();
    const modules = this.$modules();
    const paths: RenderPath[] = [
      {
        key: KEYS.draft,
        path: this._draftPath,
      },
    ];

    return {
      paths,
      modules,
      features: features,
      models: models,
    };
  }

  async render(): Promise<RenderContent[]> {
    const output: RenderContent[] = [];
    const models = this.$selection().models || [];
    const features = this.$selection().features || [];
    const modules = this.$selection().modules || [];

    // type UAttribute = { name: string; value?: any };
    let content =
      `export type UAttribute = { name: string; value?: any };\n` +
      `export type UField = {name: string; type: string; attributes: UAttribute[], ref?: MODELS; isArray: boolean; };\n` +
      `export type UModel = {name: string; module: MODULES; attributes: UAttribute[]; fields?: UField[]; enum?: Record<string, any> };\n` +
      `export type UFeature = {name: string; module: MODULES; attributes: UAttribute[]; input?: UModel; output?: UModel; };\n` +
      `export type UModule = {name: string; attributes: UAttribute[]; features: Record<string, UFeature>; models: Record<string, UModel> };\n`;

    content += `export enum MODULES {\n${modules
      .map((mod) => `  ${Case.pascal(mod.$name())} = "${mod.$name()}",`)
      .join("\n")}\n};\n\nexport enum FEATURES {\n${features
      .map(
        (feature) =>
          `  ${
            Case.pascal($attr(feature, _rootModule())?.$name() ?? "") +
            Case.pascal(feature.$name())
          } = "${
            $attr(feature, _rootModule())?.$name() + "." + feature.$name()
          }",`
      )
      .join("\n")}\n};\n\nexport enum MODELS {\n${models
      .map((model) => `  ${Case.pascal(model.$name())} = "${model.$name()}",`)
      .join("\n")}\n};\n\n`;

    const attrToStr = (attributes: UAttribute<any>[], tab = "  ") => {
      if (attributes.length == 0) return `[]`;
      return (
        "[\n" +
        attributes
          .filter(
            (a) => !["enum", "rootModule", "ref", "array"].includes(a.$name())
          )
          .map((a) => {
            let value = "";
            if (a.$value() !== null) {
              if (a.$name() == "regex") {
                value = a.$value().toString();
              } else
                value = JSON.stringify(a.$value(), null, 2)
                  .split("\n")
                  .map((line, i) => (i > 0 ? tab + "  " : "") + line)
                  .join("\n");
            }
            return value
              ? `${tab}{\n${tab}  name: "${a.$name()}"${
                  ",\n" + tab + "  value: " + value
                }\n${tab}},`
              : `${tab}{ name: "${a.$name()}" },`;
          })
          .join("\n") +
        `\n${tab.slice(2)}]`
      );
    };

    const modelToStr = (model: UModel, tab = "  ") => {
      let fields = "";
      let enumValues = "";
      let enumAttr = $attr(model, _enum());
      let rootModule = $attr(model, _rootModule());
      if (!enumAttr) {
        fields = `fields: [\n${model
          .$fields()
          .map((field) => {
            let ref = "";
            const refAttr = $attr(field, _ref());
            if (refAttr) {
              ref = `MODELS.${Case.pascal(refAttr.$name())}`;
            }
            return `${tab}    {\n${tab}      name: "${field.$name()}",\n${tab}      isArray: ${$attr(
              field,
              _array()
            )},\n${tab}      type: "${field.$type()}",\n${
              ref ? `${tab}      ref: ${ref},\n` : ""
            }${tab}      attributes: ${attrToStr(
              field.$attributes(),
              tab + "        "
            )},\n${tab}    },`;
          })
          .join("\n")}\n${tab}  ]`;
      } else {
        enumValues = `enum: {\n${Object.keys(enumAttr)
          .map(
            (k) => `${tab}    "${k}": ${JSON.stringify((enumAttr as any)[k])},`
          )
          .join("\n")}\n${tab}  }`;
      }
      return `{\n${tab}  name: "${model.$name()}",\n${
        !!rootModule
          ? `${tab}  module: MODULES.${Case.pascal(rootModule.$name())},`
          : ""
      }\n${tab}  attributes: ${attrToStr(
        model.$attributes(),
        tab + "    "
      )},\n${tab}  ${fields || enumValues},\n${tab}}`;
    };

    const featureToStr = (feature: UFeature, tab = "  ") => {
      let input = "";
      let output = "";
      const modelInput = feature.$input();
      const modelOutput = feature.$output();
      const rootModule = $attr(feature, _rootModule());

      if (modelInput) {
        input = `\n${tab}  input: uModels["${modelInput.$name()}"],`;
      }
      if (modelOutput) {
        output = `\n${tab}  output: uModels["${modelOutput.$name()}"],`;
      }
      return `{\n${tab}  name: "${feature.$name()}",\n${
        !!rootModule
          ? `${tab}  module: MODULES.${Case.pascal(rootModule.$name())},`
          : ""
      }\n${tab}  attributes: ${attrToStr(
        feature.$attributes(),
        tab + "    "
      )},${input}${output}\n  }`;
    };

    content += `export const uModel = (name: MODELS | string) =>\n  uModels[name as keyof typeof uModels] as any as UModel;\n`;
    content += `export const uFeature = (name: FEATURES | string) =>\n  uFeatures[name as keyof typeof uFeatures] as any as UFeature;\n`;
    content += `export const uModule = (name: MODULES | string) =>\n  uModules[name as keyof typeof uModules] as any as UModule;\n`;
    content += `export const uAttribute = <Type>(
  src: { attributes: UAttribute[] },
  name: string,
  defaultValue?: Type,
  defaultWhenNotPresent?: Type
): Type | null | undefined => {
  const attr = src.attributes.find((x) => x.name === name);
  if (attr) {
    if (attr.value !== undefined) return attr.value as Type;
    else return defaultValue ?? null;
  }
  return defaultWhenNotPresent ?? undefined;
};\n\n`;

    content += `const uModels = {\n${models
      .map((model) => `  "${model.$name()}": ` + modelToStr(model, "  ") + ",")
      .join("\n")}\n};\n\n`;

    content += `const uFeatures = {\n${features
      .map(
        (feature) =>
          `  "${
            $attr(feature, _rootModule())?.$name() + "." + feature.$name()
          }": ` +
          featureToStr(feature, "  ") +
          ","
      )
      .join("\n")}\n};\n\n`;

    content += `const uModules = {\n${modules
      .map(
        (mod) =>
          `  "${mod.$name()}": {\n    name: "${mod.$name()}",\n    attributes: ${attrToStr(
            mod.$attributes(),
            "      "
          )},\n    features: {\n${mod
            .$features()
            .map(
              (feature) =>
                `      "${feature.$name()}": uFeatures["${
                  $attr(feature, _rootModule())?.$name() + "." + feature.$name()
                }"],`
            )
            .join("\n")}\n    },\n    models: {\n${models
            .filter((model) => $attr(model, _rootModule()) == mod)
            .map(
              (model) =>
                `      ["${model.$name()}"]: uModels["${model.$name()}"],`
            )
            .join("\n")}\n    },\n  },`
      )
      .join("\n")}\n};\n\n`;

    content += `export const uDraft = {\n  attributes: ${attrToStr(
      this.$draft().$attributes(),
      "    "
    )},\n  modules: uModules,\n  features: uFeatures,\n  models: uModels,\n};\n\n`;

    output.push({
      key: KEYS.draft,
      content,
    });

    return output;
  }
}

export type MODULES = {
  t: "80";
};
