//------------------------------------------------------------------------------
// CERN BE-CO-HT
// White Rabbit Trigger Distribution (WRTD)
// https://ohwr.org/projects/wrtd
//------------------------------------------------------------------------------
//
// unit name:   WrtdDrv
//
// description: A SystemVerilog Class to provide an abstraction of a complete
// WRTD device.
//
//------------------------------------------------------------------------------
// Copyright CERN 2019
//------------------------------------------------------------------------------
// Copyright and related rights are licensed under the Solderpad Hardware
// 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://solderpad.org/licenses/SHL-2.0.
// Unless required by applicable law or agreed to in writing, software,
// hardware and materials distributed under this 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 __WRTD_DRV_INCLUDED
 `define __WRTD_DRV_INCLUDED

 `include "wrtd_definitions.svh"
 `include "wrtd_rule.svh"
 `include "wrtd_alarm.svh"
 `include "mock_turtle_driver.svh"

class WrtdDrv;
   uint32_t  ready;
   protected string name;
   protected byte unsigned enable_logging;
   protected MockTurtleDriver     mt;
   protected uint32_t             nbr_cpus;
   protected uint32_t             free_rule_slots[];
   protected uint32_t             hmq_words[];
   protected wrtd_root            roots[];
   protected WrtdRepCapCollection rules;
   protected WrtdRepCapCollection alarms;

   function new ( CBusAccessor acc, uint64_t base,
                  vIMockTurtleIRQ irq, string name = "",
                  byte unsigned enable_logging = 1 );

      this.name = name;

      this.mt = new (acc, base, irq, name);

      this.enable_logging = enable_logging;

   endfunction // new

   task mdisplay ( string str );
      string tmp;
      if (this.name == "")
        tmp = $sformatf("<%t> %s", $realtime, str);
      else
        tmp = $sformatf("[%s] <%t> %s", this.name, $realtime, str);
      $display (tmp);
   endtask // mdisplay

   task init ( );
      int i, j;

      uint32_t val, cpu_ready;

      WrtdRule new_rule;

      WrtdAlarm new_alarm;

      uint32_t data[];

      this.ready = 0;

      mt.init ( );

      this.nbr_cpus = mt.rom.getCoreCount ( );

      this.free_rule_slots = new[this.nbr_cpus];

      this.hmq_words = new[this.nbr_cpus];

      this.roots = new[this.nbr_cpus];

      for ( i = 0; i < this.nbr_cpus; i++ )
        begin
           this.hmq_words[i] =
                `TRTL_CONFIG_ROM_MQ_SIZE_PAYLOAD( mt.rom.getHmqDimensions( i, 0 ) );
           mt.enable_hmqi_irq ( i, 0, 1 );
           mt.enable_console_irq ( i, 1 );
           mt.reset_core ( i, 0 );
        end

      fork
         begin

            for ( i = 0; i < this.nbr_cpus; i++ )
              begin
                 // wait for ready notification from firmware
                 cpu_ready = 0;
                 while ( cpu_ready == 0 )
                   begin
                      val = 0;
                      if ( mt.pending_cpu_notifications ( i ) != 0 )
                        begin
                           mt.get_single_cpu_notification ( i, val );
                           if ( val == TRTL_CPU_NOTIFY_MAIN )
                             cpu_ready = 1;
                        end
                      # 1us;
                   end
                 // retrieve address of root
                 msg_get_config ( i, data );
                 this.roots[i].addr = data[0];
                 // retrieve root
                 msg_readw ( i, this.roots[i].addr, `WRTD_ROOT_WORD_SIZE, data );
                 this.roots[i].fw_name = new();
                 this.roots[i].fw_name.data_unpack(data[1:4]);
                 this.roots[i].fw_id         = data[5];
                 this.roots[i].capabilities  = ( data[6] & 'hff000000 ) >> 24;
                 this.roots[i].nbr_rules     = ( data[6] & 'h00ff0000 ) >> 16;
                 this.roots[i].nbr_alarms    = ( data[6] & 'h0000ff00 ) >> 8;
                 this.roots[i].nbr_devs      = ( data[6] & 'h000000ff ) >> 0;
                 for ( j = 0; j < 4; j++)
                   begin
                      this.roots[i].nbr_channels[j] = ( data[9]  >> j*8 ) & 'hff;
                      this.roots[i].channel_dir[j]  = ( data[10] >> j*8 ) & 'hff;
                   end
                 this.roots[i].rules_addr  = data[11];
                 this.roots[i].alarms_addr = data[12];
                 // init free rule slots
                 this.free_rule_slots[i] = this.roots[i].nbr_rules;
                 // turn on all logging if enabled
                 if ( this.enable_logging )
                   msg_writew ( i, this.roots[i].addr + 32, 1, { data[8] | 32'hff } );
                 mdisplay( $sformatf("CPU %0d: WRTD app '%s', id '0x%x'", i,
                                     this.roots[i].fw_name.get(),
                                     this.roots[i].fw_id) );
              end

            // initialise rules
            this.rules  = new ( this.name );
            for ( i = 0; i < this.nbr_cpus; i++ )
              for ( j = 0; j < this.roots[i].nbr_rules; j++ )
                begin
                   new_rule = new ( this.name );
                   this.rules.collection.push_back ( new_rule );
                end

            // initialise alarms
            this.alarms = new ( this.name );
            for ( i = 0; i < this.nbr_cpus; i++ )
              for ( j = 0; j < this.roots[i].nbr_alarms; j++ )
                begin
                   new_alarm = new ( this.name );
                   this.alarms.collection.push_back ( new_alarm );
                end

         end

         begin
            while ( i != this.nbr_cpus )
              begin
                 mt.update ( );
                 # 1us;
              end
         end
      join

      this.ready = 1;

   endtask // init

   task msg_get_config ( int core, ref uint32_t data[] );
      int i;

      MQueueMsg msg;

      if ( data.size() < `WRTD_CFG_MSG_WORD_SIZE )
        data = new[`WRTD_CFG_MSG_WORD_SIZE];

      msg = new ( core, 0 );
      msg.header.flags  = `TRTL_HMQ_HEADER_FLAG_RPC | `TRTL_HMQ_HEADER_FLAG_SYNC;
      msg.header.msg_id = WRTD_ACTION_GET_CONFIG;
      msg.header.len    = 0;
      mt.hmq_send_message ( msg );
      while ( mt.hmq_pending_messages ( msg.core, 0 ) == 0 ) #1us;
      mt.hmq_receive_message ( msg );
      if ( msg.header.len != `WRTD_CFG_MSG_WORD_SIZE )
        begin
           $error ( "get_config: unexpected message header length" );
           mdisplay ( $sformatf ( "message received: %s", msg.tostring() ) );
        end
      for ( i = 0; i < `WRTD_CFG_MSG_WORD_SIZE; i++ )
        data[i] = msg.data[i];

   endtask // msg_get_config

   task msg_readw ( int core, uint32_t addr,
                    uint32_t count, ref uint32_t data[] );
      int i;
      MQueueMsg msg;
      uint32_t tlen, offset;

      if ( data.size() < count )
        data = new[count];

      offset = 0;

      while ( offset < count )
        begin
           tlen = this.hmq_words[core] - `WRTD_IO_MSG_WORD_SIZE;
           if ( tlen > count - offset )
             tlen = count - offset;
           msg = new ( core, 0 );
           msg.header.flags  = `TRTL_HMQ_HEADER_FLAG_RPC | `TRTL_HMQ_HEADER_FLAG_SYNC;
           msg.header.msg_id = WRTD_ACTION_READW;
           msg.header.len    = `WRTD_IO_MSG_WORD_SIZE;
           msg.data          = '{ addr + offset, tlen };
           mt.hmq_send_message ( msg );
           while ( mt.hmq_pending_messages ( msg.core, 0 ) == 0 ) #1us;
           mt.hmq_receive_message ( msg );
           if ( msg.header.len != tlen )
             begin
                $error ( "readw: unexpected message header length" );
                mdisplay ( $sformatf ( "message received: %s", msg.tostring() ) );
             end
           for ( i = 0; i < tlen; i++ )
             data[offset+i] = msg.data[i];
           offset += tlen;
        end

   endtask // msg_readw

   task msg_writew ( int core, uint32_t addr,
                     uint32_t count, uint32_t data[] );
      int i;
      MQueueMsg msg;
      uint32_t tlen, offset;

      offset = 0;

      while ( offset < count )
        begin
           tlen = this.hmq_words[core] - `WRTD_IO_MSG_WORD_SIZE;
           if ( tlen > count - offset )
             tlen = count - offset;
           msg = new ( core, 0 );
           msg.header.flags  = `TRTL_HMQ_HEADER_FLAG_RPC | `TRTL_HMQ_HEADER_FLAG_SYNC;
           msg.header.msg_id = WRTD_ACTION_WRITEW;
           msg.header.len    = `WRTD_IO_MSG_WORD_SIZE + count;
           msg.data          = '{ addr + offset, tlen };
           for ( i = 0; i < tlen; i++ )
             msg.data[i+2] = data[offset+i];
           mt.hmq_send_message ( msg );
           while ( mt.hmq_pending_messages ( msg.core, 0 ) == 0 ) #1us;
           mt.hmq_receive_message ( msg );
           offset += tlen;
        end

   endtask // msg_writew

   task add_rule ( string rep_cap_id );
      int idx;
      idx = this.rules.add ( rep_cap_id );
   endtask // add_rule

   function int find_rule ( string rep_cap_id );
      return this.rules.find ( rep_cap_id );
   endfunction // find_rule

   task remove_rule ( string rep_cap_id );
      int idx;
      idx = this.rules.remove ( rep_cap_id );
      write_rule ( idx );
   endtask // remove_rule

   task set_rule ( string rep_cap_id,
                   string src, string dst,
                   uint32_t delay_ns );
      int idx;

      WrtdRule rule;

      idx = this.rules.find ( rep_cap_id );

      if ( idx == -1 )
        $error ( "%s repeated capability ID cannot be set because it does not exist", rep_cap_id );

      if ( this.rules.collection[idx].is_enabled ( ) )
        $error ( "%s repeated capability ID cannot be set because it is enabled", rep_cap_id );

      $cast ( rule, this.rules.collection[idx] );

      rule.set_src ( src );
      rule.set_dst ( dst );
      rule.set_delay_ns ( delay_ns );
   endtask // set_rule

   task write_rule ( int idx );
      int core, hash;
      uint32_t addr;
      uint32_t data[];

      WrtdRule rule;

      if ( idx < 0 )
        $error ( "cannot write rule with negative index" );

      $cast ( rule, this.rules.collection[idx] );

      hash = wrtd_gen_hash ( rule.get_src() ) % this.roots[core].nbr_rules;
      core = rule.get_core ( );
      addr = this.roots[core].rules_addr + hash * `WRTD_RULE_WORD_SIZE * 4;
      data = rule.data_pack ( );
      msg_writew ( core, addr, `WRTD_RULE_WORD_SIZE, data );
   endtask // write_rule

   function void map_local_channel_to_cpu ( string ch_id, int ch_dir,
                                            ref int core, ref int ch_idx );
      int i, j, k, idx_in, idx_out, dev_dir;

      string dev_id;

      idx_in  = 1;
      idx_out = 1;

      core   = -1;
      ch_idx = -1;

      for ( i = 0; i < this.nbr_cpus; i++ )
        for ( j = 0; j < this.roots[i].nbr_devs; j++ )
          begin
             dev_dir = this.roots[i].channel_dir[j];
             for ( k = 0; k < this.roots[i].nbr_channels[j]; k++ )
               begin
                  if ( dev_dir == WRTD_DIR_INPUT )
                    dev_id = $sformatf ( "LC-I%0d", idx_in++ );
                  else
                    dev_id = $sformatf ( "LC-O%0d", idx_out++ );
                  if ( ( dev_dir == ch_dir ) && ( dev_id == ch_id ) )
                    begin
                       core   = i;
                       ch_idx = k;
                       return;
                    end
               end
          end

   endfunction // map_local_channel_to_cpu

   task enable_rule ( string rep_cap_id );
      int idx, src_cpu, dst_cpu, src_ch, dst_ch;

      string id;

      WrtdRule rule;

      idx = this.rules.find ( rep_cap_id );

      if ( idx == -1 )
        $error ( "%s repeated capability ID cannot be enabled because it does not exist", rep_cap_id );

      $cast ( rule, this.rules.collection[idx] );

      /* Place the rule on the same CPU as the input source.
       If source is network, src_cpu = -1  for now */
      map_local_channel_to_cpu ( rule.get_src(), WRTD_DIR_INPUT, src_cpu, src_ch );

      // TODO: check if rule source is alarm

      /* Set destination cpu and channel */
      map_local_channel_to_cpu ( rule.get_dst(), WRTD_DIR_OUTPUT, dst_cpu, dst_ch );

      if ( dst_cpu >= 0 ) // local output device
        begin
           /* If source is network message and this cpu can receive from
            network, set cpu affinity to this cpu */
           if ( ( src_cpu == -1 ) && ( this.roots[dst_cpu].capabilities & `WRTD_CAP_NET_RX ) )
             src_cpu = dst_cpu;
           /* Otherwise find the first cpu that is capable of net RX */
           else
                for ( src_cpu = 0; src_cpu < this.nbr_cpus; src_cpu++ )
                  if ( this.roots[src_cpu].capabilities & `WRTD_CAP_NET_RX )
                    break;
        end
      else // network destination
        begin
           dst_ch = `WRTD_DEST_CH_NET;
           /* If source cpu can also send to
            network, set dest cpu affinity to that cpu */
           if ( ( src_cpu >= 0 ) && ( this.roots[src_cpu].capabilities & `WRTD_CAP_NET_TX ) )
             dst_cpu = src_cpu;
           /* Otherwise find the first cpu that is capable of net TX */
           else
             for ( dst_cpu = 0; dst_cpu < this.nbr_cpus; dst_cpu++ )
               if ( this.roots[dst_cpu].capabilities & `WRTD_CAP_NET_TX )
                 break;
        end

      // TODO: handle network to network events

      if ( ( src_cpu < 0 ) || ( dst_cpu < 0 ) ||
           ( src_cpu >= this.nbr_cpus ) || ( dst_cpu >= this.nbr_cpus ) )
        $error ( "cannot determine source and/or destination cpu for rule" );

      /* If the same CPU can handle both input and output, use special value so that
         the firmware will know not to forward this event to another CPU (it does not
         know its own index) */
      if ( src_cpu == dst_cpu )
        dst_cpu = `WRTD_DEST_CPU_LOCAL;

      // TODO: handle free_rule_slots per cpu

      rule.set_core     ( src_cpu );
      rule.set_dest_cpu ( dst_cpu );
      rule.set_dest_ch  ( dst_ch );
      rule.set_enable   ( 1 );

      // TODO: properly recalculate and rewrite rules
      write_rule ( idx );
   endtask // enable_rule

   // TODO: properly support alarms
   task add_alarm ( string rep_cap_id );
      int idx;
      idx = this.alarms.add ( rep_cap_id );
      write_alarm ( idx );
   endtask // add_alarm

   function int find_alarm ( string rep_cap_id );
      return this.alarms.find ( rep_cap_id );
   endfunction // find_alarm

   task remove_alarm ( string rep_cap_id );
      int idx;
      idx = this.alarms.remove ( rep_cap_id );
      write_alarm ( idx );
   endtask // remove_alarm

   task write_alarm ( int idx );
      int core, index;
      uint32_t addr;
      uint32_t data[];

      if ( idx < 0 )
        $error ( "cannot write alarm with negative index" );

      core  = this.alarms.collection[idx].get_core ( );
      //index = this.alarms.collection[idx].get_index ();
      addr  = this.roots[core].alarms_addr + index * `WRTD_ALRM_WORD_SIZE * 4;
      data  = this.alarms.collection[idx].data_pack( );

      msg_writew ( core, addr, `WRTD_ALRM_WORD_SIZE, data );
   endtask // write_alarm

   function string log_msg_tostring ( MQueueMsg msg );
      string ret;

      wrtd_log_msg_type   msg_type;
      wrtd_log_gen_reason gen_reason;
      wrtd_log_con_reason con_reason;
      wrtd_log_dsc_reason dsc_reason;
      wrtd_log_net_reason net_reason;

      WrtdTstamp ev_tstamp, msg_tstamp;
      WrtdId ev_id;

      ret = "";

      if ( msg.header.msg_id != `WRTD_ACTION_LOG )
        begin
           $error ( "log_msg_tostring: unknown message id %.8x", msg.header.msg_id );
           return ret;
        end

      msg_tstamp = new ();
      msg_tstamp.data_unpack ( msg.data[11:13] );
      ret = { ret, $sformatf ( "log timestamp: %s", msg_tstamp.tostring() ) };

      ev_tstamp = new ();
      ev_tstamp.data_unpack ( msg.data[2:4] );
      ret = { ret, $sformatf ( ", event timestamp: %s", ev_tstamp.tostring() ) };

      ev_id = new ();
      ev_id.data_unpack ( msg.data[5:8] );
      ret = { ret, $sformatf ( ", id: %s, seq: %0d", ev_id.get(), msg.data[9] ) };

      $cast ( msg_type, msg.data[0] );

      ret = { ret, $sformatf ( ", log type: %s", msg_type.name() ) };

      case ( msg_type )

        WRTD_LOG_MSG_EV_GENERATED :
          begin
             $cast ( gen_reason, msg.data[1] );
             ret = { ret, $sformatf ( ", reason: %s", gen_reason.name() ) };
          end

        WRTD_LOG_MSG_EV_CONSUMED :
          begin
             $cast ( con_reason, msg.data[1] );
             ret = { ret, $sformatf ( ", reason: %s", con_reason.name() ) };
          end

        WRTD_LOG_MSG_EV_DISCARDED :
          begin
             $cast ( dsc_reason, msg.data[1] );
             ret = { ret, $sformatf ( ", reason: %s", dsc_reason.name() ) };
          end

        WRTD_LOG_MSG_EV_NETWORK :
          begin
             $cast ( net_reason, msg.data[1] );
             ret = { ret, $sformatf ( ", reason: %s", net_reason.name() ) };
          end

      endcase; // case ( msg_type )

      return ret;

   endfunction // log_msg_tostring

   task check_logs ( );
      int i;

      MQueueMsg msg;
      for (int i = 0; i < this.nbr_cpus; i++)
        begin
           while ( mt.hmq_pending_messages ( i, 0 ) )
             begin
                msg = new ( i, 0 );
                mt.hmq_peek_message ( msg );
                if ( msg.header.msg_id != `WRTD_ACTION_LOG ) break;
                mt.hmq_receive_message ( msg );
                mdisplay ( $sformatf ( "LOG MSG from core %0d: %s", i, log_msg_tostring ( msg ) ) );
             end
        end
   endtask // check_logs

   task update ( );
      mt.update ( );
      if ( this.enable_logging )
        check_logs ( );
   endtask // update

endclass // WrtdDrv

`endif //  `ifndef __WRTD_DRV_INCLUDED
