import ts from "typescript";

import { ExpressionFactory } from "../factories/ExpressionFactory";
import { IdentifierFactory } from "../factories/IdentifierFactory";
import { MetadataCollection } from "../factories/MetadataCollection";
import { MetadataFactory } from "../factories/MetadataFactory";
import { StatementFactory } from "../factories/StatementFactory";
import { TypeFactory } from "../factories/TypeFactory";
import { ValueFactory } from "../factories/ValueFactory";

import { Metadata } from "../schemas/metadata/Metadata";
import { MetadataArray } from "../schemas/metadata/MetadataArray";
import { MetadataConstant } from "../schemas/metadata/MetadataConstant";
import { MetadataObject } from "../schemas/metadata/MetadataObject";
import { MetadataTuple } from "../schemas/metadata/MetadataTuple";
import { MetadataTupleType } from "../schemas/metadata/MetadataTupleType";

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

import { FeatureProgrammer } from "./FeatureProgrammer";
import { IsProgrammer } from "./IsProgrammer";
import { AtomicPredicator } from "./helpers/AtomicPredicator";
import { FunctionImporter } from "./helpers/FunctionImporter";
import { ICheckEntry } from "./helpers/ICheckEntry";
import { IExpressionEntry } from "./helpers/IExpressionEntry";
import { OptionPredicator } from "./helpers/OptionPredicator";
import { UnionExplorer } from "./helpers/UnionExplorer";
import { check_array_length } from "./internal/check_array_length";
import { check_bigint } from "./internal/check_bigint";
import { check_native } from "./internal/check_native";
import { check_number } from "./internal/check_number";
import { check_string } from "./internal/check_string";
import { check_template } from "./internal/check_template";
import { decode_union_object } from "./internal/decode_union_object";
import { postfix_of_tuple } from "./internal/postfix_of_tuple";
import { wrap_metadata_rest_tuple } from "./internal/wrap_metadata_rest_tuple";

export namespace CheckerProgrammer {
  export interface IConfig {
    prefix: string;
    path: boolean;
    trace: boolean;
    equals: boolean;
    numeric: boolean;
    addition?: () => ts.Statement[];
    decoder?: () => FeatureProgrammer.Decoder<Metadata, ts.Expression>;
    combiner: IConfig.Combiner;
    atomist: (
      explore: IExplore,
    ) => (check: ICheckEntry) => (input: ts.Expression) => ts.Expression;
    joiner: IConfig.IJoiner;
    success: ts.Expression;
  }
  export namespace IConfig {
    export interface Combiner {
      (explorer: IExplore): {
        (logic: "and" | "or"): {
          (
            input: ts.Expression,
            binaries: IBinary[],
            expected: string,
          ): ts.Expression;
        };
      };
    }
    export interface IJoiner {
      object(input: ts.Expression, entries: IExpressionEntry[]): ts.Expression;
      array(input: ts.Expression, arrow: ts.ArrowFunction): ts.Expression;
      tuple?: undefined | ((exprs: ts.Expression[]) => ts.Expression);

      failure(
        value: ts.Expression,
        expected: string,
        explore?: undefined | FeatureProgrammer.IExplore,
      ): ts.Expression;
      is?(expression: ts.Expression): ts.Expression;
      required?(exp: ts.Expression): ts.Expression;
      full?:
        | undefined
        | ((
            condition: ts.Expression,
          ) => (
            input: ts.Expression,
            expected: string,
            explore: IExplore,
          ) => ts.Expression);
    }
  }
  export type IExplore = FeatureProgrammer.IExplore;

  export interface IBinary {
    expression: ts.Expression;
    combined: boolean;
  }

  /* -----------------------------------------------------------
        WRITERS
    ----------------------------------------------------------- */
  export const compose = (props: {
    project: IProject;
    config: IConfig;
    importer: FunctionImporter;
    type: ts.Type;
    name: string | undefined;
  }) =>
    FeatureProgrammer.compose({
      ...props,
      config: configure(props.project)(props.config)(props.importer),
    });

  export const write =
    (project: IProject) => (config: IConfig) => (importer: FunctionImporter) =>
      FeatureProgrammer.write(project)(configure(project)(config)(importer))(
        importer,
      );

  export const write_object_functions =
    (project: IProject) => (config: IConfig) => (importer: FunctionImporter) =>
      FeatureProgrammer.write_object_functions(
        configure(project)(config)(importer),
      )(importer);

  export const write_union_functions =
    (project: IProject) => (config: IConfig) => (importer: FunctionImporter) =>
      FeatureProgrammer.write_union_functions(
        configure(project)({ ...config, numeric: false })(importer),
      );

  export const write_array_functions =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (collection: MetadataCollection): ts.VariableStatement[] =>
      collection
        .arrays()
        .filter((a) => a.recursive)
        .map((type, i) =>
          StatementFactory.constant(
            `${config.prefix}a${i}`,
            ts.factory.createArrowFunction(
              undefined,
              undefined,
              FeatureProgrammer.parameterDeclarations(config)(
                TypeFactory.keyword("any"),
              )(ts.factory.createIdentifier("input")),
              TypeFactory.keyword("any"),
              undefined,
              decode_array_inline(project)(config)(importer)(
                ts.factory.createIdentifier("input"),
                MetadataArray.create({
                  type,
                  tags: [],
                }),
                {
                  tracable: config.trace,
                  source: "function",
                  from: "array",
                  postfix: "",
                },
              ),
            ),
          ),
        );

  export const write_tuple_functions =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (collection: MetadataCollection): ts.VariableStatement[] =>
      collection
        .tuples()
        .filter((t) => t.recursive)
        .map((tuple, i) =>
          StatementFactory.constant(
            `${config.prefix}t${i}`,
            ts.factory.createArrowFunction(
              undefined,
              undefined,
              FeatureProgrammer.parameterDeclarations(config)(
                TypeFactory.keyword("any"),
              )(ts.factory.createIdentifier("input")),
              TypeFactory.keyword("any"),
              undefined,
              decode_tuple_inline(project)(config)(importer)(
                ts.factory.createIdentifier("input"),
                tuple,
                {
                  tracable: config.trace,
                  source: "function",
                  from: "array",
                  postfix: "",
                },
              ),
            ),
          ),
        );

  const configure =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter): FeatureProgrammer.IConfig => ({
      types: {
        input: () => TypeFactory.keyword("any"),
        output: (type, name) =>
          ts.factory.createTypePredicateNode(
            undefined,
            "input",
            ts.factory.createTypeReferenceNode(
              name ?? TypeFactory.getFullName(project.checker)(type),
            ),
          ),
      },
      trace: config.trace,
      path: config.path,
      prefix: config.prefix,
      initializer: (project) => (importer) => (type) => {
        const collection: MetadataCollection = new MetadataCollection();
        const result = MetadataFactory.analyze(
          project.checker,
          project.context,
        )({
          escape: false,
          constant: true,
          absorb: true,
        })(collection)(type);
        if (result.success === false)
          throw TransformerError.from(`typia.${importer.method}`)(
            result.errors,
          );
        return [collection, result.data];
      },
      addition: config.addition,
      decoder: () => config.decoder?.() ?? decode(project)(config)(importer),
      objector: {
        checker: () => config.decoder?.() ?? decode(project)(config)(importer),
        decoder: () => decode_object(config)(importer),
        joiner: config.joiner.object,
        unionizer: config.equals
          ? decode_union_object(decode_object(config)(importer))(
              (input, obj, explore) =>
                decode_object(config)(importer)(input, obj, {
                  ...explore,
                  tracable: true,
                }),
            )(config.joiner.is ?? ((expr) => expr))((value, expected) =>
              ts.factory.createReturnStatement(
                config.joiner.failure(value, expected),
              ),
            )
          : (input, targets, explore) =>
              config.combiner(explore)("or")(
                input,
                targets.map((obj) => ({
                  expression: decode_object(config)(importer)(
                    input,
                    obj,
                    explore,
                  ),
                  combined: true,
                })),
                `(${targets.map((t) => t.name).join(" | ")})`,
              ),
        failure: (value, expected) =>
          ts.factory.createReturnStatement(
            config.joiner.failure(value, expected),
          ),
        is: config.joiner.is,
        required: config.joiner.required,
        full: config.joiner.full,
        type: TypeFactory.keyword("boolean"),
      },
      generator: {
        unions: config.numeric
          ? () =>
              FeatureProgrammer.write_union_functions(
                configure(project)({ ...config, numeric: false })(importer),
              )
          : undefined,
        arrays: () => write_array_functions(project)(config)(importer),
        tuples: () => write_tuple_functions(project)(config)(importer),
      },
    });

  /* -----------------------------------------------------------
        DECODERS
    ----------------------------------------------------------- */
  /**
   * @internal
   */
  export const decode =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (
      input: ts.Expression,
      meta: Metadata,
      explore: IExplore,
    ): ts.Expression => {
      if (meta.any) return config.success;

      const top: IBinary[] = [];
      const binaries: IBinary[] = [];
      const add = create_add(binaries)(input);
      const getConstantValue = (value: number | string | bigint | boolean) => {
        if (typeof value === "string")
          return ts.factory.createStringLiteral(value);
        else if (typeof value === "bigint")
          return ExpressionFactory.bigint(value);
        return ts.factory.createIdentifier(value.toString());
      };

      //----
      // CHECK OPTIONAL
      //----
      // @todo -> should be elaborated
      const checkOptional: boolean = meta.empty() || meta.isUnionBucket();

      // NULLABLE
      if (checkOptional || meta.nullable)
        (meta.nullable ? add : create_add(top)(input))(
          meta.nullable,
          ValueFactory.NULL(),
        );

      // UNDEFINDABLE
      if (checkOptional || !meta.isRequired())
        (meta.isRequired() ? create_add(top)(input) : add)(
          !meta.isRequired(),
          ValueFactory.UNDEFINED(),
        );

      // FUNCTIONAL
      if (meta.functions.length)
        if (OptionPredicator.functional(project.options) || meta.size() !== 1)
          add(
            true,
            ts.factory.createStringLiteral("function"),
            ValueFactory.TYPEOF(input),
          );
        else
          binaries.push({
            combined: false,
            expression: config.success,
          });

      //----
      // VALUES
      //----
      // CONSTANT VALUES
      const constants: MetadataConstant[] = meta.constants.filter((c) =>
        AtomicPredicator.constant(meta)(c.type),
      );
      const constantLength: number = constants
        .map((c) => c.values.length)
        .reduce((a, b) => a + b, 0);
      if (constantLength >= 10) {
        const values: Array<boolean | number | string | bigint> = constants
          .map((c) => c.values.map((v) => v.value))
          .flat();
        add(
          true,
          ts.factory.createTrue(),
          ts.factory.createCallExpression(
            IdentifierFactory.access(
              importer.emplaceVariable(
                `${config.prefix}v${importer.increment()}`,
                ts.factory.createNewExpression(
                  ts.factory.createIdentifier("Set"),
                  undefined,
                  [
                    ts.factory.createArrayLiteralExpression(
                      values.map((v) =>
                        typeof v === "boolean"
                          ? v === true
                            ? ts.factory.createTrue()
                            : ts.factory.createFalse()
                          : typeof v === "bigint"
                            ? ExpressionFactory.bigint(v)
                            : typeof v === "number"
                              ? ExpressionFactory.number(v)
                              : ts.factory.createStringLiteral(v.toString()),
                      ),
                    ),
                  ],
                ),
              ),
            )("has"),
            undefined,
            [input],
          ),
        );
      } else
        for (const c of constants)
          if (AtomicPredicator.constant(meta)(c.type))
            for (const v of c.values) add(true, getConstantValue(v.value));

      // ATOMIC VALUES
      for (const atom of meta.atomics)
        if (AtomicPredicator.atomic(meta)(atom.type) === false) continue;
        else if (atom.type === "number")
          binaries.push({
            expression: config.atomist(explore)(
              check_number(project, config.numeric)(atom)(input),
            )(input),
            combined: false,
          });
        else if (atom.type === "bigint")
          binaries.push({
            expression: config.atomist(explore)(
              check_bigint(project)(atom)(input),
            )(input),
            combined: false,
          });
        else if (atom.type === "string")
          binaries.push({
            expression: config.atomist(explore)(
              check_string(project)(atom)(input),
            )(input),
            combined: false,
          });
        else
          add(
            true,
            ts.factory.createStringLiteral(atom.type),
            ValueFactory.TYPEOF(input),
          );

      // TEMPLATE LITERAL VALUES
      if (meta.templates.length)
        if (AtomicPredicator.template(meta))
          binaries.push({
            expression: config.atomist(explore)(
              check_template(meta.templates)(input),
            )(input),
            combined: false,
          });

      // NATIVE CLASSES
      for (const native of meta.natives)
        binaries.push({
          expression: check_native(native)(input),
          combined: false,
        });

      //----
      // INSTANCES
      //----
      interface IInstance {
        pre: ts.Expression;
        body: ts.Expression | null;
        expected: string;
      }
      const instances: IInstance[] = [];
      const prepare =
        (pre: ts.Expression, expected: string) =>
        (body: ts.Expression | null) =>
          instances.push({
            pre,
            expected,
            body,
          });

      // SETS
      if (meta.sets.length) {
        const install = prepare(
          check_native("Set")(input),
          meta.sets.map((elem) => `Set<${elem.getName()}>`).join(" | "),
        );
        if (meta.sets.some((elem) => elem.any)) install(null);
        else
          install(
            explore_sets(project)(config)(importer)(input, meta.sets, {
              ...explore,
              from: "array",
            }),
          );
      }

      // MAPS
      if (meta.maps.length) {
        const install = prepare(
          check_native("Map")(input),
          meta.maps
            .map(({ key, value }) => `Map<${key}, ${value}>`)
            .join(" | "),
        );
        if (meta.maps.some((elem) => elem.key.any && elem.value.any))
          install(null);
        else
          install(
            explore_maps(project)(config)(importer)(input, meta.maps, {
              ...explore,
              from: "array",
            }),
          );
      }

      // ARRAYS AND TUPLES
      if (meta.tuples.length + meta.arrays.length > 0) {
        const install = prepare(
          config.atomist(explore)({
            expected: [
              ...meta.tuples.map((t) => t.type.name),
              ...meta.arrays.map((a) => a.getName()),
            ].join(" | "),
            expression: ExpressionFactory.isArray(input),
            conditions: [],
          })(input),
          [...meta.tuples, ...meta.arrays]
            .map((elem) => elem.type.name)
            .join(" | "),
        );
        if (meta.arrays.length === 0)
          if (meta.tuples.length === 1)
            install(
              decode_tuple(project)(config)(importer)(input, meta.tuples[0]!, {
                ...explore,
                from: "array",
              }),
            );
          // TUPLE ONLY
          else
            install(
              explore_tuples(project)(config)(importer)(input, meta.tuples, {
                ...explore,
                from: "array",
              }),
            );
        else if (meta.arrays.some((elem) => elem.type.value.any)) install(null);
        else if (meta.tuples.length === 0)
          if (meta.arrays.length === 1)
            // ARRAY ONLY
            install(
              decode_array(project)(config)(importer)(input, meta.arrays[0]!, {
                ...explore,
                from: "array",
              }),
            );
          else
            install(
              explore_arrays(project)(config)(importer)(input, meta.arrays, {
                ...explore,
                from: "array",
              }),
            );
        else
          install(
            explore_arrays_and_tuples(project)(config)(importer)(
              input,
              [...meta.tuples, ...meta.arrays],
              explore,
            ),
          );
      }

      // OBJECT
      if (meta.objects.length > 0)
        prepare(
          ExpressionFactory.isObject({
            checkNull: true,
            checkArray: meta.objects.some((obj) =>
              obj.properties.every(
                (prop) => !prop.key.isSoleLiteral() || !prop.value.isRequired(),
              ),
            ),
          })(input),
          meta.objects.map((obj) => obj.name).join(" | "),
        )(
          explore_objects(config)(importer)(input, meta, {
            ...explore,
            from: "object",
          }),
        );

      if (instances.length) {
        const transformer =
          (merger: (x: ts.Expression, y: ts.Expression) => ts.Expression) =>
          (ins: IInstance) =>
            ins.body
              ? {
                  expression: merger(ins.pre, ins.body),
                  combined: true,
                }
              : {
                  expression: ins.pre,
                  combined: false,
                };
        if (instances.length === 1)
          binaries.push(
            transformer((pre, body) =>
              config.combiner(explore)("and")(
                input,
                [pre, body].map((expression) => ({
                  expression,
                  combined: expression !== pre,
                })),
                meta.getName(),
              ),
            )(instances[0]!),
          );
        else
          binaries.push({
            expression: config.combiner(explore)("or")(
              input,
              instances.map(transformer(ts.factory.createLogicalAnd)),
              meta.getName(),
            ),
            combined: true,
          });
      }

      // ESCAPED CASE
      if (meta.escaped !== null)
        binaries.push({
          combined: false,
          expression:
            meta.escaped.original.size() === 1 &&
            meta.escaped.original.natives.length === 1
              ? check_native(meta.escaped.original.natives[0]!)(input)
              : ts.factory.createLogicalAnd(
                  decode(project)(config)(importer)(
                    input,
                    meta.escaped.original,
                    explore,
                  ),
                  ts.factory.createLogicalAnd(
                    IsProgrammer.decode_to_json(false)(input),
                    decode_escaped(project)(config)(importer)(
                      input,
                      meta.escaped.returns,
                      explore,
                    ),
                  ),
                ),
        });

      //----
      // COMBINE CONDITIONS
      //----
      return top.length && binaries.length
        ? config.combiner(explore)("and")(
            input,
            [
              ...top,
              {
                expression: config.combiner(explore)("or")(
                  input,
                  binaries,
                  meta.getName(),
                ),
                combined: true,
              },
            ],
            meta.getName(),
          )
        : binaries.length
          ? config.combiner(explore)("or")(input, binaries, meta.getName())
          : config.success;
    };

  export const decode_object =
    (config: IConfig) => (importer: FunctionImporter) => {
      const func = FeatureProgrammer.decode_object(config)(importer);
      return (input: ts.Expression, obj: MetadataObject, explore: IExplore) => {
        obj.validated = true;
        return func(input, obj, explore);
      };
    };

  const decode_array =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (input: ts.Expression, array: MetadataArray, explore: IExplore) => {
      if (array.type.recursive === false)
        return decode_array_inline(project)(config)(importer)(
          input,
          array,
          explore,
        );

      explore = {
        ...explore,
        source: "function",
        from: "array",
      };
      return ts.factory.createLogicalOr(
        ts.factory.createCallExpression(
          ts.factory.createIdentifier(
            importer.useLocal(`${config.prefix}a${array.type.index}`),
          ),
          undefined,
          FeatureProgrammer.argumentsArray(config)({
            ...explore,
            source: "function",
            from: "array",
          })(input),
        ),
        config.joiner.failure(input, array.type.name, explore),
      );
    };

  const decode_array_inline =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (
      input: ts.Expression,
      array: MetadataArray,
      explore: IExplore,
    ): ts.Expression => {
      const length = check_array_length(project)(array)(input);
      const main = FeatureProgrammer.decode_array({
        prefix: config.prefix,
        trace: config.trace,
        path: config.path,
        decoder: () => decode(project)(config)(importer),
      })(importer)(config.joiner.array)(input, array, explore);
      return length.expression === null && length.conditions.length === 0
        ? main
        : ts.factory.createLogicalAnd(
            config.atomist(explore)(length)(input),
            main,
          );
    };

  const decode_tuple =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (
      input: ts.Expression,
      tuple: MetadataTuple,
      explore: IExplore,
    ): ts.Expression => {
      if (tuple.type.recursive === false)
        return decode_tuple_inline(project)(config)(importer)(
          input,
          tuple.type,
          explore,
        );
      explore = {
        ...explore,
        source: "function",
        from: "array",
      };
      return ts.factory.createLogicalOr(
        ts.factory.createCallExpression(
          ts.factory.createIdentifier(
            importer.useLocal(`${config.prefix}t${tuple.type.index}`),
          ),
          undefined,
          FeatureProgrammer.argumentsArray(config)({
            ...explore,
            source: "function",
          })(input),
        ),
        config.joiner.failure(input, tuple.type.name, explore),
      );
    };

  const decode_tuple_inline =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (
      input: ts.Expression,
      tuple: MetadataTupleType,
      explore: IExplore,
    ): ts.Expression => {
      const binaries: ts.Expression[] = tuple.elements
        .filter((meta) => meta.rest === null)
        .map((meta, index) =>
          decode(project)(config)(importer)(
            ts.factory.createElementAccessExpression(input, index),
            meta,
            {
              ...explore,
              from: "array",
              postfix: explore.postfix.length
                ? `${postfix_of_tuple(explore.postfix)}[${index}]"`
                : `"[${index}]"`,
            },
          ),
        );
      const rest: ts.Expression | null =
        tuple.elements.length && tuple.elements.at(-1)!.rest !== null
          ? decode(project)(config)(importer)(
              ts.factory.createCallExpression(
                IdentifierFactory.access(input)("slice"),
                undefined,
                [ExpressionFactory.number(tuple.elements.length - 1)],
              ),
              wrap_metadata_rest_tuple(tuple.elements.at(-1)!.rest!),
              {
                ...explore,
                start: tuple.elements.length - 1,
              },
            )
          : null;

      const arrayLength = ts.factory.createPropertyAccessExpression(
        input,
        "length",
      );
      return config.combiner(explore)("and")(
        input,
        [
          ...(rest === null
            ? tuple.elements.every((t) => t.optional === false)
              ? [
                  {
                    combined: false,
                    expression: ts.factory.createStrictEquality(
                      arrayLength,
                      ExpressionFactory.number(tuple.elements.length),
                    ),
                  },
                ]
              : [
                  {
                    combined: false,
                    expression: ts.factory.createLogicalAnd(
                      ts.factory.createLessThanEquals(
                        ExpressionFactory.number(
                          tuple.elements.filter((t) => t.optional === false)
                            .length,
                        ),
                        arrayLength,
                      ),
                      ts.factory.createGreaterThanEquals(
                        ExpressionFactory.number(tuple.elements.length),
                        arrayLength,
                      ),
                    ),
                  },
                ]
            : []),
          ...(config.joiner.tuple
            ? [
                {
                  expression: config.joiner.tuple(binaries),
                  combined: true,
                },
              ]
            : binaries.map((expression) => ({
                expression,
                combined: true,
              }))),
          ...(rest !== null
            ? [
                {
                  expression: rest,
                  combined: true,
                },
              ]
            : []),
        ],
        `[${tuple.elements.map((t) => t.getName()).join(", ")}]`,
      );
    };

  const decode_escaped =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (input: ts.Expression, meta: Metadata, explore: IExplore): ts.Expression =>
      ts.factory.createCallExpression(
        ts.factory.createParenthesizedExpression(
          ts.factory.createArrowFunction(
            undefined,
            undefined,
            [IdentifierFactory.parameter("input", TypeFactory.keyword("any"))],
            undefined,
            undefined,
            decode(project)(config)(importer)(
              ts.factory.createIdentifier("input"),
              meta,
              explore,
            ),
          ),
        ),
        undefined,
        [
          ts.factory.createCallExpression(
            IdentifierFactory.access(input)("toJSON"),
            undefined,
            [],
          ),
        ],
      );

  /* -----------------------------------------------------------
        UNION TYPE EXPLORERS
    ----------------------------------------------------------- */
  const explore_sets =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (
      input: ts.Expression,
      sets: Metadata[],
      explore: IExplore,
    ): ts.Expression =>
      ts.factory.createCallExpression(
        UnionExplorer.set({
          checker: decode(project)(config)(importer),
          decoder: decode_array(project)(config)(importer),
          empty: config.success,
          success: config.success,
          failure: (input, expected, explore) =>
            ts.factory.createReturnStatement(
              config.joiner.failure(input, expected, explore),
            ),
        })([])(input, sets, explore),
        undefined,
        undefined,
      );

  const explore_maps =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (
      input: ts.Expression,
      maps: Metadata.Entry[],
      explore: IExplore,
    ): ts.Expression =>
      ts.factory.createCallExpression(
        UnionExplorer.map({
          checker: (input, entry, explore) => {
            const func = decode(project)(config)(importer);
            return ts.factory.createLogicalAnd(
              func(
                ts.factory.createElementAccessExpression(input, 0),
                entry[0],
                {
                  ...explore,
                  postfix: `${explore.postfix}[0]`,
                },
              ),
              func(
                ts.factory.createElementAccessExpression(input, 1),
                entry[1],
                {
                  ...explore,
                  postfix: `${explore.postfix}[1]`,
                },
              ),
            );
          },
          decoder: decode_array(project)(config)(importer),
          empty: config.success,
          success: config.success,
          failure: (input, expected, explore) =>
            ts.factory.createReturnStatement(
              config.joiner.failure(input, expected, explore),
            ),
        })([])(input, maps, explore),
        undefined,
        undefined,
      );

  const explore_tuples =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (
      input: ts.Expression,
      tuples: MetadataTuple[],
      explore: IExplore,
    ): ts.Expression =>
      explore_array_like_union_types(config)(importer)(
        UnionExplorer.tuple({
          checker: decode_tuple(project)(config)(importer),
          decoder: decode_tuple(project)(config)(importer),
          empty: config.success,
          success: config.success,
          failure: (input, expected, explore) =>
            ts.factory.createReturnStatement(
              config.joiner.failure(input, expected, explore),
            ),
        }),
      )(input, tuples, explore);

  const explore_arrays =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (
      input: ts.Expression,
      arrays: MetadataArray[],
      explore: IExplore,
    ): ts.Expression =>
      explore_array_like_union_types(config)(importer)(
        UnionExplorer.array({
          checker: decode(project)(config)(importer),
          decoder: decode_array(project)(config)(importer),
          empty: config.success,
          success: config.success,
          failure: (input, expected, explore) =>
            ts.factory.createReturnStatement(
              config.joiner.failure(input, expected, explore),
            ),
        }),
      )(input, arrays, explore);

  const explore_arrays_and_tuples =
    (project: IProject) =>
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (
      input: ts.Expression,
      elements: Array<MetadataArray | MetadataTuple>,
      explore: IExplore,
    ): ts.Expression =>
      explore_array_like_union_types(config)(importer)(
        UnionExplorer.array_or_tuple({
          checker: (front, target, explore, array) =>
            target instanceof MetadataTuple
              ? decode_tuple(project)(config)(importer)(front, target, explore)
              : config.atomist(explore)({
                  expected: elements
                    .map((elem) =>
                      elem instanceof MetadataArray
                        ? elem.getName()
                        : elem.type.name,
                    )
                    .join(" | "),
                  expression: decode(project)(config)(importer)(
                    front,
                    target,
                    explore,
                  ),
                  conditions: [],
                })(array),
          decoder: (input, target, explore) =>
            target instanceof MetadataTuple
              ? decode_tuple(project)(config)(importer)(input, target, explore)
              : decode_array(project)(config)(importer)(input, target, explore),
          empty: config.success,
          success: config.success,
          failure: (input, expected, explore) =>
            ts.factory.createReturnStatement(
              config.joiner.failure(input, expected, explore),
            ),
        }),
      )(input, elements, explore);

  const explore_array_like_union_types =
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    <T extends MetadataArray | MetadataTuple>(
      factory: (
        parameters: ts.ParameterDeclaration[],
      ) => (
        input: ts.Expression,
        elements: T[],
        explore: IExplore,
      ) => ts.ArrowFunction,
    ) =>
    (input: ts.Expression, elements: T[], explore: IExplore): ts.Expression => {
      const arrow =
        (parameters: ts.ParameterDeclaration[]) =>
        (explore: IExplore) =>
        (input: ts.Expression): ts.ArrowFunction =>
          factory(parameters)(input, elements, explore);
      if (elements.every((e) => e.type.recursive === false))
        ts.factory.createCallExpression(
          arrow([])(explore)(input),
          undefined,
          [],
        );
      explore = {
        ...explore,
        source: "function",
        from: "array",
      };
      return ts.factory.createLogicalOr(
        ts.factory.createCallExpression(
          ts.factory.createIdentifier(
            importer.emplaceUnion(
              config.prefix,
              elements.map((e) => e.type.name).join(" | "),
              () =>
                arrow(
                  FeatureProgrammer.parameterDeclarations(config)(
                    TypeFactory.keyword("any"),
                  )(ts.factory.createIdentifier("input")),
                )({
                  ...explore,
                  postfix: "",
                })(ts.factory.createIdentifier("input")),
            ),
          ),
          undefined,
          FeatureProgrammer.argumentsArray(config)(explore)(input),
        ),
        config.joiner.failure(
          input,
          elements.map((e) => e.type.name).join(" | "),
          explore,
        ),
      );
    };

  const explore_objects =
    (config: IConfig) =>
    (importer: FunctionImporter) =>
    (input: ts.Expression, meta: Metadata, explore: IExplore) =>
      meta.objects.length === 1
        ? decode_object(config)(importer)(input, meta.objects[0]!, explore)
        : ts.factory.createCallExpression(
            ts.factory.createIdentifier(
              importer.useLocal(`${config.prefix}u${meta.union_index!}`),
            ),
            undefined,
            FeatureProgrammer.argumentsArray(config)(explore)(input),
          );
}

const create_add =
  (binaries: CheckerProgrammer.IBinary[]) =>
  (defaultInput: ts.Expression) =>
  (
    exact: boolean,
    left: ts.Expression,
    right: ts.Expression = defaultInput,
  ) => {
    const factory = exact
      ? ts.factory.createStrictEquality
      : ts.factory.createStrictInequality;
    binaries.push({
      expression: factory(left, right),
      combined: false,
    });
  };
