import Foundation
import JSONUtilities
import PathKit

public struct SpecFile {
    public let basePath: Path
    public let relativePath: Path
    public let jsonDictionary: JSONDictionary
    public let subSpecs: [SpecFile]

    fileprivate struct Include {
        let path: Path
        let relativePaths: Bool

        static let defaultRelativePaths = true

        init?(any: Any) {
            if let string = any as? String {
                path = Path(string)
                relativePaths = Include.defaultRelativePaths
            } else if let dictionary = any as? JSONDictionary,
                let path = dictionary["path"] as? String {
                self.path = Path(path)
                relativePaths = dictionary["relativePaths"] as? Bool ?? Include.defaultRelativePaths
            } else {
                return nil
            }
        }

        static func parse(json: Any?) -> [Include] {
            if let array = json as? [Any] {
                return array.compactMap(Include.init)
            } else if let object = json, let include = Include(any: object) {
                return [include]
            } else {
                return []
            }
        }
    }

    public init(path: Path) throws {
        try self.init(filename: path.lastComponent, basePath: path.parent())
    }

    public init(jsonDictionary: JSONDictionary, basePath: Path = "", relativePath: Path = "", subSpecs: [SpecFile] = []) {
        self.basePath = basePath
        self.relativePath = relativePath
        self.jsonDictionary = jsonDictionary
        self.subSpecs = subSpecs
    }

    fileprivate init(include: Include, basePath: Path, relativePath: Path) throws {
        let basePath = include.relativePaths ? (basePath + relativePath) : (basePath + relativePath + include.path.parent())
        let relativePath = include.relativePaths ? include.path.parent() : Path()

        try self.init(filename: include.path.lastComponent, basePath: basePath, relativePath: relativePath)
    }

    fileprivate init(filename: String, basePath: Path, relativePath: Path = "") throws {
        let path = basePath + relativePath + filename
        let jsonDictionary = try SpecFile.loadDictionary(path: path)

        let includes = Include.parse(json: jsonDictionary["include"])
        let subSpecs: [SpecFile] = try includes.map { include in
            try SpecFile(include: include, basePath: basePath, relativePath: relativePath)
        }

        self.init(jsonDictionary: jsonDictionary, basePath: basePath, relativePath: relativePath, subSpecs: subSpecs)
    }

    static func loadDictionary(path: Path) throws -> JSONDictionary {
        // Depending on the extension we will either load the file as YAML or JSON
        if path.extension?.lowercased() == "json" {
            let data: Data = try path.read()
            let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
            guard let jsonDictionary = jsonData as? [String: Any] else {
                fatalError("Invalid JSON at path \(path)")
            }
            return jsonDictionary
        } else {
            return try loadYamlDictionary(path: path)
        }
    }

    public func resolvedDictionary() -> JSONDictionary {
        let resolvedSpec = resolvingPaths()
        return resolvedSpec.mergedDictionary()
    }

    func mergedDictionary() -> JSONDictionary {
        return jsonDictionary.merged(onto:
            subSpecs
                .map { $0.mergedDictionary() }
                .reduce([:]) { $1.merged(onto: $0) })
    }

    func resolvingPaths(relativeTo basePath: Path = Path()) -> SpecFile {
        let relativePath = (basePath + self.relativePath).normalize()
        guard relativePath != Path() else {
            return self
        }

        let jsonDictionary = Project.pathProperties.resolvingPaths(in: self.jsonDictionary, relativeTo: relativePath)
        return SpecFile(
            jsonDictionary: jsonDictionary,
            relativePath: self.relativePath,
            subSpecs: subSpecs.map { $0.resolvingPaths(relativeTo: relativePath) }
        )
    }
}

extension Dictionary where Key == String, Value: Any {

    func merged(onto other: Dictionary<Key, Value>) -> Dictionary<Key, Value> {
        var merged = other

        for (key, value) in self {
            if key.hasSuffix(":REPLACE") {
                let newKey = key[key.startIndex..<key.index(key.endIndex, offsetBy: -8)]
                merged[Key(newKey)] = value
            } else if let dictionary = value as? Dictionary<Key, Value>, let base = merged[key] as? Dictionary<Key, Value> {
                merged[key] = dictionary.merged(onto: base) as? Value
            } else if let array = value as? [Any], let base = merged[key] as? [Any] {
                merged[key] = (base + array) as? Value
            } else {
                merged[key] = value
            }
        }
        return merged
    }

    func replaceString(_ template: String, with replacement: String) -> JSONDictionary {
        var replaced: JSONDictionary = self
        for (key, value) in self {
            switch value {
            case let dictionary as JSONDictionary:
                replaced[key] = dictionary.replaceString(template, with: replacement)
            case let string as String:
                replaced[key] = string.replacingOccurrences(of: template, with: replacement)
            case let array as [JSONDictionary]:
                replaced[key] = array.map { $0.replaceString(template, with: replacement) }
            case let array as [String]:
                replaced[key] = array.map { $0.replacingOccurrences(of: template, with: replacement) }
            default: break
            }
        }
        return replaced
    }
}
