// See LICENSE for license details

package chipyard.iocell

import chisel3._
import chisel3.util.{Cat, HasBlackBoxInline}
import chisel3.reflect.DataMirror
import chisel3.experimental.{Analog, BaseModule}

// The following four IO cell bundle types are bare-minimum functional connections
// for modeling 4 different IO cell scenarios. The intention is that the user
// would create wrapper modules that extend these interfaces with additional
// control signals. These are loosely similar to the sifive-blocks PinCtrl bundles
// (https://github.com/sifive/sifive-blocks/blob/master/src/main/scala/devices/pinctrl/PinCtrl.scala),
// but we want to avoid a dependency on an external libraries.

/** The base IO bundle for an analog signal (typically something with no digital buffers inside)
  *  pad: off-chip (external) connection
  *  core: internal connection
  */
class AnalogIOCellBundle extends Bundle {
  val pad = Analog(1.W) // Pad/bump signal (off-chip)
  val core = Analog(1.W) // core signal (on-chip)
}

/** The base IO bundle for a signal with runtime-controllable direction
  *  pad: off-chip (external) connection
  *  i: input to chip logic (output from IO cell)
  *  ie: enable signal for i
  *  o: output from chip logic (input to IO cell)
  *  oe: enable signal for o
  */
class DigitalGPIOCellBundle extends Bundle {
  val pad = Analog(1.W)
  val i = Output(Bool())
  val ie = Input(Bool())
  val o = Input(Bool())
  val oe = Input(Bool())
}

/** The base IO bundle for a digital output signal
  *  pad: off-chip (external) connection
  *  o: output from chip logic (input to IO cell)
  *  oe: enable signal for o
  */
class DigitalOutIOCellBundle extends Bundle {
  val pad = Output(Bool())
  val o = Input(Bool())
  val oe = Input(Bool())
}

/** The base IO bundle for a digital input signal
  *  pad: off-chip (external) connection
  *  i: input to chip logic (output from IO cell)
  *  ie: enable signal for i
  */
class DigitalInIOCellBundle extends Bundle {
  val pad = Input(Bool())
  val i = Output(Bool())
  val ie = Input(Bool())
}

trait IOCell extends BaseModule {
  var iocell_name: Option[String] = None

  /** Set IOCell name
    * @param s Proposed name for the IOCell
    *
    * @return An inherited IOCell with given the proposed name
    */
  def suggestName(s: String): this.type = {
    iocell_name = Some(s)
    super.suggestName(s)
  }
}

trait AnalogIOCell extends IOCell {
  val io: AnalogIOCellBundle
}

trait DigitalGPIOCell extends IOCell {
  val io: DigitalGPIOCellBundle
}

trait DigitalInIOCell extends IOCell {
  val io: DigitalInIOCellBundle
}

trait DigitalOutIOCell extends IOCell {
  val io: DigitalOutIOCellBundle
}

// The following Generic IO cell black boxes have verilog models that mimic a very simple
// implementation of an IO cell. For building a real chip, it is important to implement
// and use similar classes which wrap the foundry-specific IO cells.

abstract class GenericIOCell extends BlackBox with HasBlackBoxInline {
  val impl: String
  val moduleName = this.getClass.getSimpleName
  setInline(s"$moduleName.v", impl);
}

class GenericAnalogIOCell extends GenericIOCell with AnalogIOCell {
  val io = IO(new AnalogIOCellBundle)
  lazy val impl = s"""
`timescale 1ns/1ps
module GenericAnalogIOCell(
    inout pad,
    inout core
);

    assign core = 1'bz;
    assign pad = core;

endmodule"""
}

class GenericDigitalGPIOCell extends GenericIOCell with DigitalGPIOCell {
  val io = IO(new DigitalGPIOCellBundle)
  lazy val impl = s"""
`timescale 1ns/1ps
module GenericDigitalGPIOCell(
    inout pad,
    output i,
    input ie,
    input o,
    input oe
);

    assign pad = oe ? o : 1'bz;
    assign i = ie ? pad : 1'b0;

endmodule"""
}

class GenericDigitalInIOCell extends GenericIOCell with DigitalInIOCell {
  val io = IO(new DigitalInIOCellBundle)
  lazy val impl = s"""
`timescale 1ns/1ps
module GenericDigitalInIOCell(
    input pad,
    output i,
    input ie
);

    assign i = ie ? pad : 1'b0;

endmodule"""
}

class GenericDigitalOutIOCell extends GenericIOCell with DigitalOutIOCell {
  val io = IO(new DigitalOutIOCellBundle)
  lazy val impl = s"""
`timescale 1ns/1ps
module GenericDigitalOutIOCell(
    output pad,
    input o,
    input oe
);

    assign pad = oe ? o : 1'bz;

endmodule"""
}

trait IOCellTypeParams {
  def analog(): AnalogIOCell
  def gpio():   DigitalGPIOCell
  def input():  DigitalInIOCell
  def output(): DigitalOutIOCell
}

case class GenericIOCellParams() extends IOCellTypeParams {
  def analog() = Module(new GenericAnalogIOCell)
  def gpio() = Module(new GenericDigitalGPIOCell)
  def input() = Module(new GenericDigitalInIOCell)
  def output() = Module(new GenericDigitalOutIOCell)
}

object IOCell {

  /** From within a RawModule or MultiIOModule context, generate new module IOs from a given
    * signal and return the new IO and a Seq containing all generated IO cells.
    * @param coreSignal The signal onto which to add IO cells
    * @param name An optional name or name prefix to use for naming IO cells
    * @param abstractResetAsAsync When set, will coerce abstract resets to
    *        AsyncReset, and otherwise to Bool (sync reset)
    * @return A tuple of (the generated IO data node, a Seq of all generated IO cell instances)
    */
  def generateIOFromSignal[T <: Data](
    coreSignal:           T,
    name:                 String,
    typeParams:           IOCellTypeParams = GenericIOCellParams(),
    abstractResetAsAsync: Boolean = false
  ): (T, Seq[IOCell]) = {
    val padSignal = IO(DataMirror.internal.chiselTypeClone[T](coreSignal)).suggestName(name)
    val resetFn = if (abstractResetAsAsync) toAsyncReset else toSyncReset
    val iocells = IOCell.generateFromSignal(coreSignal, padSignal, Some(s"iocell_$name"), typeParams, resetFn)
    (padSignal, iocells)
  }

  /** Connect two identical signals together by adding IO cells between them and return a Seq
    * containing all generated IO cells.
    * @param coreSignal The core-side (internal) signal onto which to connect/add IO cells
    * @param padSignal The pad-side (external) signal onto which to connect IO cells
    * @param name An optional name or name prefix to use for naming IO cells
    * @return A Seq of all generated IO cell instances
    */
  val toSyncReset:  (Reset) => Bool = _.asBool
  val toAsyncReset: (Reset) => AsyncReset = _.asAsyncReset
  def generateFromSignal[T <: Data, R <: Reset](
    coreSignal:        T,
    padSignal:         T,
    name:              Option[String] = None,
    typeParams:        IOCellTypeParams = GenericIOCellParams(),
    concretizeResetFn: (Reset) => R = toSyncReset
  ): Seq[IOCell] = {
    def genCell[T <: Data](
      castToBool:   (T) => Bool,
      castFromBool: (Bool) => T
    )(coreSignal:   T,
      padSignal:    T
    ): Seq[IOCell] = {
      DataMirror.directionOf(coreSignal) match {
        case ActualDirection.Input => {
          val iocell = typeParams.input()
          name.foreach(n => {
            iocell.suggestName(n)
          })
          coreSignal := castFromBool(iocell.io.i)
          iocell.io.ie := true.B
          iocell.io.pad := castToBool(padSignal)
          Seq(iocell)
        }
        case ActualDirection.Output => {
          val iocell = typeParams.output()
          name.foreach(n => {
            iocell.suggestName(n)
          })
          iocell.io.o := castToBool(coreSignal)
          iocell.io.oe := true.B
          padSignal := castFromBool(iocell.io.pad)
          Seq(iocell)
        }
        case _ => throw new Exception(s"Signal does not have a direction and cannot be matched to an IOCell")
      }
    }
    def genCellForClock = genCell[Clock](_.asUInt.asBool, _.asClock) _
    def genCellForAsyncReset = genCell[AsyncReset](_.asBool, _.asAsyncReset) _
    def genCellForAbstractReset = genCell[Reset](_.asBool, concretizeResetFn) _

    (coreSignal, padSignal) match {
      case (coreSignal: Analog, padSignal: Analog) => {
        if (coreSignal.getWidth == 0) {
          Seq()
        } else {
          require(
            coreSignal.getWidth == 1,
            "Analogs wider than 1 bit are not supported because we can't bit-select Analogs (https://github.com/freechipsproject/chisel3/issues/536)"
          )
          val iocell = typeParams.analog()
          name.foreach(n => iocell.suggestName(n))
          iocell.io.core <> coreSignal
          padSignal <> iocell.io.pad
          Seq(iocell)
        }
      }
      case (coreSignal: Clock, padSignal: Clock) => genCellForClock(coreSignal, padSignal)
      case (coreSignal: AsyncReset, padSignal: AsyncReset) => genCellForAsyncReset(coreSignal, padSignal)
      case (coreSignal: Bits, padSignal: Bits) => {
        require(padSignal.getWidth == coreSignal.getWidth, "padSignal and coreSignal must be the same width")
        if (padSignal.getWidth == 0) {
          // This dummy assignment will prevent invalid firrtl from being emitted
          DataMirror.directionOf(coreSignal) match {
            case ActualDirection.Input => coreSignal := 0.U
            case _                     => {}
          }
          Seq()
        } else {
          DataMirror.directionOf(coreSignal) match {
            case ActualDirection.Input => {
              val iocells = padSignal.asBools.zipWithIndex.map { case (sig, i) =>
                val iocell = typeParams.input()
                // Note that we are relying on chisel deterministically naming this in the index order (which it does)
                // This has the side-effect of naming index 0 with no _0 suffix, which is how chisel names other signals
                // An alternative solution would be to suggestName(n + "_" + i)
                name.foreach(n => {
                  iocell.suggestName(n)
                })
                iocell.io.pad := sig
                iocell.io.ie := true.B
                iocell
              }
              // Note that the reverse here is because Cat(Seq(a,b,c,d)) yields abcd, but a is index 0 of the Seq
              coreSignal := Cat(iocells.map(_.io.i).reverse)
              iocells
            }
            case ActualDirection.Output => {
              val iocells = coreSignal.asBools.zipWithIndex.map { case (sig, i) =>
                val iocell = typeParams.output()
                // Note that we are relying on chisel deterministically naming this in the index order (which it does)
                // This has the side-effect of naming index 0 with no _0 suffix, which is how chisel names other signals
                // An alternative solution would be to suggestName(n + "_" + i)
                name.foreach(n => {
                  iocell.suggestName(n)
                })
                iocell.io.o := sig
                iocell.io.oe := true.B
                iocell
              }
              // Note that the reverse here is because Cat(Seq(a,b,c,d)) yields abcd, but a is index 0 of the Seq
              padSignal := Cat(iocells.map(_.io.pad).reverse)
              iocells
            }
            case _ => throw new Exception("Bits signal does not have a direction and cannot be matched to IOCell(s)")
          }
        }
      }
      case (coreSignal: Reset, padSignal: Reset) => genCellForAbstractReset(coreSignal, padSignal)
      case (coreSignal: Vec[_], padSignal: Vec[_]) => {
        require(padSignal.size == coreSignal.size, "size of Vec for padSignal and coreSignal must be the same")
        coreSignal.zip(padSignal).zipWithIndex.foldLeft(Seq.empty[IOCell]) { case (total, ((core, pad), i)) =>
          val ios = IOCell.generateFromSignal(core, pad, name.map(_ + "_" + i), typeParams)
          total ++ ios
        }
      }
      case (coreSignal: Record, padSignal: Record) => {
        coreSignal.elements.foldLeft(Seq.empty[IOCell]) { case (total, (eltName, core)) =>
          val pad = padSignal.elements(eltName)
          val ios = IOCell.generateFromSignal(core, pad, name.map(_ + "_" + eltName), typeParams)
          total ++ ios
        }
      }
      case _ => { throw new Exception("Oops, I don't know how to handle this signal.") }
    }
  }

}
