/*****************************************************************************

  Licensed to Accellera Systems Initiative Inc. (Accellera) under one or
  more contributor license agreements.  See the NOTICE file distributed
  with this work for additional information regarding copyright ownership.
  Accellera licenses this file to you under the Apache License, Version 2.0
  (the "License"); you may not use this file except in compliance with the
  License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  implied.  See the License for the specific language governing
  permissions and limitations under the License.

 *****************************************************************************/

#ifndef __SIMPLESWITCHAT_H__
#define __SIMPLESWITCHAT_H__

#include "tlm.h"

#include "tlm_utils/multi_passthrough_initiator_socket.h"
#include "tlm_utils/multi_passthrough_target_socket.h"
#include "simpleAddressMap.h"
#include "extensionPool.h"
#include "tlm_utils/instance_specific_extensions.h"
#include "tlm_utils/peq_with_cb_and_phase.h"


/*
This class is a simple crossbar switch through which an arbitrary number of initiators
may communicate in parallel as long as they do not talk to the same target.

If two masters address the same target at the same point of time,
the choice who will be allowed to communicate
is done non-deterministically (based on the SystemC process exectution order).

This could be avoided by changing the fwPEQ into a priority PEQ of some kind.

The switch ensures that the end_req and end_resp rules are not violated when
many initiator talk to the same target.
*/
class MultiSocketSimpleSwitchAT : public sc_core::sc_module, public tlm::tlm_mm_interface
{
public:
  typedef tlm::tlm_generic_payload                                 transaction_type;
  typedef tlm::tlm_phase                                           phase_type;
  typedef tlm::tlm_sync_enum                                       sync_enum_type;
  typedef tlm_utils::multi_passthrough_target_socket<MultiSocketSimpleSwitchAT>    target_socket_type;
  typedef tlm_utils::multi_passthrough_initiator_socket<MultiSocketSimpleSwitchAT> initiator_socket_type;

public:
  target_socket_type target_socket; //the target multi socket

private:
  initiator_socket_type initiator_socket; //the initiator multi socket (private to enforce use of bindTarget function)
  SimpleAddressMap m_addrMap; //a pretty simple address map
  std::vector<std::deque<transaction_type*> > m_pendingReqs; //list of pending reqs per target
  std::vector<std::deque<transaction_type*> > m_pendingResps; //list of pending resps per initiator
  std::vector<sc_dt::uint64> m_masks; //address masks for each target
  tlm_utils::instance_specific_extension_accessor accessMySpecificExtensions; //extension accessor to access private extensions
  tlm_utils::peq_with_cb_and_phase<MultiSocketSimpleSwitchAT> m_bwPEQ; //PEQ in the fw direction
  tlm_utils::peq_with_cb_and_phase<MultiSocketSimpleSwitchAT> m_fwPEQ; //PEQ in the bw direction


  //an instance specific extension that tells us whether we are in a wrapped b_transport or not
  class BTag : public tlm_utils::instance_specific_extension<BTag>{
  public:
    sc_core::sc_event event; //trigger this event when transaction is done
  };

  //an instance specific extension that holds information about source and sink of a txn
  // as well as information if the req still has to be cleared and if the txn is already
  //  complete on the target side
  class ConnectionInfo : public tlm_utils::instance_specific_extension<ConnectionInfo>{
    public:
    unsigned int  fwID; //socket number of sink
    unsigned int  bwID; //socket number of source
    bool clearReq;      //is the txn still in req phase?
    bool alreadyComplete; //has the txn already completed on the target side?
  };

  class internalPEQTypes{ //use the tpPEQ to delay connection infos
    public:
    typedef ConnectionInfo tlm_payload_type;
    typedef tlm::tlm_phase tlm_phase_type;
  };
  ExtensionPool<ConnectionInfo> m_connInfoPool; //our pool of extensions
  unsigned int m_target_count;  //number of connected targets (see bindTargetSocket for explanation)

public:
  MultiSocketSimpleSwitchAT(sc_core::sc_module_name name) :
    sc_core::sc_module(name),
    target_socket("target_socket"),
    initiator_socket("initiator_socket"),
    m_bwPEQ(this, &MultiSocketSimpleSwitchAT::bwPEQcb),
    m_fwPEQ(this, &MultiSocketSimpleSwitchAT::fwPEQcb),
    m_connInfoPool(10),
    m_target_count(0)
  {
    target_socket.register_nb_transport_fw(this, &MultiSocketSimpleSwitchAT::initiatorNBTransport);
    target_socket.register_b_transport(this, &MultiSocketSimpleSwitchAT::b_transport);
    initiator_socket.register_nb_transport_bw(this, &MultiSocketSimpleSwitchAT::targetNBTransport);
  }

  void bindTargetSocket(initiator_socket_type::base_target_socket_type& target
                       ,sc_dt::uint64 low
                       ,sc_dt::uint64 high
                       ,sc_dt::uint64 mask = 0xffffffffffffffffULL){
    initiator_socket(target); //bind sockets
    //insert into address map and increase target count
    // (we have to count the targets manually, because target_socket.size() is only reliable during simulation
    //  as it gets evaluated during end_of_elaboration)
    m_addrMap.insert(low, high, m_target_count++);
    m_masks.push_back(mask); //add the mask for this target
  }

  unsigned int decode(const sc_dt::uint64& address)
  {
    return m_addrMap.decode(address);
  }

  void start_of_simulation(){
    //initialize the lists of pending reqs and resps
    m_pendingReqs.resize(initiator_socket.size());
    m_pendingResps.resize(target_socket.size());
  }


  void b_transport(int initiator_id, transaction_type& trans, sc_core::sc_time& t){
    //first make sure that there is no BTag (just for debugging)
    BTag* btag;
    accessMySpecificExtensions(trans).get_extension(btag);
    sc_assert(!btag);
    BTag tag; //now add our BTag
    bool added_mm=!trans.has_mm(); //in case there is no MM in we add it now
    if (added_mm){
      trans.set_mm(this);
      trans.acquire(); //acquire the txn
    }
    accessMySpecificExtensions(trans).set_extension(&tag);
    phase_type phase=tlm::BEGIN_REQ; //then simply use our nb implementation (respects all the rules)
    initiatorNBTransport(initiator_id, trans, phase, t);
    wait(tag.event); //and wait for the event to be triggered
    if (added_mm){  //if we added MM
      trans.release(); //we release our reference (this will not delete the txn but trigger the tag.event as soon as the ref count is zero)
      if (trans.get_ref_count())
        wait(tag.event); //wait for the ref count to get to zero
      trans.set_mm(NULL); //remove the MM
    }
    //don't forget to remove the extension (instance specific extensions are not cleared off by MM)
    accessMySpecificExtensions(trans).clear_extension(&tag);
  }

  void free(transaction_type* txn){
    BTag* btag;
    accessMySpecificExtensions(*txn).get_extension(btag);
    sc_assert(btag);
    txn->reset(); //clean off all extension that were added down stream
    btag->event.notify();
  }

  //do a fw transmission
  void initiatorNBTransport_core(transaction_type& trans,
                                 phase_type& phase,
                                 sc_core::sc_time& t,
                                 unsigned int tgtSocketNumber){
    switch (initiator_socket[tgtSocketNumber]->nb_transport_fw(trans, phase, t)) {
      case tlm::TLM_ACCEPTED:
      case tlm::TLM_UPDATED:
        // Transaction not yet finished
        if (phase != tlm::BEGIN_REQ)
        {
          sc_assert(phase!=tlm::END_RESP);
          m_bwPEQ.notify(trans,phase,t);
        }
        break;
      case tlm::TLM_COMPLETED:
        // Transaction finished
        ConnectionInfo* connInfo;
        accessMySpecificExtensions(trans).get_extension(connInfo);
        sc_assert(connInfo);
        connInfo->alreadyComplete=true;
        phase=tlm::BEGIN_RESP;
        m_bwPEQ.notify(trans, phase, t);
        break;
      default:
        sc_assert(0); exit(1);
    };
  }

  //nb_transport_fw
  sync_enum_type initiatorNBTransport(int initiator_id,
                                      transaction_type& trans,
                                      phase_type& phase,
                                      sc_core::sc_time& t)
  {
    ConnectionInfo* connInfo;
    accessMySpecificExtensions(trans).get_extension(connInfo);
    m_fwPEQ.notify(trans,phase,t);
    if (phase==tlm::BEGIN_REQ){
      //add our private information to the txn
      sc_assert(!connInfo);
      connInfo=m_connInfoPool.construct();
      connInfo->fwID=decode(trans.get_address());
      connInfo->bwID=initiator_id;
      connInfo->clearReq=true;
      connInfo->alreadyComplete=false;
      accessMySpecificExtensions(trans).set_extension(connInfo);
    }
    else
    if (phase==tlm::END_RESP){
      return tlm::TLM_COMPLETED;
    }
    else
      {sc_assert(0); exit(1);}
    return tlm::TLM_ACCEPTED;
  }

  sync_enum_type targetNBTransport(int portId,
                                   transaction_type& trans,
                                   phase_type& phase,
                                   sc_core::sc_time& t)
  {
    if (phase != tlm::END_REQ && phase != tlm::BEGIN_RESP) {
      std::cout << "ERROR: '" << name()
                << "': Illegal phase received from target." << std::endl;
      sc_assert(false); exit(1);
    }
    //simply stuff it into the bw PEQ
    m_bwPEQ.notify(trans,phase,t);
    return tlm::TLM_ACCEPTED;
  }

  void bwPEQcb(transaction_type& trans, const phase_type& phase){
    //first get our private info from the txn
    ConnectionInfo* connInfo;
    accessMySpecificExtensions(trans).get_extension(connInfo);
    sc_assert(connInfo);
    phase_type p=phase;
    sc_core::sc_time t=sc_core::SC_ZERO_TIME;
    BTag* btag;
    accessMySpecificExtensions(trans).get_extension(btag);
    bool doCall=btag==NULL; //we only will do a bw call if we are not in a wrapped b_transport
    if ((phase==tlm::END_REQ) | (connInfo->clearReq)){ //in case the target left out end_req clearReq reminds us to unlock the req port
      sc_assert(m_pendingReqs[connInfo->fwID].size());
      sc_assert(m_pendingReqs[connInfo->fwID].front()==&trans);
      m_pendingReqs[connInfo->fwID].pop_front(); //allow another req to start at this target
      if (m_pendingReqs[connInfo->fwID].size()){ //there was a pending req
        phase_type ph=tlm::BEGIN_REQ;
        initiatorNBTransport_core(*m_pendingReqs[connInfo->fwID].front(), ph, t,connInfo->fwID);
      }
      connInfo->clearReq=false;
    }
    //no else here, since we might clear the req AND begin a resp
    if (phase==tlm::BEGIN_RESP){
      m_pendingResps[connInfo->bwID].push_back(&trans);
      doCall=m_pendingResps[connInfo->bwID].size()==1; //do a call in case the response socket was free
    }

    if (doCall){ //we have to do a call on the bw of fw path
      if (btag){ //only possible if BEGIN_RESP and resp socket was free
          phase_type ph=tlm::END_RESP;
          m_fwPEQ.notify(trans, ph, t);
      }
      else
        switch (target_socket[connInfo->bwID]->nb_transport_bw(trans, p, t)){
          case tlm::TLM_ACCEPTED:
          case tlm::TLM_UPDATED:
            break;
          case tlm::TLM_COMPLETED:{
            //covers a piggy bagged END_RESP to START_RESP
            phase_type ph=tlm::END_RESP;
            m_fwPEQ.notify(trans, ph, t);
            }
            break;
          default:
            sc_assert(0); exit(1);

        };
    }
  }

  //the following two functions (fwPEQcb and clearPEQcb) could be one, if we were allowed
  // to stick END_RESP into a PEQ
  void fwPEQcb(transaction_type& trans, const phase_type& phase){
    ConnectionInfo* connInfo;
    accessMySpecificExtensions(trans).get_extension(connInfo);
    sc_assert(connInfo);
    phase_type ph=phase;
    sc_core::sc_time t=sc_core::SC_ZERO_TIME;
    if (phase==tlm::BEGIN_REQ){
      trans.set_address(trans.get_address()&m_masks[connInfo->fwID]); //mask address
      m_pendingReqs[connInfo->fwID].push_back(&trans);
      if (m_pendingReqs[connInfo->fwID].size()==1){ //the socket is free
        initiatorNBTransport_core(trans, ph, t, connInfo->fwID);
      }
    }
    else
    {
      //phase is always END_RESP
      BTag* btag;
      accessMySpecificExtensions(trans).get_extension(btag);
      accessMySpecificExtensions(trans).clear_extension(connInfo); //remove our specific extension as it is not needed any more
      if (!connInfo->alreadyComplete) {
        sync_enum_type tmp=initiator_socket[connInfo->fwID]->nb_transport_fw(trans, ph, t);
        sc_assert(tmp==tlm::TLM_COMPLETED);
      }
      sc_assert(m_pendingResps[connInfo->bwID].size());
      m_pendingResps[connInfo->bwID].pop_front(); //remove current response
      if (m_pendingResps[connInfo->bwID].size()){ //if there was one pending
        ph=tlm::BEGIN_RESP; //schedule its transmission
        m_bwPEQ.notify(*m_pendingResps[connInfo->bwID].front(),ph,t);
      }
      m_connInfoPool.free(connInfo); //release connInfo
      if (btag) btag->event.notify(t); //release b_transport
    }
  }

  void dump_status(){
    std::cout<<"At "<<sc_core::sc_time_stamp()<<" status of "<<name()<<" is "<<std::endl
             <<"  Number of connected initiators: "<<target_socket.size()<<std::endl
             <<"  Number of connected targets: "<<initiator_socket.size()<<std::endl
             <<"  Pending requests:"<<std::endl;
    for (unsigned int i=0; i<m_pendingReqs.size(); i++)
      std::cout<<"    "<<m_pendingReqs[i].size()<<" pending requests for target number "<<i<<std::endl;
    std::cout<<"  Pending responses:"<<std::endl;
    for (unsigned int i=0; i<m_pendingResps.size(); i++)
      std::cout<<"    "<<m_pendingResps[i].size()<<" pending responses for initiator number "<<i<<std::endl;
    std::cout<<"  The address map is:"<<std::endl;
    m_addrMap.dumpMap();

  }
};

#endif
