import * as React from 'react'
import { StylableComponent, StyleContext } from '../theme'
import { FormContext } from '../form'
import { AbstractFormField, FormFieldComponentProps } from './interfaces'
import { string, object } from 'prop-types'
import { createDefaultFormField, createAsyncFormField } from './data'

/**
 * A wrapper component for form fields that
 * reads the form name of parent <Form /> elements
 */
export abstract class FormFieldWrapper<T> extends StylableComponent<T> {

    public static contextTypes = {
        formName: string,
        themeNotifier: object,

    }

    public context: any

    public formName: string

    /**
     * Set the form name
     */
    public componentWillMount() {
        // console.log('FORM FIELD WRAPPER WILL MOUNT', this)
        // if the form name is passed in as a prop that takes precedence
        this.formName =
            (this.props as any).formName ||
            (this.context ? this.context.formName : null)
    }
}

/**
 * The base form field component is responsible for:
 *
 * - initializing field data using a set of standard fields (AbstractFormField)
 * - dispatching events and updating the store
 * - running the validators prior to updating the store
 * - rendering error messages underneath the field
 * - styling success and errors in red/green
 *
 * All of these are intedend to be handled in a standard way and thus
 * have been delegated to this class. Every class that extends this one
 * must implement the renderField method to render a specific input type
 */
export abstract class BaseFormField<T> extends React.Component<FormFieldComponentProps<T>, any> {

    public static defaultProps = {
        showErrorMessages: true,
        required: true,
        disabled: false,
        autofocus: false,
        fullWidth: false,
    }

    /**
     * A reference to the form element itself
     */
    public field: any

    constructor(props: FormFieldComponentProps<any>) {
        super(props)
        this.validate = this.props.validate ?
            this.props.validate.bind(this) : this.validate.bind(this)
        this.onChange = this.onChange.bind(this)
        this.onBlur = this.onBlur.bind(this)
        this.onFocus = this.onFocus.bind(this)
    }

    /**
     * Use React refs to set the field elem
     */
    public setFieldRef = (e: any) => this.field = e

    /**
     * Initialize the field in redux store
     */

    /**
     * Initialize the field
     * this will also trigger the validation
     * but will not mark the field as dirty (only onFocus does that)
     */
    public init() {
        this.dispatchUpdate(this.getNextFieldState(this.getValueFromState()))
    }

    public abstract renderField(): JSX.Element

    public renderErrors() {
        if (this.shouldShowErrors()) {
            const { errors } = this.props.field
            return <span>{errors[0]}</span>
        }
    }

    /**
     * Default validation function
     * Checks to see if the field is required
     * And if a value is present
     *
     * @param value
     */
    public validate(value: T): string[] | boolean {
        if (this.props.field.required && !value) {
            return ['* required']
        }
        return []
    }

    /**
     * Create an object representing the next state state of the field
     *
     * @param value
     */
    public getNextFieldState(value: T) {

        const field = { ...this.props.field, value }

        if (!field) {
            return this.props.asyncValidate
                ? createAsyncFormField(this.props)
                : createDefaultFormField(this.props)
        }

        // do not mess with the validation if an async validation function is present
        if (!this.props.asyncValidate) {

            // people might think that the validation function is supposed
            // to return a boolean rather than an array of string errors
            // rather than leave this as a potential bug better to support it here
            const res = this.validate(value)
            field.errors = res === true ? [] : (res === false ? ['* error'] : res)

            field.valid = field.errors && field.errors.length ? false : true
        }

        return field
    }

    public onChange(e: any) {
        e.preventDefault()
        const { name, formName, onChange, asyncOnChange } = this.props

        // console.log('handling change', this.props)
        const field = this.getNextFieldState(this.getValueFromEvent(e))

        if (onChange) {
            onChange(name, formName, field)
        }

        if (asyncOnChange) {
            asyncOnChange(name, formName, field)
        }

        return this.dispatchUpdate(field)
    }

    public onBlur(e: any) {
        e.preventDefault()
        const field = {
            ...this.getNextFieldState(this.props.field.value),
            focused: false,
        }
        if (this.props.onBlur) {
            this.props.onBlur(
                this.props.name,
                this.props.formName,
                field,
            )
        }
        return this.dispatchUpdate(field)
    }

    public onFocus(e: any) {
        e.preventDefault()
        const field = {
            ...this.getNextFieldState(this.props.field.value),
            focused: true,
            dirty: true,
        }
        if (this.props.onFocus) {
            this.props.onFocus(
                this.props.name,
                this.props.formName,
                field,
            )
        }
        return this.dispatchUpdate(field)
    }

    public dispatchUpdate(field: AbstractFormField<T>) {
        this.props.setFormField(
            this.props.name,
            this.props.formName,
            field,
        )
    }

    public getValueFromEvent(e: React.SyntheticEvent<any>) {
        // console.log('GET VALUE FROM EVENT', e.currentTarget.valu)
        return e.currentTarget.value
    }

    /**
     * Decide which value to use - props.value or props.field.value
     * This distinction is there because some inputs are 'semi-controlled'
     * This happens when setting and updating the value is handled by the client
     * but fields such as dirty, valid, focused etc are handled by cosmo ui
     *
     * NOTE: this function should not be typed because in the case of some inputs
     * such as the number input, it is saved as a number in the state but it is
     * inputted as a string
     */
    public getValueFromState(): any {
        const { value, onChange, field, disabled } = this.props
        // if both a value and an onChange handler are provided
        // then it's clear the client wishes to control the field manually

        // if a value is provided as a prop and the field is disabled then we can infer
        // that the client just wants to read the value without changing it
        // so we should use the provided value

        // likewise if a value is provided and the field is pristine then we can infer
        // that we should use the provided value
        const pristine = !field || !field.dirty

        // if the field is dirty, however, then that means the user has made changes
        // in this case if then we must use the latest version of the field
        // since there is no onChange handler the latest version is now in the formReducer

        // if no value is provided at all then of course we just use the formReducer's value
        return value && (onChange || disabled || pristine) ? value : field.value
    }

    public shouldShowErrors() {
        const { valid, dirty, submitted, focused } = this.props.field
        // to show errors then the field must be invalid and either submitted or
        // dirty and not focused (if it's focused the user is changing stuff)
        return !valid && (submitted || (dirty && !focused))
    }

    public shouldShowValid() {
        const { valid, dirty, submitted, focused, required, errors } = this.props.field
        // obviously it becomes visible if its dirty or submitted
        // but if the field isn't required then we won't show any colour
        // at all whilst it's still pristine (hence the required part here)
        const visible = (required || dirty || submitted)
        return !this.props.disabled && visible && valid && !errors.length
    }
}
