/*
 * Copyright (C) 2019 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import {Button, CloseButton} from '@instructure/ui-buttons'
import {Heading} from '@instructure/ui-heading'
import {Modal} from '@instructure/ui-modal'
import {ProgressBar} from '@instructure/ui-progress'
import {Tabs} from '@instructure/ui-tabs'
import {Text} from '@instructure/ui-text'
import {px} from '@instructure/ui-utils'
import {isEqual} from 'es-toolkit/compat'
import {bool, element, func, instanceOf, oneOfType, shape, string} from 'prop-types'
import React, {Suspense} from 'react'
import ReactDOM from 'react-dom'
import {ACCEPTED_FILE_TYPES} from './acceptedMediaFileTypes'
import saveMediaRecording, {saveClosedCaptionsForAttachment} from './saveMediaRecording'
import {CC_FILE_MAX_BYTES, mediaExtension} from './shared/constants'
import LoadingIndicator from './shared/LoadingIndicator'
import translationShape from './translationShape'

const fileExtensionRegex = /\.\S/
const DEFAULT_EXTENSION = 'webm'

export default function UploadMedia(props) {
  return <UploadMediaModal {...props} />
}

const ComputerPanel = React.lazy(() => import('./ComputerPanel'))
const MediaRecorder = React.lazy(() => import('./MediaRecorder'))

export const PANELS = {
  COMPUTER: 0,
  RECORD: 1,
}

export class UploadMediaModal extends React.Component {
  static propTypes = {
    disableSubmitWhileUploading: bool,
    liveRegion: func,
    mountNode: oneOfType([element, func]),
    rcsConfig: shape({
      contextId: string,
      contextType: string,
      origin: string,
      headers: shape({Authorization: string}),
    }),
    onStartUpload: func,
    onUploadComplete: func,
    onDismiss: func,
    open: bool,
    tabs: shape({
      record: bool,
      upload: bool,
    }),
    uploadMediaTranslations: translationShape,
    // for testing
    computerFile: instanceOf(File),
    userLocale: string,
  }

  static defaultProps = {
    disableSubmitWhileUploading: false,
    userLocale: 'en',
  }

  constructor(props) {
    super(props)

    const defaultSelectedPanel = this.inferSelectedPanel(props.tabs)

    if (props.computerFile) {
      props.computerFile.title = props.computerFile.name
    }

    this.state = {
      hasUploadedFile: !!props.computerFile,
      uploading: false,
      progress: 0,
      selectedPanel: defaultSelectedPanel,
      computerFile: props.computerFile || null,
      subtitles: [],
      recordedFile: null,
      modalBodySize: {width: NaN, height: NaN},
    }

    this.modalBodyRef = React.createRef()
    this.computerPanelRef = React.createRef()
  }

  onRecordingSave = file => {
    if (this.state.uploading) return
    this.setState({recordedFile: file}, this.handleSubmit)
  }

  inferSelectedPanel = tabs => {
    let selectedPanel = -1

    if (tabs.upload) {
      selectedPanel = 0
    } else if (tabs.record) {
      selectedPanel = 1
    }

    return selectedPanel
  }

  handleSubmit = () => {
    switch (this.state.selectedPanel) {
      case PANELS.COMPUTER: {
        const {computerFile} = this.state
        this.computerPanelRef.current?.updateValidationMessages(computerFile)
        if (computerFile?.title?.trim()) {
          this.uploadFile(computerFile)
        }
        break
      }
      case PANELS.RECORD: {
        const button = document.getElementById('media_capture_save_button')
        if (button) button.disabled = true
        this.uploadFile(this.state.recordedFile)
        break
      }
      default: {
        throw new Error('Selected Panel is invalid') // Should never get here
      }
    }
  }

  submitEnabled = () => {
    return !this.props.disableSubmitWhileUploading || !this.state.uploading
  }

  uploadFile(file) {
    this.setState({uploading: true}, () => {
      const userEnteredTitle = file.title || file.name
      const extension = mediaExtension(file.type) || DEFAULT_EXTENSION

      // Ensure the file name has a proper extension so the K5Uploader's
      // extension-based file type check can determine the correct media type.
      // Recordings from @instructure/studio-capture often lack a file extension.
      // This must happen before onStartUpload so placeholder insertion and
      // later removal in the RCE both key off the same file name.
      if (!fileExtensionRegex.test(file.name)) {
        const newName = file.name + (file.name.endsWith('.') ? extension : `.${extension}`)
        file = new File([file], newName, {type: file.type})
      }

      file.userEnteredTitle = userEnteredTitle
      if (!fileExtensionRegex.test(file.userEnteredTitle)) {
        file.userEnteredTitle += file.userEnteredTitle.endsWith('.')
          ? `${extension}`
          : `.${extension}`
      }

      this.props.onStartUpload && this.props.onStartUpload(file)
      saveMediaRecording(
        file,
        this.props.rcsConfig,
        this.saveMediaCallback,
        this.onSaveMediaProgress,
      )
    })
  }

  onSaveMediaProgress = progress => {
    this.setState({progress})
  }

  saveMediaCallback = async (err, data) => {
    const {onUploadComplete, onDismiss, rcsConfig} = this.props
    const {selectedPanel, subtitles} = this.state
    this.setState({uploading: false})
    if (err) {
      onUploadComplete?.(err, data)
    } else {
      try {
        const {media_object} = data.mediaObject
        let captions
        if (selectedPanel === PANELS.COMPUTER && subtitles.length > 0) {
          captions = await saveClosedCaptionsForAttachment(
            media_object.attachment_id,
            subtitles,
            rcsConfig,
            CC_FILE_MAX_BYTES,
          )
        }
        onUploadComplete?.(null, data, captions?.data)
      } catch (ex) {
        onUploadComplete?.(ex, null)
      }
    }
    onDismiss?.()
    const button = document.getElementById('media_capture_save_button')
    if (button) button.disabled = false
  }

  componentDidMount() {
    this.setBodySize(this.state)
  }

  componentDidUpdate(prevProps, prevState) {
    this.setBodySize(prevState)

    const invalidPanelSelected = () =>
      (this.state.selectedPanel === PANELS.COMPUTER && !this.props.tabs.upload) ||
      (this.state.selectedPanel === PANELS.RECORD && !this.props.tabs.record)

    // If the specified tabs have not changed and the selected panel is valid,
    // don't attempt to set the selected panel state (this would trigger an
    // endless loop).
    if (isEqual(prevProps.tabs, this.props.tabs) && !invalidPanelSelected()) return

    if (prevState.selectedPanel === -1 || invalidPanelSelected()) {
      // The tabs prop has changed and the selectedPanel was
      // never set in the constructor, or the selectedPanel is invalid
      // given the available tabs. Attempt to infer the selected panel
      // based on the new tabs list
      // ** This is an eslint error which I don't have the context
      // ** to address working my current ticket. Ignore for now
      // ** since it's been working for a while now.
      this.setState({selectedPanel: this.inferSelectedPanel(this.props.tabs)})
    }
  }

  setBodySize(state) {
    if (this.modalBodyRef.current) {
      // eslint-disable-next-line react/no-find-dom-node
      const thebody = ReactDOM.findDOMNode(this.modalBodyRef.current)
      const modalBodySize = thebody.getBoundingClientRect()
      modalBodySize.height -= px('3rem') // leave room for the tabs
      if (
        modalBodySize.width !== state.modalBodySize.width ||
        modalBodySize.height !== state.modalBodySize.height
      ) {
        if (modalBodySize.width > 0 && modalBodySize.height > 0) {
          this.setState({modalBodySize})
        }
      }
    }
  }

  renderModalBody = () => {
    const {
      COMPUTER_PANEL_TITLE,
      DRAG_FILE_TEXT,
      LOADING_MEDIA,
      RECORD_PANEL_TITLE,
      MEDIA_RECORD_NOT_AVAILABLE,
    } = this.props.uploadMediaTranslations.UploadMediaStrings

    if (!this.props.open) {
      return null
    }

    return (
      <Tabs
        maxWidth="large"
        onRequestTabChange={(_, {index}) => {
          const {tabs} = this.props
          // You can only change tabs if more than one tab is available
          if (tabs.upload && tabs.record) {
            this.setState({selectedPanel: index})
          }
        }}
      >
        {this.props.tabs.upload && (
          <Tabs.Panel
            key="computer"
            isSelected={this.state.selectedPanel === PANELS.COMPUTER}
            renderTitle={() => COMPUTER_PANEL_TITLE}
          >
            <Suspense fallback={LoadingIndicator(LOADING_MEDIA)}>
              <ComputerPanel
                ref={this.computerPanelRef}
                theFile={this.state.computerFile}
                setFile={file => this.setState({computerFile: file})}
                hasUploadedFile={this.state.hasUploadedFile}
                setHasUploadedFile={uploadFileState =>
                  this.setState({hasUploadedFile: uploadFileState})
                }
                label={DRAG_FILE_TEXT}
                uploadMediaTranslations={this.props.uploadMediaTranslations}
                accept={ACCEPTED_FILE_TYPES}
                userLocale={this.props.userLocale}
                liveRegion={this.props.liveRegion}
                updateSubtitles={subtitles => {
                  this.setState({subtitles})
                }}
                bounds={this.state.modalBodySize}
                mountNode={this.props.mountNode}
              />
            </Suspense>
          </Tabs.Panel>
        )}
        {this.props.tabs.record && (
          <Tabs.Panel
            key="record"
            isSelected={this.state.selectedPanel === PANELS.RECORD}
            renderTitle={() => RECORD_PANEL_TITLE}
          >
            <Suspense fallback={LoadingIndicator(LOADING_MEDIA)}>
              <MediaRecorder
                MediaCaptureStrings={this.props.uploadMediaTranslations.MediaCaptureStrings}
                errorMessage={MEDIA_RECORD_NOT_AVAILABLE}
                onSave={this.onRecordingSave}
              />
            </Suspense>
          </Tabs.Panel>
        )}
      </Tabs>
    )
  }

  onModalClose = () => {
    this.setState({
      hasUploadedFile: false,
      selectedPanel: this.inferSelectedPanel(this.props.tabs),
      computerFile: null,
    })
    this.props.onDismiss()
  }

  renderModalFooter = () => {
    if (this.state.selectedPanel === PANELS.RECORD) {
      return null
    }

    const {CLOSE_TEXT, SUBMIT_TEXT, PROGRESS_LABEL} =
      this.props.uploadMediaTranslations.UploadMediaStrings
    return (
      <Modal.Footer>
        {this.state.uploading && (
          <ProgressBar
            screenReaderLabel={PROGRESS_LABEL}
            valueNow={this.state.progress}
            valueMax={100}
            renderValue={({valueNow}) => {
              return <Text>{valueNow}%</Text>
            }}
          />
        )}
        &nbsp;
        <Button onClick={this.onModalClose}> {CLOSE_TEXT} </Button>
        &nbsp;
        <Button
          onClick={e => {
            e.preventDefault()
            this.handleSubmit()
          }}
          color="primary"
          type="submit"
          interaction={this.submitEnabled() ? 'enabled' : 'disabled'}
        >
          {SUBMIT_TEXT}
        </Button>
      </Modal.Footer>
    )
  }

  render() {
    const {CLOSE_TEXT, UPLOAD_MEDIA_LABEL} = this.props.uploadMediaTranslations.UploadMediaStrings
    const dataProps = Object.keys(this.props)
      .filter(p => p.startsWith('data-'))
      .reduce((obj, key) => {
        obj[key] = this.props[key]
        return obj
      }, {})
    return (
      <Modal
        label={UPLOAD_MEDIA_LABEL}
        mountNode={this.props.mountNode}
        size="large"
        onDismiss={this.onModalClose}
        open={this.props.open}
        shouldCloseOnDocumentClick={false}
        liveRegion={this.props.liveRegion}
        {...dataProps}
      >
        <Modal.Header>
          <CloseButton
            onClick={this.onModalClose}
            offset="medium"
            placement="end"
            screenReaderLabel={CLOSE_TEXT}
          />
          <Heading>{UPLOAD_MEDIA_LABEL}</Heading>
        </Modal.Header>
        <Modal.Body ref={this.modalBodyRef}>{this.renderModalBody()}</Modal.Body>
        {this.renderModalFooter()}
      </Modal>
    )
  }
}
