import APIClient
import ComponentLibrary
import ComposableArchitecture
import Localization
import LyricsClient
import ShareAssetClient
import StatsigClient
import SwiftUI
import Waveform

// swiftlint:disable file_length

var trackedSelectionIndex: Int = .zero

@ObservableState
public struct VisualizerGalleryState: Equatable {
    public enum GalleryItem: Identifiable, Equatable {
        case savedRender(URL)
        case assetConfig(VisualizerAssetConfig)

        public var id: String {
            switch self {
            case .savedRender(let url):
                return "saved-render-\(url.absoluteString)"
            case .assetConfig(let visualizerAssetConfig):
                return "asset-config-\(visualizerAssetConfig.id)"
            }
        }
    }

    let clip: Clip
    let lyricsData: LyricsDataV2
    var lastGenerationURL: URL? /* Nil means no previous generation for this clip */
    let targetDestination: ShareAssetCreationTarget
    var galleryItems: [GalleryItem]
    var defaultGalleryIndex: Int = .zero
    let waveformData: WaveformData

    public init(
        clip: Clip,
        lyricsData: LyricsDataV2,
        lastGenerationURL: URL?,
        targetDestination: ShareAssetCreationTarget,
        waveformData: WaveformData
    ) {
        self.clip = clip
        self.lyricsData = lyricsData
        self.lastGenerationURL = lastGenerationURL
        self.targetDestination = targetDestination
        self.waveformData = waveformData

        galleryItems = []

        /* Save this for another time: It's a bit confusing */
        // if let lastRenderURL = lastGenerationURL {
        //    galleryItems.append(.savedRender(lastRenderURL))
        // }

        let configSet: any VisualizerGalleryConfigSet = VisualizerGalleryPresetsV1()
        defaultGalleryIndex = galleryItems.count + configSet.defaultIndex
        galleryItems.append(contentsOf: configSet.configs(clip).map { .assetConfig($0) })
    }
}

@Reducer
public struct VisualizerGalleryReducer {
    public typealias State = VisualizerGalleryState

    public enum Action {
        public enum Delegate {
            case pauseAudio
            case playAudioAtTime(_ time: TimeInterval)
            case seekTime(_ time: TimeInterval, isMuted: Bool)
            case shareSavedVideo(_ saveVideoURL: URL)
            case startRenderWithSelectedConfig(_ config: VisualizerAssetConfig, _ startTime: TimeInterval, _ endTime: TimeInterval)
        }

        public enum Internal {
            /*
                Instead of updating active index directly through TCA
                we are keeping the active index managed as state inside of SwiftUI
                and only reading it out through a trigger requested from parent.
             */
            case selectVisualizer(_ index: Int, _ startTime: TimeInterval, _ endTime: TimeInterval)
        }

        case setup
        case delegate(Delegate)
        case `internal`(Internal)
    }

    public init() {}

    public var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .setup:
                return .none

            case .internal(let internalAction):
                switch internalAction {
                case .selectVisualizer(let itemIndex, let startTime, let endTime):
                    let item = state.galleryItems[itemIndex]
                    switch item {
                    case .savedRender(let renderURL):
                        return .send(.delegate(.shareSavedVideo(renderURL)))
                    case .assetConfig(let config):
                        return .send(.delegate(.startRenderWithSelectedConfig(config, startTime, endTime)))
                    }
                }

            case .delegate:
                return .none
            }
        }
    }
}

/*
    Visualizer Gallery view internals are a black box to TCA.

    The only thing that TCA needs to know about what goes on inside of
    VisualizerGalleryView is what ends up being selected.
 */
struct VisualizerGalleryView: View {
    enum GalleryDragAction {
        case increaseIfNeeded
        case decreaseIfNeeded
        case maintainPosition
    }

    @Environment(\.dismiss) private var dismiss
    @StateObject private var observable = VisualizerGalleryObservable()
    @Bindable var store: StoreOf<VisualizerGalleryReducer>
    private let constantWaveformWidth: CGFloat = UIScreen.width * 0.75

    init(store: StoreOf<VisualizerGalleryReducer>) {
        self.store = store
    }

    var body: some View {
        VStack(spacing: .zero) {
            gallery
                .padding(.bottom, 8.0)
            actionBar
        }
        .onAppear {
            Task {
                await MainActor.run {
                    observable.showTitle()
                    let centeredRange = observable.getCenteredRangeFromClip(store.clip)
                    observable.setSelectedRange(centeredRange)
                    if observable.isMuted {
                        /* Don't Play anything yet */
                    } else {
                        store.send(.delegate(.playAudioAtTime(centeredRange.lowerBound)))
                    }
                    observable.state.activeIndex = store.defaultGalleryIndex
                }

                observable.startTimer(onResetToZeroElapsedTime: {
                    store.send(.delegate(.seekTime(observable.selectedStartTime, isMuted: observable.isMuted)))
                })
            }
        }
        .onDisappear {
            observable.stopTimer()
            observable.stopTitleTask()
        }
    }
}

private extension VisualizerGalleryView {
    struct CardSizeConfig {
        let width: CGFloat
        let height: CGFloat
        let cornerRadius: CGFloat
        let offset: CGFloat

        init(
            gallerySize: CGSize
        ) {
            let newHeight = max(gallerySize.height * 0.9, 1)
            self.height = newHeight
            self.width = max(newHeight * (9.0 / 16.0), 1.0)
            self.cornerRadius = max(newHeight * 0.025, 1.0)
            self.offset = 16.0
        }
    }

    @ViewBuilder
    var gallery: some View {
        GeometryReader { geoProxy in
            if geoProxy.size == .zero {
                Color.clear
            } else {
                let sizeConfig = CardSizeConfig(gallerySize: geoProxy.size)

                ZStack(alignment: .center) {
                    Color.clear
                    VStack(spacing: .zero) {
                        presetVisualizerCards(sizeConfig: sizeConfig)
                            .padding(.vertical, 16.0)

                        galleryPagingIndicator
                            .padding(.bottom, 8.0)
                    }
                }
                .gesture(
                    DragGesture()
                        .onChanged { event in
                            observable.state.activeDragPosition = observable.state.originalDragPosition + event.translation.width
                        }
                        .onEnded { event in
                            onDragEnd(sizeConfig: sizeConfig, event: event)
                        }
                )
            }
        }
    }

    @ViewBuilder
    var actionBar: some View {
        VStack(spacing: 0.0) {
            timeLabel
                .padding(.horizontal, 16.0)
                .padding(.bottom, 24.0)

            waveform
                .frame(height: 54.0)
                .frame(maxWidth: .infinity)
                .padding(.horizontal, 8)
                .padding(.bottom, 24.0)

            shareButton
                .padding(.top, 16.0)
                .padding(.horizontal, 16.0)
        }
        .padding(.top, 12.0)
        .padding(.bottom, 42.0)
        .background(Color.SemanticV1.backgroundPrimary)
    }

    @ViewBuilder
    var presetTitleCard: some View {
        ZStack {
            if store.galleryItems.count > observable.state.activeIndex {
                let galleryItem = store.galleryItems[observable.state.activeIndex]
                var color: Color {
                    switch galleryItem {
                    case .assetConfig:
                        return Color.SemanticV1.textPrimary
                    case .savedRender:
                        return Color.SemanticV1.textInvert
                    }
                }

                var title: String {
                    switch galleryItem {
                    case .assetConfig(let config):
                        return config.displayName
                    case .savedRender:
                        return "Ready to Share"
                    }
                }

                Text(title)
                    .typographyV1(.body3)
                    .foregroundStyle(color)
                    .padding(.vertical, 8.0)
                    .padding(.horizontal, 16.0)
                    .background {
                        ZStack {
                            switch galleryItem {
                            case .assetConfig:
                                Color.SemanticV1.backgroundQuaternary
                            case .savedRender:
                                Color.SemanticV1.backgroundInvert
                            }
                        }
                    }
                    .clipShape(.rect(cornerRadius: .infinity))
                    .opacity(observable.state.shouldShowTitle ? 1.0 : 0.0)
                    .animation(.easeInOut, value: observable.state.shouldShowTitle)
            }
        }
    }

    @ViewBuilder
    func presetVisualizerCards(sizeConfig: CardSizeConfig) -> some View {
        ZStack {
            ForEach(
                Array(store.galleryItems.enumerated()),
                id: \.element.id
            ) { elementOffset, item in
                galleryCard(elementOffset, sizeConfig: sizeConfig, item: item)
            }
        }
    }

    @ViewBuilder
    var galleryPagingIndicator: some View {
        DotPagingIndicator(
            numberOfPages: store.galleryItems.count,
            currentPage: $observable.state.activeIndex
        ) { config in
            config.indicatorSize = 5.0
            config.spacing = 7.0
        }
    }

    @ViewBuilder
    func savedGalleryCard(sizeConfig: CardSizeConfig, lastRenderURL: URL, isInFocus: Bool) -> some View {
        SavedAssetRenderCard(
            cardSize: .init(width: sizeConfig.width, height: sizeConfig.height),
            videoURL: lastRenderURL,
            isVisualInFocus: isInFocus,
            isPlaying: isInFocus,
            overrideTime: nil
        )
    }

    func visualizerCard(
        config: VisualizerAssetConfig,
        sizeConfig: CardSizeConfig,
        isInFocus: Bool
    ) -> some View {
        VisualizerCompositeCard(
            config: config,
            lyrics: store.lyricsData,
            clipTitle: store.clip.title,
            clipAuthorHandle: store.clip.handle,
            startTime: observable.selectedStartTime,
            endTime: observable.selectedEndTime,
            elapsedTime: observable.state.elapsedTime,
            clipDuration: store.clip.duration,
            isPlaying: observable.state.isPlaying,
            currentTime: observable.currentTime,
            isVisualInFocus: isInFocus,
            cardSize: .init(width: sizeConfig.width, height: sizeConfig.height),
            onTimeUpdate: { _, _, _ in }
        )
    }

    @ViewBuilder
    func galleryCard(
        _ elementOffset: Int,
        sizeConfig: CardSizeConfig,
        item: VisualizerGalleryState.GalleryItem
    ) -> some View {
        var offsetUnit: CGFloat {
            return sizeConfig.width + sizeConfig.offset
        }

        var baseOffset: CGFloat {
            return CGFloat(observable.state.activeIndex) * offsetUnit
        }

        var slideOffset: CGFloat {
            return offsetUnit * CGFloat(elementOffset) - baseOffset + observable.state.activeDragPosition
        }

        let isInFocus = elementOffset == observable.state.activeIndex
        let scaleAmount: CGFloat = isInFocus ? 1.0 : 0.95

        ZStack {
            switch item {
            case .assetConfig(let config):
                visualizerCard(config: config, sizeConfig: sizeConfig, isInFocus: isInFocus)
            case .savedRender(let url):
                savedGalleryCard(sizeConfig: sizeConfig, lastRenderURL: url, isInFocus: isInFocus)
            }
        }
        .clipShape(.rect(cornerRadius: sizeConfig.cornerRadius))
        .scaleEffect(.init(width: scaleAmount, height: scaleAmount), anchor: .center)
        .offset(x: slideOffset)
    }

    func onDragEnd(sizeConfig: CardSizeConfig, event: _ChangedGesture<DragGesture>.Value) {
        let tWidth = event.translation.width
        let animation: Animation = .easeOut(duration: 0.3)

        let endStyle: GalleryDragAction
        if (abs(tWidth) > sizeConfig.width / 2.0) || (abs(event.velocity.width) > 300.0) {
            if tWidth > 0 {
                endStyle = .decreaseIfNeeded
            } else {
                endStyle = .increaseIfNeeded
            }
        } else {
            endStyle = .maintainPosition
        }

        let newActiveIndex: Int
        switch endStyle {
        case .maintainPosition:
            newActiveIndex = observable.state.activeIndex
        case .decreaseIfNeeded:
            newActiveIndex = observable.state.activeIndex > .zero ? observable.state.activeIndex - 1 : observable.state.activeIndex
        case .increaseIfNeeded:
            newActiveIndex = observable.state.activeIndex < store.galleryItems.count - 1 ? observable.state.activeIndex + 1 : observable.state.activeIndex
        }

        withAnimation(animation) {
            if newActiveIndex != observable.state.activeIndex {
                observable.showTitle()
            }

            observable.state.activeIndex = newActiveIndex
            observable.state.activeDragPosition = .zero
        }
    }

    @ViewBuilder
    var timeLabel: some View {
        let font: TypographyV1 = .monospace.size { _ in 12.0 }

        ZStack {
            HStack {
                Menu {
                    ForEach(VisualizerGalleryObservable.WindowTime.menuOrderedDurations) { windowDuration in
                        Button {
                            UIImpactFeedbackGenerator(style: .light).impactOccurred()
                            if windowDuration.id != observable.state.windowTime.id {
                                Task {
                                    await observable.updateWindowTime(windowDuration, clipDuration: store.clip.duration)
                                    store.send(.delegate(.seekTime(observable.selectedStartTime, isMuted: observable.isMuted)))
                                }
                            }
                        } label: {
                            ZStack {
                                if windowDuration.id == observable.state.windowTime.id {
                                    Image(systemName: "checkmark")
                                }
                            }
                            .frame(width: 24.0)

                            Text("\(windowDuration.timeInterval.format_s)s")
                                .typographyV1(.subtitleMedium)
                        }
                    }
                } label: {
                    RoundedRectangle(cornerRadius: .infinity)
                        .fill(Color.SemanticV1.backgroundSecondary)
                        .frame(width: 64.0, height: 32.0)
                        .overlay {
                            HStack(spacing: .zero) {
                                Text("\(observable.state.windowTime.timeInterval.format_s)s")
                                    .typographyV1(.monospace)
                                    .foregroundStyle(Color.SemanticV1.textPrimary)
                                    .padding(.trailing, 2.0)
                                Image.Icon.chevronDown
                                    .resizable()
                                    .aspectRatio(contentMode: .fit)
                                    .frame(width: 16.0)
                                    .foregroundStyle(Color.SemanticV1.textPrimary)
                            }
                            .padding(.leading, 8.0)
                            .padding(.trailing, 4.0)
                        }
                }

                Spacer()
                Text(observable.selectedRangeAsString)
                    .typographyV1(font)
                    .foregroundStyle(Color.SemanticV1.textSecondary)
                Spacer()
                Button {
                    UIImpactFeedbackGenerator(style: .light).impactOccurred()
                    Task {
                        let isMuted = await observable.toggleMute() // Also Resets Elapsed Time

                        if isMuted {
                            store.send(.delegate(.pauseAudio))
                        } else {
                            store.send(.delegate(.seekTime(observable.selectedStartTime, isMuted: isMuted)))
                        }
                    }

                } label: {
                    RoundedRectangle(cornerRadius: .infinity)
                        .fill(Color.SemanticV1.backgroundSecondary)
                        .frame(width: 64.0, height: 32.0)
                        .overlay {
                            if observable.isMuted {
                                Image.Icon.volumeOff
                                    .resizable()
                                    .aspectRatio(contentMode: .fit)
                                    .frame(height: 18.0)
                                    .foregroundStyle(.white)
                            } else {
                                Image.Icon.volumeOn
                                    .resizable()
                                    .aspectRatio(contentMode: .fit)
                                    .frame(height: 22.0)
                                    .foregroundStyle(.white)
                            }
                        }
                }
            }
        }
    }

    func onWaveformSlideUpdate(_ update: SlideWindowWaveformView.Update) {
        switch update {
        case .onDragStart:
            observable.state.isPlaying = false

        case .onDragEnd(let timeRange):
            observable.state.isPlaying = true
            observable.state.selectedRange = timeRange
            observable.state.elapsedTime = 0
            store.send(.delegate(.seekTime(timeRange.lowerBound, isMuted: observable.isMuted)))

        case .onDragChanged(let timeRange):
            observable.state.selectedRange = timeRange

        case .onWindowUpdate(let timeRange):
            observable.state.selectedRange = timeRange
        }
    }

    @ViewBuilder
    var waveform: some View {
        let vPadding: CGFloat = 4.0
        let halfVPadding: CGFloat = vPadding / 2.0
        let hPadding: CGFloat = 24.0
        let frameHeight: CGFloat = 54.0
        let dynamicWaveformConfig = SlideWindowWaveformView.Config(
            constantFrameWidth: constantWaveformWidth,
            windowDuration: observable.state.selectedDuration,
            waveformSource: store.waveformData
        )
        Color.clear
            .background {
                SlideWindowWaveformView(
                    config: dynamicWaveformConfig,
                    onSlideUpdate: onWaveformSlideUpdate,
                    progress: observable.state.progress,
                    startTime: observable.selectedStartTime
                )
                .frame(width: constantWaveformWidth, height: frameHeight) // Fixed width
                .padding(.horizontal, hPadding)
                .padding(.vertical, halfVPadding)
            }
            .frame(height: frameHeight + vPadding)
            .clipped()
    }

    @ViewBuilder
    var shareButton: some View {
        AssetCreationButton(target: store.targetDestination) {
            store.send(.internal(.selectVisualizer(
                observable.state.activeIndex,
                observable.state.selectedRange.lowerBound,
                observable.state.selectedRange.upperBound
            )))
        }
    }
}
