/**
 * The sad, lonely enum that should be more tightly coupled
 * to the Option type...but this isn't Rust
 */
enum OptionType {
	Some = 'Some',
	None = 'None',
}

// ----------------------------------------------------------------------------
// Typeguards to handle Some/None difference
// ----------------------------------------------------------------------------

const isOption = <T>(v: any): v is Option<T> => v instanceof Option;

class OptionInnerNone {
	public type: OptionType = OptionType.None;
}

class OptionInnerSome<T> {
	public type: OptionType = OptionType.Some;

	constructor(public value: T) {}
}

type OptionInnerType<T> = OptionInnerNone | OptionInnerSome<T>;

const isSome = <T>(v: OptionInnerType<T>): v is OptionInnerSome<T> =>
	'value' in v && v.type === OptionType.Some;

/**
 * Rust-style optional type
 *
 * Based on https://gist.github.com/s-panferov/575da5a7131c285c0539
 */
export class Option<T> {
	/**
	 * The placeholder for the 'None' value type
	 */
	private static _None: Option<any> = new Option(null);

	/**
	 * Is this a 'Some' or a 'None'?
	 */
	private readonly inner: OptionInnerType<T>;

	private constructor(v?: T) {
		this.inner = (v !== undefined && v !== null)
			? new OptionInnerSome(v)
			: new OptionInnerNone();
	}

	/**
	 * The equivalent of the Rust `Option`.`None` type
	 */
	public static get None(): Option<any> {
		return Option._None;
	}

	public static Some<X>(v: any): Option<X> {
		return Option.from(v);
	}

	public static from<X>(v?: any): Option<X> {
		return (isOption(v)) ? Option.from(v.unwrap()) : new Option(v);
	}

	isSome(): boolean {
		return isSome(this.inner);
	}

	isNone(): boolean {
		return !this.isSome();
	}

	isSomeAnd(fn: (a: T) => boolean): boolean {
		return isSome(this.inner) ? fn(this.inner.value) : false;
	}

	isNoneAnd(fn: () => boolean): boolean {
		return this.isNone() ? fn() : false;
	}

	map<U>(fn: (a: T) => U): Option<U> {
		return isSome(this.inner) ? Option.from(fn(this.inner.value)) : Option.None;
	}

	mapOr<U>(def: U, f: (a: T) => U): U {
		return isSome(this.inner) ? f(this.inner.value) : def;
	}

	mapOrElse<U>(def: () => U, f: (a: T) => U): U {
		return isSome(this.inner) ? f(this.inner.value) : def();
	}

	unwrap(): T | never {
		if (isSome(this.inner)) {
			return this.inner.value;
		}

		console.error('None.unwrap()');
		throw 'None.get';
	}

	unwrapOr(def: T): T {
		return isSome(this.inner) ? this.inner.value : def;
	}

	unwrapOrElse(f: () => T): T {
		return isSome(this.inner) ? this.inner.value : f();
	}

	and<U>(optb: Option<U>): Option<U> {
		return isSome(this.inner) ? optb : Option.None;
	}

	andThen<U>(f: (a: T) => Option<U>): Option<U> {
		return isSome(this.inner) ? f(this.inner.value) : Option.None;
	}

	or(optb: Option<T>): Option<T> {
		return this.isNone() ? optb : this;
	}

	orElse(f: () => Option<T>): Option<T> {
		return this.isNone() ? f() : this;
	}

	toString(): string {
		const innerValue = (isSome(this.inner))
			? JSON.stringify(this.inner.value)
			: '';
		const prefix = this.inner.type.valueOf();

		return (innerValue.length > 0) ? `${prefix} (${innerValue})` : prefix;
	}
}

export const { Some, None } = Option;
export default Option;