import Foundation
import PackagePlugin

@main
struct SwiftLintBuildToolPlugin: BuildToolPlugin {
    func createBuildCommands(
        context: PluginContext,
        target: Target
    ) throws -> [Command] {
        try makeCommand(executable: context.tool(named: "swiftlint"),
                        swiftFiles: (target as? SourceModuleTarget).flatMap(swiftFiles) ?? [],
                        environment: environment(context: context, target: target),
                        pluginWorkDirectory: context.pluginWorkDirectory)
    }

    /// Collects the paths of the Swift files to be linted.
    private func swiftFiles(target: SourceModuleTarget) -> [Path] {
        target
            .sourceFiles(withSuffix: "swift")
            .map(\.path)
    }

    /// Creates an environment dictionary containing a value for the `BUILD_WORKSPACE_DIRECTORY` key.
    ///
    /// This method locates the topmost `.swiftlint.yml` config file within the package directory for this target
    /// and sets the config file's containing directory as the `BUILD_WORKSPACE_DIRECTORY` value. The package
    /// directory is used if a config file is not found.
    ///
    /// The `BUILD_WORKSPACE_DIRECTORY` environment variable controls SwiftLint's working directory.
    ///
    /// Reference: [https://github.com/realm/SwiftLint/blob/0.50.3/Source/swiftlint/Commands/SwiftLint.swift#L7](
    /// https://github.com/realm/SwiftLint/blob/0.50.3/Source/swiftlint/Commands/SwiftLint.swift#L7
    /// )
    private func environment(
        context: PluginContext,
        target: Target
    ) throws -> [String: String] {
        let workingDirectory: Path = try target.directory.resolveWorkingDirectory(in: context.package.directory)
        return addedToEnvironment(["BUILD_WORKSPACE_DIRECTORY": "\(workingDirectory)"])
    }

    private func makeCommand(
        executable: PluginContext.Tool,
        swiftFiles: [Path],
        environment: [String: String],
        pluginWorkDirectory path: Path
    ) throws -> [Command] {
        // Don't lint anything if there are no Swift source files in this target
        guard !swiftFiles.isEmpty else {
            return []
        }
        // Outputs the environment to the build log for reference.
        #if DEBUG
        print("Environment:", environment)
        #endif
        let arguments: [String] = [
            "lint",
            "--quiet",
            // We always pass all of the Swift source files in the target to the tool,
            // so we need to ensure that any exclusion rules in the configuration are
            // respected.
            "--force-exclude",
        ]
        // Determine whether we need to enable cache or not (for Xcode Cloud we don't)
        let cacheArguments: [String]
        if ProcessInfo.processInfo.environment["CI_XCODE_CLOUD"] == "TRUE" {
            cacheArguments = ["--no-cache"]
        } else {
            let cachePath: Path = path.appending("Cache")
            try FileManager.default.createDirectory(atPath: cachePath.string, withIntermediateDirectories: true)
            cacheArguments = ["--cache-path", "\(cachePath)"]
        }
        let outputPath: Path = path.appending("Output")
        try FileManager.default.createDirectory(atPath: outputPath.string, withIntermediateDirectories: true)
        return [
            .prebuildCommand(
                displayName: "SwiftLint",
                executable: executable.path,
                arguments: arguments + cacheArguments + swiftFiles.map(\.string),
                environment: environment,
                outputFilesDirectory: outputPath),
        ]
    }
}

private func addedToEnvironment(_ new: [String: String]) -> [String: String] {
    ProcessInfo.processInfo.environment.merging(new) { _, new in new }
}

#if canImport(XcodeProjectPlugin)

import XcodeProjectPlugin

// swiftlint:disable:next no_grouping_extension
extension SwiftLintBuildToolPlugin: XcodeBuildToolPlugin {
    func createBuildCommands(
        context: XcodePluginContext,
        target: XcodeTarget
    ) throws -> [Command] {
        try makeCommand(executable: context.tool(named: "swiftlint"),
                        swiftFiles: swiftFiles(target: target),
                        environment: environment(context: context, target: target),
                        pluginWorkDirectory: context.pluginWorkDirectory)
    }

    /// Collects the paths of the Swift files to be linted.
    private func swiftFiles(target: XcodeTarget) -> [Path] {
        target
            .inputFiles
            .filter { $0.type == .source && $0.path.extension == "swift" }
            .map(\.path)
    }

    /// Creates an environment dictionary containing a value for the `BUILD_WORKSPACE_DIRECTORY` key.
    ///
    /// This method locates the topmost `.swiftlint.yml` config file within the project directory for this target's
    /// Swift source files and sets the config file's containing directory as the `BUILD_WORKSPACE_DIRECTORY` value.
    /// The project directory is used if a config file is not found.
    ///
    /// The `BUILD_WORKSPACE_DIRECTORY` environment variable controls SwiftLint's working directory.
    ///
    /// Reference: [https://github.com/realm/SwiftLint/blob/0.50.3/Source/swiftlint/Commands/SwiftLint.swift#L7](
    /// https://github.com/realm/SwiftLint/blob/0.50.3/Source/swiftlint/Commands/SwiftLint.swift#L7
    /// )
    private func environment(
        context: XcodePluginContext,
        target: XcodeTarget
    ) throws -> [String: String] {
        let projectDirectory: Path = context.xcodeProject.directory
        let swiftFiles: [Path] = swiftFiles(target: target)
        let swiftFilesNotInProjectDirectory: [Path] = swiftFiles.filter { !$0.isDescendant(of: projectDirectory) }

        guard swiftFilesNotInProjectDirectory.isEmpty else {
            throw SwiftLintBuildToolPluginError.swiftFilesNotInProjectDirectory(projectDirectory)
        }

        let directories: [Path] = try swiftFiles.map { try $0.resolveWorkingDirectory(in: projectDirectory) }
        let workingDirectory: Path = directories.min { $0.depth < $1.depth } ?? projectDirectory
        let swiftFilesNotInWorkingDirectory: [Path] = swiftFiles.filter { !$0.isDescendant(of: workingDirectory) }

        guard swiftFilesNotInWorkingDirectory.isEmpty else {
            throw SwiftLintBuildToolPluginError.swiftFilesNotInWorkingDirectory(workingDirectory)
        }

        return addedToEnvironment(["BUILD_WORKSPACE_DIRECTORY": "\(workingDirectory)"])
    }
}

#endif
