class Crystal::Program
  # How code generated by macro should be parsed
  enum MacroExpansionMode
    Normal
    Lib
    StructOrUnion
    Enum
  end

  # Temporary files which are generated by macro runs that need to be
  # deleted after the compilation is finished.
  getter tempfiles = [] of String

  # Returns a `MacroExpander` to expand macro code into crystal code.
  getter(macro_expander) { MacroExpander.new self }

  # A cache of compiled "macro run" files.
  # The keys are filenames that were compiled, the values are  executable
  # filenames ready to be run (so they don't need to be compiled twice)
  @compiled_macros_cache = {} of String => String

  # Returns a new temporary file, which tries to be stored in the
  # cache directory associated to a program. This file is then added
  # to `tempfiles` so they can eventually be deleted.
  def new_tempfile(basename)
    filename = if cache_dir = @cache_dir
                 File.join(cache_dir, basename)
               else
                 Crystal.tempfile(basename)
               end
    tempfiles << filename
    filename
  end

  def expand_macro(a_macro : Macro, call : Call, scope : Type, path_lookup : Type? = nil, a_def : Def? = nil)
    interpreter = MacroInterpreter.new self, scope, path_lookup || scope, a_macro, call, a_def
    a_macro.body.accept interpreter
    interpreter.to_s
  end

  def expand_macro(node : ASTNode, scope : Type, path_lookup : Type? = nil, free_vars = nil, a_def : Def? = nil)
    interpreter = MacroInterpreter.new self, scope, path_lookup || scope, node.location, def: a_def
    interpreter.free_vars = free_vars
    node.accept interpreter
    interpreter.to_s
  end

  def parse_macro_source(generated_source, the_macro, node, vars, inside_def = false, inside_type = false, inside_exp = false, mode : MacroExpansionMode = MacroExpansionMode::Normal)
    parse_macro_source(generated_source, the_macro, node, vars, inside_def, inside_type, inside_exp) do |parser|
      case mode
      when .lib?
        parser.parse_lib_body
      when .struct_or_union?
        parser.parse_c_struct_or_union_body
      when .enum?
        parser.parse_enum_body
      else
        parser.parse
      end
    end
  end

  def parse_macro_source(generated_source, the_macro, node, vars, inside_def = false, inside_type = false, inside_exp = false)
    begin
      parser = Parser.new(generated_source, @program.string_pool, [vars.dup])
      parser.filename = VirtualFile.new(the_macro, generated_source, node.location)
      parser.visibility = node.visibility
      parser.def_nest = 1 if inside_def
      parser.type_nest = 1 if inside_type
      parser.wants_doc = @program.wants_doc?
      generated_node = yield parser
      normalize(generated_node, inside_exp: inside_exp)
    rescue ex : Crystal::SyntaxException
      node.raise "macro didn't expand to a valid program, it expanded to:\n\n#{"=" * 80}\n#{"-" * 80}\n#{Crystal.with_line_numbers(generated_source)}\n#{"-" * 80}\n#{ex.to_s_with_source(generated_source)}\n#{"=" * 80}"
    end
  end

  def macro_run(filename, args)
    compiled_file = @compiled_macros_cache[filename] ||= macro_compile(filename)

    command = String.build do |str|
      str << compiled_file.inspect
      args.each do |arg|
        str << " "
        str << arg.inspect
      end
    end

    result = `#{command}`
    {$?.success?, result}
  end

  def macro_compile(filename)
    source = File.read(filename)

    compiler = Compiler.new

    # Although release takes longer, once the bc is cached in .crystal
    # the subsequent times will make program execution faster.
    compiler.release = true

    # Don't cleanup old directories after compiling: it might happen
    # that in doing so we remove the directory associated with the current
    # compilation (for example if we have more than 10 macro runs, the current
    # directory will be the oldest).
    compiler.cleanup = false

    safe_filename = filename.gsub(/[^a-zA-Z\_\-\.]/, "_")
    tempfile_path = @program.new_tempfile("macro-run-#{safe_filename}")
    compiler.compile Compiler::Source.new(filename, source), tempfile_path
    tempfile_path
  end
end
