import { traverse } from './Traverse';
import { ifDefined } from './If';
import { Primitive } from '../types/Primitive';
import { Constructor, isConstructor, use } from '../types/Constructor';
import { asJson, json as typesJson } from '../types/Json';
import { choose } from '../types/Case';
import { isArray, isBoolean, isFunction, isString } from '../types/Is';
import { meta } from '../types/Meta';
import { isPageList, PageList, toPageList } from '../types/PageList';
import { List } from '../types/List';
import { isEqual } from '../types/IsEqual';
import { Get, ofGet } from '../types/Get';
import { DontInfer } from '../types/Types';
import { AnyKey } from '../types/AnyKey';
import { EnumConstructor, isEnumConstructor } from '../types/Enum';
import { Id } from '../types/Id';

type Func<T = unknown> = (a: any, key?: string) => T;
type FuncOn<S> = (a: S, key?: string) => unknown;
type Viewer = { key: string; f: Func };

type ViewType<V = any> = Primitive | EnumConstructor | Constructor | Func | View<V> | undefined;
type ViewValue<S = never> = [S] extends [never] ? ViewType : Exclude<ViewType, string | Func> | AnyKey<S> | FuncOn<S>;
type ViewRecord<V = any, S = never> = [S] extends [never]
  ? Partial<Record<keyof V | typeof spread, ViewValue<S>>>
  : Partial<Record<keyof V | typeof spread, ViewValue<S>>> & Partial<Record<Exclude<Extract<keyof S, string>, Extract<keyof V, string>>, typeof ignore>>;

const ignore = Symbol('view.ignore');
const keep = Symbol('view.keep');
const json = Symbol('view.json');
const spread = 'view.spread';

export const toViewer = (key: string, value: ViewType): Viewer =>
  choose(value)
    .is.not.defined(v => v, { key, f: () => undefined } as Viewer)
    .type(isBoolean, b => ({ key, f: () => b }))
    .equals(ignore, { key, f: () => undefined })
    .equals(keep, { key, f: (a: any) => traverse(a, key) })
    .equals(json, { key, f: (a: any) => asJson(traverse(a, key)) })
    .type(isString, s => ({ key, f: (a: any) => traverse(a, s) }))
    .type(isEnumConstructor, c => ({
      key,
      f: (a, key) => use(traverse(a, key), v => (isArray(v) ? c.byIds(v) : c.byId(v as Id))),
    }))
    .type(isConstructor, c => ({
      key,
      f: (a, key) => use(traverse(a, key), v => (isArray(v) ? v.map(i => optional(c, i)) : optional(c, v))),
    }))
    .type(isSimpleView, f => ({ key, f: (a: any) => ifDefined(traverse(a, key), v => f.from(v)) }))
    .type(isFunction, f => ({ key, f }))
    .else(v => ({ key, f: () => v }));

const optional = (c: Constructor, v: any) =>
  ifDefined(
    v,
    i => new c(i),
    () => v
  );

const toViewers = (views: ViewRecord): Viewer[] =>
  meta(views)
    .entries<ViewType>()
    .map(([k, v]) => toViewer(k, v));

export class View<V = any, S = never> {
  constructor(
    private views = {} as ViewRecord<V, S>,
    readonly startsFrom: 'scratch' | 'source' = 'scratch',
    readonly viewers: Viewer[] = toViewers(views),
    private spreads: Get<V>[] = []
  ) {}

  get fromSource(): View<V, S> {
    return new View(this.views, 'source', this.viewers, this.spreads);
  }

  spread(f: Get<V>): View<V, S> {
    return new View(this.views, this.startsFrom, this.viewers, [...this.spreads, f]);
  }

  from<T = unknown>(source: PageList<T>): PageList<V>;
  from<T = unknown>(source: List<T>): List<V>;
  from<T = unknown>(source: T[]): V[];
  from<T = unknown>(source: T): V;
  from<T = unknown>(source: PageList<T> | List<T> | T[] | T): PageList<V> | List<V> | V[] | V {
    if (isPageList(source))
      return toPageList(
        source.map(s => this.reduce(s)),
        source.options
      );
    if (isArray(source)) return source.map(s => this.reduce(s));
    return this.reduce(source);
  }

  same = (one?: unknown, another?: unknown): boolean => isEqual(this.from(one), this.from(another));

  private reduce = (source: any): any =>
    use(asJson(source), src => {
      const mapped = this.viewers.reduce(
        (acc, v) => (v.key === spread ? { ...acc, ...asJson(v.f(src, v.key)) } : typesJson.set(acc, v.key, v.f(src, v.key))),
        this.startsFrom === 'scratch' ? {} : src
      );
      return this.spreads.reduce((acc, f) => ({ ...acc, ...asJson(ofGet(f, src)) }), mapped);
    });
}

export const isSimpleView = (a: unknown): a is View => a instanceof View;

export const view = <V = any, S = never>(views: ViewRecord<DontInfer<V>, DontInfer<S>>): View<V, S> => new View<V, S>(views);

export const views = {
  ignore,
  keep,
  json,
  spread,
  skip: ignore,
  equals: (key: string, value: unknown) => (a: unknown) => isEqual(traverse(a, key), value),
  value: (value: unknown) => () => value,
  coalesce:
    (...keys: string[]) =>
    (a: unknown) =>
      keys.reduce<unknown>((result, k) => result ?? traverse(a, k), undefined),
  or: {
    key: (altKey: string) => (a: unknown, key?: string) => traverse(a, key) ?? traverse(a, altKey),
    value: (altValue: unknown) => (a: unknown, key?: string) => traverse(a, key) ?? altValue,
    func: (altFunc: Func) => (a: unknown, key?: string) => traverse(a, key) ?? altFunc(a, key),
  },
  to: <T = unknown>(ctorOrFunc: Constructor<T> | ((v: any) => T)) => {
    const apply = (v: any): T => (isConstructor(ctorOrFunc) ? new ctorOrFunc(v) : (ctorOrFunc as (v: any) => T)(v));
    const base = (a: any, key?: string): T | undefined => {
      const v = traverse(a, key);
      return v != null ? apply(v) : undefined;
    };
    return Object.assign(base, {
      or: {
        key: (altKey: string) => (a: unknown, key?: string) => base(a, key) ?? traverse(a, altKey),
        value: (altValue: T) => (a: unknown, key?: string) => base(a, key) ?? altValue,
        func: (altFunc: Func) => (a: unknown, key?: string) => base(a, key) ?? altFunc(a, key),
      },
    });
  },
} as const;
