import { Get, ofGet, Predicate } from './Get';
import { TypeGuard } from './TypeGuard';
import { Func } from './Func';
import { isDefined, isEmpty } from './Is';
import { validate } from '../validation/Validate';
import { tryTo } from './Try';

class CaseBuilder<V> {
  is = {
    true: <T>(pred: Predicate<V>, out: Get<T, V>): Case<T, V> => this.case(pred, out),
    false: <T>(pred: Predicate<V>, out: Get<T, V>): Case<T, V> => this.case(!ofGet(pred, this.v), out),
    truthy: <T>(prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!!prop(this.v), out),
    falsy: <T>(prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!prop(this.v), out),
    defined: <T>(prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(isDefined(prop(this.v)), out),
    empty: <T>(prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(isEmpty(prop(this.v)), out),
    valid: <T>(prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(validate(prop(this.v)).isValid, out),
    in: <T>(prop: Get<Array<V>, V>, out: Get<T, V>): Case<T, V> => this.case(ofGet(prop, this.v).includes(this.v), out),
    type: <T, U = unknown>(guard: TypeGuard<U>, out: Func<T, U>): Case<T, V> => this.type<T, U>(guard, out),
    not: {
      true: <T>(pred: Predicate<V>, out: Get<T, V>): Case<T, V> => this.case(!ofGet(pred, this.v), out),
      false: <T>(pred: Predicate<V>, out: Get<T, V>): Case<T, V> => this.case(pred, out),
      truthy: <T>(prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!prop(this.v), out),
      falsy: <T>(prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!!prop(this.v), out),
      defined: <T>(prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!isDefined(prop(this.v)), out),
      empty: <T>(prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!isEmpty(prop(this.v)), out),
      valid: <T>(prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!validate(prop(this.v)).isValid, out),
      in: <T>(prop: Get<Array<V>, V>, out: Get<T, V>): Case<T, V> => this.case(!ofGet(prop, this.v).includes(this.v), out),
      type: <T, U = unknown>(guard: TypeGuard<U>, out: Get<T, V>): Case<T, V> => this.case(v => !guard(v as unknown), out),
    },
  };
  if = this.is;

  constructor(readonly v: V) {}

  case<T>(pred: Predicate<V>, out: Get<T, V>): Case<T, V> {
    return new Case<T, V>(this.v).case(pred, out);
  }

  type<T, U = unknown>(guard: TypeGuard<U>, out: Func<T, U>): Case<T, V> {
    return new Case<T, V>(this.v).type<U>(guard, out);
  }

  equals<T>(value: V, out: Get<T, V>): Case<T, V> {
    return new Case<T, V>(this.v).equals(value, out);
  }
}

class Case<T, V = unknown> {
  is = {
    true: (pred: Predicate<V>, out: Get<T, V>): Case<T, V> => this.case(pred, out),
    false: (pred: Predicate<V>, out: Get<T, V>): Case<T, V> => this.case(!ofGet(pred, this.value), out),
    truthy: (prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!!prop(this.value), out),
    falsy: (prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!prop(this.value), out),
    defined: (prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(isDefined(prop(this.value)), out),
    empty: (prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(isEmpty(prop(this.value)), out),
    valid: (prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(validate(prop(this.value)).isValid, out),
    in: (prop: Get<Array<V>, V>, out: Get<T, V>): Case<T, V> => this.case(ofGet(prop, this.value).includes(this.value), out),
    type: <U>(guard: TypeGuard<U>, out: Func<T, U>): Case<T, V> => this.type<U>(guard, out),
    not: {
      true: (pred: Predicate<V>, out: Get<T, V>): Case<T, V> => this.case(!ofGet(pred, this.value), out),
      false: (pred: Predicate<V>, out: Get<T, V>): Case<T, V> => this.case(pred, out),
      truthy: (prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!prop(this.value), out),
      falsy: (prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!!prop(this.value), out),
      defined: (prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!isDefined(prop(this.value)), out),
      empty: (prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!isEmpty(prop(this.value)), out),
      valid: (prop: Func<unknown, V>, out: Get<T, V>): Case<T, V> => this.case(!validate(prop(this.value)).isValid, out),
      in: (prop: Get<Array<V>, V>, out: Get<T, V>): Case<T, V> => this.case(!ofGet(prop, this.value).includes(this.value), out),
      type: <U>(guard: TypeGuard<U>, out: Get<T, V>): Case<T, V> => this.case(v => !guard(v as unknown), out),
    },
  };
  if = this.is;

  constructor(
    protected value: V,
    protected outcome?: T
  ) {}

  case(pred: Predicate<V>, out: Get<T, V>): Case<T, V> {
    return tryTo(pred, this.value)
      .is.true()
      .map(() => ofGet(out, this.value))
      .map(res => new Found(this.value, res) as Case<T, V>)
      .or(this);
  }

  type<U>(guard: TypeGuard<U>, out: Func<T, U>): Case<T, V> {
    return tryTo(guard, this.value)
      .is.true()
      .map(() => out(this.value as unknown as U))
      .map(res => new Found(this.value, res) as Case<T, V>)
      .or(this);
  }

  equals(value: V, out: Get<T, V>): Case<T, V> {
    return this.case(this.value === value, out);
  }

  else(alt: Get<T, V>): T {
    return ofGet<T, V>(alt, this.value);
  }
}

class Found<T, V> extends Case<T, V> {
  constructor(
    protected value: V,
    protected outcome: T
  ) {
    super(value, outcome);
  }

  case(pred: Predicate<V>, out: Get<T, V>): this {
    return this;
  }

  type<U>(guard: TypeGuard<U>, out: Func<T, U>): Case<T, V> {
    return this;
  }

  equals(value: V, out: Get<T, V>): Case<T, V> {
    return this;
  }

  else(alt: Get<T, V>): T {
    return this.outcome;
  }
}

export const choose = <V>(value: V) => new CaseBuilder<V>(value);
