/**
 * This is a straight rip-off of the React.js ReactPropTypes.js proptype validators,
 * modified to make it possible to validate Immutable.js data.
 *     ImmutableTypes.listOf is patterned after React.PropTypes.arrayOf, but for Immutable.List
 *     ImmutableTypes.shape  is based on React.PropTypes.shape, but for any Immutable.Iterable
 */
var Immutable = require('immutable');

var ANONYMOUS = '<<anonymous>>';

var ImmutablePropTypes;

if (process.env.NODE_ENV !== 'production') {
  ImmutablePropTypes = {
    listOf:             createListOfTypeChecker,
    mapOf:              createMapOfTypeChecker,
    orderedMapOf:       createOrderedMapOfTypeChecker,
    setOf:              createSetOfTypeChecker,
    orderedSetOf:       createOrderedSetOfTypeChecker,
    stackOf:            createStackOfTypeChecker,
    iterableOf:         createIterableOfTypeChecker,
    recordOf:           createRecordOfTypeChecker,
    shape:              createShapeChecker,
    contains:           createShapeChecker,
    mapContains:        createMapContainsChecker,
    orderedMapContains: createOrderedMapContainsChecker,
    // Primitive Types
    list:       createImmutableTypeChecker('List', Immutable.List.isList),
    map:        createImmutableTypeChecker('Map', Immutable.Map.isMap),
    orderedMap: createImmutableTypeChecker('OrderedMap', Immutable.OrderedMap.isOrderedMap),
    set:        createImmutableTypeChecker('Set', Immutable.Set.isSet),
    orderedSet: createImmutableTypeChecker('OrderedSet', Immutable.OrderedSet.isOrderedSet),
    stack:      createImmutableTypeChecker('Stack', Immutable.Stack.isStack),
    seq:        createImmutableTypeChecker('Seq', Immutable.Seq.isSeq),
    record:     createImmutableTypeChecker('Record', function(isRecord) { return isRecord instanceof Immutable.Record; }),
    iterable:   createImmutableTypeChecker('Iterable', Immutable.Iterable.isIterable)
  };
} else {
  var productionTypeChecker = function() {
    invariant(
      false,
      'ImmutablePropTypes type checking code is stripped in production.'
    );
  };
  productionTypeChecker.isRequired = productionTypeChecker;
  var getProductionTypeChecker = function () { return productionTypeChecker };

  ImmutablePropTypes = {
    listOf:             getProductionTypeChecker,
    mapOf:              getProductionTypeChecker,
    orderedMapOf:       getProductionTypeChecker,
    setOf:              getProductionTypeChecker,
    orderedSetOf:       getProductionTypeChecker,
    stackOf:            getProductionTypeChecker,
    iterableOf:         getProductionTypeChecker,
    recordOf:           getProductionTypeChecker,
    shape:              getProductionTypeChecker,
    contains:           getProductionTypeChecker,
    mapContains:        getProductionTypeChecker,
    orderedMapContains: getProductionTypeChecker,
    // Primitive Types
    list:       productionTypeChecker,
    map:        productionTypeChecker,
    orderedMap: productionTypeChecker,
    set:        productionTypeChecker,
    orderedSet: productionTypeChecker,
    stack:      productionTypeChecker,
    seq:        productionTypeChecker,
    record:     productionTypeChecker,
    iterable:   productionTypeChecker
  };
}

ImmutablePropTypes.iterable.indexed = createIterableSubclassTypeChecker('Indexed', Immutable.Iterable.isIndexed);
ImmutablePropTypes.iterable.keyed = createIterableSubclassTypeChecker('Keyed', Immutable.Iterable.isKeyed);

function getPropType(propValue) {
  var propType = typeof propValue;
  if (Array.isArray(propValue)) {
    return 'array';
  }
  if (propValue instanceof RegExp) {
    // Old webkits (at least until Android 4.0) return 'function' rather than
    // 'object' for typeof a RegExp. We'll normalize this here so that /bla/
    // passes PropTypes.object.
    return 'object';
  }
  if (propValue instanceof Immutable.Iterable) {
    return 'Immutable.' + propValue.toSource().split(' ')[0];
  }
  return propType;
}

function createChainableTypeChecker(validate) {
  function checkType(isRequired, props, propName, componentName, location, propFullName, ...rest) {
    propFullName = propFullName || propName;
    componentName = componentName || ANONYMOUS;
    if (props[propName] == null) {
      var locationName = location;
      if (isRequired) {
        return new Error(
          `Required ${locationName} \`${propFullName}\` was not specified in ` +
          `\`${componentName}\`.`
        );
      }
    } else {
      return validate(props, propName, componentName, location, propFullName, ...rest);
    }
  }

  var chainedCheckType = checkType.bind(null, false);
  chainedCheckType.isRequired = checkType.bind(null, true);

  return chainedCheckType;
}

function createImmutableTypeChecker(immutableClassName, immutableClassTypeValidator) {
  function validate(props, propName, componentName, location, propFullName) {
    var propValue = props[propName];
    if(!immutableClassTypeValidator(propValue)) {
      var propType = getPropType(propValue);
      return new Error(
        `Invalid ${location} \`${propFullName}\` of type \`${propType}\` ` +
        `supplied to \`${componentName}\`, expected \`${immutableClassName}\`.`
      );
    }
    return null;
  }
  return createChainableTypeChecker(validate);
}

function createIterableSubclassTypeChecker(subclassName, validator) {
  return createImmutableTypeChecker(`Iterable.${subclassName}`, (propValue) =>
    Immutable.Iterable.isIterable(propValue) && validator(propValue)
  );
}

function createIterableTypeChecker(typeChecker, immutableClassName, immutableClassTypeValidator) {

  function validate(props, propName, componentName, location, propFullName, ...rest) {
    var propValue = props[propName];
    if (!immutableClassTypeValidator(propValue)) {
      var locationName = location;
      var propType = getPropType(propValue);
      return new Error(
        `Invalid ${locationName} \`${propFullName}\` of type ` +
        `\`${propType}\` supplied to \`${componentName}\`, expected an Immutable.js ${immutableClassName}.`
      );
    }

    if (typeof typeChecker !== 'function') {
      return new Error(
        `Invalid typeChecker supplied to \`${componentName}\` ` +
        `for propType \`${propFullName}\`, expected a function.`
      );
    }

    var propValues = propValue.valueSeq().toArray();
    for (var i = 0, len = propValues.length; i < len; i++) {
      var error = typeChecker(propValues, i, componentName, location, `${propFullName}[${i}]`, ...rest);
      if (error instanceof Error) {
        return error;
      }
    }
  }
  return createChainableTypeChecker(validate);
}

function createKeysTypeChecker(typeChecker) {

  function validate(props, propName, componentName, location, propFullName, ...rest) {
    var propValue = props[propName];
    if (typeof typeChecker !== 'function') {
      return new Error(
          `Invalid keysTypeChecker (optional second argument) supplied to \`${componentName}\` ` +
          `for propType \`${propFullName}\`, expected a function.`
      );
    }

    var keys = propValue.keySeq().toArray();
    for (var i = 0, len = keys.length; i < len; i++) {
      var error = typeChecker(keys, i, componentName, location, `${propFullName} -> key(${keys[i]})`, ...rest);
      if (error instanceof Error) {
        return error;
      }
    }
  }
  return createChainableTypeChecker(validate);
}

function createListOfTypeChecker(typeChecker) {
  return createIterableTypeChecker(typeChecker, 'List', Immutable.List.isList);
}

function createMapOfTypeCheckerFactory(valuesTypeChecker, keysTypeChecker, immutableClassName, immutableClassTypeValidator) {
  function validate(...args) {
    return createIterableTypeChecker(valuesTypeChecker, immutableClassName, immutableClassTypeValidator)(...args)
        || keysTypeChecker && createKeysTypeChecker(keysTypeChecker)(...args)
  }

  return createChainableTypeChecker(validate);
}

function createMapOfTypeChecker(valuesTypeChecker, keysTypeChecker) {
  return createMapOfTypeCheckerFactory(valuesTypeChecker, keysTypeChecker, 'Map', Immutable.Map.isMap);
}

function createOrderedMapOfTypeChecker(valuesTypeChecker, keysTypeChecker) {
  return createMapOfTypeCheckerFactory(valuesTypeChecker, keysTypeChecker, 'OrderedMap', Immutable.OrderedMap.isOrderedMap);
}

function createSetOfTypeChecker(typeChecker) {
  return createIterableTypeChecker(typeChecker, 'Set', Immutable.Set.isSet);
}

function createOrderedSetOfTypeChecker(typeChecker) {
  return createIterableTypeChecker(typeChecker, 'OrderedSet', Immutable.OrderedSet.isOrderedSet);
}

function createStackOfTypeChecker(typeChecker) {
  return createIterableTypeChecker(typeChecker, 'Stack', Immutable.Stack.isStack);
}

function createIterableOfTypeChecker(typeChecker) {
  return createIterableTypeChecker(typeChecker, 'Iterable', Immutable.Iterable.isIterable);
}

function createRecordOfTypeChecker(recordKeys) {
  function validate(props, propName, componentName, location, propFullName, ...rest) {
    var propValue = props[propName];
    if (!(propValue instanceof Immutable.Record)) {
      var propType = getPropType(propValue);
      var locationName = location;
      return new Error(
        `Invalid ${locationName} \`${propFullName}\` of type \`${propType}\` ` +
        `supplied to \`${componentName}\`, expected an Immutable.js Record.`
      );
    }
    for (var key in recordKeys) {
      var checker = recordKeys[key];
      if (!checker) {
        continue;
      }
      var mutablePropValue = propValue.toObject();
      var error = checker(mutablePropValue, key, componentName, location, `${propFullName}.${key}`, ...rest);
      if (error) {
        return error;
      }
    }
  }
  return createChainableTypeChecker(validate);
}

// there is some irony in the fact that shapeTypes is a standard hash and not an immutable collection
function createShapeTypeChecker(shapeTypes, immutableClassName = 'Iterable', immutableClassTypeValidator = Immutable.Iterable.isIterable) {
  function validate(props, propName, componentName, location, propFullName, ...rest) {
    var propValue = props[propName];
    if (!immutableClassTypeValidator(propValue)) {
      var propType = getPropType(propValue);
      var locationName = location;
      return new Error(
        `Invalid ${locationName} \`${propFullName}\` of type \`${propType}\` ` +
        `supplied to \`${componentName}\`, expected an Immutable.js ${immutableClassName}.`
      );
    }
    var mutablePropValue = propValue.toObject();
    for (var key in shapeTypes) {
      var checker = shapeTypes[key];
      if (!checker) {
        continue;
      }
      var error = checker(mutablePropValue, key, componentName, location, `${propFullName}.${key}`, ...rest);
      if (error) {
        return error;
      }
    }
  }
  return createChainableTypeChecker(validate);
}

function createShapeChecker(shapeTypes) {
  return createShapeTypeChecker(shapeTypes);
}

function createMapContainsChecker(shapeTypes) {
  return createShapeTypeChecker(shapeTypes, 'Map', Immutable.Map.isMap);
}

function createOrderedMapContainsChecker(shapeTypes) {
  return createShapeTypeChecker(shapeTypes, 'OrderedMap', Immutable.OrderedMap.isOrderedMap);
}

module.exports = ImmutablePropTypes;
