import SwiftUI

public struct Trimmer: View {
    public enum Update {
        case onDragStart
        case onMoveStartHandle(Double)
        case onMoveEndHandle(Double)
        case onDragEnd
    }

    struct TrimmerConfig {
        let trimmerWidth: CGFloat
        let trimmerHeight: CGFloat
        let handleWidth: CGFloat
        let handleHeight: CGFloat
        let lineWidth: CGFloat
    }

    @Binding var range: ClosedRange<Double>
    @State var isDragging: Bool = false
    private let limits: ClosedRange<Double>
    private let onUpdate: (Update) -> Void

    public init(
        range: Binding<ClosedRange<Double>>,
        limits: ClosedRange<Double> = 0.0 ... 1.0,
        onUpdate: @escaping (Update) -> Void
    ) {
        self._range = range
        self.limits = limits.clamped(to: 0.0 ... 1.0)
        self.onUpdate = onUpdate
    }

    public var body: some View {
        GeometryReader { geometry in
            var config: TrimmerConfig {
                return .init(
                    trimmerWidth: geometry.size.width,
                    trimmerHeight: geometry.size.height,
                    handleWidth: geometry.size.width * 0.03,
                    handleHeight: geometry.size.height * 0.6,
                    lineWidth: geometry.size.width * 0.01
                )
            }

            ZStack(alignment: .leading) {
                // Contour
                contour(config)

                // Left handle
                capsule(config, xRatioPos: range.lowerBound) { newPosition in
                    update(lowerBound: newPosition)
                    onUpdate(.onMoveStartHandle(newPosition))
                }

                // Right handle
                capsule(config, xRatioPos: range.upperBound) { newPosition in
                    update(upperBound: newPosition)
                    onUpdate(.onMoveEndHandle(newPosition))
                }
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

private extension Trimmer {
    func contour(_ config: TrimmerConfig) -> some View {
        RoundedRectangle(cornerRadius: config.lineWidth * 2)
            .stroke(.foreground, lineWidth: config.lineWidth)
            .background(Color.clear)
            .frame(
                width: max(config.handleWidth, (range.upperBound - range.lowerBound) * config.trimmerWidth),
                height: config.trimmerHeight
            )
            .position(
                x: ((range.lowerBound + range.upperBound) / 2) * config.trimmerWidth,
                y: config.trimmerHeight / 2
            )
    }

    @ViewBuilder
    func capsule(_ config: TrimmerConfig, xRatioPos: CGFloat, onUpdatePostion: @escaping (CGFloat) -> Void) -> some View {
        Capsule()
            .fill(.foreground)
            .frame(width: config.handleWidth, height: config.handleHeight)
            .position(
                x: xRatioPos * config.trimmerWidth,
                y: config.trimmerHeight / 2
            )
            .gesture(
                DragGesture()
                    .onChanged { value in
                        if !isDragging {
                            isDragging = true
                            onUpdate(.onDragStart)
                        }
                        let newPosition = value.location.x / config.trimmerWidth
                        onUpdatePostion(newPosition)
                    }
                    .onEnded { _ in
                        isDragging = false
                        onUpdate(.onDragEnd)
                    }
            )
    }

    func update(lowerBound position: Double) {
        let lowerBound = max(0.0, min(range.upperBound - 0.05, position))
        let newRange = lowerBound ... range.upperBound
        range = newRange.clamped(to: limits, anchor: .lowerBound)
    }

    func update(upperBound position: Double) {
        let upperBound = min(1.0, max(range.lowerBound + 0.05, position))
        let newRange = range.lowerBound ... upperBound
        range = newRange.clamped(to: limits, anchor: .upperBound)
    }
}

struct PreviewContentView: View {
    @State private var range = 0.2 ... 0.8

    var body: some View {
        VStack {
            Spacer()

            Trimmer(range: $range, onUpdate: { _ in })
                .padding()
                .foregroundStyle(.white)
                .frame(height: 150)

            Text("Range: \(range)")
                .foregroundColor(.white)
                .padding()

            Spacer()
        }
        .background(Color.black)
    }
}

#Preview {
    PreviewContentView()
}

extension ClosedRange where Bound: AdditiveArithmetic {
    var size: Bound {
        upperBound - lowerBound
    }

    enum Anchor {
        case lowerBound
        case upperBound
    }

    func clamped(to limit: ClosedRange<Bound>, anchor: Anchor) -> ClosedRange<Bound> {
        let isAboveMaxSize = self.size > limit.upperBound
        let isBelowMinSize = self.size < limit.lowerBound

        guard isAboveMaxSize || isBelowMinSize else { return self }

        let sizeLimit = isAboveMaxSize ? limit.upperBound : limit.lowerBound

        switch anchor {
        case .lowerBound:
            return lowerBound ... (lowerBound + sizeLimit)

        case .upperBound:
            return (upperBound - sizeLimit) ... upperBound
        }
    }
}
