import Foundation
import PathKit
import CLIKit
import XcodeProj

struct Project {
    let path: CLIKit.Path
    private let xcodeProject: XcodeProj
    private let sourcesPath: PathKit.Path

    init(path: CLIKit.Path) throws {
        self.path = path
        xcodeProject = try XcodeProj(pathString: path.string)
        sourcesPath = .init(path.deletingLastComponent.string)
    }
}

extension Project {

    var fileName: String {
        return path.lastComponent
    }

}

extension Project {
    
    func files() throws -> [WithTargets<PathKit.Path>] {
        return try filesAndTargets().map { WithTargets(value: $0.key, targets: $0.value) }
    }
    
    func filesAndTargets() throws -> [PathKit.Path : [String]] {
        let filesAndTargets = try xcodeProject.pbxproj.nativeTargets.flatMap { target -> [(String, PathKit.Path)] in
            let resources = try target.resourcesBuildPhase()?.files?.compactMap { try $0.file?.fullPath(sourceRoot: sourcesPath) } ?? []
            let sources = try target.sourceFiles().compactMap { try $0.fullPath(sourceRoot: sourcesPath) }
            let files = resources + sources
            return files.map { (target.name, $0) }
        }
        return Dictionary(grouping: filesAndTargets) { $0.1 }.mapValues { $0.map { $0.0 } }
    }

}

extension Project {

    private func save() throws {
        try xcodeProject.write(path: PathKit.Path(self.path.string))
    }

}

extension Project {
    
    enum FileKind {
        case source
        case resource
    }

    func writeFile(name: String, content: String, kind: FileKind = .source) throws {
        guard let data = content.data(using: .utf8) else { return }
        try writeFile(name: name, data: data)
    }

    func writeFile(name: String, data: Data, kind: FileKind = .source) throws {
        let file = try path(for: name, kind: kind)
        try data.write(to: file.url)
    }

    private func path(for file: String, kind: FileKind) throws -> PathKit.Path {
        return try files()
            .first { $0.value.lastComponent == file }?
            .value ?? create(file: file, kind: kind)
    }

    private func create(file: String, kind: FileKind) throws -> PathKit.Path {
        guard let group = try xcodeProject.pbxproj.rootProject()?.mainGroup,
            let mainPath = try group.fullPath(sourceRoot: sourcesPath) else { fatalError() }

        let path = mainPath + file
        try path.write("// here goes an autogenerated file")
        let file = try group.addFile(at: path, sourceRoot: sourcesPath)
        
        switch kind {
        case .source:
            try xcodeProject.pbxproj.sourcesBuildPhases.forEach { buildPhase in
                _ = try buildPhase.add(file: file)
            }
        case .resource:
            try xcodeProject.pbxproj.resourcesBuildPhases.forEach { buildPhase in
                _ = try buildPhase.add(file: file)
            }
        }
        try save()
        return path
    }

}

extension Project {

    func addDependencyIfNotThere(name: String,
                                 productName: String,
                                 repositoryURL: String,
                                 version: XCRemoteSwiftPackageReference.VersionRequirement) throws -> Bool {

        guard let project = try xcodeProject.pbxproj.rootProject() else { return false }
        guard !project.packages.contains(where: { $0.name == name }) else { return false }
        let packages = try project.targets.map { target in
            try project.addSwiftPackage(repositoryURL: repositoryURL,
                                        productName: productName,
                                        versionRequirement: version,
                                        targetName: target.name)
        }
        try save()
        _ = packages
        return true
    }

}

extension Project {

    func addBuildPhaseIfNotThrere(name: String,
                                  code: String) throws -> Bool {

        let targets = xcodeProject.pbxproj.nativeTargets.filter { !$0.buildPhases.contains { $0.name() == name } }
        guard !targets.isEmpty else { return false }
        let phase = PBXShellScriptBuildPhase(name: name, shellPath: "/bin/sh", shellScript: code)
        xcodeProject.pbxproj.add(object: phase)

        targets.forEach { target in
            guard let indexOfCompile = target.buildPhases.firstIndex(where: { $0.type() == .sources }) else { return }
            target.buildPhases.insert(phase, at: indexOfCompile)
        }

        try save()
        return true
    }

}

extension Project {
    
    struct Target {
        let name: String
        let configurationName: String
        let currentMacros: Set<String>
    }
    
    func changeMacros(byTarget body: (Target) -> Set<String>) throws {
        let targets = xcodeProject.pbxproj.nativeTargets
        var changes = false
        targets.forEach { target in
            guard let configs = target.buildConfigurationList?.buildConfigurations else { return }
            configs.forEach { config in
                let currentMacros = Set((config.buildSettings["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] as? String)?.split(separator: " ").map(String.init) ?? [])
                let targetInfo = Target(name: target.name, configurationName: config.name, currentMacros: currentMacros)
                let newMacros = body(targetInfo)

                guard newMacros != currentMacros else { return }
                changes = true
                let macros = newMacros.union(["$(inherited)"])
                config.buildSettings["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] = macros.joined(separator: " ")
            }
        }
        
        guard changes else { return }
        try save()
    }
    
}

extension Project {
    
    func addGraphaelloMacrosToEachTarget() throws {
        try changeMacros { target in
            let previous = target.currentMacros.filter { !$0.starts(with: "GRAPHAELLO_") }
            return Set(["GRAPHAELLO_\(target.name.snakeUpperCased)_TARGET"]).union(previous)
        }
    }

    func updateDependencyIfOutOfDate(
        name: String,
        version: XCRemoteSwiftPackageReference.VersionRequirement
    ) throws {
        guard let project = try xcodeProject.pbxproj.rootProject() else { return }
        guard let package = project.packages.first(where: { $0.name == name }) else { return }

        if package.versionRequirement != version {
            package.versionRequirement = version
            try save()
        }
    }
    
}
