package xiangshan.frontend

import chipsalliance.rocketchip.config.Parameters
import chisel3._
import chisel3.util._
import utils._
import xiangshan._
import chisel3.experimental.chiselName

import scala.math.min

trait MicroBTBPatameter{
    val nWays = 16
    val lowerBitsSize = 20
    val tagSize = 20
}

@chiselName
class MicroBTB(implicit p: Parameters) extends BasePredictor
    with MicroBTBPatameter
{
    // val tagSize = VAddrBits - log2Ceil(PredictWidth) - 1
    val untaggedBits = log2Up(PredictWidth) + instOffsetBits

    class MicroBTBResp extends Resp
    {
        val targets = Vec(PredictWidth, UInt(VAddrBits.W))
        val hits = Vec(PredictWidth, Bool())
        val takens = Vec(PredictWidth, Bool())
        val brMask = Vec(PredictWidth, Bool())
        val is_RVC = Vec(PredictWidth, Bool())
    }

    class MicroBTBBranchInfo extends Meta {}

    class MicroBTBIO extends DefaultBasePredictorIO
    {
        val out = Output(new MicroBTBResp)   //
    }

    override val debug = true
    override val io = IO(new MicroBTBIO)

    def getTag(pc: UInt) = (pc >> untaggedBits)(tagSize-1, 0)
    def getBank(pc: UInt) = pc(log2Ceil(PredictWidth), instOffsetBits)

    class MicroBTBMeta extends XSBundle
    {
        val is_Br = Bool()
        val is_RVC = Bool()
        val valid = Bool()
        val pred = UInt(2.W)
        val tag = UInt(tagSize.W)
    }

    class MicroBTBData extends XSBundle
    {
        val lower = UInt(lowerBitsSize.W)
    }

    class ReadResp extends XSBundle
    {
        val valid = Bool()
        val taken = Bool()
        val target = UInt(VAddrBits.W)
        val is_RVC = Bool()
        val is_Br = Bool()
    }

    class UBTBBank(val nWays: Int) extends XSModule with HasIFUConst {
        val io = IO(new Bundle {
            val read_pc = Flipped(Valid(UInt(VAddrBits.W)))
            val read_resp = Output(new ReadResp)
            val read_hit = Output(Bool())

            val update_write_meta = Flipped(Valid(new MicroBTBMeta))
            val update_write_data = Flipped(Valid(new MicroBTBData))
            val update_taken = Input(Bool())
        })

        val debug_io = IO(new Bundle {
            val read_hit = Output(Bool())
            val read_hit_way = Output(UInt(log2Ceil(nWays).W))

            val update_hit = Output(Bool())
            val update_hit_way = Output(UInt(log2Ceil(nWays).W))
            val update_write_way = Output(UInt(log2Ceil(nWays).W))
            val update_old_pred = Output(UInt(2.W))
            val update_new_pred = Output(UInt(2.W))
        })
        val meta = Module(new AsyncDataModuleTemplate(new MicroBTBMeta, nWays, nWays*2, 1))
        val data = Module(new AsyncDataModuleTemplate(new MicroBTBData, nWays,   nWays, 1))

        for (w <- 0 until nWays) {
            meta.io.raddr(w) := w.U
            meta.io.raddr(w+nWays) := w.U
            data.io.raddr(w) := w.U
        }
        
        val rmetas = meta.io.rdata.take(nWays)
        val rdatas = data.io.rdata
        
        val packetAlignedPC = packetAligned(io.read_pc.bits)
        val read_tag = getTag(io.read_pc.bits)
        
        val hits = VecInit(rmetas.map(m => m.valid && m.tag === read_tag))
        val takens = VecInit(rmetas.map(m => m.pred(1)))
        val hit_oh = hits.asUInt
        val hit_and_taken = VecInit((hits zip takens) map {case (h, t) => h && t}).asUInt.orR
        val hit_meta = ParallelMux(hits zip rmetas)
        val hit_data = ParallelMux(hits zip rdatas)
        val target = Cat(io.read_pc.bits(VAddrBits-1, lowerBitsSize+instOffsetBits), hit_data.lower, 0.U(instOffsetBits.W))
        
        val ren = io.read_pc.valid
        io.read_resp.valid := ren
        io.read_resp.is_RVC := ren && hit_meta.is_RVC
        io.read_resp.is_Br := ren && hit_meta.is_Br
        io.read_resp.taken := ren && hit_and_taken
        io.read_resp.target := target
        io.read_hit := hit_oh.orR

        debug_io.read_hit := hit_oh.orR
        debug_io.read_hit_way := OHToUInt(hit_oh)
        
        val do_reset = RegInit(true.B)
        val reset_way = RegInit(0.U(log2Ceil(nWays).W))
        when (RegNext(reset.asBool) && !reset.asBool) {
            do_reset := true.B
            reset_way := 0.U
        }
        when (do_reset) { reset_way := reset_way + 1.U }
        when (reset_way === (nWays-1).U) { do_reset := false.B }

        val update_rmetas = meta.io.rdata.drop(nWays)
        val update_tag = io.update_write_meta.bits.tag
        val update_hits = VecInit(update_rmetas.map(m => m.valid && m.tag === update_tag))
        val update_hit = update_hits.asUInt.orR
        val update_hit_way = OHToUInt(update_hits.asUInt)
        val update_hit_meta = ParallelMux(update_hits zip update_rmetas)
        val update_old_pred = update_hit_meta.pred
        val update_new_pred =
            Mux(update_hit,
                satUpdate(update_old_pred, 2, io.update_taken),
                Mux(io.update_taken, 3.U, 0.U))
        val update_alloc_way = {
            val source = Cat(VecInit(update_rmetas.map(_.tag)).asUInt, update_tag)
            val l = log2Ceil(nWays)
            val nChunks = (source.getWidth + l - 1) / l
            val chunks = (0 until nChunks) map { i =>
                source(min((i+1)*l, source.getWidth)-1, i*l)
            }
            ParallelXOR(chunks)
        }
        val update_emptys = update_rmetas.map(m => !m.valid)
        val update_has_empty_way = update_emptys.reduce(_||_)
        val update_empty_way = ParallelPriorityEncoder(update_emptys)
        val update_way = Mux(update_hit, update_hit_way, Mux(update_has_empty_way, update_empty_way, update_alloc_way))
    
        meta.io.waddr(0) := Mux(do_reset, reset_way, RegNext(update_way))
        meta.io.wen(0)   := do_reset || RegNext(io.update_write_meta.valid)
        meta.io.wdata(0) := Mux(do_reset,
                                0.U.asTypeOf(new MicroBTBMeta),
                                RegNext(io.update_write_meta.bits))
        meta.io.wdata(0).pred := Mux(do_reset, 0.U(2.W), RegNext(update_new_pred))
        data.io.waddr(0) := Mux(do_reset, reset_way, RegNext(update_way))
        data.io.wen(0)   := do_reset || RegNext(io.update_write_data.valid)
        data.io.wdata(0) := Mux(do_reset,
                                0.U.asTypeOf(new MicroBTBData),
                                RegNext(io.update_write_data.bits))
        
        debug_io.update_hit := update_hit
        debug_io.update_hit_way := update_hit_way
        debug_io.update_write_way := update_way
        debug_io.update_old_pred := update_old_pred
        debug_io.update_new_pred := update_new_pred
    }

    val ubtbBanks = Seq.fill(PredictWidth)(Module(new UBTBBank(nWays)))
    val banks = VecInit(ubtbBanks.map(_.io))
    
    val read_resps = VecInit(banks.map(b => b.read_resp))

    for (b <- 0 until PredictWidth) {
        banks(b).read_pc.valid := io.inMask(b)
        banks(b).read_pc.bits := io.pc.bits
        
        //only when hit and instruction valid and entry valid can output data
        io.out.targets(b) := read_resps(b).target
        io.out.hits(b)   := banks(b).read_hit && ctrl.ubtb_enable
        io.out.takens(b) := read_resps(b).taken
        io.out.is_RVC(b) := read_resps(b).is_RVC
        io.out.brMask(b) := read_resps(b).is_Br
    }

    //uBTB update 
    //backend should send fetch pc to update
    val u = RegNext(io.update.bits)
    val update_valid = RegNext(io.update.valid)
    val update_packet_pc = packetAligned(u.ftqPC)
    val update_takens = u.takens

    val update_tag = getTag(update_packet_pc)
    val update_target_lower = u.target(lowerBitsSize-1+instOffsetBits, instOffsetBits)
  
    // only when taken should we update target
    val data_write_valids = 
        VecInit((0 until PredictWidth).map(i =>
            update_valid && u.valids(i) && u.takens(i)))
    val meta_write_valids = 
        VecInit((0 until PredictWidth).map(i =>
            update_valid && u.valids(i) && (u.br_mask(i) || u.takens(i))))
        
    val update_write_metas = Wire(Vec(PredictWidth, new MicroBTBMeta))
    val update_write_datas = Wire(Vec(PredictWidth, new MicroBTBData))
    for (i <- 0 until PredictWidth) {
        update_write_metas(i).is_Br  := u.br_mask(i)
        update_write_metas(i).is_RVC := u.rvc_mask(i)
        update_write_metas(i).valid  := true.B
        update_write_metas(i).tag    := update_tag
        update_write_metas(i).pred   := DontCare

        update_write_datas(i).lower := update_target_lower
    }
    
    for (b <- 0 until PredictWidth) {
        banks(b).update_write_meta.valid := meta_write_valids(b)
        banks(b).update_write_meta.bits := update_write_metas(b)
        banks(b).update_write_data.valid := data_write_valids(b)
        banks(b).update_write_data.bits := update_write_datas(b)
        banks(b).update_taken := update_takens(b)
    }

    if (!env.FPGAPlatform) {
        XSPerfAccumulate("ubtb_commit_hits",
            PopCount((u.takens zip u.valids zip u.metas zip u.pd) map {
                case (((t, v), m), pd)  => t && v && m.ubtbHit.asBool && !pd.notCFI && update_valid}))
        XSPerfAccumulate("ubtb_commit_misses",
            PopCount((u.takens zip u.valids zip u.metas zip u.pd) map {
                case (((t, v), m), pd)  => t && v && !m.ubtbHit.asBool && !pd.notCFI && update_valid}))
    }
    
    if (BPUDebug && debug) {
        val update_pcs  = VecInit((0 until PredictWidth).map(i => update_packet_pc + (i << instOffsetBits).U))
        val update_bank = u.cfiIndex.bits
        val read_valid = io.pc.valid
        val read_req_tag = getTag(io.pc.bits)
        val debug_banks = VecInit(ubtbBanks.map(_.debug_io))
        val read_hit_vec = VecInit(debug_banks.map(b => b.read_hit))
        val read_hit_ways = VecInit(debug_banks.map(b => b.read_hit_way))
        val update_hits = VecInit(debug_banks.map(b => b.update_hit))
        val update_hit_ways = VecInit(debug_banks.map(b => b.update_hit_way))
        val update_write_ways = VecInit(debug_banks.map(b => b.update_write_way))
        val update_old_preds = VecInit(debug_banks.map(b => b.update_old_pred))
        val update_new_preds = VecInit(debug_banks.map(b => b.update_new_pred))
        XSDebug(read_valid,p"uBTB read req: pc:0x${Hexadecimal(io.pc.bits)}, tag:${Hexadecimal(read_req_tag)}\n")
        XSDebug(read_valid,p"uBTB read resp: read_hit_vec:${Binary(read_hit_vec.asUInt)}\n")
        for(i <- 0 until PredictWidth) {
            XSDebug(read_valid,
                p"bank($i) hit:${read_hit_vec(i)} way:${read_hit_ways(i)} " +
                p"valid:${read_resps(i).valid} is_RVC:${read_resps(i).is_RVC} " +
                p"taken:${read_resps(i).taken} isBr:${read_resps(i).is_Br} " +
                p"target:0x${Hexadecimal(read_resps(i).target)}\n")
            XSDebug(data_write_valids(i),
                p"uBTB update data($i): update pc:0x${Hexadecimal(update_pcs(i))} " +
                p"update_lower 0x$update_target_lower\n")
            XSDebug(meta_write_valids(i),
                p"update hit:${update_hits(i)} udpate_hit_way:${update_hit_ways(i)} " +
                p"update_write_way:${update_write_ways(i)} update_taken:${update_takens(i)}" +
                p"isBr:${u.br_mask(i)} isRVC:${u.rvc_mask(i)} update_tag:${update_tag}" +
                p"update_pred(${update_old_preds(i)} -> ${update_new_preds(i)}\n")
        }
    }
}
