// SPDX-License-Identifier: Apache-2.0

package firrtl.annotations.analysis

import firrtl.annotations._
import firrtl.annotations.TargetToken.{Instance, OfModule, Ref}
import firrtl.Utils.throwInternalError

import scala.collection.mutable

/** Used by [[firrtl.annotations.transforms.EliminateTargetPaths]] to eliminate target paths
  * Calculates needed modifications to a circuit's module/instance hierarchy
  */
case class DuplicationHelper(existingModules: Set[String]) {

  // Maps instances to the module it instantiates (an ofModule)
  type InstanceOfModuleMap = mutable.LinkedHashMap[Instance, OfModule]

  // Maps a module to the instance/ofModules it instantiates
  type ModuleHasInstanceOfModuleMap = mutable.LinkedHashMap[String, InstanceOfModuleMap]

  // Maps original module names to new duplicated modules and their encapsulated instance/ofModules
  type DupMap = mutable.LinkedHashMap[String, ModuleHasInstanceOfModuleMap]

  // Internal state to keep track of how paths duplicate
  private val dupMap = new DupMap()

  // Internal record of which paths are renamed to which new names, in the case of a collision
  private val cachedNames = mutable.LinkedHashMap[(String, Seq[(Instance, OfModule)]), String]() ++
    existingModules.map(m => (m, Nil) -> m)

  // Internal record of all paths to ensure unique name generation
  private val allModules = mutable.LinkedHashSet[String]() ++ existingModules

  /** Updates internal state (dupMap) to calculate instance hierarchy modifications so t's tokens in an instance can be
    * expressed as a tokens in a module (e.g. uniquify/duplicate the instance path in t's tokens)
    * @param t An instance-resolved component
    */
  def expandHierarchy(t: IsMember): Unit = {
    val path = t.asPath
    path.reverse.tails.map { _.reverse }.foreach { duplicate(t.module, _) }
  }

  /** Updates dupMap with how original module names map to new duplicated module names
    * @param top Root module of a component
    * @param path Path down instance hierarchy of a component
    */
  private def duplicate(top: String, path: Seq[(Instance, OfModule)]): Unit = {
    val (originalModule, instance, ofModule) = path.size match {
      case 0 => return
      case 1 => (top, path.head._1, path.head._2)
      case _ => (path(path.length - 2)._2.value, path.last._1, path.last._2)
    }
    val originalModuleToDupedModule = dupMap.getOrElseUpdate(originalModule, new ModuleHasInstanceOfModuleMap())
    val dupedModule = getModuleName(top, path.dropRight(1))
    val dupedModuleToInstances = originalModuleToDupedModule.getOrElseUpdate(dupedModule, new InstanceOfModuleMap())
    val dupedInstanceModule = getModuleName(top, path)
    dupedModuleToInstances += ((instance, OfModule(dupedInstanceModule)))

    val originalInstanceModuleToDupedModule = dupMap.getOrElseUpdate(ofModule.value, new ModuleHasInstanceOfModuleMap())
    originalInstanceModuleToDupedModule.getOrElseUpdate(dupedInstanceModule, new InstanceOfModuleMap())
  }

  /** Deterministic name-creation of a duplicated module
    * @param top
    * @param path
    * @return
    */
  def getModuleName(top: String, path: Seq[(Instance, OfModule)]): String = {
    cachedNames.get((top, path)) match {
      case None => // Need a new name
        val prefix = path.last._2.value + "___"
        val postfix = top + "_" + path.map { case (i, m) => i.value }.mkString("_")
        val ns = mutable.HashSet(allModules.toSeq: _*)
        val finalName = firrtl.Namespace.findValidPrefix(prefix, Seq(postfix), ns) + postfix
        allModules += finalName
        cachedNames((top, path)) = finalName
        finalName
      case Some(newName) => newName
    }
  }

  /** Return the duplicated module (formerly originalOfModule) instantiated by instance in newModule (formerly
    *  originalModule)
    * @param originalModule original encapsulating module
    * @param newModule new name of encapsulating module
    * @param instance instance name being declared in encapsulating module
    * @param originalOfModule original module being instantiated in originalModule
    * @return
    */
  def getNewOfModule(
    originalModule:   String,
    newModule:        String,
    instance:         Instance,
    originalOfModule: OfModule
  ): OfModule = {
    dupMap.get(originalModule) match {
      case None => // No duplication, can return originalOfModule
        originalOfModule
      case Some(newDupedModules) =>
        newDupedModules.get(newModule) match {
          case None if newModule != originalModule => throwInternalError("BAD")
          case None => // No duplication, can return originalOfModule
            originalOfModule
          case Some(newDupedModule) =>
            newDupedModule.get(instance) match {
              case None => // Not duped, can return originalOfModule
                originalOfModule
              case Some(newOfModule) =>
                newOfModule
            }
        }
    }
  }

  /** Returns the names of this module's duplicated (including the original name)
    * @param module
    * @return
    */
  def getDuplicates(module: String): Set[String] = {
    dupMap.get(module).map(_.keys.toSet[String]).getOrElse(Set.empty[String]) ++ Set(module)
  }

  /** Rewrites t with new module/instance hierarchy calculated after repeated calls to [[expandHierarchy]]
    * @param t A target
    * @return t rewritten, is a seq because if the t.module has been duplicated, it must now refer to multiple modules
    */
  def makePathless(t: IsMember): Seq[IsMember] = {
    val top = t.module
    val path = t.asPath
    val newTops = getDuplicates(top)
    newTops.map { newTop =>
      val newPath = mutable.ArrayBuffer[TargetToken]()
      path.foldLeft((top, newTop)) {
        case ((originalModule, newModule), (instance, ofModule)) =>
          val newOfModule = getNewOfModule(originalModule, newModule, instance, ofModule)
          newPath ++= Seq(instance, newOfModule)
          (ofModule.value, newOfModule.value)
      }
      val module = if (newPath.nonEmpty) newPath.last.value.toString else newTop
      t.notPath match {
        case Seq()                               => ModuleTarget(t.circuit, module)
        case Instance(i) +: OfModule(m) +: Seq() => ModuleTarget(t.circuit, module)
        case Ref(r) +: components                => ReferenceTarget(t.circuit, module, Nil, r, components)
      }
    }.toSeq
  }
}
