import { err, ok } from "@shared/utils/result.ts";

interface IOption<T> {
    /**
     * Checks if the `Option` is a `Some`.
     * ```typescript
     * // Example
     * const a = some(5)
     * const b = none
     * a.isSome() // true b.isSome() // false
     * ```
     * @returns {boolean} `true` if the Option is a Some, otherwise `false`.
     */
    isSome(): this is Some<T>;

    ifSome(fn: (value: T) => void): Option<T>;

    /**
     * Checks if the `Option<T>` is a `None`.
     * ```typescript
     * // Example
     * const a = some(5);
     * const b = none;
     * a.isNone(); // false
     * b.isNone(); // true
     * ```
     *
     * @returns {boolean} `true` if the Option is a None, otherwise `false`.
     */
    isNone(): this is None<T>;

    ifNone(fn: () => void): Option<T>;

    /**
     * Uses the function `fn` to map a value of type `T`, stored inside of the `Option<T>`, to a value of type `U`. Then wraps a mapped value to the new `Option<U>` and returns it. The difference from a `.flatMap()` is that `.flatMap()` does not wraps a mapped value into the `Option`, but rather requires `fn` to take care of it.
     *
     * ```typescript
     * // Example
     * const a = some(5);
     * const b = a.map((value) => {value + 5}); // => Some(10)
     *
     * // compare .map() and .flatMap():
     * const mapFn = (value: number) => some(value + 5);
     * const c = a.map(mapFn);     // => Some(Some(10))
     * const d = a.flatMap(mapFn); // => Some(10)
     * ```
     * @template `U` - The type of the result of the mapping.
     * @param {Function} `fn` - The function to apply to the value inside the Option.
     * @returns {Option<U>} a new `Option<U>` wrapping the mapped value.
     */
    map<U>(fn: (value: T) => U): Option<U>;

    /**
     * Uses the function `fn` to map a value of type `T`, stored inside of the `Option<T>`, to a value of type `Option<U>` and returns it. The difference from a `.map()` is that `.flatMap()` does not wrap the mapped value `U` into the Option by itself, but rather requires a function `fn` to take care of it.
     * ```typescript
     * // Example
     * const a = some(5);
     * const b = a.flatMap((value) => some(value + 5)); // Some(10)
     *
     * // compare .map() and .flatMap():
     * const mapFn = (value: number) => some(value + 5);
     * const c = a.map(mapFn);     // Some(Some(10))
     * const d = a.flatMap(mapFn); // Some(10)
     * ```
     * @param {Function} The function `fn` that takes the value inside the `Option<T>` and returns a new `Option<U>`.
     * @returns {Option<U>} A new `Option<U>` wrapping the result of the flatMap operation.
     *
     */
    flatMap<U>(fn: (value: T) => Option<U>): Option<U>;

    andThen<U>(fn: (value: T) => Option<U>): Option<U>;

    /**
     * **UNSAFE** method for extracting value `T` from an `Option<T>`.
     *  - If the `Option<T>` is `Some<T>` => returns value `T`
     *  - If the `Option<T>` is `None` => throws a Error
     *
     * Should not be used in production
     * ```typescript
     * // Example
     * const a = some(5);
     * const b = none;
     * const unwrappedA = a.unwrap(); // 5
     * const unwrappedB = a.unwrap(); // Throws error
     * ```
     * @returns {T} The value inside the Option.
     * @throws {Error} If the Option is a None, an error is thrown.
     */
    unwrap(): T;

    /**
     * safe method for extracting value `T` from an `Option<T>`.
     *  - If the `Option<T>` is `Some<T>` => returns value `T`
     *  - If the `Option<T>` is `None` => returns `defaultValue`
     * ```typescript
     * // Example
     * const a = some(5);
     * const b = none;
     * const unwrappedA = a.getOrElse(10); // 5
     * const unwrappedB = a.getOrElse(10); // 10
     * ```
     * @param {T} defaultValue The value to return if the Option is a None.
     * @returns {T} The value `T` inside the Option if it's a `Some<T>`, otherwise the `defaultValue`.
     */
    unwrapOr<U>(defaultValue: U): T | U;

    unwrapOrElse<U>(fn: () => U): T | U;

    or<U>(optb: Option<U>): Option<T | U>;

    orElse<U>(fn: () => Option<U>): Option<T | U>;

    /**
     * Matches on the `Option<T>` and applies the corresponding function for `Some<T>` or `None`.
     *  - If the `Option<T>` is `Some<T>` => applies the first function `some`
     *  - If the Option is None           => applies the second function `none`
     * ```typescript
     * // Example
     * const a = some(5);
     * const b = none;
     *
     * const someFn = (value) => value + 3;
     * const noneFn =  () => 10
     *
     * const matchedA = a.match(someFn, noneFn) // 8
     * const matchedB = b.match(someFn, noneFn) // 10
     * ```
     * @param {Function} some The function to apply if the Option is a Some.
     * @param {Function} none The function to apply if the Option is a None.
     * @returns {unknown} The result of the matching function applied.
     */
    match<A, B = A>(some: (value: T) => A, none: () => B): A | B;

    toNullable(): T | null;

    toBoolean(): boolean;

    okOrElse<E>(errFn: () => E): Result<T, E>;
}

/**
 * Represents a `Some<T>` value in the Option type, wrapping a value of type `T`.
 * @template `T` The type of the value inside the `Some<T>`.
 */
export class Some<T> implements IOption<T> {
    public readonly tag = "Some";

    /**
     * Creates a new `Some<T>` instance.
     * @param {T} The value `T` to wrap inside the `Some<T>`.
     */
    constructor(public readonly value: T) {
        Object.defineProperties(this, {
            tag: {
                writable: false,
                enumerable: false,
            },
        });
    }

    isSome(): this is Some<T> {
        return true;
    }

    ifSome(fn: (value: T) => void): this {
        fn(this.value);
        return this;
    }

    isNone(): this is None<T> {
        return false;
    }

    ifNone(fn: () => void): Option<T> {
        return this;
    }

    map<U>(fn: (value: T) => U): Option<U> {
        return new Some(fn(this.value));
    }

    flatMap<U>(fn: (value: T) => Option<U>): Option<U> {
        return fn(this.value);
    }

    andThen<U>(fn: (value: T) => Option<U>): Option<U> {
        return fn(this.value);
    }

    unwrap(): T {
        return this.value;
    }

    unwrapOr<U>(defaultValue: U): T | U {
        return this.value;
    }

    unwrapOrElse<U>(fn: () => U): T | U {
        return this.value;
    }

    or<U>(optb: Option<U>): Option<T | U> {
        return this;
    }

    orElse<U>(fn: () => Option<U>): Option<T | U> {
        return this;
    }

    match<A, B = A>(some: (value: T) => A, none: () => B): A | B {
        return some(this.value);
    }

    toJSON() {
        return {
            //_tag: this._tag,
            value: this.value,
        };
    }

    toString() {
        return `Some(${this.value})`;
    }

    toNullable(): T | null {
        return this.value;
    }

    toBoolean(): boolean {
        return true;
    }

    okOrElse<E>(errFn: () => E): Result<T, E> {
        return ok<T, E>(this.value);
    }
}

/**
 * Represents a `None` value in the Option type, indicating no value.
 * @template `T` The type that would be inside the Option, but it is not used because this is a None.
 */
export class None<T> implements IOption<T> {
    public readonly tag = "None";

    /**
     * Creates a new `None` instance.
     */
    constructor() {
        Object.defineProperties(this, {
            tag: {
                writable: false,
                enumerable: false,
            },
        });
    }

    isSome(): this is Some<T> {
        return false;
    }

    ifSome(fn: (value: T) => void): Option<T> {
        return this;
    }

    isNone(): this is None<T> {
        return true;
    }

    ifNone(fn: () => void): Option<T> {
        fn();
        return this;
    }

    map<U>(fn: (value: T) => U): Option<U> {
        return new None<U>();
    }

    andThen<U>(fn: (value: T) => Option<U>): Option<U> {
        return none;
    }

    flatMap<U>(fn: (value: T) => Option<U>): Option<U> {
        return new None<U>();
    }

    unwrap(): T {
        throw new Error("Tried to unwrap a non-existent value");
    }

    unwrapOr<U>(defaultValue: U): T | U {
        return defaultValue;
    }

    unwrapOrElse<U>(fn: () => U): T | U {
        return fn();
    }

    or<U>(optb: Option<U>): Option<T | U> {
        return optb;
    }

    orElse<U>(fn: () => Option<U>): Option<T | U> {
        return fn();
    }

    match<A, B = A>(some: (value: T) => A, none: () => B): A | B {
        return none();
    }

    toJSON() {
        return {
            _tag: this._tag,
        };
    }

    toString() {
        return `None`;
    }

    toNullable(): T | null {
        return null;
    }

    toBoolean(): boolean {
        return false;
    }

    okOrElse<E>(errFn: () => E): Result<T, E> {
        return err<T, E>(errFn());
    }
}

export type Option<T> = Some<T> | None<T>;

/**
 * Creates a new `Some` instance wrapping a `value`.
 * This function is a convenience method for creating `Some` values.
 * ```typescript
 * // Example
 * const a = some(5);    // Some(5)
 * const b = some("foo") // Some("foo")
 * const c = none
 *
 * console.log(a) // Some { _tag: "Some", value: 5 }
 * console.log(b) // Some { _tag: "Some", value: "foo" }
 * console.log(c) // None { _tag: "None" }
 *
 * // Accessing the value stored inside:
 * const valueA = a.unwrap();         // 5     | unsafe method
 * const valueB = b.getOrElse("bar"); // "foo" | safe method
 * const valueC = c.getOrElse("bar"); // "bar" | safe method
 *
 * console.log(valueA) // 5
 * console.log(valueB) // "foo"
 * console.log(valueC) // "bar"
 *
 * const unsafe = c.unwrap() // throws Error
 * ```
 * @template `T` The type of the value being wrapped in the `Some`.
 * @param {T} `value` The value to wrap inside the `Some`.
 * @returns {Option<T>} A new `Some<T>` instance wrapping the provided value.
 */
export function some<T>(value: T): Option<T>;
export function some<T extends void = void>(value: void): Option<T>;
export function some<T>(value: T): Option<T> {
    return new Some(value);
}

/**
 * A singleton representing a `None` instance.
 * This is used to represent the absence of a value and is often used as the default value for Option types.
 * ```typescript
 * // Example
 * const a = some(5);
 * const b = none;
 *
 * const valueA = a.unwrap() // 5
 * const valueB = b.unwrap() // throws a error
 *
 * const valueA = a.getOrElse(10) // 5
 * const valueB = a.getOrElse(10) // 10
 * ```
 */
export const none = new None<never>();

export function fromNullableVal<T>(value: T): Option<NonNullable<T>> {
    if (!value) {
        return none;
    }
    return some(value);
}
