require "lignite/defines"
require "lignite/enums"
require "lignite/ev3_ops"

module Lignite
  # Compiles methods for all the instructions in ev3.yml
  # The methods return the {ByteString}s corresponding to the ops.
  class OpCompiler
    include Logger
    include Bytes
    include Ev3Ops

    # @param globals [Variables,nil]
    # @param locals  [Variables,nil]
    def initialize(globals = nil, locals = nil)
      @globals = globals
      @locals = locals
    end

    private

    PRIMPAR_SHORT      = 0x00
    PRIMPAR_LONG       = 0x80

    PRIMPAR_CONST      = 0x00
    PRIMPAR_VARIABEL   = 0x40
    PRIMPAR_LOCAL      = 0x00
    PRIMPAR_GLOBAL     = 0x20
    PRIMPAR_HANDLE     = 0x10
    PRIMPAR_ADDR       = 0x08

    PRIMPAR_INDEX      = 0x1F
    PRIMPAR_CONST_SIGN = 0x20
    PRIMPAR_VALUE      = 0x3F

    PRIMPAR_BYTES      = 0x07

    PRIMPAR_STRING_OLD = 0
    PRIMPAR_1_BYTE     = 1
    PRIMPAR_2_BYTES    = 2
    PRIMPAR_4_BYTES    = 3
    PRIMPAR_STRING     = 4

    PRIMPAR_LABEL      = 0x20

    def make_lcn(n, bytes)
      case bytes
      when 0
        [n & PRIMPAR_VALUE]
      when 1
        [PRIMPAR_LONG | PRIMPAR_1_BYTE, n & 0xff]
      when 2
        [PRIMPAR_LONG | PRIMPAR_2_BYTES, n & 0xff, (n >> 8) & 0xff]
      else
        [PRIMPAR_LONG | PRIMPAR_4_BYTES,
         n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]
      end
    end

    def make_lc(n, bytes = nil)
      bytes ||= if (-31..31).cover? n
        0
      elsif (-127..127).cover? n
        1
      elsif (-32767..32767).cover? n
        2
      else
        4
      end
      make_lcn(n, bytes)
    end

    def make_v(n, local_or_global)
      vartag = PRIMPAR_VARIABEL | local_or_global
      if (0..31).cover? n
        [vartag | (n & PRIMPAR_VALUE)]
      elsif (0..255).cover? n
        [vartag | PRIMPAR_LONG | PRIMPAR_1_BYTE, n & 0xff]
      elsif (0..65535).cover? n
        [vartag | PRIMPAR_LONG | PRIMPAR_2_BYTES, n & 0xff, (n >> 8) & 0xff]
      else
        [vartag | PRIMPAR_LONG | PRIMPAR_4_BYTES,
         n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]
      end
    end

    # Reference a variable.
    # (For declaring, see {VariableDeclarer}.)
    def make_var(sym)
      raise "No variables declared, cannot process symbols" if @locals.nil? && @globals.nil?
      if @locals.key?(sym)
        o = @locals.offset(sym)
        make_v(o, PRIMPAR_LOCAL)
      elsif @globals.key?(sym)
        o = @globals.offset(sym)
        make_v(o, PRIMPAR_GLOBAL)
      else
        raise "Variable #{sym} not found"
      end
    end

    # @return [ByteString]
    def param_multiple(*args)
      args.map { |a| param_simple(a) }.join("")
    end

    # @return [ByteString]
    def param_simple(x)
      case x
      when Integer
        make_lc(x).map(&:chr).join("")
      when Complex
        # a Complex number:
        #   #real: just like an Integer above, but
        #   #imag tells how many bytes to use for it
        make_lc(x.real, x.imag).map(&:chr).join("")
      when String
        u8(0x80) + x + u8(0x00)
      when Float
        u8(0x83) + f32(x)
      when Symbol
        make_var(x).map(&:chr).join("")
      else
        raise ArgumentError, "Unexpected type: #{x.class}"
      end
    end
  end
end