// SPDX-License-Identifier: Apache-2.0

// This file contains part of the implementation of the naming static annotation system.

package chisel3.internal.naming
import chisel3.experimental.NoChiselNamePrefix

import scala.collection.mutable.Stack
import scala.collection.mutable.ListBuffer

import java.util.IdentityHashMap
import scala.collection.JavaConverters._

/** Recursive Function Namer overview
  *
  * In every function, creates a NamingContext object, which associates all vals with a string name
  * suffix, for example:
  *   val myValName = SomeStatement()
  * produces the entry in items:
  *   {ref of SomeStatement(), "myValName"}
  *
  * This is achieved with a macro transforming:
  *   val myValName = SomeStatement()
  * statements into a naming call:
  *   val myValName = context.name(SomeStatement(), "myValName")
  *
  * The context is created from a global dynamic context stack at the beginning of each function.
  * At the end of each function call, the completed context is added to its parent context and
  * associated with the return value (whose name at an enclosing function call will form the prefix
  * for all named objects).
  *
  * When the naming context prefix is given, it will name all of its items with the prefix and the
  * associated suffix name. Then, it will check its descendants for sub-contexts with references
  * matching the item reference, and if there is a match, it will (recursively) give the
  * sub-context a prefix of its current prefix plus the item reference suffix.
  *
  * Note that for Modules, the macro will insert a naming context prefix call with an empty prefix,
  * starting the recursive naming process.
  */

/** Base class for naming contexts, providing the basic API consisting of naming calls and
  * ability to take descendant naming contexts.
  */
sealed trait NamingContextInterface {
  /** Suggest a name (that will be propagated to FIRRTL) for an object, then returns the object
    * itself (so this can be inserted transparently anywhere).
    * Is a no-op (so safe) when applied on objects that aren't named, including non-Chisel data
    * types.
    */
  def name[T](obj: T, name: String): T

  /** Gives this context a naming prefix (which may be empty, "", for a top-level Module context)
    * so that actual naming calls (HasId.suggestName) can happen.
    * Recursively names descendants, for those whose return value have an associated name.
    */
  def namePrefix(prefix: String)
}

/** Dummy implementation to allow for naming annotations in a non-Builder context.
  */
object DummyNamer extends NamingContextInterface {
  def name[T](obj: T, name: String): T = obj

  def namePrefix(prefix: String): Unit = {
  }
}

/** Actual namer functionality.
  */
class NamingContext extends NamingContextInterface {
  val descendants = new IdentityHashMap[AnyRef, ListBuffer[NamingContext]]()
  val anonymousDescendants = ListBuffer[NamingContext]()
  val items = ListBuffer[(AnyRef, String)]()
  var closed = false  // a sanity check to ensure no more name() calls are done after namePrefix

  /** Adds a NamingContext object as a descendant - where its contained objects will have names
    * prefixed with the name given to the reference object, if the reference object is named in the
    * scope of this context.
    */
  def addDescendant(ref: Any, descendant: NamingContext) {
    ref match {
      case ref: AnyRef =>
        // getOrElseUpdate
        val l = descendants.get(ref)
        val buf = if (l != null) l else {
          val value = ListBuffer[NamingContext]()
          descendants.put(ref, value)
          value
        }
        buf += descendant
      case _ => anonymousDescendants += descendant
    }
  }

  def name[T](obj: T, name: String): T = {
    assert(!closed, "Can't name elements after namePrefix called")
    obj match {
      case _: NoChiselNamePrefix => // Don't name things with NoChiselNamePrefix
      case ref: AnyRef => items += ((ref, name))
      case _ =>
    }
    obj
  }

  def namePrefix(prefix: String): Unit = {
    closed = true
    for ((ref, suffix) <- items) {
      // First name the top-level object
      chisel3.internal.Builder.nameRecursively(prefix + suffix, ref, (id, name) => id.suggestName(name))

      // Then recurse into descendant contexts
      if (descendants.containsKey(ref)) {
        for (descendant <- descendants.get(ref)) {
          descendant.namePrefix(prefix + suffix + "_")
        }
        descendants.remove(ref)
      }
    }

    for (descendant <- descendants.values.asScala.flatten) {
      // Where we have a broken naming link, just ignore the missing parts
      descendant.namePrefix(prefix)
    }
    for (descendant <- anonymousDescendants) {
      descendant.namePrefix(prefix)
    }
  }
}

/** Class for the (global) naming stack object, which provides a way to push and pop naming
  * contexts as functions are called / finished.
  */
class NamingStack {
  val namingStack = Stack[NamingContext]()

  /** Creates a new naming context, where all items in the context will have their names prefixed
    * with some yet-to-be-determined prefix from object names in an enclosing scope.
    */
  def pushContext(): NamingContext = {
    val context = new NamingContext
    namingStack.push(context)
    context
  }

  /** Called at the end of a function, popping the current naming context, adding it to the
    * enclosing context's descendants, and passing through the prefix naming reference.
    * Every instance of push_context() must have a matching pop_context().
    *
    * Will assert out if the context being popped isn't the topmost on the stack.
    */
  def popContext[T <: Any](prefixRef: T, until: NamingContext): Unit = {
    assert(namingStack.top == until)
    namingStack.pop()
    if (!namingStack.isEmpty) {
      namingStack.top.addDescendant(prefixRef, until)
    }
  }

  def length() : Int = namingStack.length
}
