require_relative "eventable"

module Interpreter
  class Interpreter
    # fire events for changed pc and register contents
    include Eventable

    # current instruction or pc
    attr_reader :instruction

    # current instruction or pc
    attr_reader :clock

    # an (arm style) link register. store the return address to return to
    attr_reader :link

    # current executing block. since this is not a hardware simulator this is luxury
    attr_reader :block

    # the registers, 16
    attr_reader :registers

    # collect the output
    attr_reader :stdout

    attr_reader :state

    def initialize
      @state = "runnnig"
      @stdout = ""
      @registers = {}
      @clock = 0
      (0...16).each do |r|
        set_register "r#{r}".to_sym , "r#{r}:unknown"
      end
    end

    def start bl
      set_block  bl
    end

    def set_block bl
      return if @block == bl
      raise "Error, nil block" unless bl
      old = @block
      @block = bl
      trigger(:block_changed , old , bl)
      set_instruction bl.codes.first
    end

    def set_instruction i
      @state = "exited" unless i
      return if @instruction == i
      old = @instruction
      @instruction = i
      trigger(:instruction_changed, old , i)
    end

    def get_register( reg )
      reg = reg.symbol if reg.is_a? Register::RegisterReference
      raise "Not a register #{reg}" unless Register::RegisterReference.look_like_reg(reg)
      @registers[reg]
    end

    def set_register reg , val
      old = get_register( reg ) # also ensures format
      return if old === val
      reg = reg.symbol if reg.is_a? Register::RegisterReference
      @registers[reg] = val
      trigger(:register_changed, reg ,  old , val)
    end

    def tick
      return unless @instruction
      @clock += 1
      name = @instruction.class.name.split("::").last
      fetch = send "execute_#{name}"
      return unless fetch
      fetch_next_intruction
    end

    def fetch_next_intruction
      if(@instruction != @block.codes.last)
        set_instruction @block.codes[  @block.codes.index(@instruction)  + 1]
      else
        next_b = @block.method.source.blocks.index(@block) + 1
        set_block @block.method.source.blocks[next_b]
      end
    end

    def object_for reg
      id = get_register(reg)
      Virtual.machine.objects[id]
    end

    # Instruction interpretation starts here
    def execute_Branch
      target = @instruction.block
      set_block target
      false
    end

    def execute_LoadConstant
      to = @instruction.register
      value = @instruction.constant.object_id
      set_register( to , value )
      true
    end

    def execute_GetSlot
      object = object_for( @instruction.array )
      value = object.internal_object_get( @instruction.index )
      value = value.object_id unless value.is_a? Integer
      set_register( @instruction.register , value )
      true
    end

    def execute_SetSlot
      value = object_for( @instruction.register )
      object = object_for( @instruction.array )
      object.internal_object_set( @instruction.index , value )
      trigger(:object_changed, @instruction.array )
      true
    end

    def execute_RegisterTransfer
      value = get_register @instruction.from
      set_register @instruction.to , value
      true
    end

    def execute_FunctionCall
      @link = [@block , @instruction]
      next_block = @instruction.method.source.blocks.first
      set_block next_block
      false
    end

    def execute_SaveReturn
      object = object_for @instruction.register
      raise "save return has nothing to save" unless @link
      trigger(:object_changed, @instruction.register )
      object.internal_object_set @instruction.index , @link
      @link = nil
      true
    end

    def execute_Syscall
      name = @instruction.name
      case name
      when :putstring
        str = object_for( :r1 ) # should test length, ie r2
        raise "NO string for putstring #{str.class}:#{str.object_id}" unless str.is_a? Symbol
        @stdout += str.to_s
      when :exit
        set_instruction(nil)
        return false
      else
        raise "un-implemented syscall #{name}"
      end
      true
    end

    def execute_FunctionReturn
      object = object_for( @instruction.register )
      #wouldn't need to assign to link, but makes testing easier
      link = object.internal_object_get( @instruction.index )
      @block , @instruction = link
      # we jump back to the call instruction. so it is as if the call never happened and we continue
      true
    end
  end
end