

/**
 * The Option type encapsulates an optional value.
 * A value of type Option<T> either :
 *  - contains a value of type T,
 *  - or it is empty (represented as None).
 *
 * Using Option is a good way to deal with null and undefined.
 */
export abstract class Option<T> {
    protected _value: T;

    /**
     * Returns the option's value.
     * @note The option must be nonEmpty and this will return
     *        undefined if the option is empty.
     */
    get value () : T { return this._value; }

    valueOrElse(value:T): T {
        if(this.isDefined()) return this.value;
        else return value;
    }

    valueOrNull(): T {
        if(this.isDefined()) return this.value;
        else return null;
    }

    isDefined(): boolean {
        return this.value != undefined;
    }

    isEmpty() : boolean {
        return !this.isDefined();
    }

    flatMap<U>(fn:(T) => Option<U>): Option<U> {
        if(this.isEmpty()) return None;
        else {
            const resultOption = fn(this.value);
            if(Option.isDefinedValue(resultOption)) return resultOption;
            else return None;
        }
    }

    map<U>(fn:(T) => U): Option<U> {
        if(this.isDefined()) return Option.of(fn(this.value));
        else return None;
    }

    forEach(fn:(T) => void) {
        if(this.isDefined())
            fn(this.value);
    }

    static isDefinedValue(value:any): boolean {
        return (value != null && value != undefined);
    }

    static of<T>(value : T) : Option<T> {
        // If the provided _value is null or undefined, we let the property
        // _value undefined else we defining it!
        if(Option.isDefinedValue(value)) return new Some(value);
        else return None;
    }

    static empty<T>() : Option<T> {
        return None;
    }
}


export class Some<T> extends Option<T>{

    constructor(_value:T) {
        super();
        if(_value == null || _value == undefined)
            throw new Error(
                "Some constructor expected non null an non undefined value. " +
                _value + "given. " +
                "If you are not sure whether your value is defined or not, please " +
                "consider the static method Option.of<T>(value:T) that will " +
                "deal with the null/undefined case properly."
            );
        this._value = _value;
    }
}


class NoneT extends Option<typeof undefined> {}

/**
 * Here we use the instance of {}  as instance of None. Indeed, the '{}' can be
 * viewed as an instance of Option because of the Option class definition.
 * as  we did not  defined a '_value' attribute, every call to  the isDefined() method
 * will always yield false. It is then enough to have a correct working Option instance
 * For example see the 'map', and 'foreach' method definition.
 *
 * @type {Option<typeof undefined>}
 */
export const None = new NoneT() as Option<typeof undefined>;
