/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { audio } from '@kit.AudioKit';
import { avSession } from '@kit.AVSessionKit';
import Logger from '../common/utils/Logger';

const TAG = '[AVPlayerController]';

@Observed
export class VideoPlayerController {
  @Track state: string = 'default';
  @Track durationTime: number = 0;
  @Track currentTime: number = 0;
  @Track volume: number = 0.5;
  @Track width: number = 1920;
  @Track height: number = 1080;
  private avPlayer: media.AVPlayer;
  private surfaceID: string;
  private stateChangeListeners: Array<(newState: string, oldState: string) => void> = [];
  private positionChangeListeners: Array<(newPosition: number) => void> = [];
  private interruptListeners: Array<(playState: avSession.AVPlaybackState) => void> = [];
  private fd?: number;

  private constructor(avPlayer: media.AVPlayer, surfaceID: string) {
    this.surfaceID = surfaceID;
    this.avPlayer = avPlayer;
  }

  /**
   * Creates and initializes an AVPlayerController instance.
   * @param surfaceID The surface ID for video rendering.
   * @param videoType The type of video source ('network' or 'local').
   * @param resource The video resource URL or file path.
   * @returns Promise<AVPlayerController> Initialized player controller instance.
   */
  static async create(surfaceID: string, resource: string) {
    let avPlayer: media.AVPlayer | undefined;
    try {
      avPlayer = await media.createAVPlayer();
    } catch (err) {
      let message = 'Unknown error';
      if (err instanceof Error) {
        message = err.message;
      }
      Logger.error(TAG, `Create AVPlayer failed: ${message}.`);
      return;
    }


    let avPlayerController = new VideoPlayerController(avPlayer, surfaceID);
    avPlayerController.avPlayerLocal(resource);
    return avPlayerController;
  }

  /**
   * Sets up AVPlayer event listeners and state change handlers.
   * Handles time updates, duration changes, speed adjustments, volume changes, errors and state transitions.
   */
  setAVPlayerCallback() {
    this.avPlayer.on('timeUpdate', (currentTime: number) => {
      this.currentTime = currentTime;
    })
    this.avPlayer.on('durationUpdate', (duration: number) => {
      this.durationTime = duration;
    })
    this.avPlayer.on('error', (err: BusinessError) => {
      Logger.error(TAG, `Invoke avPlayer failed, errCode = ${err.code}, errMessage = ${err.message}.`);
      this.avPlayer.reset().catch((err: BusinessError) => {
        Logger.error(TAG, `avPlayer reset failed, errCode = ${err.code}, errMessage = ${err.message}.`);
      });
    });
    this.avPlayer.on('volumeChange', (vol: number) => {
      this.volume = vol;
      Logger.info(TAG, `AVPlayer volumeChange succeeded, seek time is ${vol}.`);
    })
    // State machine change callback function
    this.avPlayer.on('stateChange', async (state: string, _: media.StateChangeReason) => {
      this.stateChangeListeners.forEach(listener => listener(state, this.state));
      this.state = state;
      switch (state) {
        case 'idle':
          Logger.info(TAG, 'AVPlayer state idle called.');
          break;
        // Automatically call prepare after initialization.
        case 'initialized':
          Logger.info(TAG, 'AVPlayer state initialized called.');
          this.avPlayer.surfaceId = this.surfaceID;
          this.avPlayer.prepare().catch((err: BusinessError) => {
            Logger.error(TAG, `avPlayer prepare failed, errCode = ${err.code}, errMessage = ${err.message}.`);
          });
          break;
        // Automatically start playing after the prepare call succeeds.
        case 'prepared':
          Logger.info(TAG, 'AVPlayer state prepared called.');
          this.avPlayer.videoScaleType = media.VideoScaleType.VIDEO_SCALE_TYPE_FIT_CROP
          try {
            this.setAVPlayerVolume(this.volume);
          } catch (error) {
            let err = error as BusinessError;
            Logger.error(TAG, `setAVPlayerVolume failed, errCode = ${err.code}, errMessage = ${err.message}.`);
          }
          this.getVideoSize();
          this.getVideoDuration();
          this.avPlayer.play().catch((err: BusinessError) => {
            Logger.error(TAG, `avPlayer play failed, errCode = ${err.code}, errMessage = ${err.message}.`);
          });
          break;
        case 'playing':
          Logger.info(TAG, 'AVPlayer state playing called.');
          break;
        case 'paused':
          Logger.info(TAG, 'AVPlayer state paused called.');
          break;
        case 'completed':
          Logger.info(TAG, 'AVPlayer state completed called.');
          this.avPlayer.stop().catch((err: BusinessError) => {
            Logger.error(TAG, `avPlayer stop failed, errCode = ${err.code}, errMessage = ${err.message}.`);
          });
          break;
        case 'stopped':
          Logger.info(TAG, 'AVPlayer state stopped called.');
          this.avPlayer.reset().catch((err: BusinessError) => {
            Logger.error(TAG, `avPlayer reset failed, errCode = ${err.code}, errMessage = ${err.message}.`);
          });
          break;
        case 'released':
          Logger.info(TAG, 'AVPlayer state released called.');
          break;
        default:
          Logger.info(TAG, 'AVPlayer state unknown called.');
          break;
      }
    });

    // Audio InterruptCallback
    this.avPlayer?.on('audioInterrupt', async (interruptInfo: audio.InterruptEvent) => {
      Logger.info(TAG, `audioInterrupt forceType = ${interruptInfo.forceType}, hintType = ${interruptInfo.hintType}.`);
      // Before the interruption, AVPlayer is in the playback state, so the playback status of AVSession Broadcast Control Center is PLAY
      let playbackState: avSession.AVPlaybackState = {
        state: avSession.PlaybackState.PLAYBACK_STATE_PLAY,
        loopMode: avSession.LoopMode.LOOP_MODE_SINGLE
      };
      if (interruptInfo.forceType === audio.InterruptForceType.INTERRUPT_SHARE &&
        interruptInfo.hintType === audio.InterruptHint.INTERRUPT_HINT_RESUME) {
        Logger.info(TAG, 'Video resume play.');
        this.avPlayer?.play().catch((err: BusinessError) => {
          Logger.error(TAG, `avPlayer play failed, errCode = ${err.code}, errMessage = ${err.message}.`);
        });
      } else if (interruptInfo.forceType === audio.InterruptForceType.INTERRUPT_FORCE &&
        (interruptInfo.hintType === audio.InterruptHint.INTERRUPT_HINT_PAUSE ||
          interruptInfo.hintType === audio.InterruptHint.INTERRUPT_HINT_STOP)) {
        playbackState.state = avSession.PlaybackState.PLAYBACK_STATE_PAUSE;
      }
      Logger.debug(TAG, `audioInterrupt happend state = ${playbackState.state}.`);
      this.interruptListeners.forEach(listener => listener(playbackState));
    })
  }

  getVideoSize() {
    this.width = this.avPlayer.width;
    this.height = this.avPlayer.height;
  }

  getVideoDuration() {
    this.durationTime = this.avPlayer.duration;
  }

  /**
   * Configures AVPlayer for local file playback.
   * @param fileName The name/path of the local media file.
   */
  avPlayerLocal(filePath: string) {
    this.setAVPlayerCallback();
    let fdPath = 'fd://';
    let file: fs.File;
    try {
      file = fs.openSync(filePath);
    } catch (error) {
      let err = error as BusinessError;
      Logger.error(TAG, `open file failed, errCode = ${err.code}, errMessage = ${err.message}.`);
      return;
    }
    fdPath = fdPath + '' + file.fd;
    this.fd = file.fd;
    this.avPlayer.url = fdPath;
  }

  /**
   * Seeks to specified position in current media.
   * @param timeMs Target position in milliseconds.
   * @param mode Seeking mode (e.g., SEEK_PREV_SYNC).
   * @throws Error if player is not in seekable state.
   */
  async avPlayerSeek(timeMs: number, mode: media.SeekMode) {
    const validSeekStates = ['prepared', 'playing', 'paused', 'completed'];
    if (!validSeekStates.includes(this.state)) {
      Logger.error(TAG, `avPlayerSeek error,this state is ${this.state}.`);
      return;
    }
    this.avPlayer.seek(timeMs, mode);
    this.positionChangeListeners.forEach(listener => listener(timeMs));
  }

  async setAVPlayerPlaying() {
    const validPlayingStates = ['prepared', 'paused', 'completed'];
    if (validPlayingStates.includes(this.state)) {
      try {
        await this.avPlayer.play();
      } catch (error) {
        let err = error as BusinessError;
        Logger.error(TAG, `avPlayer play failed, errCode = ${err.code}, errMessage = ${err.message}.`);
      }
      return;
    } else {
      Logger.error(TAG, `setAVPlayerPlaying error,this state is ${this.state}.`);
    }
  }

  async setAVPlayerPause() {
    if (this.state === 'playing') {
      try {
        await this.avPlayer.pause();
      } catch (error) {
        let err = error as BusinessError;
        Logger.error(TAG, `avPlayer pause failed, errCode = ${err.code}, errMessage = ${err.message}.`);
      }
      return;
    }
  }

  async setAVPlayerStop() {
    const validStopStates = ['prepared', 'paused', 'completed', 'playing'];
    if (validStopStates.includes(this.state)) {
      try {
        await this.avPlayer.stop();
      } catch (error) {
        let err = error as BusinessError;
        Logger.error(TAG, `avPlayer stop failed, errCode = ${err.code}, errMessage = ${err.message}.`);
      }
      return;
    } else {
      Logger.error(TAG, `setAVPlayerStop failed, this state is ${this.state}.`);
    }
  }

  /**
   * Sets the volume level for AVPlayer.
   * @param volume The volume level to set.
   * @throws Error if player is not in a valid state (prepared/paused/completed/playing).
   */
  setAVPlayerVolume(volume: number) {
    const validStopStates = ['prepared', 'paused', 'completed', 'playing'];
    if (validStopStates.includes(this.state)) {
      this.avPlayer.setVolume(volume);
      return;
    } else {
      Logger.error(TAG, `setAVPlayerVolume error, this state is ${this.state}.`);
    }
  }

  /**
   * Registers a callback for player state changes.
   * @param listener Callback function receiving new and old state values.
   */
  onStateChange(listener: (newState: string, oldState: string) => void): void {
    this.stateChangeListeners.push(listener);
  }

  onInterrupt(listener: (playState: avSession.AVPlaybackState) => void): void {
    this.interruptListeners.push(listener);
  }

  /**
   * Registers a callback for playback position changes.
   * @param listener Callback function receiving the new position in milliseconds.
   */
  onPositionChange(listener: (newPosition: number) => void): void {
    this.positionChangeListeners.push(listener);
  }

  offStateChange(listener: (newState: string, oldState: string) => void): void {
    this.stateChangeListeners = this.stateChangeListeners.filter(l => l !== listener);
  }

  offPositionChange(listener: (newPosition: number) => void): void {
    this.positionChangeListeners = this.positionChangeListeners.filter(l => l !== listener);
  }

  /**
   * Releases player resources and removes all event listeners.
   * Closes file descriptor if currently playing a file.
   */
  async releasePlayer() {
    if (this.fd) {
      try {
        fs.closeSync(this.fd);
      } catch (error) {
        let err = error as BusinessError;
        Logger.error(TAG, `close player failed, errCode = ${err.code}, errMessage = ${err.message}.`);
      }
      this.fd = undefined;
    }
    this.avPlayer.off('timeUpdate');
    this.avPlayer.off('durationUpdate');
    this.avPlayer.off('error');
    this.avPlayer.off('volumeChange');
    this.avPlayer.off('stateChange');
    try {
      await this.avPlayer.release();
    } catch (error) {
      let err = error as BusinessError;
      Logger.error(TAG, `avPlayer release failed, errCode = ${err.code}, errMessage = ${err.message}.`);
    }
  }
}