/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2018 - present Instructure, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
import { Component } from 'react'

import { deepEqual } from '@instructure/ui-utils'
import { logWarn as warn } from '@instructure/console'
import { requestAnimationFrame } from '@instructure/ui-dom-utils'

import { allowedProps, EditableState } from './props.js'
import type {
  EditableProps,
  GetContainerProps,
  GetEditButtonProps,
  GetEditorProps,
  GetViewerProps
} from './props'

/**
---
category: components
---
**/
class Editable extends Component<EditableProps> {
  static displayName = 'Editable'
  static allowedProps = allowedProps
  static defaultProps = {
    readOnly: false,
    children: null
  }

  state: EditableState = {
    showModeToggle: false,
    valueOnEdit: null // the value when mode flips from view -> edit
  }

  _editorRef: HTMLInputElement | null = null
  _editButtonRef: HTMLButtonElement | null = null

  constructor(props: EditableProps) {
    super(props)

    warn(
      props.readOnly ? props.mode === 'view' : true,
      '[Editable] When readOnly is true, mode must be "view"'
    )
  }

  componentDidMount() {
    if (this.props.mode === 'edit') {
      this.focusEditor()
    }
  }

  componentDidUpdate(prevProps: EditableProps) {
    const { mode, value, onChange } = this.props

    // on the switch from view -> edit
    if (prevProps.mode !== 'edit' && mode === 'edit') {
      this.setState({ valueOnEdit: value })
      this.focusEditor()
    }

    // on the switch from edit to view
    if (prevProps.mode === 'edit' && mode !== 'edit') {
      this._editButtonRef && this._editButtonRef.focus()
      if (onChange && !deepEqual(this.state.valueOnEdit, value)) {
        onChange(value)
      }
    }
  }

  focusEditor() {
    warn(
      !!this._editorRef && !this.props.readOnly,
      '[Editable] Did you forget to connect editorRef to your editor component?'
    )
    if (this._editorRef) {
      this._editorRef.focus()
    }
  }

  enterView() {
    if (this.props.mode !== 'view') {
      this.setState({ showModeToggle: true })
      this.props.onChangeMode('view')
    }
  }

  enterEdit() {
    if (!this.props.readOnly) {
      this.setState({ showModeToggle: false })
      if (this.props.mode !== 'edit') {
        this.props.onChangeMode('edit')
      }
    }
  }

  handleEditESC = (event: React.KeyboardEvent) => {
    if (event.key === 'Escape') {
      event.stopPropagation()
      this.enterView()
    }
  }

  handleViewMouseOver = (event: React.MouseEvent) => {
    if (this.props.mode === 'view') {
      // because the mouse event handlers are on the container, not the view
      event.stopPropagation()
      this.setState({ showModeToggle: true })
    }
  }

  handleViewMouseOut = (event: React.MouseEvent) => {
    if (this.props.mode === 'view') {
      event.stopPropagation()
      this.setState({
        showModeToggle: document.activeElement === this._editButtonRef
      })
    }
  }

  handleViewClick = (event: React.MouseEvent) => {
    if (event.buttons === 1) {
      requestAnimationFrame(() => {
        this.enterEdit()
      })
    }
  }

  handleEditBlur = () => {
    this.enterView()
  }

  handleEditButtonFocus = () => {
    this.setState({ showModeToggle: true })
  }

  handleEditButtonBlur = () => {
    this.setState({ showModeToggle: false })
  }

  handleEditButtonClick = () => {
    this.enterEdit()
  }

  ref: Element | null = null

  handleContainerRef = (el: Element | null) => {
    const { elementRef } = this.props

    this.ref = el

    if (typeof elementRef === 'function') {
      elementRef(el)
    }
  }

  // ------- prop getters ------
  getContainerProps: GetContainerProps = (props = {}) => {
    const { mode, readOnly } = this.props
    return {
      onMouseOver: this.handleViewMouseOver,
      onMouseOut: this.handleViewMouseOut,
      onMouseDown: mode !== 'edit' ? this.handleViewClick : undefined,
      onKeyUp: mode === 'edit' ? this.handleEditESC : undefined,
      readOnly,
      ...props,
      ref: this.handleContainerRef
    }
  }

  getViewerProps: GetViewerProps = (props = {}) => {
    return {
      mode: this.props.mode,
      readOnly: this.props.readOnly,
      ...props
    }
  }

  getEditorProps: GetEditorProps = (props = {}) => {
    return {
      mode: this.props.mode,
      onBlur: this.handleEditBlur,
      editorRef: (el: HTMLElement | null) => {
        this._editorRef = el as HTMLInputElement
      },
      readOnly: this.props.readOnly,
      ...props
    }
  }

  getEditButtonProps: GetEditButtonProps = (props = {}) => {
    return {
      onClick: this.handleEditButtonClick,
      onFocus: this.handleEditButtonFocus,
      onBlur: this.handleEditButtonBlur,
      isVisible: this.state.showModeToggle,
      buttonRef: (el: Element) => {
        this._editButtonRef = el as HTMLButtonElement
      },
      readOnly: this.props.readOnly,
      ...props
    }
  }

  render() {
    const { children, render = children, mode } = this.props

    if (typeof render === 'function') {
      return render({
        mode,
        getContainerProps: this.getContainerProps,
        getViewerProps: this.getViewerProps,
        getEditorProps: this.getEditorProps,
        getEditButtonProps: this.getEditButtonProps
      })
    } else {
      return null
    }
  }
}

export default Editable
export { Editable }
