// Copyright 2017 The Tulsi Authors. All rights reserved.
//
// 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

/// This class acts as a data store for all of the RuleEntries that are read by our Aspects. It
/// effectively maps from BuildLabel to RuleEntry, with the caveat that a single BuildLabel may
/// actually have multiple RuleEntries depending on Bazel configuration splits. For example, if an
/// objc_library is depended on by an ios_application with a minimum_os_version of 9 and depended on
/// by an ios_unit_test with a minimum_os_version of 10, then we will actually discover two versions
/// of the same objc_library, one for iOS 10 and the other for iOS 9.
public class RuleEntryMap {
  private var labelToEntries = [BuildLabel: [RuleEntry]]()
  private var allEntries = [RuleEntry]()

  private let localizedMessageLogger: LocalizedMessageLogger?

  init(localizedMessageLogger: LocalizedMessageLogger? = nil) {
    self.localizedMessageLogger = localizedMessageLogger
  }

  init(_ ruleEntryMap: RuleEntryMap) {
    localizedMessageLogger = ruleEntryMap.localizedMessageLogger
    allEntries = ruleEntryMap.allEntries
    labelToEntries = ruleEntryMap.labelToEntries
  }

  public var allRuleEntries: [RuleEntry] {
    return allEntries
  }

  public func insert(ruleEntry: RuleEntry) {
    allEntries.append(ruleEntry)

    let label = ruleEntry.label
    guard var entries = labelToEntries[label] else {
      labelToEntries[label] = [ruleEntry]
      return
    }
    // Don't warn about duplicate entries with the same DeploymentTarget as they should
    // only be caused by a root-level library which we already warn about.
    entries.append(ruleEntry)
    labelToEntries[label] = entries
  }

  public func hasAnyRuleEntry(withBuildLabel buildLabel: BuildLabel) -> Bool {
    return anyRuleEntry(withBuildLabel: buildLabel) != nil
  }

  /// Returns a RuleEntry from the list of RuleEntries with the specified BuildLabel.
  public func anyRuleEntry(withBuildLabel buildLabel: BuildLabel) -> RuleEntry? {
    guard let ruleEntries = labelToEntries[buildLabel] else {
      return nil
    }
    return ruleEntries.last
  }

  /// Returns a list of RuleEntry with the specified BuildLabel.
  public func ruleEntries(buildLabel: BuildLabel) -> [RuleEntry] {
    guard let ruleEntries = labelToEntries[buildLabel] else {
      return [RuleEntry]()
    }
    return ruleEntries
  }

  /// Returns a RuleEntry which is a dep of the given RuleEntry, matched by configuration.
  public func ruleEntry(buildLabel: BuildLabel, depender: RuleEntry) -> RuleEntry? {
    guard let deploymentTarget = depender.deploymentTarget else {
      localizedMessageLogger?.warning("DependentRuleEntryHasNoDeploymentTarget",
                                      comment: "Error when a RuleEntry with deps does not have a DeploymentTarget. RuleEntry's label is in %1$@, dep's label is in %2$@.",
                                      values: depender.label.description,
                                              buildLabel.description)
      return anyRuleEntry(withBuildLabel: buildLabel)
    }
    return ruleEntry(buildLabel: buildLabel, deploymentTarget: deploymentTarget)
  }

  /// Returns a RuleEntry with the given buildLabel and deploymentTarget.
  public func ruleEntry(buildLabel: BuildLabel, deploymentTarget: DeploymentTarget) -> RuleEntry? {
    guard let ruleEntries = labelToEntries[buildLabel] else {
      return nil
    }
    guard !ruleEntries.isEmpty else {
      return nil
    }

    // If there's only one, we just assume that it's right.
    if ruleEntries.count == 1 {
      return ruleEntries.first
    }

    for ruleEntry in ruleEntries {
      if deploymentTarget == ruleEntry.deploymentTarget {
        return ruleEntry
      }
    }

    // Must be multiple. Shoot out a warning and return the last.
    localizedMessageLogger?.warning("AmbiguousRuleEntryReference",
                                    comment: "Warning when unable to resolve a RuleEntry for a given DeploymentTarget. RuleEntry's label is in %1$@.",
                                    values: buildLabel.description)
    return ruleEntries.last
  }
}
