package vexriscv.plugin

import vexriscv.{DecoderService, ExceptionCause, ExceptionService, Stage, Stageable, VexRiscv}
import spinal.core._
import spinal.lib._
import spinal.lib.bus.bmb.WeakConnector
import spinal.lib.bus.misc.{AddressMapping, DefaultMapping}
import vexriscv.Riscv.IMM

case class CfuPluginParameter(
                        CFU_VERSION : Int,
                        CFU_INTERFACE_ID_W : Int,
                        CFU_FUNCTION_ID_W : Int,
                        CFU_REORDER_ID_W : Int,
                        CFU_REQ_RESP_ID_W : Int,
                        CFU_INPUTS : Int,
                        CFU_INPUT_DATA_W : Int,
                        CFU_OUTPUTS : Int,
                        CFU_OUTPUT_DATA_W : Int,
                        CFU_FLOW_REQ_READY_ALWAYS : Boolean,
                        CFU_FLOW_RESP_READY_ALWAYS : Boolean)

case class CfuBusParameter(CFU_VERSION : Int = 0,
                           CFU_INTERFACE_ID_W : Int = 0,
                           CFU_FUNCTION_ID_W : Int,
                           CFU_CFU_ID_W : Int = 0,
                           CFU_REORDER_ID_W : Int = 0,
                           CFU_REQ_RESP_ID_W : Int = 0,
                           CFU_STATE_INDEX_NUM : Int = 0,
                           CFU_INPUTS : Int,
                           CFU_INPUT_DATA_W : Int,
                           CFU_OUTPUTS : Int,
                           CFU_OUTPUT_DATA_W : Int,
                           CFU_FLOW_REQ_READY_ALWAYS : Boolean,
                           CFU_FLOW_RESP_READY_ALWAYS : Boolean,
                           CFU_WITH_STATUS : Boolean = false,
                           CFU_RAW_INSN_W : Int = 0)

case class CfuCmd( p : CfuBusParameter ) extends Bundle{
  val function_id = UInt(p.CFU_FUNCTION_ID_W bits)
  val reorder_id = UInt(p.CFU_REORDER_ID_W bits)
  val request_id = UInt(p.CFU_REQ_RESP_ID_W bits)
  val inputs = Vec(Bits(p.CFU_INPUT_DATA_W bits), p.CFU_INPUTS)
  val state_index = UInt(log2Up(p.CFU_STATE_INDEX_NUM) bits)
  val cfu_index = UInt(p.CFU_CFU_ID_W bits)
  val raw_insn = Bits(p.CFU_RAW_INSN_W bits)
  def weakAssignFrom(m : CfuCmd): Unit ={
    def s = this
    WeakConnector(m, s, m.function_id, s.function_id, defaultValue = null, allowUpSize = false, allowDownSize = true , allowDrop = true)
    WeakConnector(m, s, m.reorder_id,  s.reorder_id,  defaultValue = null, allowUpSize = false , allowDownSize = false, allowDrop = false)
    WeakConnector(m, s, m.request_id,  s.request_id,  defaultValue = null, allowUpSize = false, allowDownSize = false, allowDrop = false)
    s.inputs := m.inputs
  }
}

case class CfuRsp(p : CfuBusParameter) extends Bundle{
  val response_id = UInt(p.CFU_REQ_RESP_ID_W bits)
  val outputs = Vec(Bits(p.CFU_OUTPUT_DATA_W bits), p.CFU_OUTPUTS)
  val status = p.CFU_WITH_STATUS generate Bits(3 bits)

  def weakAssignFrom(m : CfuRsp): Unit ={
    def s = this
    s.response_id := m.response_id
    s.outputs := m.outputs
  }
}

case class CfuBus(p : CfuBusParameter) extends Bundle with IMasterSlave{
  val cmd = Stream(CfuCmd(p))
  val rsp = Stream(CfuRsp(p))

  def <<(m : CfuBus) : Unit = {
    val s = this
    s.cmd.arbitrationFrom(m.cmd)
    m.rsp.arbitrationFrom(s.rsp)

    s.cmd.weakAssignFrom(m.cmd)
    m.rsp.weakAssignFrom(s.rsp)
  }

  override def asMaster(): Unit = {
    master(cmd)
    slave(rsp)
  }
}

object CfuPlugin{
  object Input2Kind extends SpinalEnum{
    val RS, IMM_I = newElement()
  }
}

case class CfuPluginEncoding(instruction : MaskedLiteral,
                             functionId : List[Range],
                             input2Kind : CfuPlugin.Input2Kind.E){
  val functionIdWidth = functionId.map(_.size).sum
}

class CfuPlugin(val stageCount : Int,
                val allowZeroLatency : Boolean,
                val busParameter : CfuBusParameter,
                val encodings : List[CfuPluginEncoding] = null,
                val stateAndIndexCsrOffset : Int = 0xBC0,
                val statusCsrOffset : Int = 0x801,
                val withEnable : Boolean = true,
                val enableInit : Boolean = false) extends Plugin[VexRiscv]{
  def p = busParameter

  assert(p.CFU_INPUTS <= 2)
  assert(p.CFU_OUTPUTS == 1)
//  assert(p.CFU_FUNCTION_ID_W == 3)

  var bus : CfuBus = null

  lazy val forkStage = pipeline.execute
  lazy val joinStage = pipeline.stages(Math.min(pipeline.stages.length - 1, pipeline.indexOf(forkStage) + stageCount))


  val CFU_ENABLE = new Stageable(Bool()).setCompositeName(this, "CFU_ENABLE")
  val CFU_IN_FLIGHT = new Stageable(Bool()).setCompositeName(this, "CFU_IN_FLIGHT")
  val CFU_ENCODING = new Stageable(UInt(log2Up(encodings.size) bits)).setCompositeName(this, "CFU_ENCODING")
  val CFU_INPUT_2_KIND = new Stageable(CfuPlugin.Input2Kind()).setCompositeName(this, "CFU_INPUT_2_KIND")

  override def setup(pipeline: VexRiscv): Unit = {
    import pipeline._
    import pipeline.config._

    bus = master(CfuBus(p))

    val decoderService = pipeline.service(classOf[DecoderService])
    decoderService.addDefault(CFU_ENABLE, False)

    for((encoding, id) <- encodings.zipWithIndex){
      var actions = List(
        CFU_ENABLE -> True,
        REGFILE_WRITE_VALID      -> True,
        BYPASSABLE_EXECUTE_STAGE -> Bool(stageCount == 0),
        BYPASSABLE_MEMORY_STAGE  -> Bool(stageCount <= 1),
        RS1_USE -> True,
        CFU_ENCODING -> U(id),
        CFU_INPUT_2_KIND -> encoding.input2Kind()
      )

      encoding.input2Kind match {
        case CfuPlugin.Input2Kind.RS =>
          actions :+= RS2_USE -> True
        case CfuPlugin.Input2Kind.IMM_I =>
      }

      decoderService.add(
        key = encoding.instruction,
        values = actions
      )
    }
  }

  override def build(pipeline: VexRiscv): Unit = {
    import pipeline._
    import pipeline.config._

    val csr = pipeline plug new Area{
      val factory = pipeline.service(classOf[CsrInterface])
      val en = withEnable generate (Reg(Bool()) init(enableInit))
      if(withEnable) factory.rw(stateAndIndexCsrOffset, 31, en)

      val stateId = Reg(UInt(log2Up(p.CFU_STATE_INDEX_NUM) bits)) init(0)
      if(p.CFU_STATE_INDEX_NUM > 1) {
        assert(stateAndIndexCsrOffset != -1, "CfuPlugin stateCsrIndex need to be set in the parameters")
        factory.rw(stateAndIndexCsrOffset, 16, stateId)
      }

      val cfuIndex = Reg(UInt(p.CFU_CFU_ID_W bits)) init(0)
      if(p.CFU_CFU_ID_W != 0){
        factory.rw(stateAndIndexCsrOffset, 0, cfuIndex)
      }
      val status = p.CFU_WITH_STATUS generate new Area{
        val CU, OP, FI, OF, SI, CI = RegInit(False)
        val flags = List(CU, OP, FI, OF, SI, CI).reverse
        factory.rw(statusCsrOffset,  flags.zipWithIndex.map(_.swap) :_*)
        factory.duringWrite(statusCsrOffset){
          decode.arbitration.haltByOther := True //Handle CSRW to decode
        }
      }
    }

    if(withEnable) when(decode.insert(CFU_ENABLE) && !csr.en){
      pipeline.service(classOf[DecoderService]).forceIllegal()
    }

    forkStage plug new Area{
      import forkStage._
      input(CFU_ENABLE).clearWhen(!input(LEGAL_INSTRUCTION))
      val hazard = stages.dropWhile(_ != forkStage).tail.map(s => s.arbitration.isValid && s.input(HAS_SIDE_EFFECT)).orR
      val scheduleWish = arbitration.isValid && input(CFU_ENABLE)
      val schedule = scheduleWish && !hazard
      arbitration.haltItself setWhen(scheduleWish && hazard)

      val hold = RegInit(False) setWhen(schedule) clearWhen(bus.cmd.ready)
      val fired = RegInit(False) setWhen(bus.cmd.fire) clearWhen(!arbitration.isStuck)
      insert(CFU_IN_FLIGHT) := schedule || hold || fired

      bus.cmd.valid := (schedule || hold) && !fired
      arbitration.haltItself setWhen(bus.cmd.valid && !bus.cmd.ready)

//      bus.cmd.function_id := U(input(INSTRUCTION)(14 downto 12)).resized
      val functionIdFromInstructinoWidth = encodings.map(_.functionIdWidth).max
      val functionsIds = encodings.map(e => U(Cat(e.functionId.map(r => input(INSTRUCTION)(r))), functionIdFromInstructinoWidth bits))
      bus.cmd.cfu_index := csr.cfuIndex
      bus.cmd.state_index := csr.stateId
      bus.cmd.function_id := functionsIds.read(input(CFU_ENCODING))
      bus.cmd.reorder_id := 0
      bus.cmd.request_id := 0
      bus.cmd.raw_insn   := input(INSTRUCTION).resized
      if(p.CFU_INPUTS >= 1) bus.cmd.inputs(0) := input(RS1)
      if(p.CFU_INPUTS >= 2)  bus.cmd.inputs(1) := input(CFU_INPUT_2_KIND).mux(
        CfuPlugin.Input2Kind.RS -> input(RS2),
        CfuPlugin.Input2Kind.IMM_I -> IMM(input(INSTRUCTION)).h_sext
      )
    }

    joinStage plug new Area{
      import joinStage._

      //If the CFU interface can produce a result combinatorialy and the fork stage isn't the same than the join stage
      //Then it is required to add a buffer on rsp to not propagate the fork stage ready := False in the CPU pipeline.
      val rsp = if(p.CFU_FLOW_RESP_READY_ALWAYS){
        bus.rsp.toFlow.toStream.queueLowLatency(
          size = stageCount + 1,
          latency = 0
        )
      } else if(forkStage != joinStage && allowZeroLatency) {
        bus.rsp.s2mPipe()
      } else {
        bus.rsp.combStage()
      }

      rsp.ready := False
      when(input(CFU_IN_FLIGHT)){
        arbitration.haltItself setWhen(!rsp.valid)
        rsp.ready := !arbitration.isStuckByOthers
        output(REGFILE_WRITE_DATA) := rsp.outputs(0)
        if(p.CFU_WITH_STATUS) when(arbitration.isFiring){
          switch(rsp.status) {
            for (i <- 1 to 6) is(i) {
              csr.status.flags(i-1) := True
            }
          }
        }
      }
    }

    pipeline.stages.drop(1).foreach(s => s.output(CFU_IN_FLIGHT) clearWhen(s.arbitration.isStuck))
    addPrePopTask(() => stages.dropWhile(_ != memory).reverse.dropWhile(_ != joinStage).foreach(s => s.input(CFU_IN_FLIGHT).init(False)))
  }
}


object CfuTest{

//  stageCount = 0,
//  allowZeroLatency = true,
  def getCfuParameter() = CfuBusParameter(
    CFU_VERSION = 0,
    CFU_INTERFACE_ID_W = 0,
    CFU_FUNCTION_ID_W = 3,
    CFU_REORDER_ID_W = 0,
    CFU_REQ_RESP_ID_W = 0,
    CFU_INPUTS = 2,
    CFU_INPUT_DATA_W = 32,
    CFU_OUTPUTS = 1,
    CFU_OUTPUT_DATA_W = 32,
    CFU_FLOW_REQ_READY_ALWAYS = false,
    CFU_FLOW_RESP_READY_ALWAYS = false
  )
}
case class CfuTest() extends Component{
  val io = new Bundle {
    val bus = slave(CfuBus(CfuTest.getCfuParameter()))
  }
  io.bus.rsp.arbitrationFrom(io.bus.cmd)
  io.bus.rsp.response_id := io.bus.cmd.request_id
  io.bus.rsp.outputs(0) := ~(io.bus.cmd.inputs(0) & io.bus.cmd.inputs(1))
}


case class CfuBb(p : CfuBusParameter) extends BlackBox{
  val io = new Bundle {
    val clk, reset = in Bool()
    val bus = slave(CfuBus(p))
  }

  mapCurrentClockDomain(io.clk, io.reset)
}

//case class CfuGray(p : CfuBusParameter) extends BlackBox{
//  val req_function_id = in Bits(p.CFU_FUNCTION_ID_W)
//  val req_data = in Bits(p.CFU_REQ_INPUTS)
//  val resp_data = in Bits(p.CFU_FUNCTION_ID_W)
//  input `CFU_FUNCTION_ID req_function_id,
//  input [CFU_REQ_INPUTS-1:0]`CFU_REQ_DATA req_data,
//  output [CFU_RESP_OUTPUTS-1:0]`CFU_RESP_DATA resp_data
//  io.bus.rsp.arbitrationFrom(io.bus.cmd)
//  io.bus.rsp.response_ok := True
//  io.bus.rsp.response_id := io.bus.cmd.request_id
//  io.bus.rsp.outputs(0) := ~(io.bus.cmd.inputs(0) & io.bus.cmd.inputs(1))
//}


case class CfuDecoder(p : CfuBusParameter,
                      mappings : Seq[AddressMapping],
                      pendingMax : Int = 3) extends Component{
  val io = new Bundle {
    val input = slave(CfuBus(p))
    val outputs = Vec(master(CfuBus(p)), mappings.size)
  }
  val hasDefault = mappings.contains(DefaultMapping)
  val logic = if(hasDefault && mappings.size == 1){
    io.outputs(0) << io.input
  } else new Area {
    val hits = Vec(Bool, mappings.size)
    for (portId <- 0 until mappings.length) yield {
      val slaveBus = io.outputs(portId)
      val memorySpace = mappings(portId)
      val hit = hits(portId)
      hit := (memorySpace match {
        case DefaultMapping => !hits.filterNot(_ == hit).orR
        case _ => memorySpace.hit(io.input.cmd.function_id)
      })
      slaveBus.cmd.valid := io.input.cmd.valid && hit
      slaveBus.cmd.payload := io.input.cmd.payload.resized
    }
    val noHit = if (!hasDefault) !hits.orR else False
    io.input.cmd.ready := (hits, io.outputs).zipped.map(_ && _.cmd.ready).orR || noHit

    val rspPendingCounter = Reg(UInt(log2Up(pendingMax + 1) bits)) init(0)
    rspPendingCounter := rspPendingCounter + U(io.input.cmd.fire) - U(io.input.rsp.fire)
    val rspHits = RegNextWhen(hits, io.input.cmd.fire)
    val rspPending = rspPendingCounter =/= 0
    val rspNoHitValid = if (!hasDefault) !rspHits.orR else False
    val rspNoHit = !hasDefault generate new Area{
      val doIt = RegInit(False) clearWhen(io.input.rsp.fire) setWhen(io.input.cmd.fire && noHit)
      val response_id = RegNextWhen(io.input.cmd.request_id, io.input.cmd.fire)
    }

    io.input.rsp.valid := io.outputs.map(_.rsp.valid).orR || (rspPending && rspNoHitValid)
    io.input.rsp.payload := io.outputs.map(_.rsp.payload).read(OHToUInt(rspHits))
    if(!hasDefault) when(rspNoHit.doIt) {
      io.input.rsp.valid := True
      io.input.rsp.response_id := rspNoHit.response_id
    }
    for(output <- io.outputs) output.rsp.ready := io.input.rsp.ready

    val cmdWait = (rspPending && (hits =/= rspHits || rspNoHitValid)) || rspPendingCounter === pendingMax
    when(cmdWait) {
      io.input.cmd.ready := False
      io.outputs.foreach(_.cmd.valid := False)
    }
  }
}
