import APIClient
import AVFoundation
import ComposableArchitecture
import MediaPlayer
import PlayerClient
import Utilities

/*
 Separate from our regular Player in `Player.swift`,
 WaveformEditorPlayer has limited functionality and is
 specifically designed for the WaveformEditor. You can
 play, pause, seek, and replace a clip. This can't do
 any queue management, show lyrics, or fire off any
 events tied to the OmniPlayer.
 */
@Reducer
public struct WaveformEditorPlayer {
    @ObservableState
    public struct State: Equatable, Identifiable {
        public var id: Clip.ID { clip.id }
        public var clip: Clip
        public var elapsedTime: CMTime = .zero
        public var totalTime: CMTime = .zero
        public var timeControlStatus: AVPlayer.TimeControlStatus = .waitingToPlayAtSpecifiedRate
        public var isPlaying: Bool = false
        public var shouldStartOmniPlayerAfterTeardown: Bool = false

        @Shared(.inMemory(.playingClipKey)) var previousPlayingClip: Clip?
        @Shared(.inMemory(.isPlayingKey)) var wasOmniPlayerPlaying: Bool = false

        public init(clip: Clip) {
            self.clip = clip
        }
    }

    public enum Action {
        case setup
        case replaceCurrentItem(Clip)
        case play
        case pause
        case seek(Double, play: Bool)
        case `internal`(Internal)
        case delegate(Delegate)
        case teardown

        public enum Internal {
            case setupCompletion(Result<Void, Error>)
            case timeControlStatusResponse(Result<AVPlayer.TimeControlStatus, Error>)
            case periodicTimeResponse(CMTime)
            case replaceCurrentItemResponse(Clip, CMTime)
            case didPlayToEndTime
        }

        public enum Delegate {
            case setupComplete
        }
    }

    @Dependency(PlayerClient.self) var playerClient
    @Dependency(\.mainQueue) var mainQueue
    @Dependency(\.telemetryClient) var telemetry

    public var body: some ReducerOf<Self> {
        Reduce { state, action in
            struct TimeControlStatusPublisherCancellable: Hashable {}
            struct ElapsedTimePublisherCancellable: Hashable {}
            struct DidPlayToEndTimeNotificationCancellable: Hashable {}

            switch action {
            case .setup:
                let wasPlaying = state.wasOmniPlayerPlaying
                state.shouldStartOmniPlayerAfterTeardown = wasPlaying
                return .merge(
                    .run { send in
                        if wasPlaying {
                            playerClient.pause()
                        }
                        await send(.internal(.setupCompletion(Result(catching: { try playerClient.setup() }))))
                    },
                    .publisher {
                        playerClient.timeControlStatus()
                            .map { .internal(.timeControlStatusResponse(.success($0))) }
                    }
                    .cancellable(id: TimeControlStatusPublisherCancellable(), cancelInFlight: true)
                )

            case .teardown:
                if state.shouldStartOmniPlayerAfterTeardown,
                   let previousClip = state.previousPlayingClip
                {
                    let audioUrl = previousClip.audioUrl
                    return .run { _ in
                        playerClient.pause()
                        _ = await playerClient.replaceCurrentItem(audioUrl)
                    }
                }
                return .none

            case .internal(.setupCompletion(.success)):
                return .send(.delegate(.setupComplete))

            case .internal(.setupCompletion(.failure(let error))):
                log.telemetry.error(error)
                return .none

            case .internal(.timeControlStatusResponse(.success(let status))):
                state.timeControlStatus = status
                state.isPlaying = status == .playing
                return .none

            case .internal(.timeControlStatusResponse(.failure(let error))):
                log.telemetry.error(error)
                return .none

            case .replaceCurrentItem(let clip):
                guard !clip.audioUrl.isEmpty else { return .none }
                state.clip = clip

                let url = clip.playableSceneUrl?.absoluteString ?? clip.audioUrl
                return .run { send in
                    let duration = await playerClient.replaceCurrentItem(url)
                    await send(.internal(.replaceCurrentItemResponse(clip, duration)))
                }

            case .internal(.replaceCurrentItemResponse(_, let duration)):
                state.totalTime = duration
                return .publisher {
                    NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime)
                        .map { _ in .internal(.didPlayToEndTime) }
                }
                .cancellable(id: DidPlayToEndTimeNotificationCancellable())

            case .internal(.periodicTimeResponse(let time)):
                state.elapsedTime = time
                return .none

            case .internal(.didPlayToEndTime):
                return .send(.pause)

            case .play:
                playerClient.play()
                return .publisher {
                    playerClient.periodicTime()
                        .map { .internal(.periodicTimeResponse($0)) }
                }
                .cancellable(id: ElapsedTimePublisherCancellable(), cancelInFlight: true)

            case .pause:
                playerClient.pause()
                return .none

            case .seek(let time, let play):
                let cmTime = CMTime(seconds: time, preferredTimescale: 1000)
                return .merge(
                    .run { _ in
                        playerClient.seek(cmTime)
                        if play {
                            playerClient.play()
                        }
                    },
                    .publisher {
                        playerClient.periodicTime()
                            .map { .internal(.periodicTimeResponse($0)) }
                    }
                    .cancellable(id: ElapsedTimePublisherCancellable(), cancelInFlight: true)
                )

            case .delegate:
                return .none
            }
        }
    }
}
