import { MonadPlus } from './monadplus';
import { AtomicPromise } from '../promise';
import { noop } from '../function';
import { push } from '../array';

export class Maybe<a> extends MonadPlus<a> {
  constructor(thunk: () => Maybe<a>) {
    super(thunk);
  }
  // Bug: TypeScript
  // @ts-ignore-error
  public fmap<b>(f: (a: a) => b): Maybe<b> {
    return this.bind(a => new Just(f(a)));
  }
  // Bug: TypeScript
  // @ts-ignore-error
  public ap<a, z>(this: Maybe<(a: a) => z>, a: Maybe<a>): Maybe<z>;
  // @ts-ignore-error
  public ap<a, b, z>(this: Maybe<(a: a, b: b) => z>, a: Maybe<a>): Maybe<(b: b) => z>;
  // @ts-ignore-error
  public ap<a, b, c, z>(this: Maybe<(a: a, b: b, c: c) => z>, a: Maybe<a>): Maybe<(b: b, c: c) => z>;
  // @ts-ignore-error
  public ap<a, b, c, d, z>(this: Maybe<(a: a, b: b, c: c, d: d) => z>, a: Maybe<a>): Maybe<(b: b, c: c, d: d) => z>;
  // @ts-ignore-error
  public ap<a, b, c, d, e, z>(this: Maybe<(a: a, b: b, c: c, d: d, e: e) => z>, a: Maybe<a>): Maybe<(b: b, c: c, d: d, e: e) => z>;
  // @ts-ignore-error
  public ap<a, z>(this: Maybe<(...as: any[]) => z>, a: Maybe<a>): Maybe<z> {
    return Maybe.ap(this, a);
  }
  // Bug: TypeScript
  // @ts-expect-error
  public bind<b>(f: (a: a) => Maybe<b>): Maybe<b> {
    return new Maybe<b>(() => {
      const m: Maybe<a> = this.evaluate();
      switch (m.constructor) {
        case Just:
          return f(m.extract());
        case Nothing:
          return m as Nothing;
        default:
          return m.bind(f);
      }
    });
  }
  // Bug: TypeScript
  // @ts-expect-error
  public join<b>(this: Maybe<Maybe<b>>): Maybe<b> {
    return this.bind(m => m);
  }
  public guard(cond: boolean): Maybe<a> {
    return cond ? this : Maybe.mzero;
  }
  public extract(): a;
  public extract(nothing: () => a): a;
  public extract<b>(nothing: () => b): a | b;
  public extract<b>(nothing: () => b, just: (a: a) => b): b;
  public extract<b>(nothing?: () => b, just?: (a: a) => b): a | b {
    return just === undefined
      ? this.evaluate().extract(nothing!)
      : this.fmap(just).extract(nothing!);
  }
  public static do<a>(block: () => Iterator<Maybe<a>, Maybe<a>, a>): Maybe<a> {
    const iter = block();
    let value: a | undefined;
    while (true) {
      const { value: m, done } = iter.next(value!);
      if (done) return m;
      if (m.extract(noop, a => [value = a]) === undefined) return m;
    }
  }
}
export namespace Maybe {
  export declare function fmap<a, b>(f: (a: a) => b, m: Maybe<a>): Maybe<b>;
  export function pure<a>(a: a): Maybe<a> {
    return new Just(a);
  }
  export declare function ap<a, b>(mf: Maybe<(a: a) => b>, ma: Maybe<a>): Maybe<b>;
  export declare function ap<a, b>(mf: Maybe<(a: a) => b>): (ma: Maybe<a>) => Maybe<b>;
  export const Return = pure;
  export declare function bind<a, b>(f: (a: a) => Maybe<b>, m: Maybe<a>): Maybe<b>;
  export function sequence<a>(fm: Maybe<a>[]): Maybe<a[]>;
  export function sequence<a>(fm: Maybe<PromiseLike<a>>): AtomicPromise<Maybe<a>>;
  export function sequence<a>(fm: Maybe<a>[] | Maybe<PromiseLike<a>>): Maybe<a[]> | AtomicPromise<Maybe<a>> {
    return fm instanceof Maybe
      ? fm.extract(() => AtomicPromise.resolve(mzero), a => AtomicPromise.resolve(a).then(Return))
      : fm.reduce((acc, m) => acc.bind(as => m.fmap(a => push(as, [a]))), Return([]));
  }
}

export class Just<a> extends Maybe<a> {
  constructor(private readonly value: a) {
    super(throwCallError);
  }
  public override bind<b>(f: (a: a) => Just<b>): Just<b>;
  public override bind<b>(f: (a: a) => Nothing): Nothing;
  public override bind<b>(f: (a: a) => Maybe<b>): Maybe<b>;
  public override bind<b>(f: (a: a) => Maybe<b>): Maybe<b> {
    return new Maybe(() => f(this.extract()));
  }
  public override extract(): a;
  public override extract(nothing: () => a): a;
  public override extract<b>(nothing: () => b): a;
  public override extract<b>(nothing: () => b, just: (a: a) => b): b;
  public override extract<b>(nothing?: () => b, just?: (a: a) => b): a | b {
    if (just !== undefined) return just(this.value);
    return this.value;
    assert([nothing]);
  }
}

export class Nothing extends Maybe<never> {
  constructor() {
    super(throwCallError);
  }
  public override bind<b>(f: (a: never) => Maybe<b>): Nothing {
    return this;
    assert(f);
  }
  public override extract(): never;
  public override extract<b>(nothing: () => b): b;
  public override extract<b>(nothing: () => b, just: (a: never) => b): b;
  public override extract<b>(nothing?: () => b): b {
    if (nothing !== undefined) return nothing();
    throw new Error(`Spica: Maybe: Nothing value is extracted`);
  }
}

export namespace Maybe {
  export const mzero = new Nothing();
  export function mplus<a>(ml: Maybe<a>, mr: Maybe<a>): Maybe<a> {
    return new Maybe<a>(() =>
      ml.extract(() => mr, () => ml));
  }
}

function throwCallError(): never {
  throw new Error(`Spica: Maybe: Invalid thunk call`);
}
