
import * as React from 'react'
import { findDOMNode } from 'react-dom'
import {of, Subscription } from 'rxjs'
import { tap, catchError, map, debounceTime, skip, take, throttleTime } from 'rxjs/operators'

import { InputObservable } from './InputObservable'
import { FormObservable } from './FormObservable'

const initialState = {
  dirty: false,
  formValue: {},
  submitted: false,
}


export const quarksForm = function({
  fields,
  valueChangeObs,
  debounce = 300,
  throttle = 0,
  beforeSubmit,
  afterSubmit,
  value$,
  onError = () => {}
}) {
  return (Comp) => {
   
    class QuarksForm extends React.Component {
      static displayName = `QuarksForm(${Comp.displayName || Comp.name})`

      state = this.initState()

      valueChange$ = new InputObservable({
        fields,
        initialValue: this.state.formValue,
        props: this.props,
      })
      formSubmit$ = new FormObservable(this.valueChange$, this.props.onError || onError)

      valueChangeSubscription = new Subscription()
      formSubmitSubscription = new Subscription()
      valueSubscription = new Subscription()

      formElement
      inputElements
      selectElements

      constructor(props){
          super(props);

          this.attachFormElement = this.attachFormElement.bind(this);
          this.initState = this.initState.bind(this);
          this.setInitialInputValues = this.setInitialInputValues.bind(this);
          this.hasError = this.hasError.bind(this);
          this.handleAddInputs = this.handleAddInputs.bind(this);
          this.setValue = this.setValue.bind(this);
          this.getValue = this.getValue.bind(this);
          this.handleValueChangeSuccess = this.handleValueChangeSuccess.bind(this);
      }

      getValue(){
        return this.valueChange$.getValue();
      }
     

      attachFormElement(instance) {
        const rootNode = findDOMNode(instance);

        if (rootNode) {
          if (rootNode.tagName === 'FORM') {
            this.formElement = rootNode;
          } else {
            const formElement = rootNode.querySelector('form')

            if (formElement) {
              this.formElement = formElement
            } else {
              throw new Error("Necessario elemento html form como filho");
            }
          }
        }
      }

     
      initSimplifiedFormValue() {
        return Object.keys(fields).reduce((acc, fieldName) => {
          const fieldMeta = fields[fieldName]
          return {
            ...acc,
            [fieldName]: typeof fieldMeta.value === 'function' ? fieldMeta.value(this.props) : fieldMeta.value || '',
          }
        }, {})
      }

    
      initState() {
        return Object.keys(fields).reduce((state, fieldName) => {
          const fieldMeta = fields[fieldName]
          const fieldValue = typeof fieldMeta.value === 'function' ? fieldMeta.value(this.props) : fieldMeta.value || this.props[fieldName] || ''
          const dirty = fieldValue !== ''
          let fieldError

          if (fieldMeta.validation) {
            fieldError = fieldMeta.validation(fieldValue, this.initSimplifiedFormValue(), this.props)
          }

          return {
            ...state,
            dirty: state.dirty || dirty,
            formValue: {
              ...state.formValue,
              [fieldName]: {
                dirty,
                error: fieldError,
                value: fieldValue,
              },
            },
          }
        }, initialState)
      }

     
      setInitialInputValues() {
        Object.keys(fields).forEach(inputName => {
          this.inputElements = this.inputElements || [];
          this.selectElements = this.selectElements || [];

          const inputElements = this.inputElements.filter(element => element.getAttribute('name') === inputName)
          const selectElements = this.selectElements.filter(element => element.getAttribute('name') === inputName)
          const fieldValue = this.state.formValue[inputName].value

          if (inputElements[0] && fieldValue) {
            switch (inputElements[0].getAttribute('type')) {
              case 'checkbox':
                if (typeof fieldValue === 'boolean') {
                  inputElements[0].checked = !!fieldValue
                } else {
                  throw new Error(`${inputName} nao eh boolean`)
                }
                break

              case 'radio':
                if (typeof fieldValue === 'string') {
                  inputElements.some(element => {
                    if (element.getAttribute('value') === fieldValue) {
                      element.checked = true
                      return true
                    }
                    return false
                  })
                } else {
                  throw new Error(`${inputName} nao eh string`)
                }
                break

              case 'date':
                if (fieldValue instanceof Date) {
                  inputElements[0].value = new Date(fieldValue.toString()).toISOString().substr(0, 10)
                } else {
                  throw new Error(`${inputName} nao eh Date`)
                }
                break

              case 'range':
              case 'number':
                if (typeof fieldValue === 'number') {
                  inputElements[0].value = fieldValue.toString()
                } else {
                  throw new Error(`${inputName} nao eh number`)
                }
                break

              default:
                if (typeof fieldValue === 'string') {
                  inputElements[0].value = fieldValue.toString()
                } else {
                  throw new Error(`${inputName} nao eh string`)
                }
                break
            }
          }

          if (selectElements[0] && fieldValue) {
            if (typeof fieldValue === 'string') {
              Array.from(selectElements[0].querySelectorAll('option')).forEach(optionElement => {
                optionElement.selected = optionElement.value === fieldValue
              })
            } else {
              throw new Error(`${inputName} nao eh string`)
            }
          }
        })
      }

     
      hasError() {
        let hasError = false
        Object.keys(fields).some(fieldName => {
          if (this.state.formValue[fieldName].error) {
            hasError = true
            return true
          }
          return false
        })
        return hasError
      }

      handleAddInputs(inputsNames, selectNames) {
        this.valueChange$.addInputs(
          inputsNames.reduce((acc, inputName) => {
            const inputs = this.formElement ?  Array.from(this.formElement.querySelectorAll(`input[name=${inputName}]`)) : []
            return [...acc, ...inputs]
          }, []),
          selectNames.map(selectName => document.querySelector(`select[name=${selectName}]`)),
        )
      }

      initField(key, state) {
        if (!fields[key].customInput) {
        
          if(this.inputElements) {
            this.inputElements.filter(element => element.getAttribute('name') === key).forEach(element => {
                element.value = state[key] || ''
            })
          }

          if(this.selectElements) {
            this.selectElements.filter(element => element.getAttribute('name') === key).forEach(element => {
                element.value = state[key] || ''
              })
          }
         
        }
      }

      setValue(state) {
        const key = Object.keys(state)[0]

        this.initField(key, state)
        this.valueChange$.setValue({
          [key]: {
            ...this.state.formValue[key],
            value: state[key],
          },
        })
      }

      handleValueChangeSuccess(formValue) {
        this.setState({
          dirty: true,
          formValue: {
            ...this.state.formValue,
            ...formValue,
          },
        })
      }

      handleFilterInputs(element) {
        return element.hasAttribute('name')
      }

      componentDidMount() {
        this.inputElements = this.formElement ? Array.from(this.formElement.querySelectorAll('input')).filter(this.handleFilterInputs):[];
        this.selectElements = this.formElement ? Array.from(this.formElement.querySelectorAll('select')).filter(this.handleFilterInputs):[];

        if(this.formElement){
            this.formElement.setAttribute('novalidate', 'true')
        }
        
        if (value$) {
          this.valueSubscription = (typeof value$ === 'function' ? value$(this.props) : value$)
            .pipe(take(1), catchError(() => of({})))
            .subscribe((values) => {
              const formattedState = Object.keys(this.state.formValue).reduce((acc, key) => {
                this.initField(key, values)
                return {
                  ...acc,
                  [key]: {
                    ...this.state.formValue[key],
                    value: values[key],
                  },
                }
              }, {})
              this.valueChange$.setValues(formattedState)
              this.valueSubscription.unsubscribe()
            })
        } else {
          this.setInitialInputValues()
        }

        this.valueChange$.addInputs(this.inputElements, this.selectElements)
        this.formSubmit$.init(this.formElement)

        this.valueChangeSubscription = this.valueChange$
          .pipe(debounceTime(debounce), throttleTime(throttle))
          .subscribe(this.handleValueChangeSuccess)

        this.formSubmitSubscription = this.formSubmit$
          .pipe(
            map(formValue => {
              return typeof beforeSubmit === 'function' ? beforeSubmit(formValue, this.props) : formValue
            }),
            tap(() => this.setState({ submitted: true })),
          )
          .subscribe(formValue => {
            if (this.props.onSubmit) {
              this.props.onSubmit(formValue)
            }
            if (typeof afterSubmit === 'function') {
              afterSubmit(formValue, this.props)
            }
          })
      }

      componentWillUnmount() {
        this.formSubmitSubscription.unsubscribe()
        this.valueChangeSubscription.unsubscribe()
        this.valueChange$.unsubscribe()
      }

      render() {
        return (
          <Comp
            ref={this.attachFormElement}
            formSubmit$={this.formSubmit$}
            valueChange$={valueChangeObs ? this.valueChange$.pipe(skip(value$ ? 1 : 0)) : null}
            setValue={this.setValue}
            getValue={this.getValue}
            addInputs={this.handleAddInputs}
            valid={!this.hasError()}
            submitted={this.state.submitted}
            {...this.state.formValue}
            {...this.props}
          />
        )
      }
    }

    return QuarksForm;
  }
}
