require "erb"
require_relative "erbln"

class ErbPP

  def initialize(parent=nil, erb_base=nil, **opts, &block)
    @parent = parent
    @children = []
    @opts = opts
    set erb_base: erb_base if erb_base
    @parent.add_child(self) if @parent
    instance_eval(&block) if block
  end

  attr_reader :children
  attr_accessor :parent

  def add_child(child)
    @children.push(child)
  end

  def set(**opts)
    @opts.merge!(opts)
  end

  def get(key, *args, &block)
    if respond_to?(key)
      return send(key, *args, &block)
    end
    if args.empty? && block.nil? && @opts.has_key?(key)
      return @opts[key]
    end
    if @parent
      return @parent.get(key, *args, &block)
    end
    nil
  end

  def description
    if s = @opts[:description] || @opts[:desc]
      s.gsub(/\@\{/,"[").gsub(/\@\}/,"]")
    end
  end

  alias desc description

  alias method_missing_alias method_missing

  def method_missing(_meth_id, *args, &block)
    if args.empty?
      #$stderr.puts _meth_id.inspect
      v = get(_meth_id, *args, &block)
      return v if !v.nil?
    end
    method_missing_alias(_meth_id, *args, &block)
  end

  # ERB Loader

  def load_erb(base_name)
    safe_level = nil
    trim_mode = '%<>'
    file = base_name + get(:erb_suffix)
    dirs = get(:erb_dir)
    dirs = [dirs] if !dirs.kind_of?(Array)
    dirs.each do |x|
      Dir.glob(x).each do |dir|
        path = File.join(dir,file)
        if File.exist?(path)
          if get(:line_number)
            erb = ERBLN.new(File.read(path), path, trim_mode)
          else
            erb = ERB.new(File.read(path), safe_level, trim_mode)
            erb.filename = path
          end
          return erb
        end
      end
    end
    raise "file not found: #{file.inspect} in #{dirs.inspect}"
  end

  def run
    if base = @opts[:erb_base]
      load_erb(base).run(binding)
    end
  end

  def result
    if base = @opts[:erb_base]
      load_erb(base).result(binding)
    end
  end

  def write(output)
    File.open(output,"wt") do |f|
      f.print(result)
    end
  end

  def init_def
  end

  def find_tmpl(name)
    @parent.children.find{|x| x.name == name }
  end

  def find(name)
    children.find{|x| x.name == name }
  end
end


class DefLib < ErbPP
  def initialize(parent=nil, **opts, &block)
    opts[:erb_base] ||= 'lib'
    opts[:include_files] ||= []
    super(parent, **opts, &block)
  end
  def id_assign
    ids = []
    @children.each{|c| a=c.get(:id_list); ids.concat(a) if a}
    ids.sort.uniq.map{|x| "id_#{x[1]} = rb_intern(\"#{x[0]}\");"}
  end
  def id_decl
    ids = []
    @children.each{|c| a=c.get(:id_list); ids.concat(a) if a}
    ids.sort.uniq.map{|x| "static ID id_#{x[1]};\n"}
  end
  def def_class(**opts, &block)
    DefClass.new(self, **opts, &block)
  end
  def def_module(**opts, &block)
    DefModule.new(self, **opts, &block)
  end
end

module DeclMethod
  def def_alloc_func(m, erb_path=nil, **opts, &block)
    DefAllocFunc.new(self, erb_path||m, name:m, singleton:true, **opts, &block)
  end
  def undef_alloc_func
    UndefAllocFunc.new(self)
  end
  def def_method(m, erb_path=nil, **opts, &block)
    DefMethod.new(self, erb_path||m, name:m, **opts, &block)
  end
  def undef_method(m)
    UndefMethod.new(self,name:m)
  end
  def def_singleton_method(m, erb_path=nil, **opts, &block)
    DefMethod.new(self, erb_path||m, name:m, singleton:true, **opts, &block)
  end
  def undef_singleton_method(m)
    UndefSingletonMethod.new(self,name:m)
  end
  def def_module_function(m, erb_path=nil, **opts, &block)
    DefModuleFunction.new(self, erb_path||m, name:m, **opts, &block)
  end
  def def_alias(from, to)
    DefAlias.new(self, from:from, to:to)
  end
  def def_const(m, v, **opts, &block)
    DefConst.new(self, name:m, value:v, **opts, &block)
  end
end

class DefModule < ErbPP
  include DeclMethod
  def initialize(parent, **opts, &block)
    eb = opts[:erb_base] || 'module'
    super(parent, erb_base:eb, **opts, &block)
  end
  def id_list
    @id_list ||= []
  end
  def def_id(name,var=nil)
    var = name.gsub(/\?/,"_p").gsub(/\!/,"_bang") if var.nil?
    id_list << [name,var]
  end
  def init_def
    load_erb(init_erb).result(binding)
  end
  def init_erb
    @opts[:init_erb] || "init_module"
  end
  def method_code
    @children.map{|c| c.result}.join("\n")
  end
  def _mod_var
    @opts[:module_var]
  end
end

class DefClass < DefModule
  def initialize(parent, **opts, &block)
    eb = opts[:erb_base] || 'class'
    super(parent, erb_base:eb, **opts, &block)
  end
  def _mod_var
    @opts[:class_var]
  end
  def init_erb
    @opts[:init_erb] || "init_class"
  end
  def super_class
    @opts[:super_class] || "rb_cObject"
  end
  def free_func
    @opts[:free_func] || "gsl_"+get(:name)+"_free"
  end
end

class DefMethod < ErbPP
  include DeclMethod

  def initialize(parent, erb_base, **opts, &block)
    super(parent, **opts, &block)
    set erb_base: erb_base
  end

  def id_op
    if op.size == 1
      "'#{op}'"
    else
      "id_#{c_name}"
    end
  end

  def c_name
    @opts[:name].gsub(/\?/,"_p").gsub(/\!/,"_bang").gsub(/=/,"_set")
  end

  def op_map
    @opts[:op] || @opts[:name]
  end

  def c_func(n_arg=nil)
    set n_arg: n_arg if n_arg
    s = (singleton) ? "_s" : ""
    "#{@parent.name}#{s}_#{c_name}"
  end

  def c_iter
    "iter_#{c_func}"
  end

  def define_method_args
    "#{_mod_var}, \"#{op_map}\", #{c_func}, #{n_arg}"
  end

  def init_def
    return if n_arg == :nodef
    s = (singleton) ? "_singleton" : ""
    "rb_define#{s}_method(#{define_method_args});"
  end

  def singleton
    @opts[:singleton]
  end
end

class DefModuleFunction < DefMethod
  def initialize(parent, erb_base, **opts, &block)
    super(parent, erb_base, **opts, &block)
    set singleton: true
  end

  def init_def
    return if n_arg == :nodef
    "rb_define_module_function(#{define_method_args});"
  end
end

class DefAlias < ErbPP
  def init_def
    "rb_define_alias(#{_mod_var}, \"#{from}\", \"#{to}\");"
  end
end

class DefAllocFunc < DefMethod
  def init_def
    "rb_define_alloc_func(#{_mod_var}, #{c_func});"
  end
end

class UndefAllocFunc < ErbPP
  def init_def
    "rb_undef_alloc_func(#{_mod_var});"
  end
end

class UndefMethod < ErbPP
  def init_def
    "rb_undef_method(#{_mod_var},\"#{name}\");"
  end
end

class UndefSingletonMethod < ErbPP
  def init_def
    "rb_undef_method(rb_singleton_class(#{_mod_var}),\"#{name}\");"
  end
end

class DefConst < ErbPP
  def init_def
    "/*#{desc}*/
    rb_define_const(#{_mod_var},\"#{name}\",#{value});"
  end
end

class DefError < ErbPP
  def initialize(parent, name, sup_var, **opts, &block)
    super(parent, error_name:name, error_var:"e"+name, super_var:sup_var,
          **opts, &block)
  end
  def result
    "static VALUE #{error_var};"
  end
  def init_def
    "/*#{description}*/
    #{error_var} = rb_define_class_under(#{ns_var},\"#{error_name}\",#{super_var});"
  end
end

class DefStruct < ErbPP
  def method_code
    "static VALUE #{class_var};"
  end
  def init_def
    items = members.map{|s| "\"#{s}\""}.join(",")
    "/*#{description}*/
    #{class_var} = rb_struct_define(\"#{class_name}\",#{items},NULL);"
  end
end

class DefInclueModule < ErbPP
  def initialize(parent=nil, incl_class, incl_module, **opts, &block)
    super(parent,incl_class:incl_class,incl_module:incl_module,**opts,&block)
  end
  def init_def
    "rb_include_module(#{get(:incl_class)}, #{get(:incl_module)});"
  end
end
