// See LICENSE for license details.

package chisel3.internal

import scala.util.DynamicVariable
import scala.collection.mutable.ArrayBuffer
import chisel3._
import chisel3.experimental._
import chisel3.internal.firrtl._
import chisel3.internal.naming._
import _root_.firrtl.annotations.{CircuitName, ComponentName, IsMember, ModuleName, Named, ReferenceTarget}
import chisel3.internal.Builder.Prefix

import scala.collection.mutable

private[chisel3] class Namespace(keywords: Set[String]) {
  private val names = collection.mutable.HashMap[String, Long]()
  for (keyword <- keywords)
    names(keyword) = 1

  private def rename(n: String): String = {
    val index = names(n)
    val tryName = s"${n}_${index}"
    names(n) = index + 1
    if (this contains tryName) rename(n) else tryName
  }

  private def sanitize(s: String, leadingDigitOk: Boolean = false): String = {
    // TODO what character set does FIRRTL truly support? using ANSI C for now
    def legalStart(c: Char) = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
    def legal(c: Char) = legalStart(c) || (c >= '0' && c <= '9')
    val res = s filter legal
    val headOk = (!res.isEmpty) && (leadingDigitOk || legalStart(res.head))
    if (headOk) res else s"_$res"
  }

  def contains(elem: String): Boolean = names.contains(elem)

  // leadingDigitOk is for use in fields of Records
  def name(elem: String, leadingDigitOk: Boolean = false): String = {
    val sanitized = sanitize(elem, leadingDigitOk)
    if (this contains sanitized) {
      name(rename(sanitized))
    } else {
      names(sanitized) = 1
      sanitized
    }
  }
}

private[chisel3] object Namespace {
  /** Constructs an empty Namespace */
  def empty: Namespace = new Namespace(Set.empty[String])
}

private[chisel3] class IdGen {
  private var counter = -1L
  def next: Long = {
    counter += 1
    counter
  }
}

/** Public API to access Node/Signal names.
  * currently, the node's name, the full path name, and references to its parent Module and component.
  * These are only valid once the design has been elaborated, and should not be used during its construction.
  */
trait InstanceId {
  def instanceName: String
  def pathName: String
  def parentPathName: String
  def parentModName: String
  /** Returns a FIRRTL Named that refers to this object in the elaborated hardware graph */
  def toNamed: Named
  /** Returns a FIRRTL IsMember that refers to this object in the elaborated hardware graph */
  def toTarget: IsMember
  /** Returns a FIRRTL IsMember that refers to the absolute path to this object in the elaborated hardware graph */
  def toAbsoluteTarget: IsMember
}

private[chisel3] trait HasId extends InstanceId {
  private[chisel3] def _onModuleClose: Unit = {}
  private[chisel3] val _parent: Option[BaseModule] = Builder.currentModule
  _parent.foreach(_.addId(this))

  private[chisel3] val _id: Long = Builder.idGen.next

  // TODO: remove this, but its removal seems to cause a nasty Scala compiler crash.
  override def hashCode: Int = super.hashCode()
  override def equals(that: Any): Boolean = super.equals(that)

  // Contains suggested seed (user-decided seed)
  private var suggested_seed: Option[String] = None

  // Contains the seed computed automatically by the compiler plugin
  private var auto_seed: Option[String] = None

  // Prefix at time when this class is constructed
  private val construction_prefix: Prefix = Builder.getPrefix()

  // Prefix when the latest [[suggestSeed]] or [[autoSeed]] is called
  private var prefix_seed: Prefix = List.empty[Either[String, Data]]

  // Post-seed hooks called to carry the suggested seeds to other candidates as needed
  private val suggest_postseed_hooks = scala.collection.mutable.ListBuffer.empty[String=>Unit]

  // Post-seed hooks called to carry the auto seeds to other candidates as needed
  private val auto_postseed_hooks = scala.collection.mutable.ListBuffer.empty[String=>Unit]

  /** Takes the last seed suggested. Multiple calls to this function will take the last given seed, unless
    * this HasId is a module port (see overridden method in Data.scala).
    *
    * If the final computed name conflicts with the final name of another signal, the final name may get uniquified by
    * appending a digit at the end of the name.
    *
    * Is a lower priority than [[suggestName]], in that regardless of whether [[autoSeed]]
    * was called, [[suggestName]] will always take precedence if it was called.
 *
    * @param seed Seed for the name of this component
    * @return this object
    */
  def autoSeed(seed: String): this.type = {
    auto_seed = Some(seed)
    for(hook <- auto_postseed_hooks) { hook(seed) }
    prefix_seed = Builder.getPrefix()
    this
  }

  /** Takes the first seed suggested. Multiple calls to this function will be ignored.
    * If the final computed name conflicts with another name, it may get uniquified by appending
    * a digit at the end.
    *
    * Is a higher priority than [[autoSeed]], in that regardless of whether [[autoSeed]]
    * was called, [[suggestName]] will always take precedence.
 *
    * @param seed The seed for the name of this component
    * @return this object
    */
  def suggestName(seed: =>String): this.type = {
    if(suggested_seed.isEmpty) suggested_seed = Some(seed)
    prefix_seed = Builder.getPrefix()
    for(hook <- suggest_postseed_hooks) { hook(seed) }
    this
  }

  /** Computes the name of this HasId, if one exists
    * @param defaultPrefix Optionally provide a default prefix for computing the name
    * @param defaultSeed Optionally provide default seed for computing the name
    * @return the name, if it can be computed
    */
  def computeName(defaultPrefix: Option[String], defaultSeed: Option[String]): Option[String] = {
    // Recursively builds a name if referenced fields of an aggregate type
    def buildAggName(id: HasId): Option[String] = {
      def recArg(node: Arg): Option[String] = node match {
        case Slot(imm, name) => recArg(imm).map(_ + "_" + name)
        case Index(imm, ILit(num)) => recArg(imm).map(_ + "_" + num)
        case Index(imm, n: LitArg) => recArg(imm).map(_ + "_" + n.num)
        case Index(imm, _: Node) => recArg(imm)
        case Node(id) => recArg(id.getOptionRef.get)
        case Ref(name) => Some(name)
        case ModuleIO(mod, name) if _parent.contains(mod) => Some(name)
        case ModuleIO(mod, name) => recArg(mod.getRef).map(_ + "_" + name)
      }
      id.getOptionRef.flatMap(recArg)
    }

    /** Computes a name of this signal, given the seed and prefix
      * @param seed
      * @param prefix
      * @return
      */
    def buildName(seed: String, prefix: Prefix): String = {
      val builder = new StringBuilder()
      prefix.foreach {
        case Left(s: String) => builder ++= s + "_"
        case Right(d: HasId) =>
          buildAggName(d) match {
            case Some(n) => builder ++= n + "_"
            case _ =>
          }
        case _ =>
      }
      builder ++= seed
      builder.toString
    }

    if(hasSeed) {
      Some(buildName(seedOpt.get, prefix_seed))
    } else {
      defaultSeed.map { default =>
        defaultPrefix match {
          case Some(p) => buildName(default, Left(p) +: construction_prefix)
          case None => buildName(default, construction_prefix)
        }
      }
    }
  }

  /** This resolves the precedence of [[autoSeed]] and [[suggestName]]
 *
    * @return the current calculation of a name, if it exists
    */
  private[chisel3] def seedOpt: Option[String] = suggested_seed.orElse(auto_seed)

  /** @return Whether either autoName or suggestName has been called */
  def hasSeed: Boolean = seedOpt.isDefined

  private[chisel3] def hasAutoSeed: Boolean = auto_seed.isDefined

  private[chisel3] def addSuggestPostnameHook(hook: String=>Unit): Unit = suggest_postseed_hooks += hook
  private[chisel3] def addAutoPostnameHook(hook: String=>Unit): Unit = auto_postseed_hooks += hook

  // Uses a namespace to convert suggestion into a true name
  // Will not do any naming if the reference already assigned.
  // (e.g. tried to suggest a name to part of a Record)
  private[chisel3] def forceName(prefix: Option[String], default: =>String, namespace: Namespace): Unit =
    if(_ref.isEmpty) {
      val candidate_name = computeName(prefix, Some(default)).get
      val available_name = namespace.name(candidate_name)
      setRef(Ref(available_name))
    }

  private var _ref: Option[Arg] = None
  private[chisel3] def setRef(imm: Arg): Unit = _ref = Some(imm)
  private[chisel3] def setRef(parent: HasId, name: String): Unit = setRef(Slot(Node(parent), name))
  private[chisel3] def setRef(parent: HasId, index: Int): Unit = setRef(Index(Node(parent), ILit(index)))
  private[chisel3] def setRef(parent: HasId, index: UInt): Unit = setRef(Index(Node(parent), index.ref))
  private[chisel3] def getRef: Arg = _ref.get
  private[chisel3] def getOptionRef: Option[Arg] = _ref

  // Implementation of public methods.
  def instanceName: String = _parent match {
    case Some(p) => p._component match {
      case Some(c) => _ref match {
        case Some(arg) => arg fullName c
        case None => computeName(None, None).get
      }
      case None => throwException("signalName/pathName should be called after circuit elaboration")
    }
    case None => throwException("this cannot happen")
  }
  def pathName: String = _parent match {
    case None => instanceName
    case Some(p) => s"${p.pathName}.$instanceName"
  }
  def parentPathName: String = _parent match {
    case Some(p) => p.pathName
    case None => throwException(s"$instanceName doesn't have a parent")
  }
  def parentModName: String = _parent match {
    case Some(p) => p.name
    case None => throwException(s"$instanceName doesn't have a parent")
  }
  // TODO Should this be public?
  protected def circuitName: String = _parent match {
    case None => instanceName
    case Some(p) => p.circuitName
  }

  private[chisel3] def getPublicFields(rootClass: Class[_]): Seq[java.lang.reflect.Method] = {
    // Suggest names to nodes using runtime reflection
    def getValNames(c: Class[_]): Set[String] = {
      if (c == rootClass) {
        Set()
      } else {
        getValNames(c.getSuperclass) ++ c.getDeclaredFields.map(_.getName)
      }
    }
    val valNames = getValNames(this.getClass)
    def isPublicVal(m: java.lang.reflect.Method) = {
      val noParameters = m.getParameterTypes.isEmpty
      val aVal = valNames.contains(m.getName)
      val notAssignable = !m.getDeclaringClass.isAssignableFrom(rootClass)
      val notWeirdVal = !m.getName.contains('$')
      noParameters && aVal && notAssignable && notWeirdVal
    }
    this.getClass.getMethods.filter(isPublicVal).sortWith(_.getName < _.getName)
  }
}
/** Holds the implementation of toNamed for Data and MemBase */
private[chisel3] trait NamedComponent extends HasId {
  /** Returns a FIRRTL ComponentName that references this object
    * @note Should not be called until circuit elaboration is complete
    */
  final def toNamed: ComponentName =
    ComponentName(this.instanceName, ModuleName(this.parentModName, CircuitName(this.circuitName)))

  /** Returns a FIRRTL ReferenceTarget that references this object
    * @note Should not be called until circuit elaboration is complete
    */
  final def toTarget: ReferenceTarget = {
    val name = this.instanceName
    import _root_.firrtl.annotations.{Target, TargetToken}
    Target.toTargetTokens(name).toList match {
      case TargetToken.Ref(r) :: components => ReferenceTarget(this.circuitName, this.parentModName, Nil, r, components)
      case other =>
        throw _root_.firrtl.annotations.Target.NamedException(s"Cannot convert $name into [[ReferenceTarget]]: $other")
    }
  }

  final def toAbsoluteTarget: ReferenceTarget = {
    val localTarget = toTarget
    _parent match {
      case Some(parent) => parent.toAbsoluteTarget.ref(localTarget.ref).copy(component = localTarget.component)
      case None => localTarget
    }
  }
}

// Mutable global state for chisel that can appear outside a Builder context
private[chisel3] class ChiselContext() {
  val idGen = new IdGen

  val instanceMap = mutable.HashMap[InstanceKey, BlackBox]()

  // Record the Bundle instance, class name, method name, and reverse stack trace position of open Bundles
  val bundleStack: ArrayBuffer[(Bundle, String, String, Int)] = ArrayBuffer()

  // Records the different prefixes which have been scoped at this point in time
  val prefixStack: ArrayBuffer[Either[String, HasId]] = ArrayBuffer()

  var backingModule: Option[(BaseModule, BlackBox)] = None
}

private[chisel3] class DynamicContext() {
  val globalNamespace = Namespace.empty
  val components = ArrayBuffer[Component]()
  val annotations = ArrayBuffer[ChiselAnnotation]()
  var currentModule: Option[BaseModule] = None

  /** Contains a mapping from a elaborated module to their aspect
    * Set by [[ModuleAspect]]
    */
  val aspectModule: mutable.HashMap[BaseModule, BaseModule] = mutable.HashMap.empty[BaseModule, BaseModule]

  // Set by object Module.apply before calling class Module constructor
  // Used to distinguish between no Module() wrapping, multiple wrappings, and rewrapping
  var readyForModuleConstr: Boolean = false
  var whenDepth: Int = 0 // Depth of when nesting
  var currentClock: Option[Clock] = None
  var currentReset: Option[Reset] = None
  val errors = new ErrorLog
  val namingStack = new NamingStack
}

private[chisel3] object Builder {

  // Represents the current state of the prefixes given
  type Prefix = List[Either[String, Data]]

  // All global mutable state must be referenced via dynamicContextVar!!
  private val dynamicContextVar = new DynamicVariable[Option[DynamicContext]](None)
  private def dynamicContext: DynamicContext = {
    require(dynamicContextVar.value.isDefined, "must be inside Builder context")
    dynamicContextVar.value.get
  }

  // Ensure we have a thread-specific ChiselContext
  private val chiselContext = new ThreadLocal[ChiselContext]{
    override def initialValue: ChiselContext = {
      new ChiselContext
    }
  }

  // Initialize any singleton objects before user code inadvertently inherits them.
  private def initializeSingletons(): Unit = {
    // This used to contain:
    //    val dummy = core.DontCare
    //  but this would occasionally produce hangs due to static initialization deadlock
    //  when Builder initialization collided with chisel3.package initialization of the DontCare value.
    // See:
    //  http://ternarysearch.blogspot.com/2013/07/static-initialization-deadlock.html
    //  https://bugs.openjdk.java.net/browse/JDK-8037567
    //  https://stackoverflow.com/questions/28631656/runnable-thread-state-but-in-object-wait
  }

  def namingStackOption: Option[NamingStack] = dynamicContextVar.value.map(_.namingStack)

  def idGen: IdGen = chiselContext.get.idGen

  def globalNamespace: Namespace = dynamicContext.globalNamespace
  def components: ArrayBuffer[Component] = dynamicContext.components
  def annotations: ArrayBuffer[ChiselAnnotation] = dynamicContext.annotations
  def namingStack: NamingStack = dynamicContext.namingStack

  def addInstance(key: InstanceKey, instance: BlackBox): Unit = {
    chiselContext.get.instanceMap(key) = instance
  }

  def getInstance(key: InstanceKey): Option[BlackBox] = {
    chiselContext.get.instanceMap.get(key)
  }

  def getBackingModule(): Option[(BaseModule, BlackBox)] = {
    chiselContext.get.backingModule
  }

  def setBackingModule(m: BaseModule, i: BlackBox): Unit = {
    chiselContext.get.backingModule = Some(m, i)
  }

  def clearBackingModule(): Unit = {
    chiselContext.get.backingModule = None
  }

  // Puts either a prefix string or hasId onto the prefix stack
  def pushPrefix(d: Either[String, HasId]): Unit = {
    chiselContext.get().prefixStack += d
  }

  // Puts a prefix string onto the prefix stack
  def pushPrefix(d: String): Unit = {
    chiselContext.get().prefixStack += Left(d)
  }

  // Puts a prefix data onto the prefix stack
  def pushPrefix(d: HasId): Unit = {
    chiselContext.get().prefixStack += Right(d)
  }

  // Remove a prefix from top of the stack
  def popPrefix(): Either[String, HasId] = {
    val ps = chiselContext.get().prefixStack
    ps.remove(ps.size - 1)
  }

  // Removes all prefixes from the prefix stack
  def clearPrefix(): Unit = {
    val ps = chiselContext.get().prefixStack
    ps.clear()
  }

  // Clears existing prefixes and sets to new prefix stack
  def setPrefix(prefix: Prefix): Unit = {
    val ps = chiselContext.get().prefixStack
    clearPrefix()
    ps.insertAll(0, prefix)
  }

  // Returns the prefix stack at this moment
  def getPrefix(): Prefix = chiselContext.get().prefixStack.toList.asInstanceOf[Prefix]

  def currentModule: Option[BaseModule] = dynamicContextVar.value match {
    case Some(dyanmicContext) => dynamicContext.currentModule
    case _ => None
  }
  def currentModule_=(target: Option[BaseModule]): Unit = {
    dynamicContext.currentModule = target
  }
  def aspectModule(module: BaseModule): Option[BaseModule] = dynamicContextVar.value match {
    case Some(dynamicContext) => dynamicContext.aspectModule.get(module)
    case _ => None
  }
  def addAspect(module: BaseModule, aspect: BaseModule): Unit = {
    dynamicContext.aspectModule += ((module, aspect))
  }
  def forcedModule: BaseModule = currentModule match {
    case Some(module) => module
    case None => throwException(
      "Error: Not in a Module. Likely cause: Missed Module() wrap or bare chisel API call."
      // A bare api call is, e.g. calling Wire() from the scala console).
    )
  }
  def referenceUserModule: RawModule = {
    currentModule match {
      case Some(module: RawModule) =>
        aspectModule(module) match {
          case Some(aspect: RawModule) => aspect
          case other => module
        }
      case _ => throwException(
        "Error: Not in a RawModule. Likely cause: Missed Module() wrap, bare chisel API call, or attempting to construct hardware inside a BlackBox."
        // A bare api call is, e.g. calling Wire() from the scala console).
      )
    }
  }
  def forcedUserModule: RawModule = currentModule match {
    case Some(module: RawModule) => module
    case _ => throwException(
      "Error: Not in a UserModule. Likely cause: Missed Module() wrap, bare chisel API call, or attempting to construct hardware inside a BlackBox."
      // A bare api call is, e.g. calling Wire() from the scala console).
    )
  }
  def readyForModuleConstr: Boolean = dynamicContext.readyForModuleConstr
  def readyForModuleConstr_=(target: Boolean): Unit = {
    dynamicContext.readyForModuleConstr = target
  }
  def whenDepth: Int = dynamicContext.whenDepth
  def whenDepth_=(target: Int): Unit = {
    dynamicContext.whenDepth = target
  }
  def currentClock: Option[Clock] = dynamicContext.currentClock
  def currentClock_=(newClock: Option[Clock]): Unit = {
    dynamicContext.currentClock = newClock
  }

  def currentReset: Option[Reset] = dynamicContext.currentReset
  def currentReset_=(newReset: Option[Reset]): Unit = {
    dynamicContext.currentReset = newReset
  }

  def forcedClock: Clock = currentClock.getOrElse(
    throwException("Error: No implicit clock.")
  )
  def forcedReset: Reset = currentReset.getOrElse(
    throwException("Error: No implicit reset.")
  )

  // TODO(twigg): Ideally, binding checks and new bindings would all occur here
  // However, rest of frontend can't support this yet.
  def pushCommand[T <: Command](c: T): T = {
    forcedUserModule.addCommand(c)
    c
  }
  def pushOp[T <: Data](cmd: DefPrim[T]): T = {
    // Bind each element of the returned Data to being a Op
    cmd.id.bind(OpBinding(forcedUserModule))
    pushCommand(cmd).id
  }

  // Called when Bundle construction begins, used to record a stack of open Bundle constructors to
  // record candidates for Bundle autoclonetype. This is a best-effort guess.
  // Returns the current stack of open Bundles
  // Note: elt will NOT have finished construction, its elements cannot be accessed
  def updateBundleStack(elt: Bundle): Seq[Bundle] = {
    val stackElts = Thread.currentThread().getStackTrace()
        .reverse  // so stack frame numbers are deterministic across calls
        .dropRight(2)  // discard Thread.getStackTrace and updateBundleStack

    // Determine where we are in the Bundle stack
    val eltClassName = elt.getClass.getName
    val eltStackPos = stackElts.map(_.getClassName).lastIndexOf(eltClassName)

    // Prune the existing Bundle stack of closed Bundles
    // If we know where we are in the stack, discard frames above that
    val stackEltsTop = if (eltStackPos >= 0) eltStackPos else stackElts.size
    val pruneLength = chiselContext.get.bundleStack.reverse.prefixLength { case (_, cname, mname, pos) =>
      pos >= stackEltsTop || stackElts(pos).getClassName != cname || stackElts(pos).getMethodName != mname
    }
    chiselContext.get.bundleStack.trimEnd(pruneLength)

    // Return the stack state before adding the most recent bundle
    val lastStack = chiselContext.get.bundleStack.map(_._1).toSeq

    // Append the current Bundle to the stack, if it's on the stack trace
    if (eltStackPos >= 0) {
      val stackElt = stackElts(eltStackPos)
      chiselContext.get.bundleStack.append((elt, eltClassName, stackElt.getMethodName, eltStackPos))
    }
    // Otherwise discard the stack frame, this shouldn't fail noisily

    lastStack
  }

  /** Recursively suggests names to supported "container" classes
    * Arbitrary nestings of supported classes are allowed so long as the
    * innermost element is of type HasId
    * (Note: Map is Iterable[Tuple2[_,_]] and thus excluded)
    */
  def nameRecursively(prefix: String, nameMe: Any, namer: (HasId, String) => Unit): Unit = nameMe match {
    case (id: HasId) => namer(id, prefix)
    case Some(elt) => nameRecursively(prefix, elt, namer)
    case (iter: Iterable[_]) if iter.hasDefiniteSize =>
      for ((elt, i) <- iter.zipWithIndex) {
        nameRecursively(s"${prefix}_${i}", elt, namer)
      }
    case _ => // Do nothing
  }

  def errors: ErrorLog = dynamicContext.errors
  def error(m: => String): Unit = {
    if (dynamicContextVar.value.isDefined) {
      errors.error(m)
    } else {
      throwException(m)
    }
  }
  def warning(m: => String): Unit = if (dynamicContextVar.value.isDefined) errors.warning(m)
  def deprecated(m: => String, location: Option[String] = None): Unit =
    if (dynamicContextVar.value.isDefined) errors.deprecated(m, location)

  /** Record an exception as an error, and throw it.
    *
    * @param m exception message
    */
  @throws(classOf[ChiselException])
  def exception(m: => String): Unit = {
    error(m)
    throwException(m)
  }

  def build[T <: RawModule](f: => T): (Circuit, T) = {
    dynamicContextVar.withValue(Some(new DynamicContext())) {
      errors.info("Elaborating design...")
      val mod = f
      mod.forceName(None, mod.name, globalNamespace)
      errors.checkpoint()
      errors.info("Done elaborating.")

      (Circuit(components.last.name, components, annotations), mod)
    }
  }
  initializeSingletons()
}

/** Allows public access to the naming stack in Builder / DynamicContext, and handles invocations
  * outside a Builder context.
  * Necessary because naming macros expand in user code and don't have access into private[chisel3]
  * objects.
  */
object DynamicNamingStack {
  def pushContext(): NamingContextInterface = {
    Builder.namingStackOption match {
      case Some(namingStack) => namingStack.pushContext()
      case None => DummyNamer
    }
  }

  def popReturnContext[T <: Any](prefixRef: T, until: NamingContextInterface): T = {
    until match {
      case DummyNamer =>
        require(Builder.namingStackOption.isEmpty,
          "Builder context must remain stable throughout a chiselName-annotated function invocation")
      case context: NamingContext =>
        require(Builder.namingStackOption.isDefined,
          "Builder context must remain stable throughout a chiselName-annotated function invocation")
        Builder.namingStackOption.get.popContext(prefixRef, context)
    }
    prefixRef
  }

  def length() : Int = Builder.namingStackOption.get.length
}

/** Casts BigInt to Int, issuing an error when the input isn't representable. */
private[chisel3] object castToInt {
  def apply(x: BigInt, msg: String): Int = {
    val res = x.toInt
    require(x == res, s"$msg $x is too large to be represented as Int")
    res
  }
}
