import SwiftUI
import Utilities

/// A view that displays text that doesn't fit in the available width
/// with an auto-scrolling, reversing, and repeating animation.
public struct MarqueeText: View {
    // Text to display
    public var text: String
    // Font of the text
    public var font: UIFont
    // Fade amount from the leading and trailing ends
    public var leftFade: CGFloat
    public var rightFade: CGFloat
    // Delay before the animation starts
    public var startDelay: Double
    // Alignment of the text when not scrolling,
    // otherwise it's `.topLeading`
    public var alignment: Alignment
    // Speed factor for the scrolling animation (higher = faster)
    public var scrollSpeed: Double

    @State private var animate = false

    public var stringWidth: CGFloat {
        text.boundingBox(with: font).width
    }

    public var stringHeight: CGFloat {
        text.boundingBox(with: font).height
    }

    // Auto-reversing animation
    public var animation: Animation {
        return Animation
            .linear(duration: Double(stringWidth) / scrollSpeed)
            .delay(startDelay)
            .repeatForever(autoreverses: true)
    }

    public var body: some View {
        GeometryReader { proxy in
            let needsScrolling = (stringWidth > proxy.size.width)
            ZStack {
                if needsScrolling {
                    // MARK: - Scrolling (Marquee) version

                    makeMarqueeTexts(
                        stringWidth: stringWidth,
                        stringHeight: stringHeight,
                        geoWidth: proxy.size.width,
                        animation: animation
                    )
                    .frame(
                        minWidth: 0,
                        maxWidth: .infinity,
                        minHeight: 0,
                        maxHeight: .infinity,
                        alignment: .topLeading
                    )
                    .offset(x: leftFade + 3) // Visual offset of +3 to prevent clipping
                    .mask(
                        fadeMask(
                            leftFade: leftFade,
                            rightFade: rightFade
                        )
                    )
                    .frame(width: proxy.size.width + leftFade)
                    .offset(x: -leftFade - 3) // Visual offset of +3 to prevent clipping
                } else {
                    // MARK: - Non-scrolling version

                    Text(text)
                        .font(.init(font))
                        .lineLimit(1)
                        .onChange(of: text) { _, _ in
                            self.animate = false // No scrolling needed
                        }
                        .fixedSize(horizontal: true, vertical: false) // Only take up the width we need
                        .frame(
                            maxWidth: .infinity,
                            alignment: alignment // use alignment only if not scrolling
                        )
                }
            }
            .onAppear {
                if needsScrolling {
                    // buffer for text width to be set
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                        self.animate = true
                    }
                }
            }
            .onChange(of: text) { _, newValue in
                let newStringWidth = newValue.boundingBox(with: font).width
                if newStringWidth > proxy.size.width {
                    // Stop the old animation first
                    self.animate = false

                    Task { @MainActor in
                        self.animate = true
                    }
                } else {
                    self.animate = false
                }
            }
        }
        .frame(height: stringHeight)
        .frame(maxWidth: stringWidth)
        .onDisappear {
            self.animate = false
        }
    }

    // MARK: - Marquee pair of texts

    @ViewBuilder
    private func makeMarqueeTexts(
        stringWidth: CGFloat,
        stringHeight: CGFloat,
        geoWidth _: CGFloat,
        animation: Animation
    ) -> some View {
        // Two stacked texts moving across in opposite phases
        Group {
            Text(text)
                .lineLimit(1)
                .font(.init(font))
                .offset(x: animate ? -stringWidth - stringHeight * 2 : 0)
                .animation(animate ? animation : nil, value: animate)
                .fixedSize(horizontal: true, vertical: false)

            Text(text)
                .lineLimit(1)
                .font(.init(font))
                .offset(x: animate ? 0 : stringWidth + stringHeight * 2)
                .animation(animate ? animation : nil, value: animate)
                .fixedSize(horizontal: true, vertical: false)
        }
    }

    // MARK: - Fade mask

    @ViewBuilder
    private func fadeMask(leftFade: CGFloat, rightFade: CGFloat) -> some View {
        HStack(spacing: 0) {
            Rectangle().frame(width: leftFade).opacity(0)

            LinearGradient(
                gradient: Gradient(colors: [Color.black.opacity(0), Color.black]),
                startPoint: .leading,
                endPoint: .trailing
            )
            .frame(width: leftFade)

            LinearGradient(
                gradient: Gradient(colors: [Color.black, Color.black]),
                startPoint: .leading,
                endPoint: .trailing
            )

            LinearGradient(
                gradient: Gradient(colors: [Color.black, Color.black.opacity(0)]),
                startPoint: .leading,
                endPoint: .trailing
            )
            .frame(width: rightFade)

            Rectangle().frame(width: rightFade).opacity(0)
        }
    }

    // MARK: - Initializer

    public init(
        text: String,
        font: UIFont,
        leftFade: CGFloat,
        rightFade: CGFloat,
        startDelay: Double,
        alignment: Alignment? = nil,
        scrollSpeed: Double = 24.0 // Default speed, higher = faster
    ) {
        self.text = text
        self.font = font
        self.leftFade = leftFade
        self.rightFade = rightFade
        self.startDelay = startDelay
        self.alignment = alignment ?? .topLeading
        self.scrollSpeed = scrollSpeed
    }
}
