import { BehaviorSubject, forkJoin, merge, Observable, of, Subscription } from 'rxjs'
import { tap, map, switchMap } from 'rxjs/operators'
import autobind from 'autobind-decorator'

import { createInputObservable, createSelectObservable } from './factory'

export class InputObservable extends BehaviorSubject {
  static TEXT_INPUT = ['text', 'search', 'email', 'password', 'date', 'range', 'number']
  static RADIO_INPUT = ['radio']
  static CHECKBOX_INPUT = ['checkbox']

  checkboxEvent
  textEvent
  radioEvent
  selectEvent
  fields
  props
  subscriptions = []
  inputElements = []

  constructor({
    inputElements = [],
    selectElements = [],
    fields = {},
    initialValue = {},
    checkboxEvent = 'change',
    textEvent = 'input',
    radioEvent = 'change',
    selectEvent = 'change',
    props,
  }) {
    super(initialValue)

    this.checkboxEvent = checkboxEvent
    this.textEvent = textEvent
    this.radioEvent = radioEvent
    this.selectEvent = selectEvent
    this.fields = fields
    this.props = props

    if (inputElements.length > 0 || selectElements.length > 0) {
      this.addInputs(inputElements, selectElements)
    }

    this.setValue = this.setValue.bind(this);
    this.setValues = this.setValues.bind(this);
    this.standardInputFormatter = this.standardInputFormatter.bind(this);
    this.checkboxInputFormatter = this.checkboxInputFormatter.bind(this);
    this.handleSubscribe = this.handleSubscribe.bind(this);
    this.handleError = this.handleError.bind(this);
    this.handleBeforeValidation = this.handleBeforeValidation.bind(this);
    this.handleAfterValidation = this.handleAfterValidation.bind(this);
  }

  /**
   * unsubscribe to all the subscribed Observable
   */
  unsubscribe() {
    super.unsubscribe()
    this.subscriptions.forEach(subscription => {
      subscription.unsubscribe()
    })
  }

  formatState(state) {
    /* tslint:disable */
    console.log('Quarks Input state', state, Object.keys(this.fields))
    return Object.keys(this.fields).reduce(
      (acc, fieldName) => ({
        ...acc,
        [fieldName]: state[fieldName].value,
      }),
      {},
    )
  }


  reduceField(fieldName, value, type, dirty = true, touched = true) {
    let formattedValue

    switch (type) {
      case 'date':
        formattedValue = new Date(value)
        break

      case 'range':
      case 'number':
        formattedValue = parseInt(value, 10)
        break

      default:
        formattedValue = value
    }

    return {
      [fieldName]: {
        dirty,
        touched,
        value: formattedValue,
      },
    }
  }

  setValue(formValue) {
    of(this.handleBeforeValidation(formValue))
      .pipe(switchMap(this.handleError))
      .toPromise()
      .then(state => {
        this.next({
          ...this.getValue(),
          ...state,
        })
      })
  }

  setValues(formValue) {
    forkJoin(
      Object.keys(formValue)
        .map(key => this.handleBeforeValidation({ [key]: formValue[key] }))
        .map(this.handleError),
    )
      .pipe(map(values => values.reduce((acc, value) => ({ ...acc, ...value }), {})))
      .toPromise()
      .then(this.next.bind(this))
  }

  
  standardInputFormatter(event){
    return this.reduceField(event.target.name, event.target.value, event.target.type)
  }

  /**
   * formatter for the checkbox
   * @param event - change event
   */
  checkboxInputFormatter(event) {
    return this.reduceField(event.target.name, !!event.target.checked, event.target.type)
  }

  handleSubscribe(formValues) {
    this.next({
      ...this.getValue(),
      ...formValues,
    })
  }

  handleError(formValue) {
    const inputName = Object.keys(formValue)[0]
    const field = this.fields[inputName]
    const state = this.getValue()

    if (typeof field.validation === 'function') {
      return of({
        ...state,
        [inputName]: {
          ...formValue[inputName],
          error: field.validation(formValue[inputName].value, this.formatState(state), this.props),
        },
      })
    } else if (typeof field.validation$ === 'function') {
      this.next({
        ...state,
        [inputName]: {
          ...state[inputName],
          pending: true,
        },
      })

      return field.validation$(formValue[inputName].value, this.formatState(state), this.props).pipe(
        map(error => ({
          ...state,
          [inputName]: {
            ...formValue[inputName],
            error,
          },
        })),
        tap(() => {
          this.next({
            ...state,
            [inputName]: {
              ...state[inputName],
              pending: false,
            },
          })
        }),
      )
    } else {
      return of(formValue)
    }
  }
  
  handleBeforeValidation(formValue) {
    const inputName = Object.keys(formValue)[0];

    const field = this.fields[inputName]
    const state = this.getValue()

    if (field.beforeValidation && typeof field.beforeValidation === 'function') {
      const element = this.inputElements.find(input => input.name === inputName)
      const value = field.beforeValidation(formValue[inputName].value, this.formatState(state), this.props)

      if (element && typeof value === 'string') {
        element.value = value
      }

      return {
        [inputName]: {
          ...formValue[inputName],
          value,
        },
      }
    } else {
      return formValue
    }
  }

  
  handleAfterValidation(formValue) {
    const inputName = Object.keys(formValue)[0]
    const field = this.fields[inputName]
    const state = this.getValue()

    if (typeof field.afterValidation === 'function') {
      const value = field.afterValidation(formValue[inputName].value, this.formatState(state), this.props)

      return {
        [inputName]: {
          ...formValue[inputName],
          value,
        },
      }
    } else {
      return formValue
    }
  }

  addInputs(inputElements, selectElements = []) {
    this.inputElements = [...this.inputElements, ...inputElements]

    const text$ = createInputObservable({
      elements: inputElements,
      event: this.textEvent,
      types: InputObservable.TEXT_INPUT,
    }).pipe(map(this.standardInputFormatter))

    const radio$ = createInputObservable({
      elements: inputElements,
      event: this.radioEvent,
      types: InputObservable.RADIO_INPUT,
    }).pipe(map(this.standardInputFormatter))

    const checkbox$ = createInputObservable({
      elements: inputElements,
      event: this.checkboxEvent,
      types: InputObservable.CHECKBOX_INPUT,
    }).pipe(map(this.checkboxInputFormatter))

    const select$ = createSelectObservable({ elements: selectElements }).pipe(map(this.standardInputFormatter))

    this.subscriptions.push(
      merge(text$, radio$, checkbox$, select$)
        .pipe(map(this.handleBeforeValidation), switchMap(this.handleError), map(this.handleAfterValidation))
        .subscribe(this.handleSubscribe),
    )
  }
}
