<?php

/*
 * Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace PhpOption;

use ArrayAccess;
use IteratorAggregate;

/**
 * @template T
 *
 * @implements IteratorAggregate<T>
 */
abstract class Option implements IteratorAggregate
{
    /**
     * Creates an option given a return value.
     *
     * This is intended for consuming existing APIs and allows you to easily
     * convert them to an option. By default, we treat ``null`` as the None
     * case, and everything else as Some.
     *
     * @template S
     *
     * @param S $value     The actual return value.
     * @param S $noneValue The value which should be considered "None"; null by
     *                     default.
     *
     * @return Option<S>
     */
    public static function fromValue($value, $noneValue = null)
    {
        if ($value === $noneValue) {
            return None::create();
        }

        return new Some($value);
    }

    /**
     * Creates an option from an array's value.
     *
     * If the key does not exist in the array, the array is not actually an
     * array, or the array's value at the given key is null, None is returned.
     * Otherwise, Some is returned wrapping the value at the given key.
     *
     * @template S
     *
     * @param array<string,S>|ArrayAccess<string,S>|null $array A potential array or \ArrayAccess value.
     * @param string                                     $key   The key to check.
     *
     * @return Option<S>
     */
    public static function fromArraysValue($array, $key)
    {
        if (!(is_array($array) || $array instanceof ArrayAccess) || !isset($array[$key])) {
            return None::create();
        }

        return new Some($array[$key]);
    }

    /**
     * Creates a lazy-option with the given callback.
     *
     * This is also a helper constructor for lazy-consuming existing APIs where
     * the return value is not yet an option. By default, we treat ``null`` as
     * None case, and everything else as Some.
     *
     * @template S
     *
     * @param callable $callback  The callback to evaluate.
     * @param array    $arguments The arguments for the callback.
     * @param S        $noneValue The value which should be considered "None";
    *                             null by default.
     *
     * @return LazyOption<S>
     */
    public static function fromReturn($callback, array $arguments = [], $noneValue = null)
    {
        return new LazyOption(function () use ($callback, $arguments, $noneValue) {
            /** @var mixed */
            $return = call_user_func_array($callback, $arguments);

            if ($return === $noneValue) {
                return None::create();
            }

            return new Some($return);
        });
    }

    /**
     * Option factory, which creates new option based on passed value.
     *
     * If value is already an option, it simply returns. If value is callable,
     * LazyOption with passed callback created and returned. If Option
     * returned from callback, it returns directly. On other case value passed
     * to Option::fromValue() method.
     *
     * @template S
     *
     * @param Option<S>|callable|S $value
     * @param S                    $noneValue Used when $value is mixed or
     *                                        callable, for None-check.
     *
     * @return Option<S>|LazyOption<S>
     */
    public static function ensure($value, $noneValue = null)
    {
        if ($value instanceof self) {
            return $value;
        } elseif (is_callable($value)) {
            return new LazyOption(function () use ($value, $noneValue) {
                /** @var mixed */
                $return = $value();

                if ($return instanceof self) {
                    return $return;
                } else {
                    return self::fromValue($return, $noneValue);
                }
            });
        } else {
            return self::fromValue($value, $noneValue);
        }
    }

    /**
     * Lift a function so that it accepts Option as parameters.
     *
     * We return a new closure that wraps the original callback. If any of the
     * parameters passed to the lifted function is empty, the function will
     * return a value of None. Otherwise, we will pass all parameters to the
     * original callback and return the value inside a new Option, unless an
     * Option is returned from the function, in which case, we use that.
     *
     * @template S
     *
     * @param callable $callback
     * @param mixed    $noneValue
     *
     * @return callable
     */
    public static function lift($callback, $noneValue = null)
    {
        return function () use ($callback, $noneValue) {
            /** @var array<int, mixed> */
            $args = func_get_args();

            $reduced_args = array_reduce(
                $args,
                /** @param bool $status */
                function ($status, self $o) {
                    return $o->isEmpty() ? true : $status;
                },
                false
            );
            // if at least one parameter is empty, return None
            if ($reduced_args) {
                return None::create();
            }

            $args = array_map(
                /** @return T */
                function (self $o) {
                    // it is safe to do so because the fold above checked
                    // that all arguments are of type Some
                    /** @var T */
                    return $o->get();
                },
                $args
            );

            return self::ensure(call_user_func_array($callback, $args), $noneValue);
        };
    }

    /**
     * Returns the value if available, or throws an exception otherwise.
     *
     * @throws \RuntimeException If value is not available.
     *
     * @return T
     */
    abstract public function get();

    /**
     * Returns the value if available, or the default value if not.
     *
     * @template S
     *
     * @param S $default
     *
     * @return T|S
     */
    abstract public function getOrElse($default);

    /**
     * Returns the value if available, or the results of the callable.
     *
     * This is preferable over ``getOrElse`` if the computation of the default
     * value is expensive.
     *
     * @template S
     *
     * @param callable():S $callable
     *
     * @return T|S
     */
    abstract public function getOrCall($callable);

    /**
     * Returns the value if available, or throws the passed exception.
     *
     * @param \Exception $ex
     *
     * @return T
     */
    abstract public function getOrThrow(\Exception $ex);

    /**
     * Returns true if no value is available, false otherwise.
     *
     * @return bool
     */
    abstract public function isEmpty();

    /**
     * Returns true if a value is available, false otherwise.
     *
     * @return bool
     */
    abstract public function isDefined();

    /**
     * Returns this option if non-empty, or the passed option otherwise.
     *
     * This can be used to try multiple alternatives, and is especially useful
     * with lazy evaluating options:
     *
     * ```php
     *     $repo->findSomething()
     *         ->orElse(new LazyOption(array($repo, 'findSomethingElse')))
     *         ->orElse(new LazyOption(array($repo, 'createSomething')));
     * ```
     *
     * @param Option<T> $else
     *
     * @return Option<T>
     */
    abstract public function orElse(self $else);

    /**
     * This is similar to map() below except that the return value has no meaning;
     * the passed callable is simply executed if the option is non-empty, and
     * ignored if the option is empty.
     *
     * In all cases, the return value of the callable is discarded.
     *
     * ```php
     *     $comment->getMaybeFile()->ifDefined(function($file) {
     *         // Do something with $file here.
     *     });
     * ```
     *
     * If you're looking for something like ``ifEmpty``, you can use ``getOrCall``
     * and ``getOrElse`` in these cases.
     *
     * @deprecated Use forAll() instead.
     *
     * @param callable(T):mixed $callable
     *
     * @return void
     */
    abstract public function ifDefined($callable);

    /**
     * This is similar to map() except that the return value of the callable has no meaning.
     *
     * The passed callable is simply executed if the option is non-empty, and ignored if the
     * option is empty. This method is preferred for callables with side-effects, while map()
     * is intended for callables without side-effects.
     *
     * @param callable(T):mixed $callable
     *
     * @return Option<T>
     */
    abstract public function forAll($callable);

    /**
     * Applies the callable to the value of the option if it is non-empty,
     * and returns the return value of the callable wrapped in Some().
     *
     * If the option is empty, then the callable is not applied.
     *
     * ```php
     *     (new Some("foo"))->map('strtoupper')->get(); // "FOO"
     * ```
     *
     * @template S
     *
     * @param callable(T):S $callable
     *
     * @return Option<S>
     */
    abstract public function map($callable);

    /**
     * Applies the callable to the value of the option if it is non-empty, and
     * returns the return value of the callable directly.
     *
     * In contrast to ``map``, the return value of the callable is expected to
     * be an Option itself; it is not automatically wrapped in Some().
     *
     * @template S
     *
     * @param callable(T):Option<S> $callable must return an Option
     *
     * @return Option<S>
     */
    abstract public function flatMap($callable);

    /**
     * If the option is empty, it is returned immediately without applying the callable.
     *
     * If the option is non-empty, the callable is applied, and if it returns true,
     * the option itself is returned; otherwise, None is returned.
     *
     * @param callable(T):bool $callable
     *
     * @return Option<T>
     */
    abstract public function filter($callable);

    /**
     * If the option is empty, it is returned immediately without applying the callable.
     *
     * If the option is non-empty, the callable is applied, and if it returns false,
     * the option itself is returned; otherwise, None is returned.
     *
     * @param callable(T):bool $callable
     *
     * @return Option<T>
     */
    abstract public function filterNot($callable);

    /**
     * If the option is empty, it is returned immediately.
     *
     * If the option is non-empty, and its value does not equal the passed value
     * (via a shallow comparison ===), then None is returned. Otherwise, the
     * Option is returned.
     *
     * In other words, this will filter all but the passed value.
     *
     * @param T $value
     *
     * @return Option<T>
     */
    abstract public function select($value);

    /**
     * If the option is empty, it is returned immediately.
     *
     * If the option is non-empty, and its value does equal the passed value (via
     * a shallow comparison ===), then None is returned; otherwise, the Option is
     * returned.
     *
     * In other words, this will let all values through except the passed value.
     *
     * @param T $value
     *
     * @return Option<T>
     */
    abstract public function reject($value);

    /**
     * Binary operator for the initial value and the option's value.
     *
     * If empty, the initial value is returned. If non-empty, the callable
     * receives the initial value and the option's value as arguments.
     *
     * ```php
     *
     *     $some = new Some(5);
     *     $none = None::create();
     *     $result = $some->foldLeft(1, function($a, $b) { return $a + $b; }); // int(6)
     *     $result = $none->foldLeft(1, function($a, $b) { return $a + $b; }); // int(1)
     *
     *     // This can be used instead of something like the following:
     *     $option = Option::fromValue($integerOrNull);
     *     $result = 1;
     *     if ( ! $option->isEmpty()) {
     *         $result += $option->get();
     *     }
     * ```
     *
     * @template S
     *
     * @param S                $initialValue
     * @param callable(S, T):S $callable
     *
     * @return S
     */
    abstract public function foldLeft($initialValue, $callable);

    /**
     * foldLeft() but with reversed arguments for the callable.
     *
     * @template S
     *
     * @param S                $initialValue
     * @param callable(T, S):S $callable
     *
     * @return S
     */
    abstract public function foldRight($initialValue, $callable);
}
