import AVFoundation
import Combine
import ComponentLibrary
import ComposableArchitecture
import CoreHaptics
import SwiftUI

public struct TrimmerHandles: View {
    @Binding var start: Double
    @Binding var end: Double
    let totalWidth: Double
    let duration: Double
    let minimumDuration: Double
    let maximumDuration: Double
    let accentColor: Color
    let showHandleTimestamps: Bool
    let height: CGFloat
    let onLeftHandleDrag: (Double) -> Void
    let onRightHandleDrag: (Double) -> Void
    let onLeftHandleDragEnd: (Double) -> Void
    let onRightHandleDragEnd: (Double) -> Void
    let onScrub: (Double) -> Void
    let onSelectionDrag: (Double) -> Void
    let onSelectionDragEnd: () -> Void

    public init(
        start: Binding<Double>,
        end: Binding<Double>,
        totalWidth: Double,
        duration: Double,
        minimumDuration: Double,
        maximumDuration: Double,
        accentColor: Color = Color.SemanticV1.iconLink,
        showHandleTimestamps: Bool = true,
        height: CGFloat = 60,
        onLeftHandleDrag: @escaping (Double) -> Void,
        onRightHandleDrag: @escaping (Double) -> Void,
        onLeftHandleDragEnd: @escaping (Double) -> Void,
        onRightHandleDragEnd: @escaping (Double) -> Void,
        onScrub: @escaping (Double) -> Void,
        onSelectionDrag: @escaping (Double) -> Void,
        onSelectionDragEnd: @escaping () -> Void
    ) {
        self._start = start
        self._end = end
        self.totalWidth = totalWidth
        self.duration = duration
        self.minimumDuration = minimumDuration
        self.maximumDuration = maximumDuration
        self.accentColor = accentColor
        self.showHandleTimestamps = showHandleTimestamps
        self.height = height
        self.onLeftHandleDrag = onLeftHandleDrag
        self.onRightHandleDrag = onRightHandleDrag
        self.onLeftHandleDragEnd = onLeftHandleDragEnd
        self.onRightHandleDragEnd = onRightHandleDragEnd
        self.onScrub = onScrub
        self.onSelectionDrag = onSelectionDrag
        self.onSelectionDragEnd = onSelectionDragEnd
    }

    @State private var showTimestamps: Bool = false
    @State private var isDraggingSelection: Bool = false
    @State private var dragStartLocation: CGFloat = 0
    @State private var selectionStartAtDragStart: Double = 0

    public var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                pinkBoundingBox(width: geometry.size.width)
                trimmerHandle(isLeft: true, totalWidth: geometry.size.width)
                trimmerHandle(isLeft: false, totalWidth: geometry.size.width)
            }
        }
        .frame(height: height)
    }

    @ViewBuilder
    private func pinkBoundingBox(width: CGFloat) -> some View {
        // Using Color.white.opacity(0.01) because using .clear prevents gestures,
        // but using `contentShape` doesn't respect the offset
        RoundedRectangle(cornerRadius: 4)
            .fill(Color.white.opacity(0.01))
            .border(accentColor, width: 3)
            .frame(width: max(0, (end - start) * width), height: 60)
            .offset(x: start * width)
            .gesture(dragGesture(width: width))
    }

    @ViewBuilder
    private func trimmerHandle(
        isLeft: Bool,
        totalWidth: CGFloat
    ) -> some View {
        TrimmerHandle(
            position: isLeft ? $start : $end,
            otherPosition: isLeft ? $end : $start,
            isLeft: isLeft,
            totalWidth: totalWidth,
            duration: duration,
            minimumDuration: minimumDuration,
            maximumDuration: maximumDuration,
            accentColor: accentColor,
            onDrag: { position in
                onScrub(position)
                if isLeft {
                    onLeftHandleDrag(position)
                } else {
                    onRightHandleDrag(position)
                }
            },
            onDragEnd: isLeft ? onLeftHandleDragEnd : onRightHandleDragEnd,
            showTimestamp: showHandleTimestamps ? $showTimestamps : .constant(false)
        )
    }

    private func dragGesture(width: CGFloat) -> some Gesture {
        DragGesture(minimumDistance: 0)
            .onChanged { value in
                if !isDraggingSelection {
                    isDraggingSelection = true
                    dragStartLocation = value.location.x
                    selectionStartAtDragStart = start
                }

                let translation = value.location.x - dragStartLocation
                let translationAsPercentage = translation / width

                let newStartPosition = selectionStartAtDragStart + translationAsPercentage

                onSelectionDrag(newStartPosition)
            }
            .onEnded { _ in
                isDraggingSelection = false
                onSelectionDragEnd()
            }
    }
}

/*
 Handles that show the current time of the selected portion of the video
 and allow the user to drag to trim the video.
 */
public struct TrimmerHandle: View {
    @Binding var position: Double
    @Binding var otherPosition: Double
    let isLeft: Bool
    let totalWidth: CGFloat
    let duration: Double
    let minimumDuration: Double
    let maximumDuration: Double
    let accentColor: Color
    let onDrag: (Double) -> Void
    let onDragEnd: (Double) -> Void

    @Binding var showTimestamp: Bool
    @State private var dragStartLocation: CGFloat = 0
    @State private var isDragging: Bool = false
    @State private var hitMaxDuration: Bool = false
    @State private var hitMinDuration: Bool = false

    // Keep track of different haptic triggers separately
    @State private var dragStartHapticProxy: Int = .zero
    @State private var dragEndHapticProxy: Int = .zero
    @State private var dragLimitHapticProxy: Int = .zero

    public init(
        position: Binding<Double>,
        otherPosition: Binding<Double>,
        isLeft: Bool,
        totalWidth: CGFloat,
        duration: Double,
        minimumDuration: Double,
        maximumDuration: Double,
        accentColor: Color,
        onDrag: @escaping (Double) -> Void,
        onDragEnd: @escaping (Double) -> Void,
        showTimestamp: Binding<Bool>
    ) {
        self._position = position
        self._otherPosition = otherPosition
        self.isLeft = isLeft
        self.totalWidth = totalWidth
        self.duration = duration
        self.minimumDuration = minimumDuration
        self.maximumDuration = maximumDuration
        self.accentColor = accentColor
        self.onDrag = onDrag
        self.onDragEnd = onDragEnd
        self._showTimestamp = showTimestamp
    }

    private func formatTime(time: Double) -> String {
        let minutes = Int(time) / 60
        let seconds = Int(time) % 60
        return String(format: "%02d:%02d", minutes, seconds)
    }

    private var roundedRectangleConfiguration: RoundedRectangleConfiguration {
        // Shows the rounded corners on the correct side of each handle
        if isLeft {
            return .init(topLeft: 100, topRight: 0, bottomLeft: 100, bottomRight: 0)
        } else {
            return .init(topLeft: 0, topRight: 100, bottomLeft: 0, bottomRight: 100)
        }
    }

    private var handlePosition: CGFloat {
        // Accounts for the width of the handle
        if isLeft {
            return CGFloat(position * totalWidth - 5)
        } else {
            return CGFloat(position * totalWidth + 5)
        }
    }

    private struct RoundedRectangleConfiguration {
        let topLeft: CGFloat
        let topRight: CGFloat
        let bottomLeft: CGFloat
        let bottomRight: CGFloat
    }

    public var body: some View {
        ZStack(alignment: .bottom) {
            /// HCI-compliant invisible tap area
            /// The `.position` modifier on the parent `ZStack` makes it difficult to add this tap area in a `.overlay`
            Rectangle()
                .fill(Color.clear)
                .frame(width: 44)
                .contentShape(Rectangle())
                .clipped()

            timestamp
                .opacity(showTimestamp ? 1 : 0)
                .offset(x: isLeft ? 20 : -20, y: -70)

            handle
        }
        .position(x: handlePosition, y: 30)
        .sensoryFeedbackIfEnabled(.selection, trigger: dragStartHapticProxy)
        .sensoryFeedbackIfEnabled(.selection, trigger: dragLimitHapticProxy)
        .sensoryFeedbackIfEnabled(.selection, trigger: dragEndHapticProxy)
        .gesture(dragGesture(width: totalWidth))
    }

    @ViewBuilder
    private var handle: some View {
        UnevenRoundedRectangle(
            topLeadingRadius: roundedRectangleConfiguration.topLeft,
            bottomLeadingRadius: roundedRectangleConfiguration.bottomLeft,
            bottomTrailingRadius: roundedRectangleConfiguration.bottomRight,
            topTrailingRadius: roundedRectangleConfiguration.topRight
        )
        .fill(accentColor)
        .frame(width: 12, height: 60)
        .overlay(
            RoundedRectangle(cornerRadius: 18)
                .fill(Color.black)
                .frame(width: 1.52, height: 11)
                .offset(x: isLeft ? 1.5 : -1.5)
        )
    }

    @ViewBuilder
    private var timestamp: some View {
        Text(formatTime(time: position * duration))
            .typographyV1(.monospace.lineHeight(12.0))
            .foregroundColor(.white)
            .padding(.vertical, 4)
            .padding(.horizontal, 6)
            .background(accentColor.opacity(0.8))
            .cornerRadius(8)
    }

    // swiftlint:disable:next cyclomatic_complexity
    private func dragGesture(width _: CGFloat) -> some Gesture {
        DragGesture(minimumDistance: 0)
            .onChanged { value in
                // Early return if duration is 0 to prevent division by zero
                guard duration > 0 else { return }

                if !isDragging {
                    isDragging = true
                    dragStartLocation = value.location.x
                    hitMaxDuration = false
                    hitMinDuration = false
                    dragStartHapticProxy += 1
                }

                let newPosition = (value.location.x / totalWidth)
                let currentDuration = abs(otherPosition - position) * duration
                let newDuration = abs(newPosition - otherPosition) * duration

                if currentDuration >= maximumDuration - 0.1 {
                    let isExpandingAtMax = (isLeft && newPosition < position) || (!isLeft && newPosition > position)

                    if isExpandingAtMax, !hitMaxDuration {
                        hitMaxDuration = true
                        dragLimitHapticProxy += 1
                        return
                    }
                } else {
                    hitMaxDuration = false
                }

                if newDuration >= minimumDuration - 0.2 && newDuration <= maximumDuration + 0.2 {
                    if isLeft {
                        position = max(0, min(newPosition, otherPosition - (minimumDuration / duration)))
                    } else {
                        position = min(1, max(newPosition, otherPosition + (minimumDuration / duration)))
                    }
                    onDrag(position)
                    showTimestamp = true
                    hitMinDuration = false
                } else if !hitMinDuration {
                    hitMinDuration = true
                    dragLimitHapticProxy += 1
                }
            }
            .onEnded { value in
                // Reset dragging state
                isDragging = false

                // Early return if duration is 0 to prevent division by zero
                guard duration > 0 else { return }

                if !hitMaxDuration {
                    let newPosition = (value.location.x / totalWidth)
                    let newDuration = abs(newPosition - otherPosition) * duration

                    if newDuration >= minimumDuration - 0.2 && newDuration <= maximumDuration + 0.2 {
                        if isLeft {
                            position = max(0, min(newPosition, otherPosition - (minimumDuration / duration)))
                        } else {
                            position = min(1, max(newPosition, otherPosition + (minimumDuration / duration)))
                        }
                        onDragEnd(position)
                        hitMinDuration = false
                        dragEndHapticProxy += 1
                    } else if !hitMinDuration {
                        hitMinDuration = true
                        dragLimitHapticProxy += 1
                    }
                }
                showTimestamp = false
            }
    }
}

/*
 Thumbnail strip that shows the selected portion of the video
 and dims the non-selected portions.
 */
public struct ThumbnailStrip: View {
    let thumbnails: [UIImage]
    let width: CGFloat
    let start: Double
    let end: Double
    let height: CGFloat

    public init(
        thumbnails: [UIImage],
        width: CGFloat,
        start: Double,
        end: Double,
        height: CGFloat = 60
    ) {
        self.thumbnails = thumbnails
        self.width = width
        self.start = start
        self.end = end
        self.height = height
    }

    public var body: some View {
        GeometryReader { _ in
            ZStack(alignment: .leading) {
                // Base layer of thumbnails
                HStack(spacing: 0) {
                    ForEach(thumbnails.indices, id: \.self) { index in
                        Image(uiImage: thumbnails[index])
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .frame(height: 60)
                            .clipped()
                    }
                }

                // Darkened overlays for non-selected areas
                HStack(spacing: 0) {
                    Rectangle()
                        .fill(Color.black.opacity(0.5))
                        .frame(width: start * width)

                    Rectangle()
                        .fill(Color.clear)
                        .frame(width: (end - start) * width)

                    Rectangle()
                        .fill(Color.black.opacity(0.5))
                        .frame(width: (1 - end) * width)
                }
            }
        }
        .frame(width: width, height: height)
        .clipShape(.rect(cornerRadius: 4))
        .allowsHitTesting(false)
    }
}

/*
 Cursor that shows the current time of the selected portion of the video
 and allows the user to scrub to the selected time. The frame at the cursor's
 corresponding time is shown in the preview.
 */
public struct PlaybackCursor: View {
    @Binding var start: Double
    @Binding var end: Double
    let totalWidth: Double
    let offset: CGFloat
    let height: CGFloat
    let onScrub: (Double) -> Void
    let onScrubEnd: () -> Void

    @State private var isDragging: Bool = false
    @State private var startPositionAtDragStart: Double = 0

    public init(
        start: Binding<Double>,
        end: Binding<Double>,
        totalWidth: Double,
        offset: CGFloat,
        height: CGFloat,
        onScrub: @escaping (Double) -> Void,
        onScrubEnd: @escaping () -> Void
    ) {
        self._start = start
        self._end = end
        self.totalWidth = totalWidth
        self.offset = offset
        self.height = height
        self.onScrub = onScrub
        self.onScrubEnd = onScrubEnd
    }

    public var body: some View {
        ZStack(alignment: .leading) {
            // Big invisible hit area spanning full height and 44pt width around the cursor
            Rectangle()
                .fill(Color.white.opacity(0.001))
                .frame(width: 44, height: height + 20)
                .offset(x: max(0, offset - 22))

            // Visible thin cursor
            RoundedRectangle(cornerRadius: 8)
                .fill(Color.white)
                .frame(width: 5, height: height)
                .offset(x: offset)
                .shadow(radius: 3)
                .allowsHitTesting(false)
        }
        .contentShape(Rectangle())
        .gesture(
            DragGesture(minimumDistance: 0)
                .onChanged { value in
                    let x = min(max(value.location.x, 0), totalWidth)
                    let clamped = min(max(x / totalWidth, start), end)
                    onScrub(clamped)
                }
                .onEnded { _ in
                    onScrubEnd()
                }
        )
    }
}
