'use strict';

function generatePromish(Base) {
  function isErrorClass(type) {
    while (type && (type !== Object)) {
      if ((type === Error) || (type instanceof Error)) {
        return true;
      }
      type = type.prototype;
    }
    return false;
  }

  class Promish extends Base {
    constructor(f) {
      if (f instanceof Promish) {
        return f;
      } else if ((f instanceof Promise) || (f.then instanceof Function)) {
        super((resolve, reject) => f.then(resolve, reject));
      } else if (f instanceof Error) {
        // sugar for 'rethrow'
        super((resolve, reject) => reject(f));
      } else if (f instanceof Function) {
        super(f);
      } else {
        // anything else, resolve with value
        super(resolve => resolve(f));
      }
    }

    finally(h) {
      return this.then(
        value => Promish.resolve(h()).then(() => value),
        error => Promish.resolve(h()).then(() => Promish.reject(error))
      );
    }

    catch() {
      // extend catch with type-aware or matcher handling
      var args = Array.from(arguments);
      var h = args.pop();
      return this.then(undefined, function(error) {
        // default catch - no matchers. Just return handler result
        if (!args.length) {
          return h(error);
        }

        //console.log('catch matcher', error)
        // search for a match in argument order and return handler result if found
        for (var i = 0; i < args.length; i++) {
          var matcher = args[i];
          if (isErrorClass(matcher)) {
            if (error instanceof matcher) {
              return h(error);
            }
          } else if (matcher instanceof Function) {
            //console.log('matcher function')
            if (matcher(error)) {
              //console.log('matched!!')
              return h(error);
            }
          }
        }

        // no match was found send this error to the next promise handler in the chain
        return new Promish((resolve, reject) => reject(error));
      });
    }

    delay(timeout) {
      return this.then(function(value) {
        return new Promish(function(resolve) {
          setTimeout(function() {
            resolve(value);
          }, timeout);
        });
      });
    }

    map(f) {
      return this.then(function(values) {
        return Promish.map(values, f);
      });
    }

    static map(values, f) {
      return Promish.all(
        values.map((v,i) => Promish.resolve(v).then(v2 => f(v2, i, values.length)))
      );
    }

    reduce(f, val0) {
      return this.then(function(values) {
        return Promish.reduce(values, f, val0);
      });
    }

    static reduce(values, f, val0) {
      var promise;
      var count = 0;
      if (val0 !== undefined) {
        promise = Promish.resolve(val0);
      } else if (values.length > 1) {
        promise = Promish.resolve(values[count++]);
      } else {
        return Promish.resolve(values[0]);
      }
      while (count < values.length) (function(i) {
        promise = promise.then(function(value) {
          return Promish.resolve(values[i]).then(v2 => f(value, v2, i));
        });
      })(count++);
      return promise;
    }

    spread(f) {
      return this.then(function(values) {
        return Promish.all(values);
      }).then(function(values) {
        return f.apply(undefined, values);
      });
    }

    static delay(timeout, value) {
      return new Promish(resolve => {
        setTimeout(function() {
          resolve(value);
        }, timeout);
      });
    }

    static resolve(value) {
      return new Promish(resolve => {
        resolve(value);
      });
    }

    static reject(error) {
      return new Promish((resolve, reject) => {
        reject(error);
      });
    }

    static sequence(fns, initialValue) {
      // convenience function that calls functions in sequence, each one waiting for the previous
      let promise = Promish.resolve(initialValue);
      for (let i = 0; i < fns.length; i++) {
        promise = promise.then(fns[i]);
      }
      return promise;
    }

    // Wrap a synchronous method and resolve with its return value
    static method(f) {
      return function() {
        var self = this; // is this necessary?
        var args = Array.from(arguments);
        return new Promish(resolve => resolve(f.apply(self, args)));
      };
    }

    //
    static apply(f, args) {
      // take a copy of args because a) might not be Array and b) no side-effects
      args = Array.from(args);
      return new Promish(function(resolve, reject) {
        args.push(function () {
          var error = Array.prototype.shift.apply(arguments);
          if (error) {
            reject(error);
          } else {
            if (arguments.length === 1) {
              resolve(arguments[0]);
            } else {
              resolve(arguments);
            }
          }
        });
        f.apply(undefined, args);
      });
    }
    static nfapply(f, args) {
      return Promish.apply(f, args);
    }

    static call() {
      var f = Array.prototype.shift.apply(arguments);
      return Promish.apply(f, arguments);
    }
    static nfcall() {
      return Promish.call.apply(null, arguments);
    }

    static post(o, f, a) {
      return Promish.apply(f.bind(o), a);
    }
    static npost(o,f,a) {
      return Promish.apply(f.bind(o), a);
    }

    static invoke() {
      var o = Array.prototype.shift.apply(arguments);
      var f = Array.prototype.shift.apply(arguments);
      return Promish.apply(f.bind(o), arguments);
    }
    static ninvoke() {
      return Promish.invoke(arguments);
    }

    // create curry function for nfcall
    static promisify(f) {
      return function() {
        return Promish.apply(f, arguments);
      };
    }
    static denodify(f) {
      return Promish.promisify(f);
    }

    // create Q based curry function for ninvoke
    static nbind(f, o) {
      // Why is it function, object and not object, function like the others?
      return function() {
        return Promish.post(o, f, arguments);
      };
    }

    // curry function for ninvoke with arguments in object, method order
    static bind(o, f) {
      return function() {
        return Promish.post(o, f, arguments);
      };
    }

    // Promishify every method in an object
    static promisifyAll(o, options) {
      options = options || {};
      var inPlace = options.inPlace || false;
      var suffix = options.suffix || (inPlace ? 'Async' : '');

      var p = {};
      var oo = o;
      while (oo && (oo !== Object)) {
        for (let i in oo) {
          if (!p[i + suffix] && (oo[i] instanceof Function)) {
            p[i + suffix] = Promish.bind(o, oo[i]);
          }
        }
        oo = Object.getPrototypeOf(oo) || oo.prototype;
      }

      if (inPlace) {
        for (let i in p) {
          if(p[i] instanceof Function) {
            o[i] = p[i];
          }
        }
        p = o;
      }

      return p;
    }

    static all(promises) {
      return new Promish(Promise.all(promises));
    }

    // some - the first n to resolve, win - else reject with all of the errors
    static some(promises, n) {
      return new Promish(function(resolve, reject) {
        var values = [];
        var rejects = [];
        promises.forEach(function(promise) {
          promise
            .then(function(value) {
              values.push(value);
              if (values.length >= n) {
                resolve(values);
              }
            })
            .catch(function(error) {
              rejects.push(error);
              if (rejects.length > promises.length - n){
                reject(rejects);
              }
            });
        });
      });
    }

    // any - the first to resolve, wins - else reject with all of the errors
    static any(promises) {
      return Promish.some(promises, 1)
        .then(function(values) {
          return values[0];
        });
    }

    // old-style for ease of adoption
    static defer() {
      var deferred = {};
      deferred.promise = new Promish(function(resolve, reject) {
        deferred.resolve = resolve;
        deferred.reject = reject;
      });
      return deferred;
    }

    // spread - apply array of values to function as args
    static spread(value, f) {
      return f.apply(undefined, value);
    }
  }

  return Promish;
};
module.exports = generatePromish;