import Foundation
import XcodeProj
import JSONUtilities

public typealias BreakpointActionExtensionID = XCBreakpointList.BreakpointProxy.BreakpointContent.BreakpointActionProxy.ActionExtensionID
public typealias BreakpointExtensionID = XCBreakpointList.BreakpointProxy.BreakpointExtensionID

public struct Breakpoint: Equatable {

    public enum BreakpointType: Equatable {

        public struct Exception: Equatable {

            public enum Scope: String, Equatable {
                case all = "0"
                case objectiveC = "1"
                case cpp = "2"
            }

            public enum StopOnStyle: String, Equatable {
                case `throw` = "0"
                case `catch` = "1"
            }

            public var scope: Scope
            public var stopOnStyle: StopOnStyle

            public init(scope: Breakpoint.BreakpointType.Exception.Scope = .objectiveC,
                        stopOnStyle: Breakpoint.BreakpointType.Exception.StopOnStyle = .throw) {
                self.scope = scope
                self.stopOnStyle = stopOnStyle
            }
        }
        case file(path: String, line: Int, column: Int?)
        case exception(Exception)
        case swiftError
        case openGLError
        case symbolic(symbol: String?, module: String?)
        case ideConstraintError
        case ideTestFailure
        case runtimeIssue
    }

    public enum Action: Equatable {

        public struct Log: Equatable {

            public enum ConveyanceType: String, Equatable {
                case console = "0"
                case speak = "1"
            }

            public var message: String?
            public var conveyanceType: ConveyanceType

            public init(message: String? = nil, conveyanceType: Breakpoint.Action.Log.ConveyanceType = .console) {
                self.message = message
                self.conveyanceType = conveyanceType
            }
        }

        public enum Sound: String, Equatable {
            case basso = "Basso"
            case blow = "Blow"
            case bottle = "Bottle"
            case frog = "Frog"
            case funk = "Funk"
            case glass = "Glass"
            case hero = "Hero"
            case morse = "Morse"
            case ping = "Ping"
            case pop = "Pop"
            case purr = "Purr"
            case sosumi = "Sosumi"
            case submarine = "Submarine"
            case tink = "Tink"
        }

        case debuggerCommand(String?)
        case log(Log)
        case shellCommand(path: String?, arguments: String?, waitUntilDone: Bool = false)
        case graphicsTrace
        case appleScript(String?)
        case sound(Sound)
    }

    public var type: BreakpointType
    public var enabled: Bool
    public var ignoreCount: Int
    public var continueAfterRunningActions: Bool
    public var condition: String?
    public var actions: [Breakpoint.Action]

    public init(type: BreakpointType,
                enabled: Bool = true,
                ignoreCount: Int = 0,
                continueAfterRunningActions: Bool = false,
                filePath: String? = nil,
                line: Int? = nil,
                condition: String? = nil,
                actions: [Breakpoint.Action] = []) {
        self.type = type
        self.enabled = enabled
        self.ignoreCount = ignoreCount
        self.continueAfterRunningActions = continueAfterRunningActions
        self.condition = condition
        self.actions = actions
    }
}

extension Breakpoint.BreakpointType.Exception.Scope {

    public init(string: String) throws {
        let string = string.lowercased()
        switch string {
        case "all":
            self = .all
        case "objective-c":
            self = .objectiveC
        case "c++":
            self = .cpp
        default:
            throw SpecParsingError.unknownBreakpointScope(string)
        }
    }
}

extension Breakpoint.BreakpointType.Exception.StopOnStyle {

    public init(string: String) throws {
        let string = string.lowercased()
        switch string {
        case "throw":
            self = .throw
        case "catch":
            self = .catch
        default:
            throw SpecParsingError.unknownBreakpointStopOnStyle(string)
        }
    }
}

extension Breakpoint.Action.Log.ConveyanceType {

    init(string: String) throws {
        let string = string.lowercased()
        switch string {
        case "console":
            self = .console
        case "speak":
            self = .speak
        default:
            throw SpecParsingError.unknownBreakpointActionConveyanceType(string)
        }
    }
}

extension Breakpoint.Action.Sound {

    init(name: String) throws {
        guard let sound = Self.init(rawValue: name) else {
            throw SpecParsingError.unknownBreakpointActionSoundName(name)
        }
        self = sound
    }
}

extension Breakpoint.Action: JSONObjectConvertible {

    public init(jsonDictionary: JSONDictionary) throws {
        let idString: String = try jsonDictionary.json(atKeyPath: "type")
        let id = try BreakpointActionExtensionID(string: idString)
        switch id {
        case .debuggerCommand:
            let command: String? = jsonDictionary.json(atKeyPath: "command")
            self = .debuggerCommand(command)
        case .log:
            let message: String? = jsonDictionary.json(atKeyPath: "message")
            let conveyanceType: Log.ConveyanceType
            if jsonDictionary["conveyanceType"] != nil {
                let conveyanceTypeString: String = try jsonDictionary.json(atKeyPath: "conveyanceType")
                conveyanceType = try .init(string: conveyanceTypeString)
            } else {
                conveyanceType = .console
            }
            self = .log(.init(message: message, conveyanceType: conveyanceType))
        case .shellCommand:
            let path: String? = jsonDictionary.json(atKeyPath: "path")
            let arguments: String? = jsonDictionary.json(atKeyPath: "arguments")
            let waitUntilDone = jsonDictionary.json(atKeyPath: "waitUntilDone") ?? false
            self = .shellCommand(path: path, arguments: arguments, waitUntilDone: waitUntilDone)
        case .graphicsTrace:
            self = .graphicsTrace
        case .appleScript:
            let script: String? = jsonDictionary.json(atKeyPath: "script")
            self = .appleScript(script)
        case .sound:
            let sound: Sound
            if jsonDictionary["sound"] != nil {
                let name: String = try jsonDictionary.json(atKeyPath: "sound")
                sound = try .init(name: name)
            } else {
                sound = .basso
            }
            self = .sound(sound)
        case .openGLError:
            throw SpecParsingError.unknownBreakpointActionType(idString)
        }
    }
}

extension Breakpoint: JSONObjectConvertible {

    public init(jsonDictionary: JSONDictionary) throws {
        let idString: String = try jsonDictionary.json(atKeyPath: "type")
        let id = try BreakpointExtensionID(string: idString)
        switch id {
        case .file:
            let path: String = try jsonDictionary.json(atKeyPath: "path")
            let line: Int = try jsonDictionary.json(atKeyPath: "line")
            let column: Int? = jsonDictionary.json(atKeyPath: "column")
            type = .file(path: path, line: line, column: column)
        case .exception:
            let scope: BreakpointType.Exception.Scope
            if jsonDictionary["scope"] != nil {
                let scopeString: String = try jsonDictionary.json(atKeyPath: "scope")
                scope = try .init(string: scopeString)
            } else {
                scope = .objectiveC
            }
            let stopOnStyle: BreakpointType.Exception.StopOnStyle
            if jsonDictionary["stopOnStyle"] != nil {
                let stopOnStyleString: String = try jsonDictionary.json(atKeyPath: "stopOnStyle")
                stopOnStyle = try .init(string: stopOnStyleString)
            } else {
                stopOnStyle = .throw
            }
            type = .exception(.init(scope: scope, stopOnStyle: stopOnStyle))
        case .swiftError:
            type = .swiftError
        case .openGLError:
            type = .openGLError
        case .symbolic:
            let symbol: String? = jsonDictionary.json(atKeyPath: "symbol")
            let module: String? = jsonDictionary.json(atKeyPath: "module")
            type = .symbolic(symbol: symbol, module: module)
        case .ideConstraintError:
            type = .ideConstraintError
        case .ideTestFailure:
            type = .ideTestFailure
        case .runtimeIssue:
            type = .runtimeIssue
        }
        enabled = jsonDictionary.json(atKeyPath: "enabled") ?? true
        ignoreCount = jsonDictionary.json(atKeyPath: "ignoreCount") ?? 0
        continueAfterRunningActions = jsonDictionary.json(atKeyPath: "continueAfterRunningActions") ?? false
        condition = jsonDictionary.json(atKeyPath: "condition")
        if jsonDictionary["actions"] != nil {
            actions = try jsonDictionary.json(atKeyPath: "actions", invalidItemBehaviour: .fail)
        } else {
            actions = []
        }
    }
}
