import ts from "typescript";

import { IdentifierFactory } from "../factories/IdentifierFactory";
import { StatementFactory } from "../factories/StatementFactory";
import { TypeFactory } from "../factories/TypeFactory";

import { IProject } from "../transformers/IProject";

import { CheckerProgrammer } from "./CheckerProgrammer";
import { FeatureProgrammer } from "./FeatureProgrammer";
import { IsProgrammer } from "./IsProgrammer";
import { FunctionImporter } from "./helpers/FunctionImporter";
import { OptionPredicator } from "./helpers/OptionPredicator";
import { check_object } from "./internal/check_object";

export namespace AssertProgrammer {
  export const decompose = (props: {
    project: IProject;
    equals: boolean;
    guard: boolean;
    importer: FunctionImporter;
    type: ts.Type;
    name: string | undefined;
    init: ts.Expression | undefined;
  }): FeatureProgrammer.IDecomposed => {
    const is: FeatureProgrammer.IDecomposed = IsProgrammer.decompose(props);
    const composed: FeatureProgrammer.IComposed = CheckerProgrammer.compose({
      ...props,
      config: {
        prefix: "$a",
        path: true,
        trace: true,
        numeric: OptionPredicator.numeric(props.project.options),
        equals: props.equals,
        atomist: (explore) => (entry) => (input) =>
          [
            ...(entry.expression ? [entry.expression] : []),
            ...(entry.conditions.length === 0
              ? []
              : entry.conditions.length === 1
                ? entry.conditions[0]!.map((cond) =>
                    ts.factory.createLogicalOr(
                      cond.expression,
                      create_guard_call(props.importer)(
                        explore.from === "top"
                          ? ts.factory.createTrue()
                          : ts.factory.createIdentifier("_exceptionable"),
                      )(
                        ts.factory.createIdentifier(
                          explore.postfix
                            ? `_path + ${explore.postfix}`
                            : "_path",
                        ),
                        cond.expected,
                        input,
                      ),
                    ),
                  )
                : [
                    ts.factory.createLogicalOr(
                      entry.conditions
                        .map((set) =>
                          set
                            .map((s) => s.expression)
                            .reduce((a, b) =>
                              ts.factory.createLogicalAnd(a, b),
                            ),
                        )
                        .reduce((a, b) => ts.factory.createLogicalOr(a, b)),
                      create_guard_call(props.importer)(
                        explore.from === "top"
                          ? ts.factory.createTrue()
                          : ts.factory.createIdentifier("_exceptionable"),
                      )(
                        ts.factory.createIdentifier(
                          explore.postfix
                            ? `_path + ${explore.postfix}`
                            : "_path",
                        ),
                        entry.expected,
                        input,
                      ),
                    ),
                  ]),
          ].reduce((x, y) => ts.factory.createLogicalAnd(x, y)),
        combiner: combiner(props.equals)(props.project)(props.importer),
        joiner: joiner(props.equals)(props.project)(props.importer),
        success: ts.factory.createTrue(),
      },
    });
    const arrow: ts.ArrowFunction = ts.factory.createArrowFunction(
      undefined,
      undefined,
      [
        IdentifierFactory.parameter("input", TypeFactory.keyword("any")),
        Guardian.parameter(props.init),
      ],
      props.guard
        ? ts.factory.createTypePredicateNode(
            ts.factory.createToken(ts.SyntaxKind.AssertsKeyword),
            ts.factory.createIdentifier("input"),
            ts.factory.createTypeReferenceNode(
              props.name ??
                TypeFactory.getFullName(props.project.checker)(props.type),
            ),
          )
        : ts.factory.createTypeReferenceNode(
            props.name ??
              TypeFactory.getFullName(props.project.checker)(props.type),
          ),
      undefined,
      ts.factory.createBlock(
        [
          ts.factory.createIfStatement(
            ts.factory.createStrictEquality(
              ts.factory.createFalse(),
              ts.factory.createCallExpression(
                ts.factory.createIdentifier("__is"),
                undefined,
                [ts.factory.createIdentifier("input")],
              ),
            ),
            ts.factory.createBlock(
              [
                ts.factory.createExpressionStatement(
                  ts.factory.createBinaryExpression(
                    ts.factory.createIdentifier("_errorFactory"),
                    ts.factory.createToken(ts.SyntaxKind.EqualsToken),
                    ts.factory.createIdentifier("errorFactory"),
                  ),
                ),
                ts.factory.createExpressionStatement(
                  ts.factory.createCallExpression(
                    ts.factory.createArrowFunction(
                      undefined,
                      undefined,
                      composed.parameters,
                      undefined,
                      undefined,
                      composed.body,
                    ),
                    undefined,
                    [
                      ts.factory.createIdentifier("input"),
                      ts.factory.createStringLiteral("$input"),
                      ts.factory.createTrue(),
                    ],
                  ),
                ),
              ],
              true,
            ),
            undefined,
          ),
          ...(props.guard === false
            ? [
                ts.factory.createReturnStatement(
                  ts.factory.createIdentifier(`input`),
                ),
              ]
            : []),
        ],
        true,
      ),
    );

    return {
      functions: {
        ...is.functions,
        ...composed.functions,
      },
      statements: [
        ...is.statements,
        ...composed.statements,
        StatementFactory.constant("__is", is.arrow),
        StatementFactory.mut("_errorFactory"),
      ],
      arrow,
    };
  };

  export const write =
    (project: IProject) =>
    (modulo: ts.LeftHandSideExpression) =>
    (props: boolean | { equals: boolean; guard: boolean }) =>
    (type: ts.Type, name?: string, init?: ts.Expression): ts.CallExpression => {
      if (typeof props === "boolean") props = { equals: props, guard: false };
      const importer: FunctionImporter = new FunctionImporter(modulo.getText());
      const result: FeatureProgrammer.IDecomposed = decompose({
        ...props,
        project,
        importer,
        type,
        name,
        init,
      });
      return FeatureProgrammer.writeDecomposed({
        modulo,
        importer,
        result,
      });
    };

  const combiner =
    (equals: boolean) =>
    (project: IProject) =>
    (importer: FunctionImporter): CheckerProgrammer.IConfig.Combiner =>
    (explore: CheckerProgrammer.IExplore) => {
      if (explore.tracable === false)
        return IsProgrammer.configure({
          object: assert_object(equals)(project)(importer),
          numeric: true,
        })(project)(importer).combiner(explore);

      const path: string = explore.postfix
        ? `_path + ${explore.postfix}`
        : "_path";
      return (logic) => (input, binaries, expected) =>
        logic === "and"
          ? binaries
              .map((binary) =>
                binary.combined
                  ? binary.expression
                  : ts.factory.createLogicalOr(
                      binary.expression,
                      create_guard_call(importer)(
                        explore.source === "top"
                          ? ts.factory.createTrue()
                          : ts.factory.createIdentifier("_exceptionable"),
                      )(ts.factory.createIdentifier(path), expected, input),
                    ),
              )
              .reduce(ts.factory.createLogicalAnd)
          : ts.factory.createLogicalOr(
              binaries
                .map((binary) => binary.expression)
                .reduce(ts.factory.createLogicalOr),
              create_guard_call(importer)(
                explore.source === "top"
                  ? ts.factory.createTrue()
                  : ts.factory.createIdentifier("_exceptionable"),
              )(ts.factory.createIdentifier(path), expected, input),
            );
      // : (() => {
      //       const addicted = binaries.slice();
      //       if (
      //           addicted[addicted.length - 1]!.combined === false
      //       ) {
      //           addicted.push({
      //               combined: true,
      //               expression: create_guard_call(importer)(
      //                   explore.source === "top"
      //                       ? ts.factory.createTrue()
      //                       : ts.factory.createIdentifier(
      //                             "_exceptionable",
      //                         ),
      //               )(
      //                   ts.factory.createIdentifier(path),
      //                   expected,
      //                   input,
      //               ),
      //           });
      //       }
      //       return addicted
      //           .map((b) => b.expression)
      //           .reduce(ts.factory.createLogicalOr);
      //   })();
    };

  const assert_object =
    (equals: boolean) => (project: IProject) => (importer: FunctionImporter) =>
      check_object({
        equals,
        assert: true,
        undefined: true,
        reduce: ts.factory.createLogicalAnd,
        positive: ts.factory.createTrue(),
        superfluous: (value) =>
          create_guard_call(importer)()(
            ts.factory.createAdd(
              ts.factory.createIdentifier("_path"),
              ts.factory.createCallExpression(importer.use("join"), undefined, [
                ts.factory.createIdentifier("key"),
              ]),
            ),
            "undefined",
            value,
          ),
        halt: (expr) =>
          ts.factory.createLogicalOr(
            ts.factory.createStrictEquality(
              ts.factory.createFalse(),
              ts.factory.createIdentifier("_exceptionable"),
            ),
            expr,
          ),
      })(project)(importer);

  const joiner =
    (equals: boolean) =>
    (project: IProject) =>
    (importer: FunctionImporter): CheckerProgrammer.IConfig.IJoiner => ({
      object: assert_object(equals)(project)(importer),
      array: (input, arrow) =>
        ts.factory.createCallExpression(
          IdentifierFactory.access(input)("every"),
          undefined,
          [arrow],
        ),
      failure: (value, expected, explore) =>
        create_guard_call(importer)(
          explore?.from === "top"
            ? ts.factory.createTrue()
            : ts.factory.createIdentifier("_exceptionable"),
        )(
          ts.factory.createIdentifier(
            explore?.postfix ? `_path + ${explore.postfix}` : "_path",
          ),
          expected,
          value,
        ),
      full: equals
        ? undefined
        : (condition) => (input, expected, explore) =>
            ts.factory.createLogicalOr(
              condition,
              create_guard_call(importer)(
                explore.from === "top"
                  ? ts.factory.createTrue()
                  : ts.factory.createIdentifier("_exceptionable"),
              )(ts.factory.createIdentifier("_path"), expected, input),
            ),
    });

  const create_guard_call =
    (importer: FunctionImporter) =>
    (exceptionable?: ts.Expression) =>
    (
      path: ts.Expression,
      expected: string,
      value: ts.Expression,
    ): ts.Expression =>
      ts.factory.createCallExpression(importer.use("guard"), undefined, [
        exceptionable ?? ts.factory.createIdentifier("_exceptionable"),
        ts.factory.createObjectLiteralExpression(
          [
            ts.factory.createPropertyAssignment("path", path),
            ts.factory.createPropertyAssignment(
              "expected",
              ts.factory.createStringLiteral(expected),
            ),
            ts.factory.createPropertyAssignment("value", value),
          ],
          true,
        ),
        ts.factory.createIdentifier("_errorFactory"),
      ]);

  export namespace Guardian {
    export const identifier = () => ts.factory.createIdentifier("errorFactory");
    export const parameter = (init: ts.Expression | undefined) =>
      IdentifierFactory.parameter(
        "errorFactory",
        type(),
        init ?? ts.factory.createToken(ts.SyntaxKind.QuestionToken),
      );
    export const type = () =>
      ts.factory.createFunctionTypeNode(
        undefined,
        [
          ts.factory.createParameterDeclaration(
            undefined,
            undefined,
            ts.factory.createIdentifier("p"),
            undefined,
            ts.factory.createImportTypeNode(
              ts.factory.createLiteralTypeNode(
                ts.factory.createStringLiteral("typia"),
              ),
              undefined,
              ts.factory.createQualifiedName(
                ts.factory.createIdentifier("TypeGuardError"),
                ts.factory.createIdentifier("IProps"),
              ),
              undefined,
              false,
            ),
            undefined,
          ),
        ],
        ts.factory.createTypeReferenceNode(
          ts.factory.createIdentifier("Error"),
          undefined,
        ),
      );
  }
}
