/**
 * maryamyriameliamurphies.js
 * A library of Haskell-style morphisms ported to ES2015 JavaScript. 
 *
 * monad.js
 *
 * @file Monad type class. 
 * @license ISC
 */

/** @module monad */

import {
  partial,
  id
} from './base';

import {then} from './applicative';

import {
  defines,
  dataType
} from './type';

import {error} from './error';

/**
 * A monad is an abstract datatype of sequential actions.  Instances of `Monad` must define a
 * `flatMap` method as well as all the required methods for `Functor` and `Applicative`. 
 * @param {*} - Any object
 * @returns {boolean} `true` if an object is an instance of `Monad` and `false` otherwise
 * @kind function
 */
export const Monad = defines(`fmap`, `pure`, `ap`, `flatMap`);

/**
 * Inject a value into a monadic context. 
 * <br>`Haskell> return :: a -> m a`
 * @param {Object} m - A monad
 * @param {*} a - The value to inject
 * @returns {Object} A new monad of the same type with the value injected
 * @kind function
 */
export const inject = (m, a) => {
  const inject_ = (m, a) => Monad(m) ?  dataType(m).pure(a) : error.typeError(m, inject);
  return partial(inject_, m, a);
}

/**
 * Sequentially compose two actions, passing the value produced by the first action as an argument
 * to the second action.  This function is variously referred to as bind, map, and flatMap.  This
 * library uses `flatMap` in order to avoid conflicts with JavaScript's built-in `bind` and `map`
 * methods. 
 * <br>`Haskell> (>>=) :: m a -> (a -> m b) -> m b`
 * @param {Object} m - A monad
 * @param {Function} f - A function to map over the injected value of the monad.  This function must
 * also return a monad
 * @returns {Object} A new monad of the same type, the result of mapping the function over the
 * original, injected value
 * @kind function
 */
export const flatMap = (m, f) => {
  const flatMap_ = (m, f) => Monad(m) ?  dataType(m).flatMap(m, f) : error.typeError(m, flatMap);
  return partial(flatMap_, m, f);
}

/**
 * Sequentially compose two actions, discarding any value produced by the first, like sequencing
 * operators (such as the semicolon) in imperative languages. 
 * <br>`Haskell> (>>) :: m a -> m b -> m b`
 * @param {Object} m - A monad
 * @param {Function} f - A function to call that ignores the injected value of the monad
 * @returns {Object} A new monad of the same type, the result of calling the function
 * @kind function
 *
 */
export const chain = (m, f) => {
  const chain_ = (m, f) => Monad(m) ?  then(m, f) : error.typeError(m, chain);
  return partial(chain_, m, f);
}

/**
 * The same as `flatMap` but with the arguments interchanged. 
 * <br>`Haskell> (=<<) :: Monad m => (a -> m b) -> m a -> m b`
 * @param {Function} f - A function to map over the injected value of the monad
 * @param {Object} m - A monad
 * @returns {Object} A new monad of the same type, the result of mapping the function over the
 * original, injected value
 * @kind function
 */
export const flatMapFlip = (f, m) => {
  const flatMapFlip_ = (f, m) => flatMap(m, f);
  return partial(flatMapFlip_, f, m);
}

/**
 * Remove one level of monadic structure from a monad, projecting its bound argument into the outer
 * level. 
 * <br>`Haskell> join :: Monad m => m (m a) -> m a`
 * @param {Object} m - A monad (wrapping another monad)
 * @returns {Object} The wrapped monad on its own
 * @kind function
 * @example
 * const m = just(10); // => Just 10
 * const n = just(m);  // => Just Just 10
 * join(n);            // => Just 10
 * join(m);            // => *** Error: 'Just 10' is not a valid argument to function 'join'. 
 */
export const join = m =>
  Monad(m) && Monad(flatMap(m, id)) ?  flatMap(m, id) : error.typeError(m, join);

/**
 * Convert, or lift, a function into a monad. 
 * <br>`Haskell> liftM :: Monad m => (a1 -> r) -> m a1 -> m r`
 * @param {Function} f - The function to convert into a monad
 * @param {Object} m - The monad to convert the function into
 * @returns {Object} A new monad containing the result of mapping the function over the monad
 * @kind function
 * @example
 * const mb = just(1);
 * const lst = list(1,2,3);
 * const doubleJust = x => just(x * 2);
 * const doubleList = x => list(x * 2);
 * liftM(doubleJust, mb);               // => Just Just 2
 * liftM(doubleList, lst);              // => [[2:[]]:[4:[]]:[6:[]]:[]]
 */
export const liftM = (f, m) => {
  const liftM_ = (f, m) => Monad(m) ?  dataType(m).fmap(f, m) : error.typeError(m, liftM);
  return partial(liftM_, f, m)
}

/**
 * Since there is no way to exactly replicate Haskell's "do" notation for monadic chaining, and it
 * would be useful and instructive for this library to implement a similar affordance, this class
 * provides an analogous mechanism.  See the `Do` function below for an example of how it works. 
 * @kind class
 * @alias module:monad. DoBlock
 * @private
 */
class DoBlock {
 /**
  * Create a new monadic context for chaining actions. 
  * @param {Object} m - A monad, the context for the actions
  */
  constructor(m) { this.m = () => m; }
  inject(a) { return Do(dataType(this.m()).pure(a)); }
  flatMap(f) { return Do(flatMap(this.m(), f)); }
  chain(f) { return Do(chain(this.m(), f)); }
  valueOf() { return `${this.m().typeOf()} >>= ${this.m().valueOf()}`; }
}

/**
 * Wrap a monad in a special container for the purpose of chaining actions, in imitation of the
 * syntactic sugar provided by Haskell's "do" notation. 
 * @param {Object} m - A monad
 * @returns {DoBlock} A monadic context in which to chain actions
 * @kind function
 * @example
 * const j = just(10);
 * const doubleJust = x => just(x * 2);
 * const minusOne = x => just(x - 1);
 * const lst = list(1,2,3);
 * const plusOne = x => list(x + 1);
 * const doubleList = x => list(x * 2);
 * const put = x => {
 *   print(x);
 *   return just(x);
 * }
 * const b1 = Do(j).flatMap(doubleJust).flatMap(minusOne);
 * const b2 = Do(j).flatMap(doubleJust).chain(j).flatMap(minusOne);
 * const b3 = Do(lst).flatMap(plusOne).flatMap(doubleList);
 * const b4 = Do(lst).flatMap(plusOne).chain(lst).flatMap(doubleList);
 * const b5 = Do(j).inject(100);
 * const b6 = Do(lst).inject(100);
 * print(b1);           // => Maybe number >>= Just 19
 * print(b2);           // => Maybe number >>= Just 9
 * print(b3);           // => [number] >>= [4:6:8:[]]
 * print(b4);           // => [number] >>= [2:4:6:2:4:6:2:4:6:[]]
 * print(b5);           // => Maybe number >>= Just 100
 * print(b6);           // => [number] >>= [100:[]]
 * Do(j)
 * .flatMap(put)        // => 10
 * .flatMap(doubleJust)
 * .flatMap(put)        // => 20
 * .chain(j)
 * .flatMap(put)        // => 10
 * .flatMap(minusOne)
 * .flatMap(put)        // => 9
 * .flatMap(doubleJust)
 * .flatMap(put);       // => 18
 */
export const Do = m => Monad(m) ?  new DoBlock(m) : error.typeError(Do, m);
