includes 'parser/walker' 'parser/sexp' 'parser/env' 'parser/compiler_helper'

c = walker.new
c.squish compiler_helper

c.init = { ast |
  my.ast = ast
  my.env = env.new
  my.liftable_functions = []
  my.liftable_strings = []
  my.lifted_string_index = [:]
  x = 0
  my.next_liftable = { x = x + 1 }
}

c.walk :program { node |
  c = my
  my.results = node.nodes.map({ n |
    c.process(n)
  })
  my.results = my.lifted_strings + my.liftable_functions + my.results
}

c.walk :local_var_assign { node |
  temp = my.env.new_var node.lhs
  rhs = process node.rhs, temp
  my.env.set_type temp, rhs[:type]
  true? temp == rhs.var
  { r temp, "local #{temp}\n#{rhs.out}" }
  { r temp, "#{rhs.out}\nlocal #{temp} = #{rhs.var}" }
}

c.walk :local_var_assign_interactive { node |
  temp = my.env.new_var node.lhs
  rhs = process node.rhs, temp
  my.env.set_type temp, rhs[:type]
  r temp, "#{rhs.out}\n#{temp} = #{rhs.var}"
}

c.walk :local_var_reassign, c->var_reassign
c.walk :upvar_assign, c->var_reassign

c.walk :field_assign { node, var = null |
  lhs = process node.lhs.target
  var = node.lhs.field
  rhs = process node.rhs

  out = lhs.out << "\n" << rhs.out << "
    if _type(#{lhs.var}) == 'table' then
      #{lhs.var}['#{var}'] = #{rhs.var}
    else
      _error('Cannot set method on ' .. #{lhs.var})
    end
    "

  r rhs.var, out
}

c.walk :get_local_value, c->get_a_value
c.walk :get_up_value, c->get_a_value

c.walk :get_value { node, var = null |
  res = set_result var
  res.out << get_value(node.value, res.var)
  res
}

c.walk :invoke_local { node, var = null |
  invoke node, null, ->invoke_local, var
}

c.walk :invoke_up { node, var = null |
  invoke node, null, ->invoke_local, var
}

conds = [:true_question, :false_question, :null_question]
inlineable_types = [:function, :liftable_function, :string, :simple_string, :symbol, :number, :array, :hash]
inlineable? = { n |
  n.null? || { inlineable_types.include? n.name }
}

inlineable_when? = { node |
  node.method == :when &&
    { node.args.all? ->inlineable? }
}

c.walk :invoke_self { node, var = null |
  meth = node.method
  true? conds.include?(meth) && { inlineable?(node.args[1]) && { inlineable?(node.args[2]) }}
    {
      true? meth == :true_question
        { true_if(node, var) }
        { true? meth == :false_question
          { false_if(node, var) }
          { null_if(node, var) }
        }
    }
    {
      true? inlineable_when?(node)
      {
        inline_when(node, var)
      }
      {
        invoke node, null, ->invoke_self, var
      }
    }
}

c.walk :invoke_index_get { node, var = null |
  target = process node.target, var
  node.method = null
  invoke node, target, ->invoke_index_get, var
}

c.walk :simple_index_get { node, var = null |
  target = process node.target
  invoke_index_get_direct node, target, var
}

c.walk :simple_index_set { node, var = null |
  target = process node.target
  invoke_index_set_direct node, target, var
}

c.walk :invoke_function { node, var = null |
  target = process node.target, var
  node.method = null
  invoke node, target, ->invoke_function, var
}

c.walk :call { node, var = null |
  target = process node.target
  invoke node, target, ->invoke_method, var
}

c.walk :number { node |
  val = node.value.to_f
  r val, '', type: :number
}

c.walk :array { node, var = null |
  true? node.nodes.empty?
    {
      set_result var, "array:new()", type: :array
    }
    {
      res = set_result var
      ares_var = res.var
      temp_var = my.env.next_temp
      out = "#{res.out}\ndo\nlocal #{temp_var}\n#{ares_var} = {}\n"
      comp = my

      node.nodes.each_with_index { n, i |
        elem = comp.process n, temp_var
        out << elem.out << "\n" <<
          "#{ares_var}[#{i + 1}] = #{elem.var}\n"
      }

      out << "#{ares_var} = array:new(#{ares_var})\nend\n"

      r ares_var, out, type: :array
    }
}

c.walk :hash { node, var = null |
  true? node.nodes.empty?
    {
      set_result var, "hash:new()", type: :hash
    }
    {
      res = set_result var, "{}"
      hres_var = res.var
      key_temp = my.env.next_temp
      val_temp = my.env.next_temp

      out = "#{res.out}\ndo\nlocal #{key_temp};local #{val_temp}\n"

      w = my
      node.nodes.each { pair |
        key = w.process pair.first, key_temp
        val = w.process pair.last, val_temp

        out << "#{key.out}\n#{val.out}\n#{hres_var}[#{key.var}] = #{val.var}\n"
      }

      my.env.unset key_temp
      my.env.unset val_temp

      out << "\n#{hres_var} = hash:new(#{hres_var})\nend\n"

      r hres_var, out, type: :hash
    }
}

c.walk :string { node, var = null |
  true? node.nodes.empty?
    {
      set_result var, 'string:new("")', type: :string
    }
    {
      set_result var, "string:new(\"#{escape_string node.value}\")", type: :string
    }
}

c.walk :simple_string { node, var = null |
  escaped = node.value.sub("\\[0a-z\"]", "\\%1").sub("\n", '\n')
  set_result var, "string:new('#{escaped}')", type: :string
}

c.walk :simple_symbol { node, var = null |
  escaped = node.value.sub("\\[0a-z\"]", "\\%1").sub("\n", '\n')
  lift_string escaped, var
}

c.walk :symbol { node, var = null |
  true? node.nodes.empty?
    {
      lift_string '', var
    }
    {
      lift_string escape_string(node.value), var, :double
    }
}

c.walk :function { node, var = null |
  w = my
  my.env.new_scope

  args = do_args node

  res = set_result var, "function(#{args[:arg_list]})"
  res.out << args[:out]

  body = node.body.copy
  last = body.pop

  body_res = body.map({ n | w.process(n, '_dummy').out }).join("\n")
  res.out << body_res

  true? last
   {
     last_res = my.process last
     res.out << last_res.out << "\n" << "return #{last_res.var}\n"
   }
   {
     true? body.empty?
       { res.out << "return object:null()" }
   }

  my.env.pop_scope
  res.out << "\nend\n"
  res[:type] = :function
  my.env.set_type res.var, :function
  res
}

c.walk :liftable_function { node, var = null |
  w = my
  my.env.new_scope

  args = do_args node

  res = true? node.upvars,
          { set_result "_lifted[#{my.next_liftable}]", "function(_argtable, #{args[:arg_list]})" }
          { set_result "_lifted[#{my.next_liftable}]", "function(#{args[:arg_list]})" } # just _self and real args
  true? node.upvars
    {
      res.out << node.upvars.map({ u |
      "local #{w.env[u]} = _argtable['#{w.env[u]}']"
      }).join("\n")
    }

  res.out << args[:out]

  body = node.body.copy
  last = body.pop

  body_res = body.map({ n | w.process(n, '_dummy').out }).join("\n")
  res.out << body_res

  true? last
   {
     last_res = my.process last
     res.out << last_res.out << "\n" << "return #{last_res.var}\n"
   }
   {
     true? body.empty?
       { res.out << "return object:null()" }
   }

  my.env.pop_scope
  res.out << "\nend\n"
  res[:type] = :function
  my.liftable_functions << res

  lifted_call = true? node.upvars
    {
      "_lifted_call(#{res.var}, {})"
    }
    {
      res.var
    }

  lifted = set_result var, lifted_call
  my.env.set_type lifted.var, :function
  lifted[:type] = :function

  true? node.upvars
    {
      e = my.env
      lifted.out << node.upvars.map({ u |
        "#{lifted.var}.arg_table['#{w.env[u]}'] = #{w.env[u]}"
      }).join("\n")
    }

  lifted
}

c.walk :arg { node, var = null |
  temp = my.env.new_var node.id
  r temp, '', arg_type: :arg
}

c.walk :var_arg { node, var = null |
  temp = my.env.new_var node.id
  r '...', "local #{temp} = array:new(...)\n", arg_type: :var_arg
}

c.walk :def_arg { node, var = null |
  temp = my.env.new_var node.id
  rhs = process node.value, temp
  out = "
    if #{temp} == nil then
      #{rhs.out}
  "

  false? rhs.var == temp
    { out << "#{temp} = #{rhs.var}\n" }

  out << "end\n"

  r temp, out, arg_type: :def_arg
}

c.walk :meth_access_local { node, var = null |
  res = set_result var
  res.out << get_method_local node.method, res.var
  res
}

c.walk :meth_access_up { node, var = null |
  res = set_result var
  res.out << get_method_local node.method, res.var
  res
}

c.walk :meth_access_self { node, var = null |
  res = set_result var
  res.out << get_method_self node.method, res.var
  res
}

c.walk :meth_access { node, var = null |
  res = set_result var
  target = process node.target
  res.out << target.out
  res.out << get_method target.var, node.method, res.var
  res
}

c.walk :regex { node, var = null |
  body = node.body.sub("\\", "\\\\").sub(/"/, "\\\"")
  opts = node.opts.dice.unique.join.sub(/m/, 's')
  set_result var, "regex:new(\"#{body}\", \"#{opts}\")", type: :regex
}

c.walk :string_interp { node, var = null |
  res = set_result var
  temp = my.env.next_temp
  res.out << "\ndo\nlocal #{temp} = {}\n"
  w = my
  node.nodes.each_with_index { n, i |
    true? n.name == :string
      {  res.out << "#{temp}[#{i + 1}] = \"#{w.escape_string n.value}\"\n" }
      {
        o = w.process n, "#{temp}[#{i + 1}]"
        res.out << o.out
        res.out << "#{temp}[#{i + 1}] = _tostring(#{o.var})\n"
      }
  }
  res.out << "#{res.var} = string:new(_table.concat(#{temp}))\nend\n"
  my.env.unset temp
  my.env.set_type res.var, :string
  res[:type] = :string
  res
}

c.walk :string_eval { node, var = null |
  res = set_result var
  w = my
  values = node.nodes.map { n | w.process(n, res.var) }
  res.out << values.map(:out).join("\n")
  true? res.var != values.last.var
    { res.out << "\n#{res.var} = #{values.last.var}\n" }
  res
}

c.walk :binop { node, var = null |
  res = reorder_ops node
  process res
}

c.walk :invoke_numbers { node, var = null |
  res = set_result var
  res.out << invoke_numbers node.lhs.value, node.op, node.rhs.value, res.var
  res
}

c.walk :invoke_number { node, var = null |
  res = set_result var
  rhs = process node.rhs
  res.out << rhs.out << "\n"
  res.out << invoke_number_lhs node.lhs.value, node.op, rhs.var, res.var
  res
}

c.walk :invoke_number_rhs { node, var = null |
  res = set_result var
  lhs = process node.lhs
  res.out << lhs.out << "\n"
  res.out << invoke_number_rhs lhs.var, node.op, node.rhs.value, res.var
  res
}

export c, :compiler