import QuartzCore
import UIKit

// MARK: - VoiceBlobView

/// Three-layer animated blob visualization for voice recording.
/// Ported from Telegram-iOS `AudioBlob/Sources/BlobView.swift`.
///
/// Architecture:
/// - Small blob (innermost): circle, solid fill, subtle scale pulsing
/// - Medium blob: organic shape morphing, 0.3 alpha
/// - Big blob: organic shape morphing, 0.15 alpha
///
/// Audio level drives both blob scale and morph speed.
final class VoiceBlobView: UIView {

    typealias BlobRange = (min: CGFloat, max: CGFloat)

    private let smallBlob: BlobLayer
    private let mediumBlob: BlobLayer
    private let bigBlob: BlobLayer

    private let maxLevel: CGFloat
    private var displayLink: CADisplayLink?

    private var audioLevel: CGFloat = 0
    private(set) var presentationAudioLevel: CGFloat = 0
    private(set) var isAnimating = false

    // MARK: - Init

    init(
        frame: CGRect = .zero,
        maxLevel: CGFloat = 1.0,
        smallBlobRange: BlobRange = (min: 0.45, max: 0.55),
        mediumBlobRange: BlobRange = (min: 0.52, max: 0.87),
        bigBlobRange: BlobRange = (min: 0.57, max: 1.0)
    ) {
        self.maxLevel = maxLevel

        self.smallBlob = BlobLayer(
            pointsCount: 8,
            minRandomness: 0.1, maxRandomness: 0.5,
            minSpeed: 0.2, maxSpeed: 0.6,
            minScale: smallBlobRange.min, maxScale: smallBlobRange.max,
            isCircle: true
        )
        self.mediumBlob = BlobLayer(
            pointsCount: 8,
            minRandomness: 1.0, maxRandomness: 1.0,
            minSpeed: 0.9, maxSpeed: 4.0,
            minScale: mediumBlobRange.min, maxScale: mediumBlobRange.max,
            isCircle: false
        )
        self.bigBlob = BlobLayer(
            pointsCount: 8,
            minRandomness: 1.0, maxRandomness: 1.0,
            minSpeed: 0.9, maxSpeed: 4.0,
            minScale: bigBlobRange.min, maxScale: bigBlobRange.max,
            isCircle: false
        )

        super.init(frame: frame)

        layer.addSublayer(bigBlob.shapeLayer)
        layer.addSublayer(mediumBlob.shapeLayer)
        layer.addSublayer(smallBlob.shapeLayer)
    }

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

    deinit {
        displayLink?.invalidate()
    }

    // MARK: - Public API

    func setColor(_ color: UIColor) {
        smallBlob.setColor(color)
        mediumBlob.setColor(color.withAlphaComponent(0.3))
        bigBlob.setColor(color.withAlphaComponent(0.15))
    }

    func updateLevel(_ level: CGFloat, immediately: Bool = false) {
        let normalized = min(1, max(level / maxLevel, 0))

        smallBlob.updateSpeedLevel(to: normalized)
        mediumBlob.updateSpeedLevel(to: normalized)
        bigBlob.updateSpeedLevel(to: normalized)

        audioLevel = normalized
        if immediately {
            presentationAudioLevel = normalized
        }
    }

    func startAnimating(immediately: Bool = false) {
        guard !isAnimating else { return }
        isAnimating = true

        if !immediately {
            animateScale(of: mediumBlob.shapeLayer, from: 0.75, to: 1.0, duration: 0.35)
            animateScale(of: bigBlob.shapeLayer, from: 0.75, to: 1.0, duration: 0.35)
        }

        updateBlobsState()
        startDisplayLink()
    }

    func stopAnimating(duration: Double = 0.15) {
        guard isAnimating else { return }
        isAnimating = false

        animateScale(of: mediumBlob.shapeLayer, from: 1.0, to: 0.75, duration: duration)
        animateScale(of: bigBlob.shapeLayer, from: 1.0, to: 0.75, duration: duration)

        updateBlobsState()
        stopDisplayLink()
    }

    // MARK: - Layout

    override func layoutSubviews() {
        super.layoutSubviews()
        let size = bounds.size
        smallBlob.updateBounds(size)
        mediumBlob.updateBounds(size)
        bigBlob.updateBounds(size)
        updateBlobsState()
    }

    // MARK: - Display Link

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

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

    @objc private func displayLinkTick() {
        presentationAudioLevel = presentationAudioLevel * 0.9 + audioLevel * 0.1

        smallBlob.level = presentationAudioLevel
        mediumBlob.level = presentationAudioLevel
        bigBlob.level = presentationAudioLevel
    }

    // MARK: - Helpers

    private func updateBlobsState() {
        if isAnimating, bounds.size != .zero {
            smallBlob.startAnimating()
            mediumBlob.startAnimating()
            bigBlob.startAnimating()
        } else {
            smallBlob.stopAnimating()
            mediumBlob.stopAnimating()
            bigBlob.stopAnimating()
        }
    }

    private func animateScale(of layer: CAShapeLayer, from: CGFloat, to: CGFloat, duration: Double) {
        let anim = CABasicAnimation(keyPath: "transform.scale")
        anim.fromValue = from
        anim.toValue = to
        anim.duration = duration
        anim.fillMode = .forwards
        anim.isRemovedOnCompletion = false
        anim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        layer.add(anim, forKey: "blobScale")
    }
}

// MARK: - BlobLayer

/// Single animated blob shape using CAShapeLayer + Bezier morphing.
/// Ported from Telegram's `BlobNode` (AsyncDisplayKit → pure CALayer).
private final class BlobLayer {

    let shapeLayer = CAShapeLayer()

    let pointsCount: Int
    let smoothness: CGFloat
    let minRandomness: CGFloat
    let maxRandomness: CGFloat
    let minSpeed: CGFloat
    let maxSpeed: CGFloat
    let minScale: CGFloat
    let maxScale: CGFloat
    let isCircle: Bool

    var level: CGFloat = 0 {
        didSet {
            guard abs(level - oldValue) > 0.01 else { return }
            let lv = minScale + (maxScale - minScale) * level
            CATransaction.begin()
            CATransaction.setDisableActions(true)
            shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1)
            CATransaction.commit()
        }
    }

    private var speedLevel: CGFloat = 0
    private var boundsSize: CGSize = .zero

    init(
        pointsCount: Int,
        minRandomness: CGFloat,
        maxRandomness: CGFloat,
        minSpeed: CGFloat,
        maxSpeed: CGFloat,
        minScale: CGFloat,
        maxScale: CGFloat,
        isCircle: Bool
    ) {
        self.pointsCount = pointsCount
        self.minRandomness = minRandomness
        self.maxRandomness = maxRandomness
        self.minSpeed = minSpeed
        self.maxSpeed = maxSpeed
        self.minScale = minScale
        self.maxScale = maxScale
        self.isCircle = isCircle

        let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
        self.smoothness = ((4.0 / 3.0) * tan(angle / 4.0)) / sin(angle / 2.0) / 2.0

        shapeLayer.strokeColor = nil
        shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1)
    }

    func setColor(_ color: UIColor) {
        shapeLayer.fillColor = color.cgColor
    }

    func updateSpeedLevel(to newLevel: CGFloat) {
        speedLevel = max(speedLevel, newLevel)
    }

    func updateBounds(_ size: CGSize) {
        boundsSize = size
        // DON'T set shapeLayer.bounds — leave at default (0,0,0,0).
        // Telegram BlobNode.layout(): shapeLayer has zero-sized bounds + position at center.
        // With zero bounds, local (0,0) maps to the position point = view center.
        // Blob paths are centered at (0,0), so they render at the view's center. Correct!
        // Setting bounds to (0,0,68,68) shifts local (0,0) to the view's top-left. Wrong!
        shapeLayer.position = CGPoint(x: size.width / 2, y: size.height / 2)

        if isCircle {
            let hw = size.width / 2
            shapeLayer.path = UIBezierPath(
                roundedRect: CGRect(x: -hw, y: -hw, width: size.width, height: size.height),
                cornerRadius: hw
            ).cgPath
        }
    }

    func startAnimating() {
        guard !isCircle else { return }
        animateToNewShape()
    }

    func stopAnimating() {
        shapeLayer.removeAnimation(forKey: "path")
    }

    // MARK: - Shape Animation

    private func animateToNewShape() {
        guard !isCircle, boundsSize != .zero else { return }

        if shapeLayer.path == nil {
            let points = generateBlob(for: boundsSize)
            shapeLayer.path = BezierSmooth.smoothCurve(through: points, length: boundsSize.width, smoothness: smoothness).cgPath
        }

        let nextPoints = generateBlob(for: boundsSize)
        let nextPath = BezierSmooth.smoothCurve(through: nextPoints, length: boundsSize.width, smoothness: smoothness).cgPath

        let anim = CABasicAnimation(keyPath: "path")
        let previous = shapeLayer.path
        shapeLayer.path = nextPath
        anim.fromValue = previous
        anim.toValue = nextPath
        anim.duration = CFTimeInterval(1.0 / (minSpeed + (maxSpeed - minSpeed) * speedLevel))
        anim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        anim.isRemovedOnCompletion = false
        anim.fillMode = .forwards
        anim.delegate = AnimationDelegate { [weak self] finished in
            if finished {
                self?.animateToNewShape()
            }
        }

        shapeLayer.add(anim, forKey: "path")
        speedLevel = 0
    }

    private func generateBlob(for size: CGSize) -> [CGPoint] {
        let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel
        return blobPoints(count: pointsCount, randomness: randomness).map {
            CGPoint(x: $0.x * size.width, y: $0.y * size.height)
        }
    }

    private func blobPoints(count: Int, randomness: CGFloat) -> [CGPoint] {
        let angle = (CGFloat.pi * 2) / CGFloat(count)
        let rangeStart: CGFloat = 1.0 / (1.0 + randomness / 10.0)
        let startAngle = angle * CGFloat(arc4random_uniform(100)) / 100.0

        return (0..<count).map { i in
            let rng = CGFloat(arc4random_uniform(1000)) / 1000.0
            let randOffset = (rangeStart + rng * (1.0 - rangeStart)) / 2.0
            let angleRandomness = angle * 0.1
            let randAngle = angle + angle * ((angleRandomness * CGFloat(arc4random_uniform(100)) / 100.0) - angleRandomness * 0.5)
            let px = sin(startAngle + CGFloat(i) * randAngle)
            let py = cos(startAngle + CGFloat(i) * randAngle)
            return CGPoint(x: px * randOffset, y: py * randOffset)
        }
    }
}

// MARK: - Animation Delegate Helper

private final class AnimationDelegate: NSObject, CAAnimationDelegate {
    let completion: (Bool) -> Void

    init(completion: @escaping (Bool) -> Void) {
        self.completion = completion
    }

    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        completion(flag)
    }
}

// MARK: - Bezier Smooth Curve

/// Generates smooth closed Bezier curves through a set of points.
/// Ported from Telegram's `UIBezierPath.smoothCurve` extension.
private enum BezierSmooth {

    static func smoothCurve(through points: [CGPoint], length: CGFloat, smoothness: CGFloat) -> UIBezierPath {
        let smoothPoints = points.enumerated().map { index, curr -> SmoothPoint in
            let prevIdx = index - 1
            let prev = points[prevIdx >= 0 ? prevIdx : points.count + prevIdx]
            let next = points[(index + 1) % points.count]

            let angle: CGFloat = {
                let dx = next.x - prev.x
                let dy = -next.y + prev.y
                let a = atan2(dy, dx)
                return a < 0 ? abs(a) : 2 * .pi - a
            }()

            return SmoothPoint(
                point: curr,
                inAngle: angle + .pi,
                inLength: smoothness * distance(prev, curr),
                outAngle: angle,
                outLength: smoothness * distance(curr, next)
            )
        }

        let path = UIBezierPath()
        path.move(to: smoothPoints[0].point)
        for i in 0..<smoothPoints.count {
            let curr = smoothPoints[i]
            let next = smoothPoints[(i + 1) % smoothPoints.count]
            path.addCurve(to: next.point, controlPoint1: curr.smoothOut(), controlPoint2: next.smoothIn())
        }
        path.close()
        return path
    }

    private static func distance(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
        sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y))
    }

    private struct SmoothPoint {
        let point: CGPoint
        let inAngle: CGFloat
        let inLength: CGFloat
        let outAngle: CGFloat
        let outLength: CGFloat

        func smoothIn() -> CGPoint {
            CGPoint(x: point.x + inLength * cos(inAngle), y: point.y + inLength * sin(inAngle))
        }

        func smoothOut() -> CGPoint {
            CGPoint(x: point.x + outLength * cos(outAngle), y: point.y + outLength * sin(outAngle))
        }
    }
}
