//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import Defaults
import JellyfinAPI
import MediaPlayer
import Stinsen
import SwiftUI
import VLCUI

// TODO: organize
// TODO: localization necessary for toast text?
// TODO: entire gesture layer should be separate

struct LiveVideoPlayer: View {

    @Default(.VideoPlayer.jumpBackwardLength)
    private var jumpBackwardLength
    @Default(.VideoPlayer.jumpForwardLength)
    private var jumpForwardLength

    @Default(.VideoPlayer.Gesture.horizontalPanGesture)
    private var horizontalPanGesture
    @Default(.VideoPlayer.Gesture.horizontalSwipeGesture)
    private var horizontalSwipeGesture
    @Default(.VideoPlayer.Gesture.longPressGesture)
    private var longPressGesture
    @Default(.VideoPlayer.Gesture.multiTapGesture)
    private var multiTapGesture
    @Default(.VideoPlayer.Gesture.doubleTouchGesture)
    private var doubleTouchGesture
    @Default(.VideoPlayer.Gesture.pinchGesture)
    private var pinchGesture
    @Default(.VideoPlayer.Gesture.verticalPanGestureLeft)
    private var verticalGestureLeft
    @Default(.VideoPlayer.Gesture.verticalPanGestureRight)
    private var verticalGestureRight

    @Default(.VideoPlayer.Subtitle.subtitleColor)
    private var subtitleColor
    @Default(.VideoPlayer.Subtitle.subtitleFontName)
    private var subtitleFontName
    @Default(.VideoPlayer.Subtitle.subtitleSize)
    private var subtitleSize

    @EnvironmentObject
    private var router: LiveVideoPlayerCoordinator.Router

    @ObservedObject
    private var currentProgressHandler: VideoPlayerManager.CurrentProgressHandler
    @StateObject
    private var splitContentViewProxy: SplitContentViewProxy = .init()
    @ObservedObject
    private var videoPlayerManager: LiveVideoPlayerManager

    @State
    private var audioOffset: Int = 0
    @State
    private var isAspectFilled: Bool = false
    @State
    private var isGestureLocked: Bool = false
    @State
    private var isPresentingOverlay: Bool = false
    @State
    private var isScrubbing: Bool = false
    @State
    private var playbackSpeed: Double = 1
    @State
    private var subtitleOffset: Int = 0

    private let gestureStateHandler: VideoPlayer.GestureStateHandler = .init()
    private let updateViewProxy: UpdateViewProxy = .init()

    @ViewBuilder
    private var playerView: some View {
        SplitContentView(splitContentWidth: 400)
            .proxy(splitContentViewProxy)
            .content {
                ZStack {
                    VLCVideoPlayer(configuration: videoPlayerManager.currentViewModel.vlcVideoPlayerConfiguration)
                        .proxy(videoPlayerManager.proxy)
                        .onTicksUpdated { ticks, _ in

                            let newSeconds = ticks / 1000
                            var newProgress = CGFloat(newSeconds) / CGFloat(videoPlayerManager.currentViewModel.item.runTimeSeconds)
                            if newProgress.isInfinite || newProgress.isNaN {
                                newProgress = 0
                            }
                            currentProgressHandler.progress = newProgress
                            currentProgressHandler.seconds = newSeconds

                            guard !isScrubbing else { return }
                            currentProgressHandler.scrubbedProgress = newProgress
                        }
                        .onStateUpdated { state, _ in

                            videoPlayerManager.onStateUpdated(newState: state)

                            if state == .ended {
                                if let _ = videoPlayerManager.nextViewModel,
                                   Defaults[.VideoPlayer.autoPlayEnabled]
                                {
                                    videoPlayerManager.selectNextViewModel()
                                } else {
                                    router.dismissCoordinator {}
                                }
                            }
                        }

                    GestureView()
                        .onHorizontalPan {
                            handlePan(action: horizontalPanGesture, state: $0, point: $1.x, velocity: $2, translation: $3)
                        }
                        .onHorizontalSwipe(translation: 100, velocity: 1500, sameSwipeDirectionTimeout: 1, handleHorizontalSwipe)
                        .onLongPress(minimumDuration: 2, handleLongPress)
                        .onPinch(handlePinchGesture)
                        .onTap(samePointPadding: 10, samePointTimeout: 0.7, handleTapGesture)
                        .onDoubleTouch(handleDoubleTouchGesture)
                        .onVerticalPan {
                            if $1.x <= 0.5 {
                                handlePan(action: verticalGestureLeft, state: $0, point: -$1.y, velocity: $2, translation: $3)
                            } else {
                                handlePan(action: verticalGestureRight, state: $0, point: -$1.y, velocity: $2, translation: $3)
                            }
                        }

                    LiveVideoPlayer.Overlay()
                        .environmentObject(splitContentViewProxy)
                        .environmentObject(videoPlayerManager)
                        .environmentObject(videoPlayerManager.currentProgressHandler)
                        .environmentObject(videoPlayerManager.currentViewModel!)
                        .environmentObject(videoPlayerManager.proxy)
                        .environment(\.aspectFilled, $isAspectFilled)
                        .environment(\.isPresentingOverlay, $isPresentingOverlay)
                        .environment(\.isScrubbing, $isScrubbing)
                        .environment(\.playbackSpeed, $playbackSpeed)
                }
            }
            .splitContent {
                // Wrapped due to navigation controller popping due to published changes
                WrappedView {
                    NavigationViewCoordinator(PlaybackSettingsCoordinator()).view()
                }
                .cornerRadius(20, corners: [.topLeft, .bottomLeft])
                .environmentObject(splitContentViewProxy)
                .environmentObject(videoPlayerManager)
                .environmentObject(videoPlayerManager.currentViewModel)
                .environment(\.audioOffset, $audioOffset)
                .environment(\.subtitleOffset, $subtitleOffset)
            }
            .onChange(of: videoPlayerManager.currentProgressHandler.scrubbedProgress) { newValue in
                guard !newValue.isNaN && !newValue.isInfinite else {
                    return
                }
                DispatchQueue.main.async {
                    videoPlayerManager.currentProgressHandler
                        .scrubbedSeconds = Int(CGFloat(videoPlayerManager.currentViewModel.item.runTimeSeconds) * newValue)
                }
            }
            .overlay(alignment: .top) {
                UpdateView(proxy: updateViewProxy)
                    .padding(.top)
            }
            .videoPlayerKeyCommands(
                gestureStateHandler: gestureStateHandler,
                updateViewProxy: updateViewProxy
            )
    }

    var body: some View {
        Group {
            if let _ = videoPlayerManager.currentViewModel {
                playerView
            } else {
                VideoPlayer.LoadingView()
            }
        }
        .navigationBarHidden(true)
        .statusBar(hidden: true)
        .ignoresSafeArea()
        .onChange(of: audioOffset) { newValue in
            videoPlayerManager.proxy.setAudioDelay(.ticks(newValue))
        }
        .onChange(of: isGestureLocked) { newValue in
            if newValue {
                updateViewProxy.present(systemName: "lock.fill", title: "Gestures Locked")
            } else {
                updateViewProxy.present(systemName: "lock.open.fill", title: "Gestures Unlocked")
            }
        }
        .onChange(of: isScrubbing) { newValue in
            guard !newValue else { return }
            videoPlayerManager.proxy.setTime(.seconds(currentProgressHandler.scrubbedSeconds))
        }
        .onChange(of: subtitleColor) { newValue in
            videoPlayerManager.proxy.setSubtitleColor(.absolute(newValue.uiColor))
        }
        .onChange(of: subtitleFontName) { newValue in
            videoPlayerManager.proxy.setSubtitleFont(newValue)
        }
        .onChange(of: subtitleOffset) { newValue in
            videoPlayerManager.proxy.setSubtitleDelay(.ticks(newValue))
        }
        .onChange(of: subtitleSize) { newValue in
            videoPlayerManager.proxy.setSubtitleSize(.absolute(24 - newValue))
        }
        .onChange(of: videoPlayerManager.currentViewModel) { newViewModel in
            guard let newViewModel else { return }

            videoPlayerManager.proxy.playNewMedia(newViewModel.vlcVideoPlayerConfiguration)

            isAspectFilled = false
            audioOffset = 0
            subtitleOffset = 0
        }
    }
}

extension LiveVideoPlayer {

    init(manager: LiveVideoPlayerManager) {
        self.init(
            currentProgressHandler: manager.currentProgressHandler,
            videoPlayerManager: manager
        )
    }
}

// MARK: Gestures

// TODO: refactor to be split into other files
// TODO: refactor so that actions are separate from the gesture calculations, so that actions are more general

extension LiveVideoPlayer {

    private func handlePan(
        action: PanAction,
        state: UIGestureRecognizer.State,
        point: CGFloat,
        velocity: CGFloat,
        translation: CGFloat
    ) {
        guard !isGestureLocked else { return }

        switch action {
        case .none:
            return
        case .audioffset:
            audioOffsetAction(state: state, point: point, velocity: velocity, translation: translation)
        case .brightness:
            brightnessAction(state: state, point: point, velocity: velocity, translation: translation)
        case .playbackSpeed:
            playbackSpeedAction(state: state, point: point, velocity: velocity, translation: translation)
        case .scrub:
            scrubAction(state: state, point: point, velocity: velocity, translation: translation, rate: 1)
        case .slowScrub:
            scrubAction(state: state, point: point, velocity: velocity, translation: translation, rate: 0.1)
        case .subtitleOffset:
            subtitleOffsetAction(state: state, point: point, velocity: velocity, translation: translation)
        case .volume:
            volumeAction(state: state, point: point, velocity: velocity, translation: translation)
        }
    }

    private func handleHorizontalSwipe(
        unitPoint: UnitPoint,
        direction: Bool,
        amount: Int
    ) {
        guard !isGestureLocked else { return }

        switch horizontalSwipeGesture {
        case .none:
            return
        case .jump:
            jumpAction(unitPoint: .init(x: direction ? 1 : 0, y: 0), amount: amount)
        }
    }

    private func handleLongPress(point: UnitPoint) {
        switch longPressGesture {
        case .none:
            return
        case .gestureLock:
            guard !isPresentingOverlay else { return }
            isGestureLocked.toggle()
        }
    }

    private func handlePinchGesture(state: UIGestureRecognizer.State, unitPoint: UnitPoint, scale: CGFloat) {
        guard !isGestureLocked else { return }

        switch pinchGesture {
        case .none:
            return
        case .aspectFill:
            aspectFillAction(state: state, unitPoint: unitPoint, scale: scale)
        }
    }

    private func handleTapGesture(unitPoint: UnitPoint, taps: Int) {
        guard !isGestureLocked else {
            updateViewProxy.present(systemName: "lock.fill", title: "Gestures Locked")
            return
        }

        if taps > 1 && multiTapGesture != .none {

            withAnimation(.linear(duration: 0.1)) {
                isPresentingOverlay = false
            }

            switch multiTapGesture {
            case .none:
                return
            case .jump:
                jumpAction(unitPoint: unitPoint, amount: taps - 1)
            }
        } else {
            withAnimation(.linear(duration: 0.1)) {
                isPresentingOverlay = !isPresentingOverlay
            }
        }
    }

    private func handleDoubleTouchGesture(unitPoint: UnitPoint, taps: Int) {
        guard !isGestureLocked else {
            updateViewProxy.present(systemName: "lock.fill", title: "Gestures Locked")
            return
        }

        switch doubleTouchGesture {
        case .none:
            return
        case .aspectFill: ()
//            aspectFillAction(state: state, unitPoint: unitPoint, scale: <#T##CGFloat#>)
        case .gestureLock:
            guard !isPresentingOverlay else { return }
            isGestureLocked.toggle()
        case .pausePlay: ()
        }
    }
}

// MARK: Actions

extension LiveVideoPlayer {

    private func aspectFillAction(state: UIGestureRecognizer.State, unitPoint: UnitPoint, scale: CGFloat) {
        guard state == .began || state == .changed else { return }
        if scale > 1, !isAspectFilled {
            isAspectFilled = true
            UIView.animate(withDuration: 0.2) {
                videoPlayerManager.proxy.aspectFill(1)
            }
        } else if scale < 1, isAspectFilled {
            isAspectFilled = false
            UIView.animate(withDuration: 0.2) {
                videoPlayerManager.proxy.aspectFill(0)
            }
        }
    }

    private func audioOffsetAction(
        state: UIGestureRecognizer.State,
        point: CGFloat,
        velocity: CGFloat,
        translation: CGFloat
    ) {
        if state == .began {
            gestureStateHandler.beginningPanProgress = currentProgressHandler.progress
            gestureStateHandler.beginningHorizontalPanUnit = point
            gestureStateHandler.beginningAudioOffset = audioOffset
        } else if state == .ended {
            return
        }

        let newOffset = gestureStateHandler.beginningAudioOffset - round(
            Int((gestureStateHandler.beginningHorizontalPanUnit - point) * 2000),
            toNearest: 100
        )

        updateViewProxy.present(systemName: "speaker.wave.2.fill", title: newOffset.millisecondLabel)
        audioOffset = clamp(newOffset, min: -30000, max: 30000)
    }

    private func brightnessAction(
        state: UIGestureRecognizer.State,
        point: CGFloat,
        velocity: CGFloat,
        translation: CGFloat
    ) {
        if state == .began {
            gestureStateHandler.beginningPanProgress = currentProgressHandler.progress
            gestureStateHandler.beginningHorizontalPanUnit = point
            gestureStateHandler.beginningBrightnessValue = UIScreen.main.brightness
        } else if state == .ended {
            return
        }

        let newBrightness = gestureStateHandler.beginningBrightnessValue - (gestureStateHandler.beginningHorizontalPanUnit - point)
        let clampedBrightness = clamp(newBrightness, min: 0, max: 1.0)
        let flashPercentage = Int(clampedBrightness * 100)

        if flashPercentage >= 67 {
            updateViewProxy.present(systemName: "sun.max.fill", title: "\(flashPercentage)%", iconSize: .init(width: 30, height: 30))
        } else if flashPercentage >= 33 {
            updateViewProxy.present(systemName: "sun.max.fill", title: "\(flashPercentage)%")
        } else {
            updateViewProxy.present(systemName: "sun.min.fill", title: "\(flashPercentage)%", iconSize: .init(width: 20, height: 20))
        }

        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
            UIScreen.main.brightness = clampedBrightness
        }
    }

    // TODO: decide on overlay behavior?
    private func jumpAction(
        unitPoint: UnitPoint,
        amount: Int
    ) {
        if unitPoint.x <= 0.5 {
            videoPlayerManager.proxy.jumpBackward(Int(jumpBackwardLength.rawValue))

            updateViewProxy.present(systemName: "gobackward", title: "\(amount * Int(jumpBackwardLength.rawValue))s")
        } else {
            videoPlayerManager.proxy.jumpForward(Int(jumpForwardLength.rawValue))

            updateViewProxy.present(systemName: "goforward", title: "\(amount * Int(jumpForwardLength.rawValue))s")
        }
    }

    private func playbackSpeedAction(
        state: UIGestureRecognizer.State,
        point: CGFloat,
        velocity: CGFloat,
        translation: CGFloat
    ) {
        if state == .began {
            gestureStateHandler.beginningPanProgress = currentProgressHandler.progress
            gestureStateHandler.beginningHorizontalPanUnit = point
            gestureStateHandler.beginningPlaybackSpeed = playbackSpeed
        } else if state == .ended {
            return
        }

        let newPlaybackSpeed = round(
            gestureStateHandler.beginningPlaybackSpeed - Double(gestureStateHandler.beginningHorizontalPanUnit - point) * 2,
            toNearest: 0.25
        )
        let clampedPlaybackSpeed = clamp(newPlaybackSpeed, min: 0.25, max: 5.0)

        updateViewProxy.present(systemName: "speedometer", title: clampedPlaybackSpeed.rateLabel)

        playbackSpeed = clampedPlaybackSpeed
        videoPlayerManager.proxy.setRate(.absolute(Float(clampedPlaybackSpeed)))
    }

    private func scrubAction(
        state: UIGestureRecognizer.State,
        point: CGFloat,
        velocity: CGFloat,
        translation: CGFloat,
        rate: CGFloat
    ) {
        if state == .began {
            isScrubbing = true

            gestureStateHandler.beginningPanProgress = currentProgressHandler.progress
            gestureStateHandler.beginningHorizontalPanUnit = point
            gestureStateHandler.beganPanWithOverlay = isPresentingOverlay
        } else if state == .ended {
            if !gestureStateHandler.beganPanWithOverlay {
                isPresentingOverlay = false
            }

            isScrubbing = false

            return
        }

        let newProgress = gestureStateHandler.beginningPanProgress - (gestureStateHandler.beginningHorizontalPanUnit - point) * rate
        currentProgressHandler.scrubbedProgress = clamp(newProgress, min: 0, max: 1)
    }

    private func subtitleOffsetAction(
        state: UIGestureRecognizer.State,
        point: CGFloat,
        velocity: CGFloat,
        translation: CGFloat
    ) {
        if state == .began {
            gestureStateHandler.beginningPanProgress = currentProgressHandler.progress
            gestureStateHandler.beginningHorizontalPanUnit = point
            gestureStateHandler.beginningSubtitleOffset = subtitleOffset
        } else if state == .ended {
            return
        }

        let newOffset = gestureStateHandler.beginningSubtitleOffset - round(
            Int((gestureStateHandler.beginningHorizontalPanUnit - point) * 2000),
            toNearest: 100
        )
        let clampedOffset = clamp(newOffset, min: -30000, max: 30000)

        updateViewProxy.present(systemName: "captions.bubble.fill", title: clampedOffset.millisecondLabel)

        subtitleOffset = clampedOffset
    }

    private func volumeAction(
        state: UIGestureRecognizer.State,
        point: CGFloat,
        velocity: CGFloat,
        translation: CGFloat
    ) {
        let volumeView = MPVolumeView()
        guard let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider else { return }

        if state == .began {
            gestureStateHandler.beginningPanProgress = currentProgressHandler.progress
            gestureStateHandler.beginningHorizontalPanUnit = point
            gestureStateHandler.beginningVolumeValue = AVAudioSession.sharedInstance().outputVolume
        } else if state == .ended {
            return
        }

        let newVolume = gestureStateHandler.beginningVolumeValue - Float(gestureStateHandler.beginningHorizontalPanUnit - point)

        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
            slider.value = newVolume
        }
    }
}
