//
//  SvgVector.swift
//
//  Created by Damian Mehers on 04.04.21.
//

import SwiftUI

public struct SvgVectorView: View {
    
    private let commands: [Command]
    
    // From https://www.w3.org/TR/SVG/paths.html#PathData - I've kept the argument names from that document, except using dx/dy for relative arguments
    fileprivate enum Command {
        case moveAbsolute(xy: CGPoint)
        case moveRelative(dx: CGFloat, dy: CGFloat)
        case closePath
        case lineToAbsolute(xy: CGPoint)
        case lineToRelative(dx: CGFloat, dy: CGFloat)
        case horizontalLineToAbsolute(x: CGFloat)
        case horizontalLineToRelative(dx: CGFloat)
        case verticalLineToAbsolute(y: CGFloat)
        case verticalLineToRelative(dy: CGFloat)
        case curveToAbsolute(xy1: CGPoint, xy2: CGPoint, xy: CGPoint)
        case curveToRelative(dx1: CGFloat, dy1: CGFloat, dx2: CGFloat, dy2: CGFloat, dx: CGFloat, dy: CGFloat)
        case smoothCurveToAbsolute(xy2: CGPoint, xy: CGPoint)
        case smoothCurveToRelative(dx2: CGFloat, dy2: CGFloat, dx: CGFloat, dy: CGFloat)
        case quadraticBezierCurveToAbsolute(xy1: CGPoint, xy: CGPoint)
        case quadraticBezierCurveToRelative(dx1: CGFloat, dy1: CGFloat, dx: CGFloat, dy: CGFloat)
        case smoothQuadraticBezierCurveToAbsolute(xy: CGPoint)
        case smoothQuadraticBezierCurveToRelative(dx: CGFloat, dy: CGFloat)
        case elllipticalArcAbsolute(rx: CGFloat, ry: CGFloat, xAxisRotation: CGFloat, largeArcFlag: Bool, sweepFlag: Bool, xy: CGPoint)
        case elllipticalArcRelative(rx: CGFloat, ry: CGFloat, xAxisRotation: CGFloat, largeArcFlag: Bool, sweepFlag: Bool, dx: CGFloat, dy: CGFloat)
        case invalid(command: String, expected: Int, actual: Int)
    }
    
    public init(pathData: String) {
        var parser = PathDataParser(pathData: pathData)
        commands = parser.parse()
    }
    
    
    public var body: some View {
        Path { path in
            
            // Some commands needs this parameter from previous commands
            var secondControlPointOfPreviousCommand: CGPoint? = nil
            
            
            // Not all commands are handled, and some are probably not implemented properly - I've just done the ones
            // I need for now. Pull requests welcome.
            for command in commands {
                
                switch command {
                
                case .moveAbsolute(xy: let xy):
                    path.move(to: xy)
                case .moveRelative(dx: let x, dy: let y):
                    if let currentPoint = path.currentPoint {
                        path.move(to: CGPoint(x: currentPoint.x + x, y: currentPoint.y + y))
                    } else {
                        expectedCurrentPoint(command: command)
                    }
                case .closePath:
                    path.closeSubpath()
                case .lineToAbsolute(xy: let xy):
                    path.addLine(to: xy)
                case .lineToRelative(dx: let dx, dy: let dy):
                    if let currentPoint = path.currentPoint {
                        path.addLine(to: CGPoint(x: currentPoint.x + dx, y: currentPoint.y + dy))
                    } else {
                        expectedCurrentPoint(command: command)
                    }
                case .horizontalLineToAbsolute(x: let x):
                    if let currentPoint = path.currentPoint {
                        path.addLine(to: CGPoint(x: x, y: currentPoint.y))
                    } else {
                        expectedCurrentPoint(command: command)
                    }
                case .horizontalLineToRelative(dx: let dx):
                    if let currentPoint = path.currentPoint {
                        path.addLine(to: CGPoint(x: currentPoint.x + dx, y: currentPoint.y))
                    } else {
                        expectedCurrentPoint(command: command)
                    }
                case .verticalLineToAbsolute(y: let y):
                    if let currentPoint = path.currentPoint {
                        path.addLine(to: CGPoint(x: currentPoint.x, y: y))
                    } else {
                        expectedCurrentPoint(command: command)
                    }
                case .verticalLineToRelative(dy: let dy):
                    if let currentPoint = path.currentPoint {
                        path.addLine(to: CGPoint(x: currentPoint.x, y: dy + currentPoint.y))
                    } else {
                        expectedCurrentPoint(command: command)
                    }
                case .curveToAbsolute(xy1: let xy1, xy2: let xy2, xy: let xy):
                    secondControlPointOfPreviousCommand = xy2
                    path.addCurve(to: xy, control1: xy1, control2: xy2)
                case .curveToRelative(dx1: let dx1, dy1: let dy1, dx2: let dx2, dy2: let dy2, dx: let dx, dy: let dy):
                    if let currentPoint = path.currentPoint {
                        secondControlPointOfPreviousCommand = CGPoint(x: currentPoint.x + dx2, y: currentPoint.y + dy2)
                        path.addCurve(to: CGPoint(x: currentPoint.x + dx, y: currentPoint.y + dy), control1: CGPoint(x: currentPoint.x + dx1, y: currentPoint.y + dy1), control2: CGPoint(x: currentPoint.x + dx2, y: currentPoint.y + dy2))
                    } else {
                        expectedCurrentPoint(command: command)
                    }
                case .smoothCurveToAbsolute(xy2: let xy2, xy: let xy):
                    
                    // https://stackoverflow.com/questions/5287559/calculating-control-points-for-a-shorthand-smooth-svg-path-bezier-curve
                    if let secondControlPointOfPreviousCommand = secondControlPointOfPreviousCommand,
                       let currentPoint = path.currentPoint{
                        let x1 = 2 * currentPoint.x - secondControlPointOfPreviousCommand.x
                        let y1 = 2 * currentPoint.y - secondControlPointOfPreviousCommand.y
                        path.addCurve(to: xy, control1: CGPoint(x: x1, y: y1), control2: xy2)
                    } else {
                        expectedCurrentPoint(command: command)
                    }
                    secondControlPointOfPreviousCommand = xy2
                    
                case .smoothCurveToRelative(dx2: _, dy2:  _, dx:  _, dy:  _):
                    unhandled(command: command)
                case .quadraticBezierCurveToAbsolute(xy1: let xy1, xy: let xy):
                    path.addQuadCurve(to: xy, control: xy1)
                case .quadraticBezierCurveToRelative(dx1: let dx1, dy1: let dy1, dx: let dx, dy: let dy):
                    if let currentPoint = path.currentPoint {
                        path.addQuadCurve(to: CGPoint(x: currentPoint.x + dx, y: currentPoint.y + dy), control: CGPoint(x: currentPoint.x + dx1, y: currentPoint.y + dy1))
                    } else {
                        expectedCurrentPoint(command: command)
                    }
                case .smoothQuadraticBezierCurveToAbsolute(xy: let xy):
                    if let secondControlPointOfPreviousCommand = secondControlPointOfPreviousCommand,
                       let currentPoint = path.currentPoint {
                        let x1 = 2 * currentPoint.x - secondControlPointOfPreviousCommand.x
                        let y1 = 2 * currentPoint.y - secondControlPointOfPreviousCommand.y
                        path.addQuadCurve(to: xy, control: CGPoint(x: x1, y: y1))
                    } else {
                        expectedCurrentPoint(command: command)
                    }
                case .smoothQuadraticBezierCurveToRelative(dx: _, dy: _):
                    unhandled(command: command)
                case .elllipticalArcAbsolute(rx: _, ry: _, xAxisRotation: _, largeArcFlag: _, sweepFlag: _, xy: _):
                    unhandled(command: command)
                case .elllipticalArcRelative(rx: _, ry: _, xAxisRotation: _, largeArcFlag: _, sweepFlag: _, dx: _, dy: _):
                    unhandled(command: command)
                case .invalid(command: _, expected: _, actual: _):
                    unhandled(command: command)
                }
            }
            
        }
    }
    
    private func expectedCurrentPoint(command: Command) {
        print("No current point for \(command)")
    }
    
    private func unhandled(command: Command) {
        print("Don't know how to handle \(command)")
    }
    
    // How many arguments does each command need
    private static let separatorArgmentCounts = [
        "M": 2,
        "m": 2,
        "Z": 0,
        "z": 0,
        "L": 2,
        "l": 2,
        "H": 1,
        "h": 1,
        "V": 1,
        "v": 1,
        "C": 6,
        "c": 6,
        "S": 4,
        "s": 4,
        "Q": 4,
        "q": 4,
        "T": 2,
        "t": 2,
        "A": 7,
        "a": 7,
    ]
    
    
    
    fileprivate struct PathDataParser {
        let pathData: String
        let numberFormatter = NumberFormatter()
        var commands = [Command]()
        
        var arguments = [CGFloat]()
        var currentArgment = ""
        
        var currentCommand: String = ""
        
        mutating func parse() -> [Command] {
            for ch in pathData {
                if SvgVectorView.separatorArgmentCounts.keys.contains(String(ch)) {
                    addCommand(ch: String(ch))
                } else {
                    currentCommand += String(ch)
                    if ch == "," || ch == " " {
                        addCurrentArgument()
                        currentArgment = ""
                    } else if ch == "-" {
                        addCurrentArgument()
                        currentArgment = "-"
                    } else if ch == "." && currentArgment.contains(".") { // a new arg can just start by introducing a new period 0.25.456
                        addCurrentArgument()
                        currentArgment = "."
                    } else {
                        currentArgment.append(ch)
                    }
                }
            }
            return commands
            
        }
        
        mutating func addCurrentArgument() {
            guard !currentArgment.isEmpty else { return }

            let currentArgment = self.currentArgment // save it because addCommmand wipes it out
            
            
            // Multiple commands can occur by just adding more arguments: L124 456 789 101 // Two L commands: L124 456 and L789 101
            let ch = String(currentCommand.first!)
            if let expectedArgumentCount = SvgVectorView.separatorArgmentCounts[ch],
               arguments.count == expectedArgumentCount {
                self.currentArgment = ""
                addCommand(ch: ch)
            }
            
            if let n = numberFormatter.number(from: currentArgment) {
                arguments.append(CGFloat(truncating: n))
            } else {
                print("Can't parse number \(currentArgment)")
            }
            
        }
        
        mutating func addCommand(ch: String) {
            if !currentCommand.trimmingCharacters(in: .whitespaces).isEmpty {
                if !currentArgment.trimmingCharacters(in: .whitespaces).isEmpty {
                    if let n = numberFormatter.number(from: currentArgment) {
                        arguments.append(CGFloat(truncating: n))
                    } else {
                        print("Can't parse number \(currentCommand)")
                    }
                }
                
                let ch = String(currentCommand.first!)
                if let argumentCount = SvgVectorView.separatorArgmentCounts[ch],
                   argumentCount == arguments.count {
                    commands.append(generateCommand(ch: ch, args: arguments))
                } else {
                    print("Bad arguments: \(currentCommand)")
                }
            }
            currentCommand = String(ch)
            currentArgment = ""
            arguments = [CGFloat]()
            
        }
        
        func generateCommand(ch: String, args: [CGFloat]) -> Command {
            guard let expectedArgumentCount = SvgVectorView.separatorArgmentCounts[ch] else {
                print("Unknown separator: \(ch)")
                return .invalid(command: ch, expected: 0, actual: args.count)
            }
            
            guard expectedArgumentCount == args.count else {
                return .invalid(command: ch, expected: expectedArgumentCount, actual: args.count)
            }
            
            switch ch {
            case "M":
                return .moveAbsolute(xy: CGPoint(x: args[0], y: args[1]))
            case "m":
                return .moveRelative(dx: args[0], dy: args[1])
            case "Z":
                return .closePath
            case "z":
                return .closePath
            case "L":
                return .lineToAbsolute(xy: CGPoint(x: args[0], y: args[1]))
            case "l":
                return .lineToRelative(dx: args[0], dy: args[1])
            case "H":
                return .horizontalLineToAbsolute(x: args[0])
            case "h":
                return .horizontalLineToRelative(dx: args[0])
            case "V":
                return .verticalLineToAbsolute(y: args[0])
            case "v":
                return .verticalLineToRelative(dy: args[0])
            case "C":
                return .curveToAbsolute(xy1: CGPoint(x: args[0], y: args[1]), xy2: CGPoint(x: args[2], y: args[3]), xy: CGPoint(x: args[4], y: args[5]))
            case "c":
                return .curveToRelative(dx1: args[0], dy1: args[1], dx2: args[2], dy2: args[3], dx: args[4], dy: args[5])
            case "S":
                return .smoothCurveToAbsolute(xy2: CGPoint(x: args[0], y: args[1]), xy: CGPoint( x: args[2], y: args[3]))
            case "s":
                return .smoothCurveToRelative(dx2: args[0], dy2: args[1], dx: args[2], dy: args[3])
            case "Q":
                return .quadraticBezierCurveToAbsolute(xy1: CGPoint(x: args[0], y: args[1]), xy: CGPoint( x: args[2], y: args[3]))
            case "q":
                return .quadraticBezierCurveToRelative(dx1: args[0], dy1: args[1], dx: args[2], dy: args[3])
            case "T":
                return .smoothQuadraticBezierCurveToAbsolute(xy: CGPoint(x: args[0], y: args[1]))
            case "t":
                return .smoothQuadraticBezierCurveToRelative(dx: args[0], dy: args[1])
            case "A":
                return .elllipticalArcAbsolute(rx: args[0], ry: args[1], xAxisRotation: args[2], largeArcFlag: args[3] != 0, sweepFlag: args[4] != 0, xy: CGPoint(x: args[5], y: args[6]))
            case "a":
                return .elllipticalArcRelative(rx: args[0], ry: args[1], xAxisRotation: args[2], largeArcFlag: args[3] != 0, sweepFlag: args[4] != 0, dx: args[5], dy: args[6])
            default:
                return .invalid(command: ch, expected: 0, actual: args.count)
            }
        }
        
        func checkArguments(_ ch: String, _ args: [CGFloat], expected: Int) -> Command? {
            guard args.count == expected else {
                return .invalid(command: ch, expected: expected, actual: args.count)
            }
            return nil
        }
        
        
    }
}

struct SvgVector_Previews: PreviewProvider {
    static var previews: some View {
        SvgVectorView(pathData:  square)
            .frame(width: 100, height: 100)
        SvgVectorView(pathData:  circle)
            .frame(width: 100, height: 100)
        SvgVectorView(pathData:  key)
            .scaleEffect(CGSize(width: 0.25, height: 0.25))
            .frame(width: 200, height: 200)
        
    }
    static let circle = "M48,24c0,13.255-10.745,24-24,24S0,37.255,0,24S10.745,0,24,0S48,10.745,48,24z"
    static let square = "M32,12H16c-2.2,0-4,1.8-4,4v16c0,2.2,1.8,4,4,4h16c2.2,0,4-1.8,4-4V16C36,13.8,34.2,12,32,12z M34,32c0,1.103-0.897,2-2,2H16c-1.103,0-2-0.897-2-2V16c0-1.103,0.897-2,2-2h16c1.103,0,2,0.897,2,2V32z"
    static let key = """
M356.5 16.375l-174.906 255.22 1.53 1.06 31.97 22.314 175.062-255.5L356.5 16.374zm90.063 62.22c-20.16 29.418-44.122 23.1-68.25 8.905l-48.688 72.875c21.278 16.55 36.46 35.645 18.594 61.72l42.967 29.468 28.907-42.157-14.72-9.156c-3.167 1.844-6.85 2.906-10.78 2.906-11.85 0-21.47-9.62-21.47-21.47 0-11.847 9.62-21.436 21.47-21.436s21.437 9.59 21.437 21.438c0 .195-.025.4-.03.593l15.906 9.907 17.938-26.218-37.688-23.5 11.03-17.72 14.94 9.313 10.093-16.188 24.25 15.094 17.092-24.94-43-29.436zM141.22 268.624c-.31.01-.628.023-.94.063-.827.104-1.652.284-2.53.562-3.51 1.11-7.4 4.066-10.125 7.938-2.724 3.87-4.16 8.487-4 12.125.16 3.637 1.257 6.338 5.25 9.125l76.594 53.468c3.283 2.293 5.727 2.35 9.124 1.156 3.396-1.192 7.323-4.26 10.125-8.218 2.8-3.96 4.352-8.66 4.31-12.188-.04-3.53-.89-5.787-4.374-8.22L148.03 270.97c-2.546-1.78-4.657-2.42-6.81-2.345zM84.28 312.78c-24.354.41-45.504 9.52-57.655 27.25-16.95 24.737-11.868 59.753 9.625 90.283-1.838 4.72-2.875 9.84-2.875 15.187 0 23.243 19.07 42.313 42.313 42.313 8.635 0 16.692-2.625 23.406-7.125 43.208 18.488 88.07 12.714 108.28-16.782 18.695-27.28 10.884-66.912-16.374-99.312l-63.094-44.03c-14.016-5.107-28.07-7.7-41.25-7.783-.792-.004-1.59-.012-2.375 0zm-8.593 109.126c13.143 0 23.594 10.45 23.594 23.594 0 13.143-10.45 23.625-23.593 23.625-13.142 0-23.624-10.482-23.624-23.625s10.482-23.594 23.624-23.594z
"""
}