/**
 * Contains `Either` constructs (`Right`, `Left`,  `either` etc.) and associated operations.
 * Created by elyde on 12/10/2016.
 * @module either
 */
import {isset, curry, id, toFunction} from 'fjl';
import {Just} from '../maybe/Maybe';
import Monad from '../monad/Monad';

/**
 * `Left` representation of `Either` construct.
 * @class module:either.Left
 * @param x {*}
 * @property value {*}
 * @extends module:monad.Monad
 */
export class Left extends Monad {
    /**
     * Same as `new Left(...)`.
     * @method module:either.Left.of
     * @static
     * @param x {*}
     * @returns {Left}
     */
    static of (x) { return new Left(x); }
}

/**
 * @class module:either.Right
 * @param x {*}
 * @property value {*}
 * @extends module:maybe.Just
 */
export class Right extends Just {
    /**
     * Maps a function over contained value and returns result wrapped.
     * @method module:either.Right#map
     * @param fn {Function} - Unary operation.
     * @returns {Either}
     */
    map (fn) {
        const value = this.valueOf();
        if (isLeft(value)) {
            return value;
        }
        else if (!isset(value)) {
            return Left.of(
                `TypeError: Cannot operate on \`${value}\`.`
            );
        }
        return Right.of(fn(value));
    }

    /**
     * Same as `new Right(...)`.
     * @method module:either.Right.of
     * @static
     * @param x {*}
     * @returns {Right}
     */
    static of (x) { return new Right(x); }
}

export const

    /**
     * Returns a new `Left`
     * @function module:either.left
     * @param x {*}
     * @returns {Left}
     */
    left = x => new Left(x),

    /**
     * Returns a `Right`.
     * @function module:either.right
     * @param x {*}
     * @returns {Right}
     */
    right = x => new Right(x),

    /**
     * Checks for instance of `Right` constructor.
     * @function module:either.isRight
     * @param x {*}
     * @returns {boolean}
     */
    isRight = x => x instanceof Right,

    /**
     * Checks for instance of `Left` constructor.
     * @function module:either.isLeft
     * @param x {*}
     * @returns {boolean}
     */
    isLeft = x => x instanceof Left,

    /**
     * Returns a `Right` - if not a `Right` creates one from given, else returns given.
     * @function module:either.toRight
     * @param x {*}
     * @returns {Right}
     */
    toRight = x => isRight(x) ? x : right(x),

    /**
     * Returns a `Left` - if not a `Left` creates one from given, else returns given.
     * @function module:either.toLeft
     * @param x {*}
     * @returns {Left}
     */
    toLeft = x => isLeft(x) ? x : left(x),

    /**
     * Converts given to an either (`Right`|`Left`)
     * @function module:either.toEither
     * @param x {*}
     * @returns {Left|Right}
     */
    toEither = x => isLeft(x) || isRight(x) ? x : right(x).map(id),

    /**
     * Calls matching callback on incoming `Either`'s type;  If is a `Left`
     * (after mapping identity func on it) then calls left-callback and unwraps result
     * else calls right-callback and does the same.  Think of it like a functional
     * ternary statement (lol).
     * @function module:either.either
     * @param leftCallback {Function} - Mapped over value of `monad`'s identity.
     * @param rightCallback {Function} - "".
     * @param _either_ {Either|*}
     * @return {*} - Value of unwrapped resulting value of `flatMap`ped, passed-in callback's on passed in monad.
     * @example
     * expect(
         either(() => 404, () => 200, compose(right, right, right, right)(true))
       ).toEqual(undefined);
     */
    either = curry((leftCallback, rightCallback, _either_) => {
        const identity = toEither(_either_).flatMap(id),
            out = isRight(_either_) ?
                identity.flatMap(toFunction(rightCallback)) :
                identity.flatMap(leftCallback)
            ;
        return isset(out) ? out.join() : out;
    })

;