/*
  Interp
*/

#include "interp.h"

#include "arith.h"
#include "compare.h"
#include "console.h"
#include "dummy.h"
#include "file.h"
#include "head.h"
#include "input.h"
#include "jump.h"
#include "mem.h"
#include "object.h"
#include "page.h"
#include "pc.h"
#include "print.h"
#include "prop.h"
#include "random.h"
#include "shared.h"
#include "stack.h"
#include "status.h"
#include "stop.h"
#include "support.h"
#include "var.h"
#include "window.h"
#include "wio.h"

static void illegal_opcode(void)
{
  display((byte *) "Illegal opcode");
  quit();
}

void init_interpreter(bool init_pc)
{
  if(init_pc)
  {
    word start = hd_start();
    stk_init();
    set_pc(pg_page(start), pg_offset(start));
  }
  init_print();
  if(!hd_plus())
    hd_set_screen();
  else
    hd_init();
  split_screen(0);
}

static void push_params(word modes, int count)
{
  extern word param_stack[];
  int i = 0;
  while(i < count)
  {
    int shift = 2 * (count - (i + 1));
    int mode = (modes >> shift) & 3;
    if(mode != 3)
      param_stack[++i] = load(mode);
    else
      break;
  }
  param_stack[0] = i;
}

/*
  For now, if an opcode is disallowed say so.
  Use a goto to share the common code between branches (reduces code size).
  Can remove the tests simply by redefining the macro; this is a refinment.
*/

#if 1
#define require(p)  if(!(p)) goto illegal
#else
#define require(p)
#endif

static void dispatch4(word op)
{
  extern word param_stack[];
  require(hd_five());
  switch(op)
  {
    case 0x00:
      save_game();
      break;
    case 0x01:
      restore_game();
      break;
    case 0x02:
      logical_shift(param_stack[1], param_stack[2]);
      break;
    case 0x03:
      arithmetic_shift(param_stack[1], param_stack[2]);
      break;
    case 0x05:
      clear_flag();
      break;
    case 0x06:
      test_byte_array(param_stack[1], param_stack[2]);
      break;
    case 0x07:
      set_flag();
      break;
    case 0x08:
      /* null(); */
      break;
    case 0x04:
      store(0); /* set_font */
      break;
    case 0x09:
      pg_save_undo();
      break;
    case 0x0A:
      pg_restore_undo();
      break;
    default:
    illegal:
      illegal_opcode();
      break;
  }
}

static void dispatch0(word op)
{
  switch(op)
  {
    case 0x0:
      ret_true();
      break;
    case 0x1:
      ret_false();
      break;
    case 0x2:
      wrt();
      break;
    case 0x3:
      writeln();
      break;
    case 0x4:
      /* null(); */
      break;
    case 0x5:
      require(!hd_five());
      save_game();
      break;
    case 0x6:
      require(!hd_five());
      restore_game();
      break;
    case 0x7:
      restart();
      break;
    case 0x8:
      rts();
      break;
    case 0x9:
      if(hd_five())
        adv_pop_stack();
      else
        (void) stk_pop();
      break;
    case 0xA:
      quit();
      break;
    case 0xB:
      new_line();
      break;
    case 0xC:
      require(hd_version() == VERSION_3);
      status();
      break;
    case 0xD:
      require(!EARLY || hd_version() >= VERSION_3);
      verify();
      break;
    case 0xE:
      {
        int actual_op = next_byte() & 0x3F;
        push_params(next_byte(), 4);
        dispatch4(actual_op);
      }
      break;
    case 0xF:
      require(hd_five());
      ret_value(1);
      break;
    illegal:
      illegal_opcode();
  }
}

static void dispatch1(word op, word param)
{
  switch(op)
  {
    case 0x0:
      cp_zero(param);
      break;
    case 0x1:
      obj_link(param);
      break;
    case 0x2:
      obj_holds(param);
      break;
    case 0x3:
      obj_loc(param);
      break;
    case 0x4:
      prop_get_p_len(param);
      break;
    case 0x5:
      inc_var(param);
      break;
    case 0x6:
      dec_var(param);
      break;
    case 0x7:
      print1(param);
      break;
    case 0x8:
      require(hd_plus());
      gosub2(param);
      break;
    case 0x9:
      obj_remove(param);
      break;
    case 0xA:
      obj_print(param);
      break;
    case 0xB:
      active_return(param);
      break;
    case 0xC:
      jump(param);
      break;
    case 0xD:
      print2(param);
      break;
    case 0xE:
      get_var(param);
      break;
    case 0xF:
      if(hd_five())
        gosub5(param);
      else
        not(param);
      break;
    illegal:
      illegal_opcode();
  }
}

static void stack2(word param1, word param2)
{
  extern word param_stack[];
  param_stack[0] = 2;
  param_stack[1] = param1;
  param_stack[2] = param2;
}

static void dispatch2f(word op, word param1, word param2)
{
  switch(op)
  {
    case 0x01:
      stack2(param1, param2);
      compare();
      break;
    case 0x02:
      less_than(param1, param2);
      break;
    case 0x03:
      greater_than(param1, param2);
      break;
    case 0x04:
      dec_chk(param1, param2);
      break;
    case 0x05:
      inc_chk(param1, param2);
      break;
    case 0x06:
      obj_check(param1, param2);
      break;
    case 0x07:
      bit(param1, param2);
      break;
    case 0x08:
      or(param1, param2);
      break;
    case 0x09:
      and(param1, param2);
      break;
    case 0x0A:
      obj_test(param1, param2);
      break;
    case 0x0B:
      obj_set(param1, param2);
      break;
    case 0x0C:
      obj_clr(param1, param2);
      break;
    case 0x0D:
      put_var(param1, param2);
      break;
    case 0x0E:
      obj_transfer(param1, param2);
      break;
    case 0x0F:
      load_word_array(param1, param2);
      break;
    case 0x10:
      load_byte_array(param1, param2);
      break;
    case 0x11:
      prop_getprop(param1, param2);
      break;
    case 0x12:
      prop_get_prop_addr(param1, param2);
      break;
    case 0x13:
      prop_get_next_prop(param1, param2);
      break;
    case 0x14:
      plus(param1, param2);
      break;
    case 0x15:
      minus(param1, param2);
      break;
    case 0x16:
      multiply(param1, param2);
      break;
    case 0x17:
      divide(param1, param2);
      break;
    case 0x18:
      mod(param1, param2);
      break;
    case 0x19:
      stack2(param1, param2);
      active_gosub();
      break;
    case 0x1A:
      require(hd_five());
      stack2(param1, param2);
      gosub4();
      break;
    case 0x1B:
      require(hd_five());
      set_text_colour(param1, param2);
      break;
    case 0x1C:
      require(hd_five());
      throw_away_stack_frame(param1, param2);
      break;
    default:
    illegal:
      illegal_opcode();
      break;
  }
}

static void dispatch2v(word op)
{
  extern word param_stack[];
  /* Just special case those which can have more than 2 arguments */
  switch(op)
  {
    case 0x01: compare(); break;
    case 0x19: active_gosub(); break;
    case 0x1A: gosub4(); break;
    default:   dispatch2f(op, param_stack[1], param_stack[2]); break;
  }
}

static void dispatch3(word op)
{
  extern word param_stack[];
  word param1 = param_stack[1];
  word param2 = param_stack[2];
  if(hd_five())
    switch(op)
    {
      case 0x17:
        adv_compare2();
        return;
      case 0x18:
        not(param1);
        return;
      case 0x19:
        gosub4();
        return;
      case 0x1A:
        gosub4();
        return;
      case 0x1B:
        parse();
        return;
      case 0x1C:
        ncrypt(param1, param2, param_stack[3], param_stack[4]);
        return;
      case 0x1D:
        block_copy(param1, param2, param_stack[3]);
        return;
      case 0x1E:
        print_text();
        return;
      case 0x1F:
        num_local_params(param1);
        return;
    }
  if(hd_plus())
    switch(op)
    {
      case 0x07:
        plus_random(param1);
        return;
      case 0x0A:
        split_screen(param1);
        return;
      case 0x0B:
        set_current_window(param1);
        return;
      case 0x0D:
        do_clear_screen(param1);
        return;
      case 0x0E:
        erase_line(param1);
        return;
      case 0x0F:
        set_cursor_posn(param1, param2);
        return;
      case 0x10:
        /* null(); */
        return;
      case 0x11:
        set_text_mode(param1);
        return;
      case 0x12:
        io_buffer_mode(param1);
        return;
      case 0x13:
        io_mode(param1, param2);
        return;
      case 0x14:
        /* null(); */
        return;
      case 0x15:
        do_beep(param1);
        return;
      case 0x16:
        get_key();
        return;
      case 0x17:
        plus_compare2();
        return;
    }
  switch(op)
  {
    case 0x00:
    case 0x0C:
      active_gosub();
      break;

    case 0x01:
      save_word_array(param1, param2, param_stack[3]);
      break;
    case 0x02:
      save_byte_array(param1, param2, param_stack[3]);
      break;
    case 0x03:
      prop_put_prop(param1, param2, param_stack[3]);
      break;
    case 0x04:
      input();
      break;
    case 0x05:
      print_char(param1);
      break;
    case 0x06:
      print_num(param1);
      break;
    case 0x07:
      std_random(param1);
      break;
    case 0x08:
      push(param1);
      break;
    case 0x09:
      pop(param1);
      break;
    case 0x0A:
      split_screen(param1);
      break;
    case 0x0B:
      set_current_window(param1);
      break;
    default:
      illegal_opcode();
      break;
  }
}

void execute_opcode(void)
{
  while(!stopped())
  {
    word op = next_byte();
    switch(op >> 4)
    {
      case 0x0: case 0x1: case 0x2: case 0x3:
      case 0x4: case 0x5: case 0x6: case 0x7:
        /*
          Splitting these branches is an obvious optimisation:
            It allows the first (and maybe second) conditional to be removed
            That reduces the load to calls to next_byte in the first case and
            to next_byte followed by an indirection in the second (inlining the
            logic from load() in var.c
        */
        {
          word param1 = load((op & 0x40) ? 2 : 1);
          word param2 = load((op & 0x20) ? 2 : 1);
          dispatch2f(op & 0x1F, param1, param2);
        }
        break;
      case 0x8: case 0x9: case 0xA:
        {
          word param = load((op >> 4) & 3);
          dispatch1(op & 0x0F, param);
        }
        break;
      case 0xB:
        dispatch0(op & 0x0F);
        break;
      case 0xC: case 0xD:
        push_params(next_byte(), 4);
        dispatch2v(op & 0x1F);
        break;
      case 0xE: case 0xF:
        if(op == 0xEC || op == 0xFA)
          push_params(next_word(), 8);
        else
          push_params(next_byte(), 4);
        dispatch3(op & 0x1F);
        break;
    }
  }
}
