import APIClient
import CoreMedia.CMTime
import Foundation
import StatsigClient

public struct AlignedLyricsMap: Equatable {
    static let maxCacheCount: Int = 1

    var isLyricsAppearanceV2Enabled: Bool {
        FeatureFlag.legacy.lyricsAppearanceV2
    }

    var shortsShouldUseStreamedLyricsOnCreate: Bool {
        FeatureFlag.legacy.shortsShouldUseStreamedLyricsOnCreate
    }

    public static let defaultValue: AlignedLyricsMap = .init()

    private var lyricsMap: [Clip.ID: Lyrics] = [:]

    private var mostRecentLyricsTrackerIndex: Int = .zero
    private var mostRecentLyrics = Array(repeating: Clip.ID(remoteId: ""), count: AlignedLyricsMap.maxCacheCount)

    init() {}

    public mutating func updateMostRecentLyricsAndRemoveOld(_ clipID: Clip.ID) {
        let newClipID = clipID.remoteId
        let trackerIndex = mostRecentLyricsTrackerIndex
        let oldClipID = mostRecentLyrics[trackerIndex]

        /*
             Remove the old entry from the cache if it exists
             and is not still referenced in the tracker array
         */
        if !oldClipID.remoteId.isEmpty,
           oldClipID.remoteId != newClipID,
           !mostRecentLyrics.contains(where: { $0 == oldClipID })
        {
            lyricsMap.removeValue(forKey: oldClipID)
        }

        /* Update the tracker and move to the next index; circular */
        mostRecentLyrics[trackerIndex] = clipID
        mostRecentLyricsTrackerIndex = (trackerIndex + 1) % AlignedLyricsMap.maxCacheCount

        for (id, _) in lyricsMap {
            if mostRecentLyrics.contains(where: { $0 == id }) {
                /* Skip */
            } else {
                lyricsMap.removeValue(forKey: id)
            }
        }
    }

    public mutating func lyricsForClip(_ clip: Clip) -> Lyrics {
        return newOrExistingLyricsDataForClip(clip)
    }

    public mutating func updateLyricTime(clip: Clip, _ time: TimeInterval?) {
        var mutableLyrics = newOrExistingLyricsDataForClip(clip)
        mutableLyrics.lyricTime = time
        lyricsMap[clip.id] = mutableLyrics
    }

    func shouldContinuePolling(_ id: Clip.ID) -> Bool {
        /*
            If it exists in the map & it isn't complete keep polling it.
            There are at max 10 songs in the map at a time
         */
        guard let lyrics = lyricsMap[id] else { return false }
        return lyrics.shouldContinuePolling
    }

    mutating func updateScrollViewUsage(clip: Clip) {
        var mutableLyrics = newOrExistingLyricsDataForClip(clip)
        if isLyricsAppearanceV2Enabled {
            if shortsShouldUseStreamedLyricsOnCreate {
                mutableLyrics.shouldUseScrollView = clip.status == .streaming && !clip.isScene
            } else {
                mutableLyrics.shouldUseScrollView = clip.status == .streaming
            }
        } else {
            mutableLyrics.shouldUseScrollView = true
        }

        lyricsMap[clip.id] = mutableLyrics
    }

    mutating func onLyricsResponseSuccess(
        _ newAlignedLyrics: AlignedLyrics,
        clip: Clip,
        givenTotalTime: CMTime
    ) {
        var mutableLyrics = newOrExistingLyricsDataForClip(clip)

        mutableLyrics.alignedLyrics = newAlignedLyrics.alignedWords.groupedByNewLine()
        mutableLyrics.timedLyrics = mutableLyrics.alignedLyrics.toTimedLyrics()
            .filter { !$0.line.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
        let lyricStyle: Lyrics.DisplayStyle = newAlignedLyrics.isStreamed ? .streamed : .complete

        /* Partial Update */
        lyricsMap[clip.id] = mutableLyrics

        /* Further Processing */
        processLyricsCacheMap(lyricStyle, clip: clip, givenTotalTime: givenTotalTime)

        /* Need to get the latest updates from the processing */
        if newAlignedLyrics.isStreamed {
            /* Continue */
        } else {
            mutableLyrics = newOrExistingLyricsDataForClip(clip)
            mutableLyrics.shouldContinuePolling = false
            lyricsMap[clip.id] = mutableLyrics
        }
    }

    public mutating func onLyricsResponseFail(
        _ clip: Clip,
        givenTotalTime: CMTime
    ) {
        var mutableLyrics = newOrExistingLyricsDataForClip(clip)

        guard mutableLyrics.displayStyle == .none else { return }
        mutableLyrics.shouldUseScrollView = true
        mutableLyrics.timedLyrics = clip.promptToTimedLyrics()
            .filter { !$0.line.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }

        /* Partial Update */
        lyricsMap[clip.id] = mutableLyrics

        /* Further Processing */
        processLyricsCacheMap(.fallback, clip: clip, givenTotalTime: givenTotalTime)
    }
}

private extension AlignedLyricsMap {
    func fallbackTotalTimeSeconds(_ clip: Clip) -> Double {
        return clip.isScene ? 30 : 120
    }

    mutating func newOrExistingLyricsDataForClip(_ clip: Clip) -> Lyrics {
        var mutableLyrics: Lyrics
        if let currentLyricsValue = lyricsMap[clip.id] {
            mutableLyrics = currentLyricsValue
        } else {
            mutableLyrics = .init(timedLyrics: clip.promptToTimedLyrics())
            lyricsMap[clip.id] = mutableLyrics
            updateScrollViewUsage(clip: clip)
        }
        return mutableLyrics
    }

    mutating func processLyricsCacheMap(
        _ displayStyle: Lyrics.DisplayStyle,
        clip: Clip,
        givenTotalTime: CMTime
    ) {
        var mutableLyrics = newOrExistingLyricsDataForClip(clip)

        guard !mutableLyrics.isProcessingLyrics else { return }

        let lastLyricTime = mutableLyrics.timedLyrics.last?.startsAt ?? .zero
        let stateTotalTime = givenTotalTime.roundedSingleDecimalSeconds
        let totalTime: TimeInterval?
        let fallbackTime = fallbackTotalTimeSeconds(clip)

        switch displayStyle {
        case .none:
            totalTime = .zero
        case .streamed, .fallback:
            totalTime = fallbackTime
        case .complete:
            let isStateTotalTimeInvalid = stateTotalTime < 1.0 || stateTotalTime.isNaN
            if clip.isScene {
                totalTime = isStateTotalTimeInvalid ? fallbackTime : stateTotalTime
            } else {
                let lastLyricBufferTime = 10.0 // as long as it is over the last lyric time it will show up, even past this.
                totalTime = isStateTotalTimeInvalid ? (lastLyricTime + lastLyricBufferTime) : stateTotalTime
            }
        }

        // Delay non-complete lyrics just in generally; It's ok if they are more out of sync
        let overallOffset: TimeInterval = displayStyle == .complete ? .zero : 5.0
        mutableLyrics.isProcessingLyrics = true
        let timedLyrics = mutableLyrics.timedLyrics

        let newMap = LyricsAppearance.createCacheAppearanceMap(
            totalTime,
            overallOffset: overallOffset,
            lyrics: timedLyrics
        )

        switch (mutableLyrics.displayStyle, displayStyle) {
        case (.none, .fallback),
             (.none, .streamed),
             (.fallback, .streamed),
             (.none, .complete),
             (.streamed, .complete),
             (.fallback, .complete):
            if isLyricsAppearanceV2Enabled {
                if displayStyle == .complete {
                    mutableLyrics.shouldUseScrollView = false
                }
            }

            mutableLyrics.cachedLyricsAppearanceMap = newMap
            mutableLyrics.renderProxy += 1

        default:
            break
        }

        mutableLyrics.displayStyle = displayStyle
        mutableLyrics.isProcessingLyrics = false

        lyricsMap[clip.id] = mutableLyrics
    }
}
