import Foundation
import JSONUtilities
import PathKit
import Yams

public struct Project: BuildSettingsContainer {

    public var basePath: Path
    public var name: String
    public var targets: [Target] {
        didSet {
            targetsMap = Dictionary(uniqueKeysWithValues: targets.map { ($0.name, $0) })
        }
    }

    public var aggregateTargets: [AggregateTarget] {
        didSet {
            aggregateTargetsMap = Dictionary(uniqueKeysWithValues: aggregateTargets.map { ($0.name, $0) })
        }
    }

    public var packages: [String: SwiftPackage]

    public var settings: Settings
    public var settingGroups: [String: Settings]
    public var configs: [Config]
    public var schemes: [Scheme]
    public var breakpoints: [Breakpoint]
    public var options: SpecOptions
    public var attributes: [String: Any]
    public var fileGroups: [String]
    public var configFiles: [String: String]
    public var include: [String] = []
    public var projectReferences: [ProjectReference] = [] {
        didSet {
            projectReferencesMap = Dictionary(uniqueKeysWithValues: projectReferences.map { ($0.name, $0) })
        }
    }

    private var targetsMap: [String: Target]
    private var aggregateTargetsMap: [String: AggregateTarget]
    private var projectReferencesMap: [String: ProjectReference]

    public init(
        basePath: Path = "",
        name: String,
        configs: [Config] = Config.defaultConfigs,
        targets: [Target] = [],
        aggregateTargets: [AggregateTarget] = [],
        settings: Settings = .empty,
        settingGroups: [String: Settings] = [:],
        schemes: [Scheme] = [],
        breakpoints: [Breakpoint] = [],
        packages: [String: SwiftPackage] = [:],
        options: SpecOptions = SpecOptions(),
        fileGroups: [String] = [],
        configFiles: [String: String] = [:],
        attributes: [String: Any] = [:],
        projectReferences: [ProjectReference] = []
    ) {
        self.basePath = basePath
        self.name = name
        self.targets = targets
        targetsMap = Dictionary(uniqueKeysWithValues: self.targets.map { ($0.name, $0) })
        self.aggregateTargets = aggregateTargets
        aggregateTargetsMap = Dictionary(uniqueKeysWithValues: self.aggregateTargets.map { ($0.name, $0) })
        self.configs = configs
        self.settings = settings
        self.settingGroups = settingGroups
        self.schemes = schemes
        self.breakpoints = breakpoints
        self.packages = packages
        self.options = options
        self.fileGroups = fileGroups
        self.configFiles = configFiles
        self.attributes = attributes
        self.projectReferences = projectReferences
        projectReferencesMap = Dictionary(uniqueKeysWithValues: self.projectReferences.map { ($0.name, $0) })
    }

    public func getProjectReference(_ projectName: String) -> ProjectReference? {
        projectReferencesMap[projectName]
    }

    public func getTarget(_ targetName: String) -> Target? {
        targetsMap[targetName]
    }

    public func getPackage(_ packageName: String) -> SwiftPackage? {
        packages[packageName]
    }

    public func getAggregateTarget(_ targetName: String) -> AggregateTarget? {
        aggregateTargetsMap[targetName]
    }

    public func getProjectTarget(_ targetName: String) -> ProjectTarget? {
        targetsMap[targetName] ?? aggregateTargetsMap[targetName]
    }

    public func getConfig(_ configName: String) -> Config? {
        configs.first { $0.name == configName }
    }

    public var defaultProjectPath: Path {
        basePath + "\(name).xcodeproj"
    }
}

extension Project: CustomDebugStringConvertible {

    public var debugDescription: String {
        var string = "Name: \(name)"
        let indent = "  "
        if !include.isEmpty {
            string += "\nInclude:\n\(indent)" + include.map { $0.description }.joined(separator: "\n\(indent)")
        }

        if !settingGroups.isEmpty {
            string += "\nSetting Groups:\n\(indent)" + settingGroups.keys
                .sorted()
                .joined(separator: "\n\(indent)")
        }

        if !targets.isEmpty {
            string += "\nTargets:\n\(indent)" + targets.map { $0.description }.joined(separator: "\n\(indent)")
        }
        if !aggregateTargets.isEmpty {
            string += "\nAggregate Targets:\n\(indent)" + aggregateTargets.map { $0.description }.joined(separator: "\n\(indent)")
        }
        if !schemes.isEmpty {
            let allSchemes = targets.filter { $0.scheme != nil }.map { $0.name } + schemes.map { $0.name }
            string += "\nSchemes:\n\(indent)" + allSchemes.joined(separator: "\n\(indent)")
        }

        return string
    }
}

extension Project: Equatable {

    public static func == (lhs: Project, rhs: Project) -> Bool {
        lhs.name == rhs.name &&
            lhs.targets == rhs.targets &&
            lhs.aggregateTargets == rhs.aggregateTargets &&
            lhs.settings == rhs.settings &&
            lhs.settingGroups == rhs.settingGroups &&
            lhs.configs == rhs.configs &&
            lhs.schemes == rhs.schemes &&
            lhs.breakpoints == rhs.breakpoints &&
            lhs.fileGroups == rhs.fileGroups &&
            lhs.configFiles == rhs.configFiles &&
            lhs.options == rhs.options &&
            lhs.packages == rhs.packages &&
            NSDictionary(dictionary: lhs.attributes).isEqual(to: rhs.attributes)
    }
}

extension Project {

    public init(path: Path) throws {
        let spec = try SpecFile(path: path)
        try self.init(spec: spec)
    }

    public init(spec: SpecFile) throws {
        try self.init(
            basePath: spec.basePath,
            jsonDictionary: spec.resolvedDictionary(),
            targetDeclarationOrder: spec.resolvedTargetDeclarationOrder()
        )
    }

    public init(basePath: Path = "", jsonDictionary: JSONDictionary, targetDeclarationOrder: [String] = []) throws {
        self.basePath = basePath

        var jsonDictionary = jsonDictionary
        if var rawTargets = jsonDictionary["targets"] as? [String: JSONDictionary] {
            for (index, key) in targetDeclarationOrder.enumerated() where rawTargets[key] != nil {
                rawTargets[key]?[Target.declarationIndexKey] = index
            }
            jsonDictionary["targets"] = rawTargets
        }

        jsonDictionary = Project.resolveProject(jsonDictionary: jsonDictionary)
        let buildSettingsParser = BuildSettingsParser(jsonDictionary: jsonDictionary)

        name = try jsonDictionary.json(atKeyPath: "name")

        settings = try buildSettingsParser.parse()
        settingGroups = try buildSettingsParser.parseSettingGroups()

        let configs: [String: String] = jsonDictionary.json(atKeyPath: "configs") ?? [:]
        self.configs = configs.isEmpty ? Config.defaultConfigs :
            configs.map { Config(name: $0, type: ConfigType(rawValue: $1)) }.sorted { $0.name < $1.name }
        let parsedTargets: [Target] = try jsonDictionary.json(atKeyPath: "targets", parallel: true)
        let resolvedTargets = jsonDictionary["targets"] as? [String: JSONDictionary] ?? [:]
        var targetOrder: [String: Int] = [:]
        for (key, target) in resolvedTargets {
            let name = target["name"] as? String ?? key
            targetOrder[name] = (target[Target.declarationIndexKey] as? Int) ?? .max
        }
        targets = parsedTargets.sorted { (targetOrder[$0.name] ?? .max, $0.name) < (targetOrder[$1.name] ?? .max, $1.name) }
        aggregateTargets = try jsonDictionary.json(atKeyPath: "aggregateTargets").sorted { $0.name < $1.name }
        projectReferences = try jsonDictionary.json(atKeyPath: "projectReferences").sorted { $0.name < $1.name }
        schemes = try jsonDictionary.json(atKeyPath: "schemes")
        if jsonDictionary["breakpoints"] != nil {
            breakpoints = try jsonDictionary.json(atKeyPath: "breakpoints", invalidItemBehaviour: .fail)
        } else {
            breakpoints = []
        }
        fileGroups = jsonDictionary.json(atKeyPath: "fileGroups") ?? []
        configFiles = jsonDictionary.json(atKeyPath: "configFiles") ?? [:]
        attributes = jsonDictionary.json(atKeyPath: "attributes") ?? [:]
        include = jsonDictionary.json(atKeyPath: "include") ?? []
        if jsonDictionary["packages"] != nil {
            packages = try jsonDictionary.json(atKeyPath: "packages", invalidItemBehaviour: .fail)
        } else {
            packages = [:]
        }
        // For backward compatibility of old `localPackages:` format
        if let localPackages: [String] = jsonDictionary.json(atKeyPath: "localPackages") {
            packages.merge(localPackages.reduce(into: [String: SwiftPackage]()) {
                // Project name will be obtained by resolved abstractpath's lastComponent for dealing with some path case, like "../"
                let packageName = (basePath + Path($1).normalize()).lastComponent
                $0[packageName] = .local(path: $1, group: nil, excludeFromProject: false, traits: nil)
            }
            )
        }
        if jsonDictionary["options"] != nil {
            options = try jsonDictionary.json(atKeyPath: "options")
        } else {
            options = SpecOptions()
        }
        targetsMap = Dictionary(uniqueKeysWithValues: targets.map { ($0.name, $0) })
        aggregateTargetsMap = Dictionary(uniqueKeysWithValues: aggregateTargets.map { ($0.name, $0) })
        projectReferencesMap = Dictionary(uniqueKeysWithValues: projectReferences.map { ($0.name, $0) })
    }

    static func resolveProject(jsonDictionary: JSONDictionary) -> JSONDictionary {
        var jsonDictionary = jsonDictionary

        // resolve multiple times so that we support both multi-platform templates,
        // as well as platform specific templates in multi-platform targets
        jsonDictionary = Target.resolveMultiplatformTargets(jsonDictionary: jsonDictionary)
        jsonDictionary = Target.resolveTargetTemplates(jsonDictionary: jsonDictionary)
        jsonDictionary = Scheme.resolveSchemeTemplates(jsonDictionary: jsonDictionary)
        jsonDictionary = Target.resolveMultiplatformTargets(jsonDictionary: jsonDictionary)

        return jsonDictionary
    }
}

extension Project: PathContainer {

    static var pathProperties: [PathProperty] {
        [
            .string("configFiles"),
            .object("options", SpecOptions.pathProperties),
            .object("targets", Target.pathProperties),
            .object("targetTemplates", Target.pathProperties),
            .object("aggregateTargets", AggregateTarget.pathProperties),
            .object("schemes", Scheme.pathProperties),
            .object("projectReferences", ProjectReference.pathProperties),
            .object("packages", SwiftPackage.pathProperties),
            .string("localPackages"),
            .string("fileGroups")
        ]
    }
}

extension Project {

    public var allTrackedFiles: [Path] {
        var files: [Path] = []
        files.append(contentsOf: configFilePaths)
        for fileGroup in fileGroups {
            let fileGroupPath = basePath + fileGroup
            let fileGroupChildren = (try? fileGroupPath.recursiveChildren()) ?? []
            files.append(contentsOf: fileGroupChildren)
            files.append(fileGroupPath)
        }

        for target in aggregateTargets {
            files.append(contentsOf: target.configFilePaths)
        }

        for target in targets {
            files.append(contentsOf: target.configFilePaths)
            for source in target.sources {
                let sourcePath = basePath + source.path
                
                let type = source.type ?? options.defaultSourceDirectoryType ?? .group
                if type.projectTracksChildren {
                    let sourceChildren = (try? sourcePath.recursiveChildren()) ?? []
                    files.append(contentsOf: sourceChildren)
                }
                files.append(sourcePath)
            }
        }
        return files
    }
}

extension SourceType {
    
    var projectTracksChildren: Bool {
        switch self {
        case .file: false
        case .folder: false
        case .group: true
        case .syncedFolder: false
        }
    }
}

extension BuildSettingsContainer {

    fileprivate var configFilePaths: [Path] {
        configFiles.values.map { Path($0) }
    }
}

extension Project: JSONEncodable {
    public func toJSONValue() -> Any {
        toJSONDictionary()
    }

    public func toJSONDictionary() -> JSONDictionary {
        let targetPairs = targets.map { ($0.name, $0.toJSONValue()) }
        let configsPairs = configs.map { ($0.name, $0.type?.rawValue) }
        let aggregateTargetsPairs = aggregateTargets.map { ($0.name, $0.toJSONValue()) }
        let schemesPairs = schemes.map { ($0.name, $0.toJSONValue()) }
        let projectReferencesPairs = projectReferences.map { ($0.name, $0.toJSONValue()) }

        var dictionary: JSONDictionary = [:]
        dictionary["name"] = name
        dictionary["options"] = options.toJSONValue()
        dictionary["settings"] = settings.toJSONValue()
        dictionary["fileGroups"] = fileGroups
        dictionary["configFiles"] = configFiles
        dictionary["include"] = include
        dictionary["attributes"] = attributes
        dictionary["packages"] = packages.mapValues { $0.toJSONValue() }
        dictionary["targets"] = Dictionary(uniqueKeysWithValues: targetPairs)
        dictionary["configs"] = Dictionary(uniqueKeysWithValues: configsPairs)
        dictionary["aggregateTargets"] = Dictionary(uniqueKeysWithValues: aggregateTargetsPairs)
        dictionary["schemes"] = Dictionary(uniqueKeysWithValues: schemesPairs)
        dictionary["settingGroups"] = settingGroups.mapValues { $0.toJSONValue() }
        dictionary["projectReferences"] = Dictionary(uniqueKeysWithValues: projectReferencesPairs)

        return dictionary
    }
}
