import SwiftUI

// MARK: - SVGPathShape

/// A SwiftUI Shape that renders an SVG path string within the given rect.
/// Scales the path from the original viewBox to fit the rect.
struct SVGPathShape: Shape {
    let pathData: String
    let viewBox: CGSize

    func path(in rect: CGRect) -> Path {
        guard viewBox.width > 0, viewBox.height > 0 else { return Path() }
        var parser = SVGPathParser(pathData: pathData)
        var output = Path(parser.parse())
        output = output.applying(.init(
            scaleX: rect.width / viewBox.width,
            y: rect.height / viewBox.height
        ))
        return output
    }
}

// MARK: - SVGPathToken

enum SVGPathToken {
    case command(Character)
    case number(CGFloat)
}

// MARK: - SVGPathTokenizer

/// Converts an SVG path data string into a sequence of command and number tokens.
/// Handles decimal points, negative signs, scientific notation, and implicit separators
/// (consecutive decimals like `.5.3` are split into `0.5` and `0.3`).
struct SVGPathTokenizer {
    static func tokenize(_ source: String) -> [SVGPathToken] {
        var tokens: [SVGPathToken] = []
        let chars = Array(source)
        var index = 0

        while index < chars.count {
            let ch = chars[index]

            if ch.isWhitespace || ch == "," { index += 1; continue }
            if ch.isLetter { tokens.append(.command(ch)); index += 1; continue }

            if ch.isNumber || ch == "-" || ch == "+" || ch == "." {
                let start = index
                var hasDot = (ch == ".")
                index += 1

                while index < chars.count {
                    let c = chars[index]
                    let prev = chars[index - 1]

                    if c.isNumber { index += 1; continue }

                    // Second decimal point starts a new number (e.g., `.5.3` → `0.5`, `0.3`)
                    if c == "." {
                        if hasDot { break }
                        hasDot = true
                        index += 1
                        continue
                    }

                    if c == "e" || c == "E" { index += 1; continue }
                    if (c == "-" || c == "+"), (prev == "e" || prev == "E") { index += 1; continue }
                    break
                }

                let fragment = String(chars[start..<index])
                if let value = Double(fragment) { tokens.append(.number(CGFloat(value))) }
                continue
            }

            index += 1
        }

        return tokens
    }
}

// MARK: - SVGPathParser

/// Parses SVG path data tokens into a CGPath.
///
/// Supports commands: M, L, H, V, C, A, Z (and lowercase relative variants).
/// Arc (A/a) is converted to cubic Bézier curves using the W3C SVG spec
/// endpoint-to-center parameterization algorithm.
struct SVGPathParser {
    private let tokens: [SVGPathToken]
    private var index: Int = 0
    private var lastCommand: Character = "M"
    private var current = CGPoint.zero
    private var subpathStart = CGPoint.zero
    private var cgPath = CGMutablePath()

    init(pathData: String) {
        self.tokens = SVGPathTokenizer.tokenize(pathData)
    }

    mutating func parse() -> CGPath {
        while index < tokens.count {
            let command = readCommandOrReuse()
            switch command {
            case "M", "m": parseMove(command)
            case "L", "l": parseLine(command)
            case "H", "h": parseHorizontal(command)
            case "V", "v": parseVertical(command)
            case "C", "c": parseCubic(command)
            case "A", "a": parseArc(command)
            case "Z", "z":
                cgPath.closeSubpath()
                current = subpathStart
            default:
                skipToNextCommand()
            }
        }
        return cgPath.copy() ?? CGMutablePath()
    }

    // MARK: - Token Reading

    private var isAtCommand: Bool {
        guard index < tokens.count else { return false }
        if case .command = tokens[index] { return true }
        return false
    }

    private mutating func readCommandOrReuse() -> Character {
        guard index < tokens.count else { return lastCommand }
        if case let .command(command) = tokens[index] {
            index += 1
            lastCommand = command
            return command
        }
        return lastCommand
    }

    private mutating func readNumber() -> CGFloat? {
        guard index < tokens.count else { return nil }
        if case let .number(value) = tokens[index] {
            index += 1
            return value
        }
        return nil
    }

    private func resolvedPoint(x: CGFloat, y: CGFloat, relative: Bool) -> CGPoint {
        relative ? CGPoint(x: current.x + x, y: current.y + y) : CGPoint(x: x, y: y)
    }

    private mutating func readPoint(relative: Bool) -> CGPoint? {
        guard let x = readNumber(), let y = readNumber() else { return nil }
        return resolvedPoint(x: x, y: y, relative: relative)
    }

    // MARK: - Command Parsers

    private mutating func parseMove(_ command: Character) {
        let relative = command.isLowercase
        guard let first = readPoint(relative: relative) else { return }
        cgPath.move(to: first)
        current = first
        subpathStart = first

        // Subsequent coordinate pairs after M/m are treated as L/l
        while !isAtCommand, let point = readPoint(relative: relative) {
            cgPath.addLine(to: point)
            current = point
        }

        lastCommand = relative ? "l" : "L"
    }

    private mutating func parseLine(_ command: Character) {
        let relative = command.isLowercase
        while !isAtCommand, let point = readPoint(relative: relative) {
            cgPath.addLine(to: point)
            current = point
        }
    }

    private mutating func parseHorizontal(_ command: Character) {
        let relative = command.isLowercase
        while !isAtCommand, let value = readNumber() {
            current = CGPoint(x: relative ? current.x + value : value, y: current.y)
            cgPath.addLine(to: current)
        }
    }

    private mutating func parseVertical(_ command: Character) {
        let relative = command.isLowercase
        while !isAtCommand, let value = readNumber() {
            current = CGPoint(x: current.x, y: relative ? current.y + value : value)
            cgPath.addLine(to: current)
        }
    }

    private mutating func parseCubic(_ command: Character) {
        let relative = command.isLowercase
        while !isAtCommand {
            guard let x1 = readNumber(),
                  let y1 = readNumber(),
                  let x2 = readNumber(),
                  let y2 = readNumber(),
                  let x = readNumber(),
                  let y = readNumber()
            else { return }

            let c1 = resolvedPoint(x: x1, y: y1, relative: relative)
            let c2 = resolvedPoint(x: x2, y: y2, relative: relative)
            let end = resolvedPoint(x: x, y: y, relative: relative)

            cgPath.addCurve(to: end, control1: c1, control2: c2)
            current = end
        }
    }

    // MARK: - Arc

    /// Parses SVG elliptical arc commands (A/a).
    /// Parameters: rx ry x-rotation large-arc-flag sweep-flag x y
    private mutating func parseArc(_ command: Character) {
        let relative = command.isLowercase
        while !isAtCommand {
            guard let rxRaw = readNumber(),
                  let ryRaw = readNumber(),
                  let xRotationDeg = readNumber(),
                  let largeArcFlag = readNumber(),
                  let sweepFlag = readNumber(),
                  let x = readNumber(),
                  let y = readNumber()
            else { return }

            let end = resolvedPoint(x: x, y: y, relative: relative)
            arcToBezier(
                from: current,
                to: end,
                rx: abs(rxRaw),
                ry: abs(ryRaw),
                xRotation: xRotationDeg * .pi / 180,
                largeArc: largeArcFlag != 0,
                sweep: sweepFlag != 0
            )
            current = end
        }
    }

    /// Converts an SVG elliptical arc to cubic Bézier curves.
    ///
    /// Uses the endpoint-to-center parameterization from the W3C SVG spec:
    /// https://www.w3.org/TR/SVG11/implnote.html#ArcConversionEndpointToCenter
    ///
    /// Splits arcs into ≤90° segments and approximates each with a cubic Bézier.
    private mutating func arcToBezier(
        from p1: CGPoint,
        to p2: CGPoint,
        rx rxIn: CGFloat,
        ry ryIn: CGFloat,
        xRotation phi: CGFloat,
        largeArc: Bool,
        sweep: Bool
    ) {
        // Degenerate: identical endpoints
        if p1.x == p2.x && p1.y == p2.y { return }

        // Degenerate: zero radius → straight line
        if rxIn == 0 || ryIn == 0 {
            cgPath.addLine(to: p2)
            return
        }

        var rx = rxIn
        var ry = ryIn
        let cosPhi = cos(phi)
        let sinPhi = sin(phi)

        // Step 1: Compute (x1′, y1′)
        let dx = (p1.x - p2.x) / 2
        let dy = (p1.y - p2.y) / 2
        let x1p = cosPhi * dx + sinPhi * dy
        let y1p = -sinPhi * dx + cosPhi * dy

        // Ensure radii are large enough
        let x1p2 = x1p * x1p
        let y1p2 = y1p * y1p
        var rx2 = rx * rx
        var ry2 = ry * ry
        let lambda = x1p2 / rx2 + y1p2 / ry2
        if lambda > 1 {
            let s = sqrt(lambda)
            rx *= s; ry *= s
            rx2 = rx * rx; ry2 = ry * ry
        }

        // Step 2: Compute (cx′, cy′)
        let num = max(0, rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2)
        let den = rx2 * y1p2 + ry2 * x1p2
        var sq: CGFloat = den > 0 ? sqrt(num / den) : 0
        if largeArc == sweep { sq = -sq }
        let cxp = sq * rx * y1p / ry
        let cyp = -sq * ry * x1p / rx

        // Step 3: Compute center (cx, cy) from (cx′, cy′)
        let cx = cosPhi * cxp - sinPhi * cyp + (p1.x + p2.x) / 2
        let cy = sinPhi * cxp + cosPhi * cyp + (p1.y + p2.y) / 2

        // Step 4: Compute θ₁ and Δθ
        let theta1 = vectorAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry)
        var dtheta = vectorAngle(
            (x1p - cxp) / rx, (y1p - cyp) / ry,
            (-x1p - cxp) / rx, (-y1p - cyp) / ry
        )
        if !sweep && dtheta > 0 { dtheta -= 2 * .pi }
        if sweep && dtheta < 0 { dtheta += 2 * .pi }

        // Split into ≤90° segments and approximate each with cubic Bézier
        let segments = max(1, Int(ceil(abs(dtheta) / (.pi / 2))))
        let segAngle = dtheta / CGFloat(segments)

        for i in 0..<segments {
            let a1 = theta1 + CGFloat(i) * segAngle
            let a2 = a1 + segAngle
            let alpha = sin(segAngle)
                * (sqrt(4 + 3 * pow(tan(segAngle / 2), 2)) - 1) / 3

            let cosA1 = cos(a1), sinA1 = sin(a1)
            let cosA2 = cos(a2), sinA2 = sin(a2)

            // Tangent vectors at start/end of segment
            let dx1 = -rx * sinA1, dy1 = ry * cosA1
            let dx2 = -rx * sinA2, dy2 = ry * cosA2

            // Endpoint of this segment
            let ep2 = CGPoint(
                x: cosPhi * rx * cosA2 - sinPhi * ry * sinA2 + cx,
                y: sinPhi * rx * cosA2 + cosPhi * ry * sinA2 + cy
            )

            // Control points
            let cp1 = CGPoint(
                x: cosPhi * rx * cosA1 - sinPhi * ry * sinA1 + cx
                    + alpha * (cosPhi * dx1 - sinPhi * dy1),
                y: sinPhi * rx * cosA1 + cosPhi * ry * sinA1 + cy
                    + alpha * (sinPhi * dx1 + cosPhi * dy1)
            )
            let cp2 = CGPoint(
                x: ep2.x - alpha * (cosPhi * dx2 - sinPhi * dy2),
                y: ep2.y - alpha * (sinPhi * dx2 + cosPhi * dy2)
            )

            cgPath.addCurve(to: ep2, control1: cp1, control2: cp2)
        }
    }

    /// Signed angle between two 2D vectors.
    private func vectorAngle(
        _ ux: CGFloat, _ uy: CGFloat,
        _ vx: CGFloat, _ vy: CGFloat
    ) -> CGFloat {
        let dot = ux * vx + uy * vy
        let len = sqrt(ux * ux + uy * uy) * sqrt(vx * vx + vy * vy)
        guard len > 0 else { return 0 }
        var a = acos(max(-1, min(1, dot / len)))
        if ux * vy - uy * vx < 0 { a = -a }
        return a
    }

    private mutating func skipToNextCommand() {
        while index < tokens.count {
            if case .command = tokens[index] { return }
            index += 1
        }
    }
}
