import {
    isEmpty, isArray, isNumber, isObject, isString, partial, defaults, isNil, isDate,
} from 'lodash';
import invariant from 'invariant';

import { intercept } from 'mobx';

import {
    field as mobxField,
    session as mobxSession,
    identifier as mobxIdentifier,
} from 'mobx-decorated-models';

const VALIDATORS = {
    number: isNumber,
    string: isString,
    array:  isArray,
    object: isObject,
    date:   isDate,
};

const COERCE = {
    date(change) {
        if (!isDate(change.newValue)) {
            change.newValue = new Date(change.newValue); // eslint-disable-line no-param-reassign
        }
    },
};

function addLazyInitializer(target, fn) {
    target.__mobxLazyInitializers.push(fn);
}

function validateChangeType(validator, propertyName, change) {
    if (isNil(change.newValue)) { return change; }
    invariant(VALIDATORS[validator], `Unknown TypeCheck: ${validator} does not exist`);
    if (COERCE[validator]) { COERCE[validator](change); }
    invariant(VALIDATORS[validator](change.newValue),
              `Bad Type: Attempted to set ${propertyName} to '${change.newValue}' which is not ${validator}`);
    return change;
}

function addTypeSafeInterceptor(target, property, type) {
    if (isEmpty(type)) { return; }
    addLazyInitializer(target, (partial(intercept, partial.placeholder, property,
                                        partial(validateChangeType, type, property))));
}

function decoratorWrapper(decorator, defaultOptions = {}) {
    return (targetOrOptions, ...args) => {
        const options = isEmpty(args) ?
              defaults(targetOrOptions, defaultOptions) : defaultOptions;

        const wrap = (target, property, descriptor) => {
            const decorationFn = decorator(options);
            const description = decorationFn(target, property, descriptor);
            addTypeSafeInterceptor(target, property, options.type);
            return description;
        };

        if (isEmpty(args)) { // we were given options
            return (target, property, descriptor) => wrap(target, property, descriptor);
        }
        return wrap(targetOrOptions, args[0], args[1]);
    };
}

const field = decoratorWrapper(mobxField);
const session = decoratorWrapper(mobxSession);
const identifier = decoratorWrapper(mobxIdentifier, { type: 'number' });

export { field, session, identifier };

@@ -159,16 +159,55 @@
 ntercept
+, isObservableArray, isObservableObject
  %7D from 
@@ -421,40 +421,122 @@
 rray
-:  isArray,%0A    object: isObject
+(a) %7B return isArray(a) %7C%7C isObservableArray(a); %7D,%0A    object(o) %7B return isObject(o) %7C%7C isObservableObject(o); %7D
 ,%0A  

allow array and object to be set with observables