import SwiftUI

public struct SlideWindowWaveformView: View {
    public enum Update {
        case onDragStart
        case onDragChanged(ClosedRange<TimeInterval>)
        case onDragEnd(ClosedRange<TimeInterval>)
        case onWindowUpdate(ClosedRange<TimeInterval>)
    }

    public struct Config: Equatable {
        let constantFrameWidth: CGFloat
        let basePointsPerSecond: CGFloat
        let windowDuration: TimeInterval
        let windowLineWidth: CGFloat
        let barWidth: CGFloat
        let barHeightRatio: CGFloat
        let lowOpacityFactor: CGFloat
        let barSpacing: CGFloat
        let waveformSource: WaveformData
        let waveformFrameColor: Color?

        var effectivePointsPerSecond: CGFloat {
            return basePointsPerSecond * zoomScale
        }

        var zoomScale: CGFloat {
            let baseWidth = windowDuration * basePointsPerSecond
            return constantFrameWidth / baseWidth
        }

        // Can be calculated from init
        let resampledValues: WaveformDataMap

        public init(
            constantFrameWidth: CGFloat,
            basePointsPerSecond: CGFloat = 6.0,
            windowDuration: TimeInterval,
            windowLineWidth: CGFloat = 4.0,
            barWidth: CGFloat = 3.0,
            barHeightRatio: CGFloat = 0.65,
            lowOpacityFactor: CGFloat = 0.2,
            barSpacing: CGFloat = 3.0,
            waveformSource: WaveformData,
            waveformFrameColor: Color? = nil
        ) {
            self.constantFrameWidth = constantFrameWidth
            self.basePointsPerSecond = basePointsPerSecond
            self.windowDuration = windowDuration
            self.windowLineWidth = windowLineWidth
            self.barWidth = barWidth
            self.barHeightRatio = barHeightRatio
            self.lowOpacityFactor = lowOpacityFactor
            self.barSpacing = barSpacing
            self.waveformSource = waveformSource
            self.waveformFrameColor = waveformFrameColor

            // Calculate
            let totalDuration = waveformSource.totalDuration
            let baseWidth = windowDuration * basePointsPerSecond
            let zoomScale = constantFrameWidth / baseWidth

            let contentWidth = totalDuration * basePointsPerSecond * zoomScale // Scalable content
            self.resampledValues = waveformSource.fitIn(
                width: contentWidth,
                barWidth: barWidth,
                spacing: barSpacing
            )
        }
    }

    private let config: Config
    private let onSlideUpdate: (SlideWindowWaveformView.Update) -> Void

    @State private var isDragging: Bool = false
    @State private var size: CGSize = .zero
    @State private var dragOriginalOffset: CGFloat = .zero // where the drag began
    @State private var dragActiveOffset: CGFloat = .zero // live offset (incl. rubber-band)
    @State private var displayTimeRange: ClosedRange<TimeInterval> = 0 ... 30

    private var progress: CGFloat // Current playback position

    public init(
        config: Config,
        onSlideUpdate: @escaping (SlideWindowWaveformView.Update) -> Void,
        progress: CGFloat,
        startTime: TimeInterval
    ) {
        self.config = config
        self.onSlideUpdate = onSlideUpdate
        self.progress = progress

        // Set the initial offset
        let startTimeOffset = centeredStartTime - startTime
        self._dragActiveOffset = State(initialValue: startTimeOffset * config.effectivePointsPerSecond)
    }

    public var body: some View {
        ZStack {
            sizer
            dimmedWavesBars
            brightInWindowBars
            if !isDragging {
                highlightingCursorBars
            }
            windowFrame
        }
        .contentShape(.rect)
        .contentShape(Rectangle())
        .simultaneousGesture(
            DragGesture(minimumDistance: 0)
                .onChanged { event in
                    let wasDragging = isDragging
                    if !wasDragging {
                        isDragging = true
                        // Store the original offset immediately
                        dragOriginalOffset = clampOffsetDragDelta
                        // Send drag start immediately
                        onSlideUpdate(.onDragStart)
                    }
                    // Update drag offset immediately
                    dragActiveOffset = dragOriginalOffset + event.translation.width
                    let timeRange = selectedOffsetRange
                    onSlideUpdate(.onDragChanged(timeRange))
                }
                .onEnded { _ in
                    dragOriginalOffset = clampOffsetDragDelta
                    let timeRange = selectedOffsetRange
                    onSlideUpdate(.onDragEnd(timeRange))
                    isDragging = false
                }
        )
        /// Properly calculate window changes
        .onChange(of: config) { newValue, oldValue in
            /// To prevent view update deadlocks
            DispatchQueue.main.async {
                // Get zoom ratio
                let zoomRatio = oldValue.zoomScale / newValue.zoomScale

                // Recalculate drag offset in case `config.windowDuration` changed.
                dragActiveOffset = dragActiveOffset * zoomRatio

                let timeRange = selectedOffsetRange
                onSlideUpdate(.onWindowUpdate(timeRange))
            }
        }
    }

    var centerRange: ClosedRange<TimeInterval> {
        return config.waveformSource.centerRange(config.windowDuration)
    }

    @ViewBuilder
    var sizer: some View {
        GeometryReader { _ in
            Color.clear
        }
        .onGeometryChange(for: CGSize.self) { geoProxy in
            geoProxy.size
        } action: { newSize in
            size = newSize
        }
    }

    @ViewBuilder
    func waveform(range: ClosedRange<TimeInterval>) -> some View {
        WaveformView(
            waveformSource: config.waveformSource,
            // This renders the full waveform width for the entire audio duration, while selectedDurationWidth (used for the window frame) shows only the selected portion
            waveformWidthOverride: contentWidth,
            barWidth: config.barWidth,
            barHeightRatio: config.barHeightRatio,
            barSpacing: config.barSpacing,
            lowOpacityFactor: config.lowOpacityFactor,
            selectedRange: range,
            resampledValues: config.resampledValues
        )
    }

    var inWindowBackground: some View {
        RoundedRectangle(cornerRadius: 10)
            .fill(Color(hex: 0x252020))
            .modifier(windowSizeModifer)
    }

    @ViewBuilder
    var dimmedWavesBars: some View {
        waveform(range: pastDurationClosedRange)
            .frame(width: contentWidth, height: size.height)
            .offset(x: isDragging ? dragActiveOffset : clampOffsetDragDelta)
    }

    @ViewBuilder
    var brightInWindowBars: some View {
        waveform(range: fullDurationClosedRange)
            .frame(width: contentWidth, height: size.height)
            .offset(x: isDragging ? dragActiveOffset : clampOffsetDragDelta)
            .mask {
                RoundedRectangle(cornerRadius: 10)
                    .fill(.white)
                    .modifier(windowSizeModifer)
            }
    }

    @ViewBuilder
    private var highlightingCursorBars: some View {
        HStack {
            RoundedRectangle(cornerRadius: 10)
                .fill(
                    LinearGradient(
                        colors: [
                            Color(hex: 0xDE472F),
                            Color(hex: 0xDE3B78),
                        ],
                        startPoint: .bottomLeading,
                        endPoint: .topTrailing
                    )
                )
                .frame(width: min(1, progress) * selectedDurationWidth, height: size.height)

            Color.clear
                .frame(width: max(0, 1.0 - progress) * selectedDurationWidth, height: size.height)
        }
        .modifier(windowSizeModifer)
        .mask {
            ZStack {
                waveform(range: fullDurationClosedRange)
                    .frame(width: contentWidth, height: size.height)
                    .offset(x: isDragging ? dragActiveOffset : clampOffsetDragDelta)
            }
            .mask {
                RoundedRectangle(cornerRadius: 10)
                    .fill(.white)
                    .modifier(windowSizeModifer)
            }
        }
    }

    @ViewBuilder
    var windowFrame: some View {
        RoundedRectangle(cornerRadius: 10)
            .stroke(
                config.waveformFrameColor.map { AnyShapeStyle($0) } ?? AnyShapeStyle(
                    LinearGradient(
                        colors: [
                            Color(hex: 0xDE472F),
                            Color(hex: 0xDE3B78),
                        ],
                        startPoint: .bottomLeading,
                        endPoint: .topTrailing
                    )
                ),
                lineWidth: config.windowLineWidth
            )
            .modifier(windowSizeModifer)
    }

    // Original Time
    var centeredStartTime: TimeInterval {
        return centerRange.lowerBound
    }

    var centeredEndTime: TimeInterval {
        return centerRange.upperBound
    }

    var totalDuration: TimeInterval {
        return config.waveformSource.totalDuration
    }

    var selectedDuration: TimeInterval {
        return centeredEndTime - centeredStartTime
    }

    var selectedDurationWidth: CGFloat {
        if totalDuration.isZero || totalDuration.isNaN || contentWidth.isZero || contentWidth.isNaN {
            return 1.0
        } else {
            return (selectedDuration / totalDuration) * contentWidth
        }
    }

    // Offset Time
    var timeOffset: TimeInterval {
        return TimeInterval(dragActiveOffset / config.effectivePointsPerSecond)
    }

    var offsetStartTime: TimeInterval {
        return centeredStartTime - timeOffset
    }

    var offsetEndTime: TimeInterval {
        return centeredEndTime - timeOffset
    }

    var selectedOffsetRange: ClosedRange<TimeInterval> {
        let start = min(max(0.0, offsetStartTime), totalDuration - selectedDuration)
        let end = min(max(selectedDuration, offsetEndTime), totalDuration)
        return start ... end
    }

    var offsetTimeDelta: CGFloat {
        return centeredStartTime - selectedOffsetRange.lowerBound
    }

    var clampOffsetDragDelta: CGFloat {
        return (offsetTimeDelta / totalDuration) * contentWidth
    }

    var waveformWidth: CGFloat {
        return config.constantFrameWidth // Fixed width
    }

    var contentWidth: CGFloat {
        return totalDuration * config.effectivePointsPerSecond // Scalable content
    }

    var pastDurationClosedRange: ClosedRange<TimeInterval> {
        // To highlight nothing
        return (totalDuration + 1.0) ... (totalDuration + 2.0)
    }

    var fullDurationClosedRange: ClosedRange<TimeInterval> {
        return 0.0 ... totalDuration
    }

    var windowSizeModifer: WindowSizedModifier {
        WindowSizedModifier(
            selectedDurationWidth: selectedDurationWidth,
            size: size
        )
    }

    struct WindowSizedModifier: ViewModifier {
        let selectedDurationWidth: CGFloat
        let size: CGSize

        func body(content: Content) -> some View {
            content
                .frame(width: selectedDurationWidth, height: size.height)
                .position(x: size.width / 2, y: size.height / 2)
        }
    }
}
