import ts from "typescript";

import { ExpressionFactory } from "../../factories/ExpressionFactory";

import { IMetadataTypeTag } from "../../schemas/metadata/IMetadataTypeTag";

export namespace RandomRanger {
  export interface IDefaults {
    minimum: number;
    maximum: number;
    gap: number;
  }

  export const length =
    (coalesce: (method: string) => ts.Expression) =>
    (defs: IDefaults) =>
    (acc: length.IAccessors) =>
    (tags: IMetadataTypeTag[]): ts.Expression | undefined => {
      const props = {
        minimum: getter(tags)(acc.minimum),
        maximum: getter(tags)(acc.maximum),
      };
      if (props.minimum === undefined && props.maximum === undefined)
        return undefined;

      if (props.maximum !== undefined && props.minimum === undefined) {
        if (props.maximum <= 0) {
          props.maximum = 0;
          props.minimum = 0;
        } else if (props.maximum < defs.gap)
          props.minimum = defs.minimum === 0 ? 0 : 1;
      }
      props.minimum ??= defs.minimum;
      props.maximum ??= defs.maximum;
      if (props.maximum < props.minimum) (props.maximum as number) += defs.gap;

      return ts.factory.createCallExpression(coalesce("integer"), undefined, [
        ExpressionFactory.number(props.minimum),
        ExpressionFactory.number(props.maximum),
      ]);
    };
  export namespace length {
    export interface IAccessors {
      minimum: string;
      maximum: string;
    }
  }

  export const number =
    (config: number.IConfig) =>
    (defs: IDefaults) =>
    (tags: IMetadataTypeTag[]): ts.Expression => {
      const range = {
        minimum: {
          value: getter(tags)("minimum") ?? getter(tags)("exclusiveMinimum"),
          exclusive: getter(tags)("exclusiveMinimum") !== undefined,
        },
        maximum: {
          value: getter(tags)("maximum") ?? getter(tags)("exclusiveMaximum"),
          exclusive: getter(tags)("exclusiveMaximum") !== undefined,
        },
        stepper: undefined,
        multiply: getter(tags)("multipleOf"),
      };

      //----
      // MULTIPLIERS
      //----
      if (range.multiply !== undefined) {
        const { minimum, maximum } = multiplier(defs.gap)(range)(
          range.multiply,
        );
        return ts.factory.createMultiply(
          config.transform(range.multiply),
          config.setter([minimum, maximum]),
        );
      }

      //----
      // RANGE
      //----
      // INT
      const integer = (value: number) => value === Math.floor(value);
      if (config.type === "int" || config.type === "uint") {
        if (range.minimum.value !== undefined) {
          if (range.minimum.exclusive) {
            range.minimum.exclusive = false;
            if (integer(range.minimum.value)) range.minimum.value += 1;
          }
          range.minimum.value = Math.ceil(range.minimum.value);
        }
        if (range.maximum.value !== undefined) {
          if (range.maximum.exclusive) {
            range.maximum.exclusive = false;
            if (integer(range.maximum.value)) range.maximum.value -= 1;
          }
          range.maximum.value = Math.floor(range.maximum.value);
        }
      }

      // UNSIGNED INT
      if (config.type === "uint") {
        if (range.minimum.value === undefined) range.minimum.value = 0;
        else if (range.minimum.value <= 0) {
          range.minimum.value = 0;
          range.minimum.exclusive = false;
        }
      }

      const minimum =
        range.minimum.value ??
        (range.maximum.value !== undefined
          ? range.maximum.value - defs.gap
          : defs.minimum);
      const maximum =
        range.maximum.value ??
        (range.minimum.value !== undefined
          ? range.minimum.value + defs.gap
          : defs.maximum);
      return config.setter([minimum, maximum]);
    };
  export namespace number {
    export interface IConfig {
      setter: (args: number[]) => ts.Expression;
      transform: (value: number) => ts.Expression;
      type: "int" | "uint" | "double";
    }
  }
}

const getter =
  (tags: IMetadataTypeTag[]) =>
  (kind: string): number | undefined => {
    const value: bigint | number | undefined = tags.find(
      (t) =>
        t.kind === kind &&
        (typeof t.value === "number" || typeof t.value === "bigint"),
    )?.value;
    return value !== undefined ? Number(value) : undefined;
  };

const multiplier = (gap: number) => (range: IRange) => (m: number) => {
  const minimum: number =
    range.minimum.value === undefined
      ? 0
      : (() => {
          const x: number = m * Math.ceil(range.minimum.value / m);
          return range.minimum.exclusive && x === range.minimum.value
            ? x + m
            : x;
        })() / m;
  const maximum: number =
    range.maximum.value === undefined
      ? gap
      : (() => {
          const y: number = m * Math.floor(range.maximum.value / m);
          return range.maximum.exclusive && y === range.maximum.value
            ? y - m
            : y;
        })() / m;
  return { minimum, maximum };
};

interface IRange {
  minimum: IScalar;
  maximum: IScalar;
}
interface IScalar {
  value?: undefined | number;
  exclusive: boolean;
}
