/*
 * Copyright 2019 Google
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import Foundation

/// A struct to build a .modulemap in a given framework directory.
///
struct ModuleMapBuilder {
  /// Information associated with a framework
  private class FrameworkInfo {
    let isSourcePod: Bool
    let versionedPod: CocoaPodUtils.VersionedPod
    let subspecs: Set<String>
    var transitiveFrameworks: Set<String>?
    var transitiveLibraries: Set<String>?

    init(isSourcePod: Bool, versionedPod: CocoaPodUtils.VersionedPod, subspecs: Set<String>) {
      self.isSourcePod = isSourcePod
      self.versionedPod = versionedPod
      self.subspecs = subspecs
    }
  }

  struct ModuleMapContents {
    /// Placeholder to fill in umbrella header.
    static let umbrellaPlaceholder = "UMBRELLA_PLACEHOLDER"
    let contents: String
    init(module: String, frameworks: Set<String>, libraries: Set<String>) {
      var content = """
      framework module \(module) {
      umbrella header "\(ModuleMapBuilder.ModuleMapContents.umbrellaPlaceholder)"
      export *
      module * { export * }

      """

      if module == "FirebaseFirestoreInternal" {
        content += """
          link framework "openssl_grpc"
          link framework "grpc"
          link framework "grpcpp"
        """
      }

      for framework in frameworks.sorted() {
        content += "  link framework " + framework + "\n"
      }
      for library in libraries.sorted() {
        content += "  link " + library + "\n"
      }
      // The empty line at the end is intentional, do not remove it.
      content += "}\n"
      contents = content
    }

    func get(umbrellaHeader: String) -> String {
      return contents.replacingOccurrences(of:
        ModuleMapBuilder.ModuleMapContents.umbrellaPlaceholder, with: umbrellaHeader)
    }
  }

  /// The directory containing the Xcode project and Pods folder.
  private let projectDir: URL

  /// Custom CocoaPods spec repos to be used. If not provided, the tool will only use the CocoaPods
  /// master repo.
  private let customSpecRepos: [URL]?

  /// Dictionary of all installed pods. Update moduleMapContents here.
  private var allPods: [String: CocoaPodUtils.PodInfo]

  /// Dictionary of installed pods required for this module.
  private var installedPods: [String: FrameworkInfo]

  /// The platform for this build.
  private let platform: Platform

  /// The path containing local podspec URLs, if specified.
  private let localPodspecPath: URL?

  /// Default initializer.
  init(customSpecRepos: [URL]?,
       selectedPods: [String: CocoaPodUtils.PodInfo],
       platform: Platform,
       paths: ZipBuilder.FilesystemPaths) {
    projectDir = FileManager.default.temporaryDirectory(withName: "module")
    CocoaPodUtils.podInstallPrepare(inProjectDir: projectDir, templateDir: paths.templateDir)

    self.customSpecRepos = customSpecRepos
    allPods = selectedPods

    var installedPods: [String: FrameworkInfo] = [:]
    for pod in selectedPods {
      let versionedPod = CocoaPodUtils.VersionedPod(name: pod.key, version: pod.value.version)
      installedPods[pod.key] = FrameworkInfo(isSourcePod: pod.value.isSourcePod,
                                             versionedPod: versionedPod,
                                             subspecs: pod.value.subspecs)
    }
    self.installedPods = installedPods

    self.platform = platform
    localPodspecPath = paths.localPodspecPath
  }

  // MARK: - Public Functions

  /// Build the module map files for the source frameworks.
  ///
  func build() {
    for framework in installedPods.values
      where framework.isSourcePod &&
      framework.versionedPod.name != "Firebase" &&
      framework.transitiveFrameworks == nil {
      generate(framework: framework)
    }
  }

  // MARK: - Internal Functions

  /// Build a module map for a single framework. A CocoaPod install is run to extract the required
  /// frameworks
  /// and libraries from the generated xcconfig. All previously installed dependent pods are put
  /// into the Podfile
  /// to make sure we install the right version and from the right location.
  private func generate(framework: FrameworkInfo) {
    let podName = framework.versionedPod.name
    let deps = CocoaPodUtils.transitiveVersionedPodDependencies(for: podName, in: allPods).filter {
      // Don't include Interop pods in the module map calculation since they shouldn't add anything
      // and it uses the platform-independent version of the dependency list, which causes a crash
      // for the iOS-only RecaptchaInterop pod when the subsequent code tries to `pod install` it
      // for macOS. All this module code should go away when we switch to building dynamic
      // frameworks.
      !$0.name.hasSuffix("Interop")
    }

    CocoaPodUtils.installPods(allSubspecList(framework: framework) + deps,
                              inDir: projectDir,
                              platform: platform,
                              customSpecRepos: customSpecRepos,
                              localPodspecPath: localPodspecPath,
                              linkage: .forcedStatic)

    let xcconfigFile = projectDir.appendingPathComponents(["Pods", "Target Support Files",
                                                           "Pods-FrameworkMaker",
                                                           "Pods-FrameworkMaker.release.xcconfig"])
    allPods[podName]?
      .moduleMapContents = makeModuleMap(forFramework: framework, withXcconfigFile: xcconfigFile)
  }

  /// Convert a list of versioned pods to a list of versioned pods specified with all needed
  /// subspecs.
  private func allSubspecList(framework: FrameworkInfo) -> [CocoaPodUtils.VersionedPod] {
    let name = framework.versionedPod.name
    let version = framework.versionedPod.version

    guard framework.subspecs.count > 0 else {
      return [CocoaPodUtils.VersionedPod(name: name, version: version)]
    }

    return framework.subspecs.map { subspec in
      CocoaPodUtils.VersionedPod(name: "\(name)/\(subspec)", version: version)
    }
  }

  // Extract the framework and library dependencies for a framework from
  // the xcconfig file from an app generated from a Podfile only with that
  // CocoaPod and removing the frameworks and libraries from its transitive dependencies.
  private func getModuleDependencies(withXcconfigFile xcconfigFile: URL) ->
    (frameworks: [String], libraries: [String]) {
    do {
      let text = try String(contentsOf: xcconfigFile)
      let lines = text.components(separatedBy: .newlines)
      for line in lines {
        if line.hasPrefix("OTHER_LDFLAGS =") {
          var dependencyFrameworks: [String] = []
          var dependencyLibraries: [String] = []
          let tokens = line.components(separatedBy: " ")
          var addNext = false
          for token in tokens {
            if addNext {
              dependencyFrameworks.append(token)
              addNext = false
            } else if token == "-framework" {
              addNext = true
            } else if token.hasPrefix("-l") {
              let index = token.index(token.startIndex, offsetBy: 2)
              dependencyLibraries.append(String(token[index...]))
            }
          }
          return (dependencyFrameworks, dependencyLibraries)
        }
      }
    } catch {
      fatalError("Failed to open \(xcconfigFile): \(error)")
    }
    return ([], [])
  }

  private func makeModuleMap(forFramework framework: FrameworkInfo,
                             withXcconfigFile xcconfigFile: URL) -> ModuleMapContents {
    let name = framework.versionedPod.name
    let dependencies = getModuleDependencies(withXcconfigFile: xcconfigFile)
    let frameworkDeps = Set(dependencies.frameworks)
    let libraryDeps = Set(dependencies.libraries)
    var transitiveFrameworkDeps = frameworkDeps
    var transitiveLibraryDeps = libraryDeps
    var myFrameworkDeps = frameworkDeps
    var myLibraryDeps = libraryDeps

    // Iterate through the deps looking for pods. At time of authoring, TensorFlowLiteObjC is the
    // only pod in frameworkDeps instead of libraryDeps.
    for library in libraryDeps.union(frameworkDeps) {
      let lib = library.replacingOccurrences(of: "\"", with: "")
      if let dependencyPod = installedPods[lib] {
        myLibraryDeps.remove(library)
        myFrameworkDeps.remove(library)
        if lib == name {
          continue
        }
        var podFrameworkDeps = dependencyPod.transitiveFrameworks
        var podLibraryDeps = dependencyPod.transitiveLibraries
        if podFrameworkDeps == nil {
          generate(framework: dependencyPod)
          podFrameworkDeps = dependencyPod.transitiveFrameworks
          podLibraryDeps = dependencyPod.transitiveLibraries
        }
        if let fdeps = podFrameworkDeps {
          transitiveFrameworkDeps.formUnion(fdeps)
        }
        if let ldeps = podLibraryDeps {
          transitiveLibraryDeps.formUnion(ldeps)
        }
      }
    }
    installedPods[name]?.transitiveFrameworks = transitiveFrameworkDeps
    installedPods[name]?.transitiveLibraries = transitiveLibraryDeps

    return ModuleMapContents(module: name, frameworks: myFrameworkDeps, libraries: myLibraryDeps)
  }
}
