import AVFAudio
import Lottie
import QuartzCore
import UIKit

// MARK: - RecordingPreviewPanelDelegate

@MainActor
protocol RecordingPreviewPanelDelegate: AnyObject {
    func previewPanelDidTapSend(_ panel: RecordingPreviewPanel, trimRange: ClosedRange<TimeInterval>)
    func previewPanelDidTapDelete(_ panel: RecordingPreviewPanel)
    func previewPanelDidTapRecordMore(_ panel: RecordingPreviewPanel, trimRange: ClosedRange<TimeInterval>, isTrimmed: Bool)
}

// MARK: - RecordingPreviewPanel

/// Telegram-parity recording preview.
/// Layout: [delete glass circle] [dark glass panel with waveform + accent fill + trim + play pill + send]
/// RecordMore floats directly above the send button.
final class RecordingPreviewPanel: UIView, UIGestureRecognizerDelegate {

    private enum PanMode {
        case scrub
        case trimLeft
        case trimRight
    }

    private enum PlayPauseVisualState {
        case play
        case pause
    }

    weak var delegate: RecordingPreviewPanelDelegate?

    // MARK: - Background elements

    private let deleteGlassCircle = TelegramGlassUIView(frame: .zero)
    private let centerGlassBackground = TelegramGlassUIView(frame: .zero)
    private let recordMoreGlassCircle = TelegramGlassUIView(frame: .zero)
    private let accentFillView = UIImageView()

    // MARK: - Buttons

    private let deleteButton = UIButton(type: .system)
    private let recordMoreButton = UIButton(type: .system)
    private let sendButton = UIButton(type: .system)

    // MARK: - Waveform

    private let waveformContainer = UIView()
    private let waveformView = WaveformView()

    // MARK: - Trim handles (at panel level)

    private let leftTrimHandle = UIView()
    private let rightTrimHandle = UIView()
    private let leftCapsuleView = UIView()
    private let rightCapsuleView = UIView()

    // MARK: - Play button pill (at panel level)

    private let playButtonPill = UIButton(type: .custom)
    private let playPillBackground = UIImageView()
    private let playPauseAnimationView: LottieAnimationView = {
        let config = LottieConfiguration(renderingEngine: .mainThread)
        return LottieAnimationView(configuration: config)
    }()
    private let durationLabel = UILabel()

    // MARK: - Audio Playback

    private var audioPlayer: AVAudioPlayer?
    private var displayLink: CADisplayLink?
    private var isPlaying = false
    private var playPauseState: PlayPauseVisualState = .pause
    private let fileURL: URL
    private let duration: TimeInterval
    private let waveformSamples: [Float]

    // MARK: - Trim / Scrub

    private var trimStart: TimeInterval = 0
    private var trimEnd: TimeInterval = 0
    private var minTrimDuration: TimeInterval = 1
    private var activePanMode: PanMode?

    // MARK: - Layout cache

    private var centerPanelFrame: CGRect = .zero
    private var waveformOriginX: CGFloat = 0
    private var waveformWidth: CGFloat = 0

    // MARK: - Colors

    private var panelControlColor: UIColor {
        UIColor { traits in
            traits.userInterfaceStyle == .dark ? UIColor.white : UIColor.black
        }
    }

    private var panelControlAccentColor: UIColor {
        UIColor(red: 0.2, green: 0.565, blue: 0.925, alpha: 1) // #3390EC — same as outgoing message bubble
    }

    private var panelSecondaryTextColor: UIColor {
        panelControlColor.withAlphaComponent(0.7)
    }

    private var playPillBackgroundColor: UIColor {
        UIColor { traits in
            traits.userInterfaceStyle == .dark
                ? UIColor(white: 0.12, alpha: 0.7)
                : UIColor(white: 0.95, alpha: 0.7)
        }
    }

    var selectedTrimRange: ClosedRange<TimeInterval> {
        trimStart...trimEnd
    }

    // MARK: - Init

    init(frame: CGRect, fileURL: URL, duration: TimeInterval, waveform: [Float]) {
        self.fileURL = fileURL
        self.duration = max(0, duration)
        self.waveformSamples = waveform
        super.init(frame: frame)
        self.trimEnd = self.duration
        clipsToBounds = false
        accessibilityIdentifier = "voice.preview.panel"
        setupSubviews()
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError() }

    // MARK: - Hit testing (RecordMore floats above bounds)

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        if recordMoreButton.frame.contains(point) { return true }
        return super.point(inside: point, with: event)
    }

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // RecordMore floating button
        if !recordMoreButton.isHidden, recordMoreButton.frame.contains(point) {
            return recordMoreButton
        }
        // Play pill (ensure it receives taps over pan gesture)
        if !playButtonPill.isHidden, playButtonPill.alpha > 0.1 {
            let pillPoint = convert(point, to: playButtonPill)
            if playButtonPill.bounds.contains(pillPoint) {
                return playButtonPill
            }
        }
        return super.hitTest(point, with: event)
    }

    // MARK: - Gesture delegate (don't intercept button taps)

    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        let loc = gestureRecognizer.location(in: self)
        // Don't start pan on buttons
        if deleteButton.frame.contains(loc) { return false }
        if sendButton.frame.contains(loc) { return false }
        if recordMoreButton.frame.contains(loc) { return false }
        if playButtonPill.alpha > 0.1, playButtonPill.frame.contains(loc) { return false }
        return true
    }

    // MARK: - Setup

    private func setupSubviews() {
        // A) Delete glass circle + button
        deleteGlassCircle.fixedCornerRadius = 20
        deleteGlassCircle.isUserInteractionEnabled = false
        addSubview(deleteGlassCircle)

        deleteButton.setImage(VoiceRecordingAssets.image(.delete, templated: true), for: .normal)
        deleteButton.tintColor = panelControlColor
        deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
        deleteButton.accessibilityLabel = "Delete recording"
        deleteButton.accessibilityIdentifier = "voice.preview.delete"
        addSubview(deleteButton)

        // B) Central dark glass panel (includes send button area)
        centerGlassBackground.isUserInteractionEnabled = false
        addSubview(centerGlassBackground)

        // Blue accent fill (dynamic trim range, BELOW waveform in z-order)
        accentFillView.isUserInteractionEnabled = false
        addSubview(accentFillView)

        // Waveform container (ABOVE accent fill — bars visible on top of blue)
        waveformContainer.clipsToBounds = true
        addSubview(waveformContainer)

        waveformView.setSamples(waveformSamples)
        waveformView.progress = 0
        waveformContainer.addSubview(waveformView)

        // Trim handles at panel level
        leftTrimHandle.backgroundColor = .clear
        rightTrimHandle.backgroundColor = .clear
        addSubview(leftTrimHandle)
        addSubview(rightTrimHandle)

        leftCapsuleView.backgroundColor = .white
        leftCapsuleView.layer.cornerRadius = 1.5
        leftTrimHandle.addSubview(leftCapsuleView)

        rightCapsuleView.backgroundColor = .white
        rightCapsuleView.layer.cornerRadius = 1.5
        rightTrimHandle.addSubview(rightCapsuleView)

        // Play button pill at panel level
        playPillBackground.isUserInteractionEnabled = false
        playButtonPill.addSubview(playPillBackground)

        playPauseAnimationView.backgroundBehavior = .pauseAndRestore
        playPauseAnimationView.contentMode = .scaleAspectFit
        playPauseAnimationView.isUserInteractionEnabled = false
        if let animation = LottieAnimation.named(VoiceRecordingLottieAsset.playPause.rawValue) {
            playPauseAnimationView.animation = animation
        }
        playButtonPill.addSubview(playPauseAnimationView)

        durationLabel.font = .monospacedDigitSystemFont(ofSize: 13, weight: .semibold)
        durationLabel.textAlignment = .right
        playButtonPill.addSubview(durationLabel)

        playButtonPill.addTarget(self, action: #selector(playTapped), for: .touchUpInside)
        playButtonPill.accessibilityLabel = "Play recording"
        playButtonPill.accessibilityIdentifier = "voice.preview.playPause"
        addSubview(playButtonPill)
        configurePlayButton(playing: false, animated: false)

        // Send button INSIDE center panel (right edge)
        sendButton.setImage(VoiceRecordingAssets.image(.send, templated: true), for: .normal)
        sendButton.backgroundColor = panelControlAccentColor
        sendButton.layer.cornerRadius = 18
        sendButton.clipsToBounds = true
        sendButton.tintColor = .white
        sendButton.addTarget(self, action: #selector(sendTapped), for: .touchUpInside)
        sendButton.accessibilityLabel = "Send recording"
        sendButton.accessibilityIdentifier = "voice.preview.send"
        addSubview(sendButton)

        // RecordMore glass circle floating above send button
        recordMoreGlassCircle.fixedCornerRadius = 20
        recordMoreGlassCircle.isUserInteractionEnabled = false
        addSubview(recordMoreGlassCircle)

        recordMoreButton.setImage(VoiceRecordingAssets.image(.iconMicrophone, templated: true), for: .normal)
        recordMoreButton.tintColor = panelControlColor
        recordMoreButton.addTarget(self, action: #selector(recordMoreTapped), for: .touchUpInside)
        recordMoreButton.accessibilityLabel = "Record more"
        recordMoreButton.accessibilityIdentifier = "voice.preview.recordMore"
        addSubview(recordMoreButton)

        // Pan gesture on self for waveform scrubbing/trimming
        let pan = UIPanGestureRecognizer(target: self, action: #selector(handleWaveformPan(_:)))
        pan.delegate = self
        addGestureRecognizer(pan)

        updateThemeColors()
    }

    // MARK: - Layout

    override func layoutSubviews() {
        super.layoutSubviews()
        let h = bounds.height
        let w = bounds.width

        // Delete — glass circle, left edge
        let deleteSize: CGFloat = 40
        let deleteFrame = CGRect(x: 0, y: (h - deleteSize) / 2, width: deleteSize, height: deleteSize)
        deleteGlassCircle.frame = deleteFrame
        deleteGlassCircle.applyCornerRadius()
        deleteButton.frame = deleteFrame

        // Send button — inside center panel, at right edge with inset
        let sendSize: CGFloat = 36
        let sendInset: CGFloat = 3
        let sendX = w - sendInset - sendSize
        sendButton.frame = CGRect(x: sendX, y: (h - sendSize) / 2, width: sendSize, height: sendSize)

        // Central dark glass panel — from after delete gap to right edge (includes send)
        let panelGap: CGFloat = 6
        let panelX = deleteSize + panelGap
        let panelW = w - panelX
        let panelH = deleteSize  // Same height as delete circle (40pt) — Telegram parity
        let panelY = (h - panelH) / 2
        let panelCornerRadius = panelH / 2
        centerGlassBackground.frame = CGRect(x: panelX, y: panelY, width: panelW, height: panelH)
        centerGlassBackground.fixedCornerRadius = panelCornerRadius
        centerGlassBackground.applyCornerRadius()
        centerPanelFrame = CGRect(x: panelX, y: panelY, width: panelW, height: panelH)

        // Waveform — symmetric 21pt inset from panel edges (Telegram: x=21, width=panelW-42)
        let wfLeftInset: CGFloat = 21
        let wfX = panelX + wfLeftInset
        let wfRightEdge = sendButton.frame.minX - 21  // Symmetric with left
        let wfW = max(0, wfRightEdge - wfX)
        let wfH: CGFloat = 13
        let wfY = floor((h - wfH) / 2)
        waveformContainer.frame = CGRect(x: wfX, y: wfY, width: wfW, height: wfH)
        waveformView.frame = waveformContainer.bounds
        waveformOriginX = wfX
        waveformWidth = wfW

        // RecordMore — floating directly above send button
        let rmSize: CGFloat = 40
        let rmX = sendButton.frame.midX - rmSize / 2
        let rmFrame = CGRect(x: rmX, y: -52, width: rmSize, height: rmSize)
        recordMoreGlassCircle.frame = rmFrame
        recordMoreGlassCircle.applyCornerRadius()
        recordMoreButton.frame = rmFrame

        // Trim
        minTrimDuration = VoiceRecordingParityConstants.minTrimDuration(
            duration: duration,
            waveformWidth: waveformContainer.bounds.width
        )
        trimEnd = max(trimEnd, min(duration, trimStart + minTrimDuration))
        updateTrimVisuals()
        updateDurationLabel(isPlaying ? remainingFromPlayer() : (trimEnd - trimStart))
    }

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        guard previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle else { return }
        updateThemeColors()
    }

    // MARK: - Play/Pause

    @objc private func playTapped() {
        if isPlaying {
            pausePlayback()
        } else {
            startPlayback()
        }
    }

    private func startPlayback() {
        // After recording, audio route may be earpiece. Switch to speaker for preview.
        try? AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker])
        try? AVAudioSession.sharedInstance().setActive(true)
        try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)

        if audioPlayer == nil {
            do {
                let player = try AVAudioPlayer(contentsOf: fileURL)
                player.volume = 1.0
                player.prepareToPlay()
                audioPlayer = player
            } catch {
                print("[VoicePreview] AVAudioPlayer init failed: \(error)")
                return
            }
        }
        guard let player = audioPlayer else { return }
        if player.currentTime < trimStart || player.currentTime > trimEnd {
            player.currentTime = trimStart
        }
        guard player.play() else {
            print("[VoicePreview] player.play() returned false")
            return
        }
        isPlaying = true
        configurePlayButton(playing: true, animated: true)
        startDisplayLink()
    }

    private func pausePlayback() {
        audioPlayer?.pause()
        isPlaying = false
        configurePlayButton(playing: false, animated: true)
        stopDisplayLink()
    }

    func stopPlayback(resetToTrimStart: Bool = true) {
        audioPlayer?.stop()
        if resetToTrimStart {
            audioPlayer?.currentTime = trimStart
            waveformView.progress = CGFloat((duration > 0 ? trimStart / duration : 0))
        } else {
            waveformView.progress = 0
        }
        isPlaying = false
        configurePlayButton(playing: false, animated: false)
        updateDurationLabel(trimEnd - trimStart)
        stopDisplayLink()
    }

    private func configurePlayButton(playing: Bool, animated: Bool) {
        let targetState: PlayPauseVisualState = playing ? .pause : .play
        guard playPauseState != targetState else { return }
        let previous = playPauseState
        playPauseState = targetState

        if playPauseAnimationView.animation != nil {
            switch (previous, targetState) {
            case (.play, .pause):
                if animated {
                    playPauseAnimationView.play(fromFrame: 0, toFrame: 41, loopMode: .playOnce)
                } else {
                    playPauseAnimationView.currentFrame = 41
                }
            case (.pause, .play):
                if animated {
                    playPauseAnimationView.play(fromFrame: 41, toFrame: 83, loopMode: .playOnce)
                } else {
                    playPauseAnimationView.currentFrame = 0
                }
            case (.play, .play):
                playPauseAnimationView.currentFrame = 0
            case (.pause, .pause):
                playPauseAnimationView.currentFrame = 41
            }
        }
    }

    // MARK: - Display Link

    private func startDisplayLink() {
        guard displayLink == nil else { return }
        let link = CADisplayLink(target: self, selector: #selector(displayLinkTick))
        link.preferredFrameRateRange = CAFrameRateRange(minimum: 24, maximum: 30, preferred: 30)
        link.add(to: .main, forMode: .common)
        displayLink = link
    }

    private func stopDisplayLink() {
        displayLink?.invalidate()
        displayLink = nil
    }

    @objc private func displayLinkTick() {
        guard let player = audioPlayer else { return }
        if !player.isPlaying && isPlaying {
            stopPlayback()
            return
        }
        if player.currentTime >= trimEnd {
            stopPlayback()
            return
        }
        let progress = duration > 0 ? player.currentTime / duration : 0
        waveformView.progress = CGFloat(progress)
        updateDurationLabel(remainingFromPlayer())
    }

    // MARK: - Trim / Scrub

    @objc private func handleWaveformPan(_ gesture: UIPanGestureRecognizer) {
        guard duration > 0, waveformWidth > 1 else { return }
        let location = gesture.location(in: self)
        let wfRelativeX = location.x - waveformOriginX
        let fraction = min(1, max(0, wfRelativeX / waveformWidth))
        let targetTime = TimeInterval(fraction) * duration

        switch gesture.state {
        case .began:
            let leftX = xForTime(trimStart)
            let rightX = xForTime(trimEnd)
            if abs(wfRelativeX - leftX) <= 17 {
                activePanMode = .trimLeft
            } else if abs(wfRelativeX - rightX) <= 17 {
                activePanMode = .trimRight
            } else if wfRelativeX >= -10, wfRelativeX <= waveformWidth + 10 {
                activePanMode = .scrub
            } else {
                return
            }
            if activePanMode != .scrub {
                pausePlayback()
            }
            if activePanMode == .scrub {
                UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) {
                    self.playButtonPill.alpha = 0
                }
            }
        case .changed:
            switch activePanMode {
            case .trimLeft:
                trimStart = min(max(0, targetTime), trimEnd - minTrimDuration)
                if let player = audioPlayer, player.currentTime < trimStart {
                    player.currentTime = trimStart
                }
            case .trimRight:
                trimEnd = max(min(duration, targetTime), trimStart + minTrimDuration)
                if let player = audioPlayer, player.currentTime > trimEnd {
                    player.currentTime = trimEnd
                }
            case .scrub:
                let clamped = min(trimEnd, max(trimStart, targetTime))
                if audioPlayer == nil {
                    audioPlayer = try? AVAudioPlayer(contentsOf: fileURL)
                    audioPlayer?.prepareToPlay()
                }
                audioPlayer?.currentTime = clamped
                waveformView.progress = CGFloat(clamped / duration)
            case .none:
                break
            }
            updateTrimVisuals()
            if activePanMode == .scrub {
                updateDurationLabel(max(0, trimEnd - (audioPlayer?.currentTime ?? trimStart)))
            } else {
                updateDurationLabel(trimEnd - trimStart)
            }
        default:
            activePanMode = nil
            UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) {
                self.playButtonPill.alpha = 1
            }
        }
    }

    private func updateTrimVisuals() {
        let wfW = waveformContainer.bounds.width
        guard wfW > 0 else { return }

        let startX = xForTime(trimStart)
        let endX = xForTime(trimEnd)

        // Trim handles at panel level, clamped to not overlap send button
        let handleW: CGFloat = 16
        let handleH = centerPanelFrame.height
        let handleY = centerPanelFrame.minY
        let maxRightHandleX = sendButton.frame.minX - 4 - handleW

        leftTrimHandle.frame = CGRect(
            x: max(centerPanelFrame.minX, waveformOriginX + startX - handleW),
            y: handleY,
            width: handleW,
            height: handleH
        )
        rightTrimHandle.frame = CGRect(
            x: min(maxRightHandleX, waveformOriginX + endX),
            y: handleY,
            width: handleW,
            height: handleH
        )

        // Capsule indicators
        let capsuleW: CGFloat = 3
        let capsuleH: CGFloat = 12
        let capsuleY = (handleH - capsuleH) / 2
        leftCapsuleView.frame = CGRect(x: handleW - 8 - capsuleW, y: capsuleY, width: capsuleW, height: capsuleH)
        rightCapsuleView.frame = CGRect(x: 8, y: capsuleY, width: capsuleW, height: capsuleH)

        let showTrim = duration >= 2.0
        leftTrimHandle.isHidden = !showTrim
        rightTrimHandle.isHidden = !showTrim

        // Blue accent fill — spans from left trim handle to right trim handle (Telegram parity)
        // When trimming, the blue area moves with the handles.
        let accentInsetH: CGFloat = 0  // horizontal (left) — flush with panel edge
        let accentInsetV: CGFloat = 3  // vertical (top/bottom) — proportional gap
        let accentY = centerPanelFrame.minY + accentInsetV
        let accentH = centerPanelFrame.height - accentInsetV * 2
        if showTrim {
            let fillX = leftTrimHandle.frame.minX
            let fillEndX = rightTrimHandle.frame.maxX
            let clampedX = max(centerPanelFrame.minX + accentInsetH, fillX)
            let clampedEnd = min(sendButton.frame.minX - 3, fillEndX)
            accentFillView.frame = CGRect(x: clampedX, y: accentY, width: max(0, clampedEnd - clampedX), height: accentH)
        } else {
            let accentX = centerPanelFrame.minX + accentInsetH
            let accentW = max(0, sendButton.frame.minX - 3 - accentX)
            accentFillView.frame = CGRect(x: accentX, y: accentY, width: accentW, height: accentH)
        }

        // Play button pill — centered between trim handles, at panel level
        let space = endX - startX
        let pillW: CGFloat = space >= 70 ? 63 : 27
        let pillH: CGFloat = 22
        let pillX = waveformOriginX + startX + (space - pillW) / 2
        let pillY = (bounds.height - pillH) / 2
        playButtonPill.frame = CGRect(x: pillX, y: pillY, width: pillW, height: pillH)

        // Pill background
        if playPillBackground.image == nil {
            playPillBackground.image = Self.makeStretchablePill(
                diameter: pillH, color: .white
            )?.withRenderingMode(.alwaysTemplate)
            playPillBackground.tintColor = playPillBackgroundColor
        }
        playPillBackground.frame = playButtonPill.bounds
        playPillBackground.layer.cornerRadius = pillH / 2
        playPillBackground.clipsToBounds = true

        // Lottie icon inside pill
        playPauseAnimationView.frame = CGRect(x: 3, y: 0.5, width: 21, height: 21)

        // Duration label inside pill
        durationLabel.isHidden = pillW <= 27
        durationLabel.frame = CGRect(x: 18, y: 1, width: 38, height: 20)
    }

    private func xForTime(_ time: TimeInterval) -> CGFloat {
        guard duration > 0 else { return 0 }
        let normalized = min(1, max(0, time / duration))
        return CGFloat(normalized) * waveformContainer.bounds.width
    }

    private func remainingFromPlayer() -> TimeInterval {
        guard let player = audioPlayer else { return trimEnd - trimStart }
        return max(0, trimEnd - player.currentTime)
    }

    // MARK: - Duration Formatting

    private func updateDurationLabel(_ time: TimeInterval) {
        let totalSeconds = Int(max(0, time))
        let minutes = totalSeconds / 60
        let seconds = totalSeconds % 60
        durationLabel.text = String(format: "%d:%02d", minutes, seconds)
    }

    private func updateThemeColors() {
        waveformView.backgroundColor_ = UIColor.white.withAlphaComponent(0.4)
        waveformView.foregroundColor_ = UIColor.white
        waveformView.setNeedsDisplay()

        deleteButton.tintColor = panelControlColor
        recordMoreButton.tintColor = panelControlColor
        durationLabel.textColor = panelSecondaryTextColor
        applyPlayPauseTintColor(panelSecondaryTextColor)
        playPillBackground.tintColor = playPillBackgroundColor

        accentFillView.image = Self.makeStretchablePill(
            diameter: 34,
            color: panelControlAccentColor
        )
    }

    private func applyPlayPauseTintColor(_ color: UIColor) {
        var r: CGFloat = 0
        var g: CGFloat = 0
        var b: CGFloat = 0
        var a: CGFloat = 0
        color.getRed(&r, green: &g, blue: &b, alpha: &a)
        playPauseAnimationView.setValueProvider(
            ColorValueProvider(LottieColor(r: Double(r), g: Double(g), b: Double(b), a: Double(a))),
            keypath: AnimationKeypath(keypath: "**.Color")
        )
    }

    // MARK: - Actions

    @objc private func deleteTapped() {
        stopPlayback()
        delegate?.previewPanelDidTapDelete(self)
    }

    @objc private func recordMoreTapped() {
        stopPlayback(resetToTrimStart: false)
        let isTrimmed = trimStart > 0.1 || trimEnd < duration - 0.1
        delegate?.previewPanelDidTapRecordMore(self, trimRange: selectedTrimRange, isTrimmed: isTrimmed)
    }

    @objc private func sendTapped() {
        stopPlayback(resetToTrimStart: false)
        delegate?.previewPanelDidTapSend(self, trimRange: selectedTrimRange)
    }

    // MARK: - Animate In

    func animateIn() {
        alpha = 0
        transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
        UIView.animate(withDuration: 0.25, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0) {
            self.alpha = 1
            self.transform = .identity
        }
    }

    // MARK: - Animate Out

    func animateOut(completion: (() -> Void)? = nil) {
        stopPlayback()
        UIView.animate(withDuration: 0.15, animations: {
            self.alpha = 0
            self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
        }, completion: { _ in
            self.removeFromSuperview()
            completion?()
        })
    }

    // MARK: - Helpers

    private static func makeStretchablePill(diameter: CGFloat, color: UIColor) -> UIImage? {
        let size = CGSize(width: diameter, height: diameter)
        let renderer = UIGraphicsImageRenderer(size: size)
        let image = renderer.image { _ in
            color.setFill()
            UIBezierPath(ovalIn: CGRect(origin: .zero, size: size)).fill()
        }
        let half = diameter / 2
        return image.resizableImage(withCapInsets: UIEdgeInsets(
            top: half, left: half, bottom: half, right: half
        ))
    }

    deinit {
        stopDisplayLink()
    }
}
