import React from 'react';
import {
    InputGroup as InputGroupStyled,
    InputText as InputTextStyled,
    InputTextarea as InputTextAreaStyled,
    Label as LabelStyled
} from './Form';

import formUnique from './FormUnique';

/**
 * Parent Class for a Component that will be a bindForm()
 */
class FormApi extends React.Component {

    constructor(props) {
        super(props);

        if('function' !== typeof props.handleSubmit) {
            throw new Error('FormApi: `props.handleSubmit` is not a function');
        }
    }

    /**
     * Factory for an InputGroupText component, valid for this FormApi
     * @return {function(*): *}
     * @constructor
     */
    render() {
        return (
            <form className={this.props.className} onSubmit={this.props.handleSubmit}>
                {this.props.children}
            </form>
        );
    }
}

const InputText = (props) => {
    return (
        <InputTextStyled
            {...props}
        />
    );
};

const InputGroupText = (props) => {
    return (
        <InputGroupStyled {...props.group}>
            <LabelStyled {...props.label}>{props.label_val}</LabelStyled>
            <InputText
                {...props.input}
            />
        </InputGroupStyled>
    );
};

const InputGroupTextarea = (props) => {
    return (
        <InputGroupStyled {...props.group}>
            <LabelStyled {...props.label}>{props.label_val}</LabelStyled>
            <InputTextAreaStyled
                {...props.input}
            />
        </InputGroupStyled>
    );
};

/**
 * HOC for a Two-Way Data Source Bound Form, loading initial data into input fiels and sending data when submitting form
 *
 * @param WrappedComponent
 * @param {function(Object):(boolean|Object)|boolean} sender the api handler which will return an response
 * @param {function(Object):(boolean|Object)|boolean} receiver the api handler which will return an response
 * @param {Object} fields which fields are possible to match to input fields
 */
function bindForm(WrappedComponent, sender = false, receiver = false, fields = []) {

    /**
     * Injected as `prop` to be able to add api connectors from inside WrappedComponent
     * @type {{sender: function, receiver: function, fields: []}}
     */
    const connect = {
        sender: (exec) => {
            sender = exec;
        },
        receiver: (exec) => {
            receiver = exec;
        },
        fields: (exec) => {
            fields = exec;
        }
    };

    return class FormApiHOC extends React.Component {
        constructor(props) {
            super(props);

            /**
             * Unique ID for this bounded form, to generate valid `htmlFor` and `id` props on input fields
             */
            this.id = formUnique();

            this.state = {
                /**
                 * LifeCycle of `process` state:
                 * 1. `false` when just created
                 * 2. `receiving` when loading data
                 * 2. `receiving+error` when loading data
                 * 3. `received` when data loaded - is persistent until submitting
                 * 4. `sending` when data is submitted
                 * 4. `sending+error` when data is submitted
                 * 5. `sended` when data is submitted
                 */
                process: false,
                errSend: false,
                // todo: add errReceive
                /**
                 * Data source for input fields, bound during loading and input field generation and used when submitting
                 */
                data: {},
                touched: []
            };

            this.handleSubmit = this.handleSubmit.bind(this);
            this.bindField = this.bindField.bind(this);
            this.clearForm = this.clearForm.bind(this);
            this.calcInputGroup = this.calcInputGroup.bind(this);
            this.onChange = this.onChange.bind(this);
        }

        /**
         * Clean the current bindings
         */
        componentWillUnmount() {
            this.setState({
                process: false,
                data: {}
            })
        }

        clearForm() {
            const tmp_data = {...this.state.data};
            for(let inp_name in tmp_data) {
                if(tmp_data.hasOwnProperty(inp_name)) {
                    tmp_data[inp_name].touched = false;
                    tmp_data[inp_name].value = '';
                }
            }
            this.setState({
                data: tmp_data
            });
        }

        /**
         * Load the data into bound input fields
         *
         * @return {Promise<void>}
         */
        async componentDidMount() {
            if(false === receiver) {
                // when only sending form, directly switch to received
                this.setState({
                    process: 'received'
                });
                return;
            }

            this.setState({
                process: 'receiving'
            });

            // Fetch the data
            const data = await (receiver(this.props)
                .then((res) => {
                    if(res && res.data) {
                        return res.data;
                    }
                    return false;
                })
                .catch((err) => {
                    this.setState({
                        process: 'receiving+error'
                    });
                    throw err;
                }));

            // parse received fields into bounded data fields, checking if received field is allowed to be displayed
            const tmp_data = {...this.state.data};
            if(data) {
                for(let inp_name in data) {
                    if(data.hasOwnProperty(inp_name) && -1 !== fields.indexOf(inp_name)) {
                        if('object' !== typeof tmp_data[inp_name]) {
                            tmp_data[inp_name] = {};
                        }
                        tmp_data[inp_name].touched = false;
                        tmp_data[inp_name].value = data[inp_name];
                    }
                }
            }

            // finished receiving, updated components
            this.setState({
                process: 'received',
                data: tmp_data
            });
        }

        /**
         * Form submitting handler, reads updated data and sends the data to the server
         * @param evt
         * @return {Promise<void>}
         */
        handleSubmit(evt) {
            evt.persist();
            evt.preventDefault();

            this.setState({
                process: 'sending',
                errSend: false,
            });

            // process values bound and add valid to submitting data
            let data = {};
            for(let name in this.state.data) {
                if(
                    this.state.data.hasOwnProperty(name) &&
                    'string' === typeof this.state.data[name].value &&
                    '' !== this.state.data[name].value.trim()
                ) {
                    data[name] = this.state.data[name].value.trim();
                }
            }

            // sending data to server
            sender(evt, data, this.state.touched, this.props)
                .then((res) => {
                    this.setState({
                        process: 'sended',
                    });
                })
                .catch((err) => {
                    this.setState({
                        process: 'sending+error',
                        errSend: err,
                    });
                });
        }

        /**
         * Handles changing of any input with bound data
         * @todo check if state is okay without destructurizing at beginning and on setState
         */
        onChange = (evt) => {
            const {name, value} = evt.target;
            let input_data = this.state.data;
            let touch = [...this.state.touched];
            if(true !== input_data[name].touched) {
                // when not already touch, mark as touched in index array
                touch.push(name);
            }
            input_data[name].touched = true;
            input_data[name].value = value;
            this.setState({
                data: input_data,
                touched: touch
            });
        };

        /**
         * Creates the binding for a field if needed and returns it
         * @param name
         * @return {{value: *, onChange: FormApiHOC.onChange}}
         */
        bindField(name) {
            if(!this.state.data[name]) {
                // set this state directly, as async setting with setState it could not be returned directly
                // eslint-disable-next-line
                this.state.data[name] = {
                    value: '',
                    touched: false
                }
            }

            // this is spread to the actual `input`, `select` or whatever form input used
            return {
                value: this.state.data[name].value,
                onChange: this.onChange
            }
        }

        /**
         * Returns the needed props for a whole input group
         *
         * @param props
         */
        calcInputGroup(props) {
            let type = props.type || 'text';
            const tmp_id = this.id + '-' + props.name;
            return {
                group: {
                    required: props.required,
                    style: props.style
                },
                label_val: props.lbl,
                label: {
                    htmlFor: tmp_id,
                    required: props.required
                },
                input: {
                    id: tmp_id,
                    type: type,
                    name: props.name,
                    required: props.required,
                    readOnly: props.readOnly,
                    ...this.bindField(props.name)
                }
            };
        }

        /**
         * Holds automatic binding components which are pushed into the wrapped component
         */
        factory = {
            InputGroupText: (props) => {
                return (
                    <InputGroupText {...this.calcInputGroup(props)}/>
                );
            },
            InputGroupTextarea: (props) => {
                const p = this.calcInputGroup(props);
                // textarea doesn't need a type
                delete p.input.type;
                return (
                    <InputGroupTextarea {...p}/>
                );
            },
            bindField: (name) => {
                return this.bindField(name);
            },
            FormApi: (props) => {
                const new_props = {...props};
                new_props.handleSubmit = this.handleSubmit;
                return <FormApi {...new_props}/>
            },
            resetForm: () => {
                this.clearForm();
            },
            updateProcess: (process) => {
                this.setState({
                    process: process
                });
            }
        };

        render() {
            return (
                /* Actual Component which renders the form through the factory */
                <WrappedComponent process={this.state.process}
                                  errSend={this.state.errSend}
                                  connect={connect}
                                  {...this.factory}
                                  {...this.props}/>
            );
        }
    };
}

export {bindForm};
