import { serialize } from 'object-to-formdata'
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import Errors from './Errors'
import { deepCopy, hasFiles } from './util'

interface Progress {
  total: number
  loaded: number
  percentage: number
}

class Form {
  [key: string]: any
  originalData: Record<string, any> = {}

  /**
   * Indicates if the form is sent to the server.
   */
  busy: boolean = false

  /**
   * Indicates if the response form the server was successful.
   */
  successful: boolean = false

  /**
   * Indicates if the response form the server was recently successful.
   */
  recentlySuccessful: boolean = false
  recentlySuccessfulTimeoutId: number | undefined = undefined

  /**
   * The validation errors from the server.
   */
  errors: Errors = new Errors()

  /**
   * The upload progress object.
   */
  progress: Progress | undefined = undefined

  static axios: AxiosInstance
  static routes: Record<string, string> = {}
  static errorMessage = 'Something went wrong. Please try again.'
  static recentlySuccessfulTimeout = 2000
  static ignore = ['busy', 'successful', 'errors', 'progress', 'originalData', 'recentlySuccessful', 'recentlySuccessfulTimeoutId']

  /**
   * Create a new form instance.
   */
  constructor (data: Record<string, any> = {}) {
    this.update(data)
  }

  /**
   * Create a new form instance.
   */
  static make<T extends typeof Form, U> (this: T, augment?: U) {
    return new this(augment) as InstanceType<T> & U
  }

  /**
   * Update the form data.
   */
  update (data: Record<string, any>) {
    this.originalData = Object.assign({}, this.originalData, deepCopy(data))

    Object.assign(this, data)
  }

  /**
   * Fill the form data.
   */
  fill (data: Record<string, any> = {}) {
    this.keys().forEach((key) => {
      (this as any)[key] = data[key]
    })
  }

  /**
   * Get the form data.
   */
  data (): Record<string, any> {
    return this.keys().reduce((data, key) => (
      { ...data, [key]: (this as any)[key] }
    ), {})
  }

  /**
   * Get the form data keys.
   */
  keys (): string[] {
    return Object.keys(this).filter(key => !Form.ignore.includes(key))
  }

  /**
   * Start processing the form.
   */
  startProcessing () {
    this.errors.clear()
    this.busy = true
    this.successful = false
    this.progress = undefined
    this.recentlySuccessful = false
    clearTimeout(this.recentlySuccessfulTimeoutId)
  }

  /**
   * Finish processing the form.
   */
  finishProcessing () {
    this.busy = false
    this.successful = true
    this.progress = undefined
    this.recentlySuccessful = true
    this.recentlySuccessfulTimeoutId = setTimeout(() => {
      this.recentlySuccessful = false
    }, Form.recentlySuccessfulTimeout)
  }

  /**
   * Clear the form errors.
   */
  clear () {
    this.errors.clear()
    this.successful = false
    this.recentlySuccessful = false
    this.progress = undefined
    clearTimeout(this.recentlySuccessfulTimeoutId)
  }

  /**
   * Reset the form data.
   */
  reset () {
    Object.keys(this)
      .filter(key => !Form.ignore.includes(key))
      .forEach((key) => {
        (this as any)[key] = deepCopy(this.originalData[key])
      })
  }

  /**
   * Submit the form via a GET request.
   */
  get<T = any> (url: string, config: AxiosRequestConfig = {}): Promise<AxiosResponse<T>> {
    return this.submit('get', url, config)
  }

  /**
   * Submit the form via a POST request.
   */
  post<T = any> (url: string, config: AxiosRequestConfig = {}): Promise<AxiosResponse<T>> {
    return this.submit('post', url, config)
  }

  /**
   * Submit the form via a PATCH request.
   */
  patch<T = any> (url: string, config: AxiosRequestConfig = {}): Promise<AxiosResponse<T>> {
    return this.submit('patch', url, config)
  }

  /**
   * Submit the form via a PUT request.
   */
  put<T = any> (url: string, config: AxiosRequestConfig = {}): Promise<AxiosResponse<T>> {
    return this.submit('put', url, config)
  }

  /**
   * Submit the form via a DELETE request.
   */
  delete<T = any> (url: string, config: AxiosRequestConfig = {}): Promise<AxiosResponse<T>> {
    return this.submit('delete', url, config)
  }

  /**
   * Submit the form data via an HTTP request.
   */
  submit<T = any> (method: string, url: string, config: AxiosRequestConfig = {}): Promise<AxiosResponse<T>> {
    this.startProcessing()

    config = {
      data: {},
      params: {},
      url: this.route(url),
      method: method as any,
      onUploadProgress: this.handleUploadProgress.bind(this),
      ...config
    }

    if (method.toLowerCase() === 'get') {
      config.params = { ...this.data(), ...config.params }
    } else {
      config.data = { ...this.data(), ...config.data }

      if (hasFiles(config.data) && !config.transformRequest) {
        config.transformRequest = [data => serialize(data)]
      }
    }

    return new Promise((resolve, reject) => {
      (Form.axios || axios).request(config)
        .then((response: AxiosResponse) => {
          this.finishProcessing()
          resolve(response)
        })
        .catch((error: AxiosError) => {
          this.handleErrors(error)
          reject(error)
        })
    })
  }

  /**
   * Handle the errors.
   */
  handleErrors (error: AxiosError) {
    this.busy = false
    this.progress = undefined

    if (error.response) {
      this.errors.set(this.extractErrors(error.response))
    }
  }

  /**
   * Extract the errors from the response object.
   */
  extractErrors (response: AxiosResponse): Record<string, any> {
    if (!response.data || typeof response.data !== 'object') {
      return { error: Form.errorMessage }
    }

    if (response.data.errors) {
      return { ...response.data.errors }
    }

    if (response.data.message) {
      return { error: response.data.message }
    }

    return { ...response.data }
  }

  /**
   * Handle the upload progress.
   */
  handleUploadProgress (event: ProgressEvent) {
    this.progress = {
      total: event.total,
      loaded: event.loaded,
      percentage: Math.round((event.loaded * 100) / event.total)
    }
  }

  /**
   * @deprecated
   */
  route (name: string, parameters: any = {}) {
    let url = name

    if (Object.prototype.hasOwnProperty.call(Form.routes, name)) {
      url = decodeURI(Form.routes[name])
    }

    if (typeof parameters !== 'object') {
      parameters = { id: parameters }
    }

    Object.keys(parameters).forEach((key) => {
      url = url.replace(`{${key}}`, parameters[key])
    })

    return url
  }

  /**
   * Clear errors on keydown.
   */
  onKeydown (event: KeyboardEvent) {
    const target = event.target as HTMLInputElement

    if (target.name) {
      this.errors.clear(target.name)
    }
  }
}

export default Form
