import ComponentLibrary
import FeatureClipDetail
import LyricsClient
import SwiftUI

struct LyricsView: View {
    @Binding var isExpanded: Bool
    @Binding var contextHeight: CGFloat
    @State var primaryBlockHeight: CGFloat = .zero
    @State var nextDistanceOneBlockHeight: CGFloat = .zero

    let isScrubbing: Bool
    let lyrics: [LyricsAppearance.TimedLyric]
    let inputLyricTime: TimeInterval?
    let lyricsAppearanceMap: LyricsAppearance.CacheMap
    let expandedLyricsTimeMarkerOverride: TimeInterval?

    init(isExpanded: Binding<Bool>,
         isScrubbing: Bool,
         contextHeight: Binding<CGFloat>,
         lyrics: [LyricsAppearance.TimedLyric],
         inputLyricTime: TimeInterval?,
         lyricsAppearanceMap: LyricsAppearance.CacheMap,
         expandedLyricsTimeMarkerOverride: TimeInterval?)
    {
        self._isExpanded = isExpanded
        self.isScrubbing = isScrubbing
        self._contextHeight = contextHeight
        self.lyrics = lyrics
        self.inputLyricTime = inputLyricTime
        self.lyricsAppearanceMap = lyricsAppearanceMap
        self.expandedLyricsTimeMarkerOverride = expandedLyricsTimeMarkerOverride
    }

    var body: some View {
        ZStack {
            Color.clear
            if let lyricTime,
               let indexStyle = lyricsAppearanceMap.appearanceSetStyle(lyricTime)
            {
                ForEach(lyrics.indices, id: \.self) { lyricIndex in

                    // Lyrics track a half second faster so that words can animate
                    // into place by the time it is necessary for them to show up.
                    let advanceTimeTrackingBy = 0.5 // seconds
                    let appearancePosition = lyricsAppearanceMap.appearancePosition(lyricTime + advanceTimeTrackingBy, lyricIndex: lyricIndex)
                    let scaleValue = scale(appearancePosition)

                    if lyrics.indices.contains(lyricIndex) {
                        let lyric = lyrics[lyricIndex]
                        VStack(alignment: .leading, spacing: .zero) {
                            if let notesText = lyric.sectionMarker, isExpanded {
                                Text(notesText)
                                    .typographyV1(.caption3.neueMontrealMedium())
                                    .foregroundStyle(Color.SemanticV1.textOnDark)
                                    .opacity(0.5)
                            }
                            LyricsLineView(
                                lineText: lyric.line.trimmingCharacters(in: .whitespacesAndNewlines),
                                fontScale: proportionalScalar
                            )
                            .onGeometryChange(for: CGRect.self) { proxy in proxy.frame(in: .local) } action: {
                                setCurrentPrimarySize(appearancePosition, height: $0.height)
                            }
                            .onTapGesture {
                                isExpanded.toggle()
                            }
                        }
                        .scaleEffect(.init(width: scaleValue, height: scaleValue), anchor: .leading)
                        .offset(y: yOffset(appearancePosition))
                        .blur(radius: blur(appearancePosition))
                        .opacity(opacity(appearancePosition, indexStyle: indexStyle))
                        .id(lyricIndex)
                    }
                }
            }
        }
        .padding(.horizontal, 16.0)
        .contentShape(.rect)
        .animation(.easeInOut(duration: 0.2), value: primaryBlockHeight)
        .animation(.easeInOut(duration: 0.2), value: nextDistanceOneBlockHeight)
        .animation(shouldAnimate ? .easeInOut : .none, value: lyricTime)
        .opacity(isLowOpacity ? 0.6 : .one)
    }
}

private extension LyricsView {
    enum Constants {
        static let smallScale: CGFloat = 0.75
        static let tinyScale: CGFloat = 0.5
        static let blurSmall: CGFloat = 1.0
        static let blurBig: CGFloat = 4.0
        static let blurOffScreen: CGFloat = 32.0
        static let oneAwayDistance: CGFloat = 120.0
        static let twoAwayDistance: CGFloat = 200.0
        static let offScreenDistance: CGFloat = 250.0
    }

    var lyricTime: TimeInterval? {
        return expandedLyricsTimeMarkerOverride ?? inputLyricTime
    }

    var isLowOpacity: Bool {
        if let _ = expandedLyricsTimeMarkerOverride {
            return true
        } else {
            return isScrubbing
        }
    }

    var shouldAnimate: Bool {
        if let _ = expandedLyricsTimeMarkerOverride {
            return false
        } else {
            let lyricTime = inputLyricTime ?? .zero
            return !(lyricTime < 1.0 || isScrubbing)
        }
    }

    func yOffset(_ appearancePosition: LyricsAppearance.CacheMap.AppearancePosition) -> CGFloat {
        let distanceFromPrimaryLyric = -distanceAway(appearancePosition)
        let additionalSpaceNeededBasedOnTheSizeOfPrimaryLyric = secondaryOffsetOnPrimary(appearancePosition)
        return (distanceFromPrimaryLyric - additionalSpaceNeededBasedOnTheSizeOfPrimaryLyric) * proportionalScalar
    }

    // swiftlint:disable:next cyclomatic_complexity
    func distanceAway(_ appearancePosition: LyricsAppearance.CacheMap.AppearancePosition) -> CGFloat {
        if isExpanded {
            var expandedDistanceOffset: CGFloat {
                /*
                 If the primary block height is too large
                 then we are using some proportional size of the primary block
                 height to adjust the overall distance offset for
                 non-primary on screen lyrics

                 If we haven't met that threshold then ignore
                 */
                return primaryBlockHeight > 150 ? primaryBlockHeight * 0.5 : .zero
            }

            // Five Line Lyrics
            switch appearancePosition {
            case .offScreenAbove:
                return Constants.offScreenDistance + expandedDistanceOffset
            case .previousDistanceTwo:
                return Constants.twoAwayDistance + expandedDistanceOffset
            case .previousDistanceOne:
                return Constants.oneAwayDistance + expandedDistanceOffset
            case .primary:
                return .zero
            case .nextDistanceOne:
                return -Constants.oneAwayDistance - expandedDistanceOffset
            case .nextDistanceTwo:
                return -Constants.twoAwayDistance - expandedDistanceOffset
            case .offScreenBelow:
                return -Constants.offScreenDistance - expandedDistanceOffset
            }
        } else {
            /*
                In the mini view ---
                Calculate some reasonable offset for the nextDistanceOne lyric in mini view
             */
            var expandedPrimaryDistanceOffset: CGFloat {
                return primaryBlockHeight > 70 ? primaryBlockHeight * 0.1 : .zero
            }

            var expandedNextDistanceOffset: CGFloat {
                if nextDistanceOneBlockHeight > 70.0 {
                    return nextDistanceOneBlockHeight * 0.25
                } else if nextDistanceOneBlockHeight < 35.0 {
                    return -nextDistanceOneBlockHeight * 0.25
                } else {
                    return .zero
                }
            }

            var combinedDistanceOffset = expandedNextDistanceOffset + expandedPrimaryDistanceOffset
            combinedDistanceOffset = min(combinedDistanceOffset, 24)

            // Two Line Lyrics
            switch appearancePosition {
            case .offScreenAbove, .previousDistanceTwo, .previousDistanceOne:
                return -150.0
            case .primary:
                return -225.0
            case .nextDistanceOne, .nextDistanceTwo:
                return -300.0 - combinedDistanceOffset
            case .offScreenBelow:
                return -400.0
            }
        }
    }

    func blur(_ appearancePosition: LyricsAppearance.CacheMap.AppearancePosition) -> CGFloat {
        if isExpanded {
            switch appearancePosition {
            case .offScreenAbove, .offScreenBelow:
                return Constants.blurOffScreen
            case .previousDistanceTwo, .nextDistanceTwo:
                return Constants.blurBig
            case .previousDistanceOne, .nextDistanceOne:
                return Constants.blurSmall
            case .primary:
                return .zero
            }
        } else {
            switch appearancePosition {
            case .offScreenAbove, .offScreenBelow, .previousDistanceTwo, .nextDistanceTwo:
                return Constants.blurOffScreen
            case .nextDistanceOne, .previousDistanceOne:
                return Constants.blurSmall
            case .primary:
                return .zero
            }
        }
    }

    func scale(_ appearancePosition: LyricsAppearance.CacheMap.AppearancePosition) -> CGFloat {
        if isExpanded {
            switch appearancePosition {
            case .offScreenAbove, .offScreenBelow:
                return .zero
            case .previousDistanceTwo, .nextDistanceTwo:
                return Constants.tinyScale
            case .previousDistanceOne, .nextDistanceOne:
                return Constants.smallScale
            case .primary:
                return .one
            }
        } else {
            switch appearancePosition {
            case .offScreenAbove, .offScreenBelow:
                return .zero
            case .previousDistanceTwo, .nextDistanceTwo:
                return Constants.smallScale
            case .previousDistanceOne, .nextDistanceOne:
                return Constants.smallScale
            case .primary:
                return .one
            }
        }
    }

    func opacity(_ appearancePosition: LyricsAppearance.CacheMap.AppearancePosition,
                 indexStyle: LyricsAppearance.CacheSet.PrimaryIndexType) -> CGFloat
    {
        if isExpanded {
            switch appearancePosition {
            case .offScreenAbove, .offScreenBelow:
                return .zero
            case .previousDistanceTwo, .nextDistanceTwo:
                return 0.5
            case .previousDistanceOne, .nextDistanceOne:
                return 0.75
            case .primary:
                return .one
            }
        } else {
            if indexStyle == .standardPrimary {
                switch appearancePosition {
                case .offScreenAbove, .offScreenBelow, .previousDistanceTwo, .previousDistanceOne, .nextDistanceTwo:
                    return .zero
                case .nextDistanceOne:
                    return 0.75
                case .primary:
                    return 1.0
                }
            } else {
                return 0.5
            }
        }
    }

    func setCurrentPrimarySize(
        _ appearancePosition: LyricsAppearance.CacheMap.AppearancePosition,
        height: CGFloat
    ) {
        switch appearancePosition {
        case .primary:
            primaryBlockHeight = height
        case .nextDistanceOne:
            nextDistanceOneBlockHeight = height
        case .nextDistanceTwo, .offScreenAbove, .offScreenBelow, .previousDistanceOne, .previousDistanceTwo:
            break
        }
    }

    func secondaryOffsetOnPrimary(_ appearancePosition: LyricsAppearance.CacheMap.AppearancePosition) -> CGFloat {
        guard !isExpanded, case .primary = appearancePosition else { return .zero }
        if primaryBlockHeight > 100.0 {
            return primaryBlockHeight * 0.25
        } else if primaryBlockHeight < 40 {
            return -primaryBlockHeight * 0.50
        } else {
            return .zero
        }
    }

    var proportionalScalar: CGFloat {
        // This is tuned at a base clip view size with height of 839
        // however using this proportionalScalar it can flex to any
        // proportional clip size screen size
        return contextHeight / 839.0
    }
}
