/*
 * Copyright (c) 2021 Samuel Lindemer <samuel.lindemer@ri.se>
 *
 * SPDX-License-Identifier: MIT
 */

package vexriscv.plugin

import vexriscv.{VexRiscv, _}
import vexriscv.plugin.MemoryTranslatorPort.{_}
import spinal.core._
import spinal.lib._
import spinal.lib.fsm._

/* Each 32-bit pmpcfg# register contains four 8-bit configuration sections.
 * These section numbers contain flags which apply to regions defined by the
 * corresponding pmpaddr# register.
 *
 *    3                   2                   1
 *  1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 * |    pmp3cfg    |    pmp2cfg    |    pmp1cfg    |    pmp0cfg    | pmpcfg0
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 * |    pmp7cfg    |    pmp6cfg    |    pmp5cfg    |    pmp4cfg    | pmpcfg2
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *
 *     7       6       5       4       3       2       1       0
 * +-------+-------+-------+-------+-------+-------+-------+-------+
 * |   L   |       0       |       A       |   X   |   W   |   R   | pmp#cfg
 * +-------+-------+-------+-------+-------+-------+-------+-------+
 *
 *	  L: locks configuration until system reset (including M-mode)
 *	  0: hardwired to zero
 *	  A: 0 = OFF (null region / disabled)
 *	     1 = TOR (top of range)
 * 	     2 = NA4 (naturally aligned four-byte region)
 *	     3 = NAPOT (naturally aligned power-of-two region, > 7 bytes)
 *	  X: execute
 *	  W: write
 *	  R: read
 *
 * TOR: Each 32-bit pmpaddr# register defines the upper bound of the pmp region
 * right-shifted by two bits. The lower bound of the region is the previous
 * pmpaddr# register. In the case of pmpaddr0, the lower bound is address 0x0.
 *
 *    3                   2                   1
 *  1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 * |                        address[33:2]                          | pmpaddr#
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *
 * NAPOT: Each 32-bit pmpaddr# register defines the region address and the size
 * of the pmp region. The number of concurrent 1s begging at the LSB indicates
 * the size of the region as a power of two (e.g. 0x...0 = 8-byte, 0x...1 =
 * 16-byte, 0x...11 = 32-byte, etc.).
 *
 *    3                   2                   1
 *  1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 * |                        address[33:2]                |0|1|1|1|1| pmpaddr#
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *
 * NA4: This is essentially an edge case of NAPOT where the entire pmpaddr#
 * register defines a 4-byte wide region.
 * 
 * N.B. THIS IMPLEMENTATION ONLY SUPPORTS NAPOT ADDRESSING. REGIONS ARE NOT
 * ORDERED BY PRIORITY. A PERMISSION IS GRANTED TO AN ACCESS IF ANY MATCHING
 * PMP REGION HAS THAT PERMISSION ENABLED.
 */

trait Pmp {
  def OFF = 0
  def TOR = 1
  def NA4 = 2
  def NAPOT = 3

  def xlen = 32
  def rBit = 0
  def wBit = 1
  def xBit = 2
  def aBits = 4 downto 3
  def lBit = 7
}

class PmpSetter(cutoff : Int) extends Component with Pmp {
  val io = new Bundle {
    val addr = in UInt(xlen bits)
    val base, mask = out UInt(xlen - cutoff bits)
  }

  val ones = io.addr & ~(io.addr + 1)
  io.base := io.addr(xlen - 3 downto cutoff - 2) ^ ones(xlen - 3 downto cutoff - 2)
  io.mask := ~(ones(xlen - 4 downto cutoff - 2) @@ U"1")
}

case class ProtectedMemoryTranslatorPort(bus : MemoryTranslatorBus)

class PmpPluginNapot(regions : Int, granularity : Int, ioRange : UInt => Bool) extends Plugin[VexRiscv] with MemoryTranslator with Pmp {
  assert(regions % 4 == 0 & regions <= 16)
  assert(granularity >= 8)

  var setter : PmpSetter = null
  var dPort, iPort : ProtectedMemoryTranslatorPort = null
  val cutoff = log2Up(granularity) - 1
  
  override def newTranslationPort(priority : Int, args : Any): MemoryTranslatorBus = {
    val port = ProtectedMemoryTranslatorPort(MemoryTranslatorBus(new MemoryTranslatorBusParameter(0, 0)))
    priority match {
      case PRIORITY_INSTRUCTION => iPort = port
      case PRIORITY_DATA => dPort = port
    }
    port.bus
  }

  override def setup(pipeline: VexRiscv): Unit = {
    setter = new PmpSetter(cutoff)
  }

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

    val csrService = pipeline.service(classOf[CsrInterface])
    val privilegeService = pipeline.service(classOf[PrivilegeService])

    val state = pipeline plug new Area {
      val pmpaddr = Mem(UInt(xlen bits), regions)
      val pmpcfg = Vector.fill(regions)(Reg(Bits(8 bits)) init (0))
      val base, mask = Vector.fill(regions)(Reg(UInt(xlen - cutoff bits)))
    }

    def machineMode : Bool = privilegeService.isMachine()

    execute plug new Area {
      import execute._

      val fsmPending = RegInit(False) clearWhen(!arbitration.isStuck)
      val fsmComplete = False
      val hazardFree = csrService.isHazardFree()

      val csrAddress = input(INSTRUCTION)(csrRange)
      val pmpNcfg = csrAddress(log2Up(regions) - 1 downto 0).asUInt
      val pmpcfgN = pmpNcfg(log2Up(regions) - 3 downto 0)
      val pmpcfgCsr = input(INSTRUCTION)(31 downto 24) === 0x3a
      val pmpaddrCsr = input(INSTRUCTION)(31 downto 24) === 0x3b

      val pmpNcfg_ = Reg(UInt(log2Up(regions) bits))
      val pmpcfgN_ = Reg(UInt(log2Up(regions) - 2 bits))
      val pmpcfgCsr_ = RegInit(False)
      val pmpaddrCsr_ = RegInit(False)
      val writeData_ = Reg(Bits(xlen bits))

      csrService.duringAnyRead {
        when (machineMode) {
          when (pmpcfgCsr) {
            csrService.allowCsr()
            csrService.readData() :=
              state.pmpcfg(pmpcfgN @@ U(3, 2 bits)) ##
              state.pmpcfg(pmpcfgN @@ U(2, 2 bits)) ##
              state.pmpcfg(pmpcfgN @@ U(1, 2 bits)) ##
              state.pmpcfg(pmpcfgN @@ U(0, 2 bits))
          }
          when (pmpaddrCsr) {
            csrService.allowCsr()
            csrService.readData() := state.pmpaddr(pmpNcfg).asBits
          }
        }
      }

      csrService.duringAnyWrite {
        when ((pmpcfgCsr | pmpaddrCsr) & machineMode) {
          csrService.allowCsr()
          arbitration.haltItself := !fsmComplete
          when (!fsmPending && hazardFree) {
            fsmPending := True
            writeData_ := csrService.writeData()
            pmpNcfg_ := pmpNcfg
            pmpcfgN_ := pmpcfgN
            pmpcfgCsr_ := pmpcfgCsr
            pmpaddrCsr_ := pmpaddrCsr
          }
        }
      }

      val fsm = new StateMachine {
        val fsmEnable = RegInit(False)
        val fsmCounter = Reg(UInt(log2Up(regions) bits)) init(0)

        val stateIdle : State = new State with EntryPoint {
          onEntry {
            fsmPending := False
            fsmEnable := False
            fsmComplete := True
            fsmCounter := 0
          }
          whenIsActive {
            when (fsmPending) {
              goto(stateWrite)
            }
          }
        }

        val stateWrite : State = new State {
          whenIsActive {
            when (pmpcfgCsr_) {
              val overwrite = writeData_.subdivideIn(8 bits)
              for (i <- 0 until 4) {
                when (~state.pmpcfg(pmpcfgN_ @@ U(i, 2 bits))(lBit)) {
                  state.pmpcfg(pmpcfgN_ @@ U(i, 2 bits)).assignFromBits(overwrite(i))
                }
              }
              goto(stateCfg)
            }
            when (pmpaddrCsr_) {
              when (~state.pmpcfg(pmpNcfg_)(lBit)) {
                state.pmpaddr(pmpNcfg_) := writeData_.asUInt
              }
              goto(stateAddr)
            }
          }
          onExit (fsmEnable := True)
        }

        val stateCfg : State = new State {
          onEntry (fsmCounter := pmpcfgN_ @@ U(0, 2 bits))
          whenIsActive {
            fsmCounter := fsmCounter + 1
            when (fsmCounter(1 downto 0) === 3) {
              goto(stateIdle)
            }
          }
        }

        val stateAddr : State = new State {
          onEntry (fsmCounter := pmpNcfg_)
          whenIsActive (goto(stateIdle))
        }

        when (pmpaddrCsr_) {
          setter.io.addr := writeData_.asUInt
        } otherwise {
          setter.io.addr := state.pmpaddr(fsmCounter)
        }
        
        when (fsmEnable & ~state.pmpcfg(fsmCounter)(lBit)) {
          state.base(fsmCounter) := setter.io.base
          state.mask(fsmCounter) := setter.io.mask
        }
      }
    }

    pipeline plug new Area {
      def getHits(address : UInt) = {
        (0 until regions).map(i =>
            ((address & state.mask(U(i, log2Up(regions) bits))) === state.base(U(i, log2Up(regions) bits))) &
            (state.pmpcfg(i)(lBit) | ~machineMode) & (state.pmpcfg(i)(aBits) === NAPOT)
        )
      }

      def getPermission(hits : IndexedSeq[Bool], bit : Int) = {
        MuxOH(OHMasking.first(hits), state.pmpcfg.map(_(bit)))
      }

      val dGuard = new Area {
        val address = dPort.bus.cmd(0).virtualAddress
        dPort.bus.rsp.physicalAddress := address
        dPort.bus.rsp.isIoAccess := ioRange(address)
        dPort.bus.rsp.isPaging := False
        dPort.bus.rsp.exception := False
        dPort.bus.rsp.refilling := False
        dPort.bus.rsp.allowExecute := False
        dPort.bus.busy := False

        val hits = getHits(address(31 downto cutoff))

        when(~hits.orR) {
          dPort.bus.rsp.allowRead := machineMode
          dPort.bus.rsp.allowWrite := machineMode
        } otherwise {
          dPort.bus.rsp.allowRead := getPermission(hits, rBit)
          dPort.bus.rsp.allowWrite := getPermission(hits, wBit)
        }
      }

      val iGuard = new Area {
        val address = iPort.bus.cmd(0).virtualAddress
        iPort.bus.rsp.physicalAddress := address
        iPort.bus.rsp.isIoAccess := ioRange(address)
        iPort.bus.rsp.isPaging := False
        iPort.bus.rsp.exception := False
        iPort.bus.rsp.refilling := False
        iPort.bus.rsp.allowRead := False
        iPort.bus.rsp.allowWrite := False
        iPort.bus.busy := False

        val hits = getHits(address(31 downto cutoff))

        when(~hits.orR) {
          iPort.bus.rsp.allowExecute := machineMode
        } otherwise {
          iPort.bus.rsp.allowExecute := getPermission(hits, xBit)
        }
      }
    }
  }
}
