import React, { Component, PropTypes } from 'react';

import { fromJS, toJS, List, Iterable } from 'immutable';


const NO_FIELD = "__NO_FIELD__";

//TODO refactor - stabilize API, use form handle instead of many methods
//TODO add errors API
export const form = (WrappedFormComponent, options = {}) => {
    let { 
        readData = (props) => props.data,
        validate = (data, error) => {
            return true;
        }
    } = options;

    const Injected = class extends Component {

        constructor(props) {
            super(props);
            this.state = {
                errors: {},
                values: fromJS({})
            };


            this.onChangeHandler = this.onChangeHandler.bind(this);
            this.valueOf = this.valueOf.bind(this);
            this.setValues = this.setValues.bind(this);
            this._setValue = this._setValue.bind(this);
            this.getData = this.getData.bind(this);
            this.mapData = this.mapData.bind(this);
            this.resetData = this.resetData.bind(this);
            this.submit = this.submit.bind(this);

            //TODO refactor API
            /*
                onChangeHandler={ this.onChangeHandler } 
                valueOf={ this.valueOf }
                getData={ this.getData }
                resetData={ () => this.mapData(this.props) }
            */

            this.form = {
                onChangeHandler: this.onChangeHandler,
                valueOf: this.valueOf,
                getData: this.getData,
                resetData: this.resetData
            };
        }


        componentWillMount() {
            this.mapData(this.props);
            this._isMounted = true;
        }


        componentWillUnmount() {
            this._isMounted = false;
        }


        componentWillReceiveProps(props) {
            this.mapData(props, this.props);
        }


        mapData(props, oldProps) {
            let data = readData(props),
                values = this.state.values;

            if (!data && values) {
                //no data provided - use already set values
                return;
            }

            values = Iterable.isIterable(data) ? data : fromJS(data || {});

            let oldData = oldProps ? readData(oldProps) : null;
            //should component update?
            if (oldData) {
                let oldValues = Iterable.isIterable(oldData) ? oldData : fromJS(oldData);
                if (oldValues.equals(values)) {
                    return;
                }

            }
            this.setState({ values });
        }


        onChangeHandler(path, readValueFn = (e) => e.target.value, onChange, afterChange) {
            let p = path.split(".");
            let changeHandler = this.props.onChangeHandler;

            //propagate changes into parent form?
            if (typeof changeHandler === "function") {
                return (...args) => {
                    let value = readValueFn(...args);
                    let values = this._setValue(p, value, onChange);
                    changeHandler(values);
                }
            }

            return (...args) => {
                let value = readValueFn(...args);
                this._setValue(p, value, onChange, afterChange);
            }
        }


        valueOf(path, defaultValue) {
            let values = this.state.values;
            if (!path) {
                return values.toJS();
            }
            let p = path.split(".");
            let value = values.getIn(p);
            if (value || value === false) {
                return value;
            }

            if (value === 0) {
                return "0";
            }

            return defaultValue;
        }


        _setValue(path, value, onChange, afterChange) {
            if (!Array.isArray(path)) {
                path = path.split(".");
            }
            let values = this.state.values;
            values = values.setIn(path, value);

            if (typeof onChange === "function") {
                let vals = onChange(value, values);
                if (vals) {
                    if (Iterable.isIterable(vals)) {
                        values = vals;
                    } else {
                        values = fromJS(vals);
                    }
                }
            }

            this.setState({ values });
            return values;
        }


        setValues(path, value, onChange, afterChange) {
            let values = this.state.values;
            if (path) {
                if (!Array.isArray(path)) {
                    path = path.split(".");
                }
                values = values.setIn(path, value);
                this.setState({ values });
                return values;
            }

            values = values.mergeDeep(value);

            if (typeof onChange === "function") {
                values = onChange(value, values) || values;
            }

            this.setState({ values }, afterChange);
            return values;
        }


        getData() {
            return this.state.values.toJS();
        }


        resetData() {
            return this.mapData(this.props);
        }


        //TODO refactor 
        async submit(onSubmit) {
            let data = this.getData();
            let errors = {};

            if (typeof onSubmit != "function") {
                onSubmit = this.props.onSubmit;
            }

            let reportError = (error) => {
                if (!error) {
                    return;
                }
                let field = error.field;
                if (Array.isArray(field)) {
                    for (let i = 0; i < field.length; i++) {
                        let errs = errors[field[i]];
                        if (!Array.isArray(errs)) {
                            errors[field[i]] = (errs = []);
                        }
                        errs.push(error);
                    }
                } else {
                    if (!field) {
                        field = NO_FIELD;
                    }
                    let errs = errors[field];
                    if (!Array.isArray(errs)) {
                        errors[field] = (errs = []);
                    }
                    errs.push(error);
                }
            }

            if (typeof validate === "function") {
                let valid = await validate(data, reportError, this.props);
                if (!valid) {
                    if (typeof options.onValidationFailed === "function") {
                        options.onValidationFailed(errors);
                    }
                    this.setState({ errors })
                    return false;
                }
            }

            if (this.refs.form && typeof this.refs.form.validate === "function") {
                let valid = await this.refs.form.validate(data, reportError);
                if (!valid) {
                    if (typeof options.onValidationFailed === "function") {
                        options.onValidationFailed(errors);
                    }
                    this.setState({ errors })
                    return false;
                }
            }

            if (typeof onSubmit === "function") {
                try {
                    let result = await onSubmit(data, reportError);
                } catch (e) {
                    console.error("error during onSubmit", e);
                }
            }

            if (this._isMounted) {
                this.setState({ errors })
            }

            return true;
        }


        render() {
            return <WrappedFormComponent 
                {...this.props} 

                onChangeHandler={ this.onChangeHandler } 
                valueOf={ this.valueOf }
                getData={ this.getData }
                resetData={ this.resetData }
                setValues={ this.setValues }

                errors={ this.state.errors }
                submit={ this.submit }

                form={ this.form }
                ref="form"
                
                />
        }

    }

    return Injected;
}

export default form;
