import React, { Component, forwardRef, SyntheticEvent } from 'react'
import { getPosX, throttle } from './utils'
import { OnSeek } from './index'

// ProgressBar encapsulates all logic related to displaying and interacting with
// the audio playback progress: (1) current position, (2) buffered segments,
// (3) drag & seek interactions (mouse + touch), and (4) accessibility ARIA data.
//
// NOTABLE BEHAVIOR / DESIGN CHOICES:
// - Drag logic is fully managed by adding / removing global window listeners so that
//   seeking continues even if the pointer leaves the bar's bounding box.
// - Time updates are throttled via the provided progressUpdateInterval to limit
//   React re-renders during playback (performance optimization).
// - When an optional async onSeek callback is provided, UI waits (state.waitingForSeekCallback)
//   so that timeupdate events do not fight with the in-flight seek.
// - Download (buffer) progress is animated by toggling a short-lived flag which
//   influences CSS transition durations.
// - Supports externally supplied srcDuration (e.g., for streams where metadata
//   duration might be unknown or unreliable initially). If srcDuration is passed
//   it short-circuits reliance on audio.duration.

interface ProgressBarForwardRefProps {
  audio: HTMLAudioElement
  progressUpdateInterval: number
  showDownloadProgress: boolean
  showFilledProgress: boolean
  srcDuration?: number
  onSeek?: OnSeek
  onChangeCurrentTimeError?: (err: Error) => void
  i18nProgressBar: string
}
interface ProgressBarProps extends ProgressBarForwardRefProps {
  progressBar: React.RefObject<HTMLDivElement>
}

interface ProgressBarState {
  isDraggingProgress: boolean
  currentTimePos?: string
  hasDownloadProgressAnimation: boolean
  downloadProgressArr: DownloadProgress[]
  waitingForSeekCallback: boolean
}

interface DownloadProgress {
  left: string
  width: string
}

interface TimePosInfo {
  currentTime: number
  currentTimePos: string
}

class ProgressBar extends Component<ProgressBarProps, ProgressBarState> {
  audio?: HTMLAudioElement

  // The candidate time (in seconds) corresponding to the most recent pointer move
  // while the user is actively dragging. We don't update audio.currentTime until
  // the drag ends (unless an async onSeek handler is provided) to avoid excessive
  // media seeks and to keep UI snappy.
  timeOnMouseMove = 0 // Audio's current time while mouse is down and moving over the progress bar

  // Prevents adding duplicate media element listeners if component re-renders
  // or receives the same audio element again.
  hasAddedAudioEventListener = false

  downloadProgressAnimationTimer?: number

  state: ProgressBarState = {
    isDraggingProgress: false,
    currentTimePos: '0%',
    hasDownloadProgressAnimation: false,
    downloadProgressArr: [],
    waitingForSeekCallback: false,
  }

  getDuration(): number {
    const { audio, srcDuration } = this.props
    return typeof srcDuration === 'undefined' ? audio.duration : srcDuration
  }

  // Get time info while dragging indicator by mouse or touch
  getCurrentProgress = (event: MouseEvent | TouchEvent): TimePosInfo => {
    const { audio, progressBar } = this.props
    // A single-file progressive download (non-blob) can have transient states
    // where currentTime is not yet finite. In those cases return zeros to avoid
    // NaN propagation.
    const isSingleFileProgressiveDownload =
      audio.src.indexOf('blob:') !== 0 && typeof this.props.srcDuration === 'undefined'

    if (isSingleFileProgressiveDownload && (!audio.src || !isFinite(audio.currentTime) || !progressBar.current)) {
      return { currentTime: 0, currentTimePos: '0%' }
    }

    const progressBarRect = progressBar.current.getBoundingClientRect()
    const maxRelativePos = progressBarRect.width
    let relativePos = getPosX(event) - progressBarRect.left

    if (relativePos < 0) {
      relativePos = 0
    } else if (relativePos > maxRelativePos) {
      relativePos = maxRelativePos
    }
    const duration = this.getDuration()
    const currentTime = (duration * relativePos) / maxRelativePos
    return { currentTime, currentTimePos: `${((relativePos / maxRelativePos) * 100).toFixed(2)}%` }
  }

  handleContextMenu = (event: SyntheticEvent): void => {
    event.preventDefault()
  }

  /* Handle mouse down or touch start on progress bar event */
  handleMouseDownOrTouchStartProgressBar = (event: React.MouseEvent | React.TouchEvent): void => {
    event.stopPropagation()
    const { currentTime, currentTimePos } = this.getCurrentProgress(event.nativeEvent)

    if (isFinite(currentTime)) {
      this.timeOnMouseMove = currentTime
      this.setState({ isDraggingProgress: true, currentTimePos })
      // Attach global listeners so drag remains responsive even if the pointer
      // leaves the progress bar element. Distinguish mouse vs touch for proper events.
      if (event.nativeEvent instanceof MouseEvent) {
        window.addEventListener('mousemove', this.handleWindowMouseOrTouchMove)
        window.addEventListener('mouseup', this.handleWindowMouseOrTouchUp)
      } else {
        window.addEventListener('touchmove', this.handleWindowMouseOrTouchMove)
        window.addEventListener('touchend', this.handleWindowMouseOrTouchUp)
      }
    }
  }

  handleWindowMouseOrTouchMove = (event: TouchEvent | MouseEvent): void => {
    if (event instanceof MouseEvent) {
      event.preventDefault()
    }
    event.stopPropagation()
    // Prevent Chrome drag selection bug
    const windowSelection: Selection | null = window.getSelection()
    if (windowSelection && windowSelection.type === 'Range') {
      windowSelection.empty()
    }

    const { isDraggingProgress } = this.state
    if (isDraggingProgress) {
      const { currentTime, currentTimePos } = this.getCurrentProgress(event)
      this.timeOnMouseMove = currentTime
      this.setState({ currentTimePos })
    }
  }

  handleWindowMouseOrTouchUp = (event: MouseEvent | TouchEvent): void => {
    event.stopPropagation()
    const newTime = this.timeOnMouseMove
    const { audio, onChangeCurrentTimeError, onSeek } = this.props

    if (onSeek) {
      // When an async onSeek is provided, we don't update audio.currentTime here;
      // instead we delegate timing to the callback so the integrator can control
      // buffering / custom seek logic (e.g., remote media, HLS, etc.). While
      // waiting, we suppress timeupdate-driven UI updates.
      this.setState({ isDraggingProgress: false, waitingForSeekCallback: true }, () => {
        onSeek(audio, newTime).then(
          () => this.setState({ waitingForSeekCallback: false }),
          (err: unknown) => {
            const message = err instanceof Error ? err.message : String(err)
            throw new Error(message)
          }
        )
      })
    } else {
      const newProps: { isDraggingProgress: boolean; currentTimePos?: string } = {
        isDraggingProgress: false,
      }
      if (audio.readyState === audio.HAVE_NOTHING || audio.readyState === audio.HAVE_METADATA || !isFinite(newTime)) {
        try {
          audio.load()
        } catch (err) {
          newProps.currentTimePos = '0%'
          return onChangeCurrentTimeError && onChangeCurrentTimeError(err as Error)
        }
      }

      audio.currentTime = newTime
      this.setState(newProps)
    }

    if (event instanceof MouseEvent) {
      window.removeEventListener('mousemove', this.handleWindowMouseOrTouchMove)
      window.removeEventListener('mouseup', this.handleWindowMouseOrTouchUp)
    } else {
      window.removeEventListener('touchmove', this.handleWindowMouseOrTouchMove)
      window.removeEventListener('touchend', this.handleWindowMouseOrTouchUp)
    }
  }

  handleAudioTimeUpdate = throttle((e: Event): void => {
    const { isDraggingProgress } = this.state
    const audio = e.target as HTMLAudioElement
    // Avoid updating UI while user is dragging (we show the drag position instead)
    // or while an async seek is pending (prevents jitter / race conditions).
    if (isDraggingProgress || this.state.waitingForSeekCallback === true) return

    const { currentTime } = audio
    const duration = this.getDuration()

    this.setState({
      currentTimePos: `${((currentTime / duration) * 100 || 0).toFixed(2)}%`,
    })
  }, this.props.progressUpdateInterval)

  handleAudioDownloadProgressUpdate = (e: Event): void => {
    const audio = e.target as HTMLAudioElement
    const duration = this.getDuration()

    const downloadProgressArr: DownloadProgress[] = []
    for (let i = 0; i < audio.buffered.length; i++) {
      const bufferedStart: number = audio.buffered.start(i)
      const bufferedEnd: number = audio.buffered.end(i)
      downloadProgressArr.push({
        left: `${Math.round((100 / duration) * bufferedStart) || 0}%`,
        width: `${Math.round((100 / duration) * (bufferedEnd - bufferedStart)) || 0}%`,
      })
    }

    clearTimeout(this.downloadProgressAnimationTimer)
    // Setting animation flag makes subsequent render use CSS transition for a
    // short burst, creating a smooth buffer-bar update effect.
    this.setState({ downloadProgressArr, hasDownloadProgressAnimation: true })
    this.downloadProgressAnimationTimer = setTimeout(() => {
      this.setState({ hasDownloadProgressAnimation: false })
    }, 200) as unknown as number
  }

  initialize(): void {
    const { audio } = this.props
    if (audio && !this.hasAddedAudioEventListener) {
      this.audio = audio
      this.hasAddedAudioEventListener = true
      audio.addEventListener('timeupdate', this.handleAudioTimeUpdate)
      audio.addEventListener('progress', this.handleAudioDownloadProgressUpdate)
    }
  }

  componentDidMount(): void {
    this.initialize()
  }

  componentDidUpdate(): void {
    this.initialize()
  }

  componentWillUnmount(): void {
    if (this.audio && this.hasAddedAudioEventListener) {
      this.audio.removeEventListener('timeupdate', this.handleAudioTimeUpdate)
      this.audio.removeEventListener('progress', this.handleAudioDownloadProgressUpdate)
    }
    clearTimeout(this.downloadProgressAnimationTimer)
  }

  render(): React.ReactNode {
    const { showDownloadProgress, showFilledProgress, progressBar, i18nProgressBar } = this.props
    const { currentTimePos, downloadProgressArr, hasDownloadProgressAnimation } = this.state

    return (
      <div
        className="rhap_progress-container"
        ref={progressBar}
        aria-label={i18nProgressBar}
        role="progressbar"
        aria-valuemin={0}
        aria-valuemax={100}
        aria-valuenow={Number(currentTimePos.split('%')[0])}
        tabIndex={0}
        onMouseDown={this.handleMouseDownOrTouchStartProgressBar}
        onTouchStart={this.handleMouseDownOrTouchStartProgressBar}
        onContextMenu={this.handleContextMenu}
      >
        <div className={`rhap_progress-bar ${showDownloadProgress ? 'rhap_progress-bar-show-download' : ''}`}>
          <div className="rhap_progress-indicator" style={{ left: currentTimePos }} />
          {showFilledProgress && <div className="rhap_progress-filled" style={{ width: currentTimePos }} />}
          {showDownloadProgress &&
            downloadProgressArr.map(({ left, width }, i) => (
              <div
                key={i}
                className="rhap_download-progress"
                style={{ left, width, transitionDuration: hasDownloadProgressAnimation ? '.2s' : '0s' }}
              />
            ))}
        </div>
      </div>
    )
  }
}

const ProgressBarForwardRef = (
  props: ProgressBarForwardRefProps,
  ref: React.Ref<HTMLDivElement>
): React.ReactElement => <ProgressBar {...props} progressBar={ref as React.RefObject<HTMLDivElement>} />

export default forwardRef(ProgressBarForwardRef)
export { ProgressBar, ProgressBarForwardRef }
