// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Create triggers necessary for scheduling across
//                         virtual interfaces
//
// Code available from: https://verilator.org
//
//*************************************************************************
//
// Copyright 2003-2025 by Wilson Snyder. This program is free software; you
// can redistribute it and/or modify it under the terms of either the GNU
// Lesser General Public License Version 3 or the Perl Artistic License
// Version 2.0.
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
//
//*************************************************************************
// V3SchedVirtIface's Transformations:
//
// Each interface type written to via virtual interface, or written to normally but read via
// virtual interface:
//     Create a trigger var for it
// Each statement:
//     If it writes to a virtual interface, or to a variable read via virtual interface:
//         Set the corresponding trigger to 1
//         If the write is done by an AssignDly, the trigger is also set by AssignDly
//
//*************************************************************************

#include "V3PchAstNoMT.h"  // VL_MT_DISABLED_CODE_UNIT

#include "V3AstNodeExpr.h"
#include "V3Sched.h"
#include "V3UniqueNames.h"

VL_DEFINE_DEBUG_FUNCTIONS;

namespace V3Sched {

namespace {

class VirtIfaceVisitor final : public VNVisitor {
private:
    // NODE STATE
    // AstIface::user1() -> AstVarScope*. Trigger var for this interface
    const VNUser1InUse m_user1InUse;

    // TYPES
    using OnWriteToVirtIface = std::function<void(AstVarRef*, AstIface*)>;
    using OnWriteToVirtIfaceMember
        = std::function<void(AstVarRef*, AstIface*, const std::string&)>;

    // STATE
    AstNetlist* const m_netlistp;  // Root node
    AstAssign* m_trigAssignp = nullptr;  // Previous/current trigger assignment
    AstIface* m_trigAssignIfacep = nullptr;  // Interface type whose trigger is assigned
                                             // by m_trigAssignp
    AstVar* m_trigAssignMemberVarp = nullptr;  // Member pointer whose trigger is assigned
    V3UniqueNames m_vifTriggerNames{"__VvifTrigger"};  // Unique names for virt iface
                                                       // triggers
    VirtIfaceTriggers m_triggers;  // Interfaces and corresponding trigger vars

    // METHODS
    // For each write across a virtual interface boundary
    static void foreachWrittenVirtIface(AstNode* const nodep, const OnWriteToVirtIface& onWrite) {
        nodep->foreach([&](AstVarRef* const refp) {
            if (refp->access().isReadOnly()) return;
            if (AstIfaceRefDType* const dtypep = VN_CAST(refp->varp()->dtypep(), IfaceRefDType)) {
                if (dtypep->isVirtual() && VN_IS(refp->firstAbovep(), MemberSel)) {
                    onWrite(refp, dtypep->ifacep());
                }
            } else if (AstIface* const ifacep = refp->varp()->sensIfacep()) {
                onWrite(refp, ifacep);
            }
        });
    }
    // For each write across a virtual interface boundary (member-level tracking)
    static void foreachWrittenVirtIfaceMember(
        AstNode* const nodep, const std::function<void(AstVarRef*, AstIface*, AstVar*)>& onWrite) {
        nodep->foreach([&](AstVarRef* const refp) {
            if (refp->access().isReadOnly()) return;
            if (AstIfaceRefDType* const dtypep = VN_CAST(refp->varp()->dtypep(), IfaceRefDType)) {
                if (dtypep->isVirtual()) {
                    if (AstMemberSel* const memberSelp = VN_CAST(refp->firstAbovep(), MemberSel)) {
                        // Extract the member varp from the MemberSel node
                        AstVar* memberVarp = memberSelp->varp();
                        onWrite(refp, dtypep->ifacep(), memberVarp);
                    }
                }
            } else if (AstIface* const ifacep = refp->varp()->sensIfacep()) {
                AstVar* memberVarp = refp->varp();
                onWrite(refp, ifacep, memberVarp);
            }
        });
    }
    // Returns true if there is a write across a virtual interface boundary
    static bool writesToVirtIface(const AstNode* const nodep) {
        return nodep->exists([](const AstVarRef* const refp) {
            if (!refp->access().isWriteOrRW()) return false;
            AstIfaceRefDType* const dtypep = VN_CAST(refp->varp()->dtypep(), IfaceRefDType);
            const bool writesToVirtIfaceMember
                = (dtypep && dtypep->isVirtual() && VN_IS(refp->firstAbovep(), MemberSel));
            const bool writesToIfaceSensVar = refp->varp()->sensIfacep();
            return writesToVirtIfaceMember || writesToIfaceSensVar;
        });
    }
    // Error on write across a virtual interface boundary
    static void unsupportedWriteToVirtIface(AstNode* nodep, const char* locationp) {
        if (!nodep) return;
        foreachWrittenVirtIface(nodep, [locationp](AstVarRef* const selp, AstIface*) {
            selp->v3warn(E_UNSUPPORTED,
                         "Unsupported: Write to virtual interface in " << locationp);
        });
    }
    // Create trigger var for the given interface if it doesn't exist; return a write ref to it
    AstVarRef* createVirtIfaceTriggerRefp(FileLine* const flp, AstIface* ifacep) {
        if (!ifacep->user1()) {
            AstScope* const scopeTopp = m_netlistp->topScopep()->scopep();
            AstVarScope* const vscp = scopeTopp->createTemp(m_vifTriggerNames.get(ifacep), 1);
            ifacep->user1p(vscp);
            m_triggers.addIfaceTrigger(ifacep, vscp);
        }
        return new AstVarRef{flp, VN_AS(ifacep->user1p(), VarScope), VAccess::WRITE};
    }

    // Create trigger reference for a specific interface member
    AstVarRef* createVirtIfaceMemberTriggerRefp(FileLine* const flp, AstIface* ifacep,
                                                const AstVar* memberVarp) {
        // Check if we already have a trigger for this specific member
        AstVarScope* existingTrigger = m_triggers.findMemberTrigger(ifacep, memberVarp);
        if (!existingTrigger) {
            AstScope* const scopeTopp = m_netlistp->topScopep()->scopep();
            // Create a unique name for this member trigger
            const std::string triggerName
                = m_vifTriggerNames.get(ifacep) + "_Vtrigm_" + memberVarp->name();
            AstVarScope* const vscp = scopeTopp->createTemp(triggerName, 1);
            m_triggers.addMemberTrigger(ifacep, memberVarp, vscp);
            existingTrigger = vscp;
        }
        return new AstVarRef{flp, existingTrigger, VAccess::WRITE};
    }

    // VISITORS
    void visit(AstNodeProcedure* nodep) override {
        VL_RESTORER(m_trigAssignp);
        m_trigAssignp = nullptr;
        VL_RESTORER(m_trigAssignIfacep);
        m_trigAssignIfacep = nullptr;
        VL_RESTORER(m_trigAssignMemberVarp);
        m_trigAssignMemberVarp = nullptr;
        // Not sure if needed, but be paranoid to match previous behavior as didn't optimize
        // before ..
        if (VN_IS(nodep, AlwaysPost) && writesToVirtIface(nodep)) {
            nodep->foreach([](AstVarRef* refp) { refp->varScopep()->optimizeLifePost(false); });
        }
        iterateChildren(nodep);
    }
    void visit(AstCFunc* nodep) override {
        VL_RESTORER(m_trigAssignp);
        m_trigAssignp = nullptr;
        VL_RESTORER(m_trigAssignIfacep);
        m_trigAssignIfacep = nullptr;
        VL_RESTORER(m_trigAssignMemberVarp);
        m_trigAssignMemberVarp = nullptr;
        iterateChildren(nodep);
    }
    void visit(AstNodeIf* nodep) override {
        unsupportedWriteToVirtIface(nodep->condp(), "if condition");
        {
            VL_RESTORER(m_trigAssignp);
            VL_RESTORER(m_trigAssignIfacep);
            VL_RESTORER(m_trigAssignMemberVarp);
            iterateAndNextNull(nodep->thensp());
        }
        {
            VL_RESTORER(m_trigAssignp);
            VL_RESTORER(m_trigAssignIfacep);
            VL_RESTORER(m_trigAssignMemberVarp);
            iterateAndNextNull(nodep->elsesp());
        }
        if (v3Global.usesTiming()) {
            // Clear the trigger assignment, as there could have been timing controls in either
            // branch
            m_trigAssignp = nullptr;
            m_trigAssignIfacep = nullptr;
            m_trigAssignMemberVarp = nullptr;
        }
    }
    void visit(AstLoop* nodep) override {
        UASSERT_OBJ(!nodep->contsp(), nodep, "'contsp' only used before LinkJump");
        {
            VL_RESTORER(m_trigAssignp);
            VL_RESTORER(m_trigAssignIfacep);
            VL_RESTORER(m_trigAssignMemberVarp);
            iterateAndNextNull(nodep->stmtsp());
        }
        if (v3Global.usesTiming()) {
            // Clear the trigger assignment, as there could have been timing controls in the loop
            m_trigAssignp = nullptr;
            m_trigAssignIfacep = nullptr;
            m_trigAssignMemberVarp = nullptr;
        }
    }
    void visit(AstLoopTest* nodep) override {
        unsupportedWriteToVirtIface(nodep->condp(), "loop condition");
    }
    void visit(AstJumpBlock* nodep) override {
        {
            VL_RESTORER(m_trigAssignp);
            VL_RESTORER(m_trigAssignIfacep);
            VL_RESTORER(m_trigAssignMemberVarp);
            iterateChildren(nodep);
        }
        if (v3Global.usesTiming()) {
            // Clear the trigger assignment, as there could have been timing controls in the jump
            // block
            m_trigAssignp = nullptr;
            m_trigAssignIfacep = nullptr;
            m_trigAssignMemberVarp = nullptr;
        }
    }
    void visit(AstNodeStmt* nodep) override {
        if (v3Global.usesTiming()
            && nodep->exists([](AstNode* nodep) { return nodep->isTimingControl(); })) {
            m_trigAssignp = nullptr;
            m_trigAssignIfacep = nullptr;
            m_trigAssignMemberVarp = nullptr;
        }
        FileLine* const flp = nodep->fileline();

        foreachWrittenVirtIfaceMember(nodep, [&](AstVarRef*, AstIface* ifacep,
                                                 AstVar* memberVarp) {
            if (ifacep != m_trigAssignIfacep || memberVarp != m_trigAssignMemberVarp) {
                // Write to different interface member than before - need new trigger assignment
                m_trigAssignIfacep = ifacep;
                m_trigAssignMemberVarp = memberVarp;
                m_trigAssignp = nullptr;
            }
            if (!m_trigAssignp) {
                m_trigAssignp
                    = new AstAssign{flp, createVirtIfaceMemberTriggerRefp(flp, ifacep, memberVarp),
                                    new AstConst{flp, AstConst::BitTrue{}}};
                nodep->addNextHere(m_trigAssignp);
            }
        });
        // Fallback to whole-interface tracking if no member-specific assignments found
        if (!m_trigAssignp) {
            foreachWrittenVirtIface(nodep, [&](AstVarRef*, AstIface* ifacep) {
                if (ifacep != m_trigAssignIfacep) {
                    m_trigAssignIfacep = ifacep;
                    m_trigAssignMemberVarp = nullptr;
                    m_trigAssignp = nullptr;
                }
                if (!m_trigAssignp) {
                    m_trigAssignp = new AstAssign{flp, createVirtIfaceTriggerRefp(flp, ifacep),
                                                  new AstConst{flp, AstConst::BitTrue{}}};
                    nodep->addNextHere(m_trigAssignp);
                }
            });
        }
    }
    void visit(AstNodeExpr*) override {}  // Accelerate
    void visit(AstNode* nodep) override { iterateChildren(nodep); }

public:
    // CONSTRUCTORS
    explicit VirtIfaceVisitor(AstNetlist* nodep)
        : m_netlistp{nodep} {
        iterate(nodep);
    }
    ~VirtIfaceVisitor() override = default;

    // METHODS
    VirtIfaceTriggers take_triggers() { return std::move(m_triggers); }
};

}  //namespace

VirtIfaceTriggers makeVirtIfaceTriggers(AstNetlist* nodep) {
    UINFO(2, __FUNCTION__ << ":");
    VirtIfaceTriggers triggers{};
    if (v3Global.hasVirtIfaces()) {
        triggers = VirtIfaceVisitor{nodep}.take_triggers();
        // Dump afer destructor so VNDeleter runs
        V3Global::dumpCheckGlobalTree("sched_vif", 0, dumpTreeEitherLevel() >= 6);
    }
    return triggers;
}

}  //namespace V3Sched
