/**
    @arg {string} name - form state will appear in this.state[name]
    @arg {object} instance - `this` for the component
    @arg {array} fields - ['username', 'save', ...]
    @arg {object} initialValues required for checkboxes {save: false, ...}
    @arg {function} validation - values => ({ username: ! values.username ? 'Required' : null, ... })
*/
export default function reactForm({name, instance, fields, initialValues, validation = () => {}}) {
    if(typeof instance !== 'object') throw new TypeError('instance is a required object')
    if(!Array.isArray(fields)) throw new TypeError('fields is a required array')
    if(typeof initialValues !== 'object') throw new TypeError('initialValues is a required object')

    // Give API users access to this.props, this.state, this.etc..
    validation = validation.bind(instance)

    const formState = instance.state = instance.state || {}
    formState[name] = {
        // validate: () => setFormState(instance, fields, validation),
        handleSubmit: submitCallback => event => {
            event.preventDefault()
            const {valid} = setFormState(name, instance, fields, validation)
            if(!valid) return
            const data = getData(fields, instance.state)
            let formValid = true
            const fs = instance.state[name] || {}
            fs.submitting = true

            // User can call this function upon successful submission
            const updateInitialValues = () => {
                setInitialValuesFromForm(name, instance, fields, initialValues)
                formState[name].resetForm()
            }

            instance.setState(
                {[name]: fs},
                () => {
                    // TODO, support promise ret
                    const ret = submitCallback({data, event, updateInitialValues}) || {}
                    // Look for field level errors
                    for(const fieldName of Object.keys(ret)) {
                        const error = ret[fieldName]
                        if(!error) continue
                        const value = instance.state[fieldName] || {}
                        value.error = error
                        value.touched = true
                        if(error) formValid = false
                        instance.setState({[fieldName]: value})
                    }
                    fs.submitting = false
                    fs.valid = formValid
                    instance.setState({[name]: fs})
                }
            )
        },
        resetForm: () => {
            for(const field of fields) {
                const fieldName = n(field)
                const f = instance.state[fieldName]
                const def = initialValues[fieldName]
                f.props.onChange(def)
            }
        },
        clearForm: () => {
            for(const field of fields) {
                const fieldName = n(field)
                const f = instance.state[fieldName]
                f.props.onChange()
            }
        },
    }

    for(const field of fields) {
        const fieldName = n(field)
        const fieldType = t(field)

        const fs = formState[fieldName] = {
            value: null,
            error: null,
            touched: false,
        }

        // Caution: fs.props is expanded <input {...fieldName.props} />, so only add valid props for the component
        fs.props = {name: fieldName}

        {
            const initialValue = initialValues[fieldName]
            if(fieldType === 'checked') {
                fs.value = toString(initialValue)
                fs.props.checked = toBoolean(initialValue)
            } else if(fieldType === 'selected') {
                fs.props.selected = toString(initialValue)
                fs.value = fs.props.selected
            } else {
                fs.props.value = toString(initialValue)
                fs.value = fs.props.value
            }
        }

        fs.props.onChange = e => {
            const value = e && e.target ? e.target.value : e // API may pass value directly
            const v = {...(instance.state[fieldName] || {})}
            const initialValue = initialValues[fieldName]

            if(fieldType === 'checked') {
                v.touched = toString(value) !== toString(initialValue)
                v.value = v.props.checked = toBoolean(value)
                v.value = value
            } else if(fieldType === 'selected') {
                v.touched = toString(value) !== toString(initialValue)
                v.value = v.props.selected = toString(value)
            } else {
                v.touched = toString(value) !== toString(initialValue)
                v.value = v.props.value = toString(value)
            }

            instance.setState(
                {[fieldName]: v},
                () => {setFormState(name, instance, fields, validation)}
            )
        }

        fs.props.onBlur = () => {
            // Some errors are better shown only after blur === true
            const v = {...(instance.state[fieldName] || {})}
            v.blur = true
            instance.setState({[fieldName]: v})
        }
    }
}

function setFormState(name, instance, fields, validation) {
    let formValid = true
    let formTouched = false
    const v = validation(getData(fields, instance.state))
    for(const field of fields) {
        const fieldName = n(field)
        const validate = v[fieldName]
        const error = validate ? validate : null
        const value = {...(instance.state[fieldName] || {})}
        value.error = error
        formTouched = formTouched || value.touched
        if(error) formValid = false
        instance.setState({[fieldName]: value})
    }
    const fs = {...(instance.state[name] || {})}
    fs.valid = formValid
    fs.touched = formTouched
    instance.setState({[name]: fs})
    return fs
}

function setInitialValuesFromForm(name, instance, fields, initialValues) {
    const data = getData(fields, instance.state)
    for(const field of fields) {
        const fieldName = n(field)
        initialValues[fieldName] = data[fieldName]
    }
}

function getData(fields, state) {
    const data = {}
    for(const field of fields) {
        const fieldName = n(field)
        data[fieldName] = state[fieldName].value
    }
    return data
}

/*
    @arg {string} field - field:type
    <pre>
        type = checked (for checkbox or radio)
        type = selected (for seelct option)
        type = string
    </pre>
    @return {string} type
*/
function t(field) {
    const [, type = 'string'] = field.split(':')
    return type
}

/**
    @return {string} name
*/
function n(field) {
    const [name] = field.split(':')
    return name
}

const hasValue = v => v == null ? false : (typeof v === 'string' ? v.trim() : v) === '' ? false : true
const toString = v => hasValue(v) ? v : ''
const toBoolean = v => hasValue(v) ? JSON.parse(v) : ''
