defmodule Kernel.CLI do
  @moduledoc false

  @blank_config %{commands: [], output: ".", compile: [],
                  halt: true, compiler_options: [], errors: [],
                  pa: [], pz: [], verbose_compile: false}

  @doc """
  This is the API invoked by Elixir boot process.
  """
  def main(argv) do
    argv = for arg <- argv, do: IO.chardata_to_string(arg)

    {config, argv} = parse_argv(argv)
    System.argv(argv)

    run fn _ ->
      errors = process_commands(config)

      if errors != [] do
        Enum.each(errors, &IO.puts(:stderr, &1))
        System.halt(1)
      end
    end, config.halt
  end

  @doc """
  Runs the given function by catching any failure
  and printing them to stdout. `at_exit` hooks are
  also invoked before exiting.

  This function is used by Elixir's CLI and also
  by escripts generated by Elixir.
  """
  def run(fun, halt \\ true) do
    {ok_or_shutdown, status} = exec_fun(fun, {:ok, 0})
    if ok_or_shutdown == :shutdown or halt do
      {_, status} = at_exit({ok_or_shutdown, status})

      # Ensure Logger messages are flushed before halting
      case :erlang.whereis(Logger) do
        pid when is_pid(pid) -> Logger.flush()
        _ -> :ok
      end

      System.halt(status)
    end
  end

  @doc false
  def parse_argv(argv) do
    parse_argv(argv, @blank_config)
  end

  @doc false
  def process_commands(config) do
    results = Enum.map(Enum.reverse(config.commands), &process_command(&1, config))
    errors  = for {:error, msg} <- results, do: msg
    Enum.reverse(config.errors, errors)
  end

  ## Helpers

  defp at_exit(res) do
    hooks = :elixir_config.get_and_put(:at_exit, [])
    res = Enum.reduce(hooks, res, &exec_fun/2)
    if hooks == [], do: res, else: at_exit(res)
  end

  defp exec_fun(fun, res) when is_function(fun, 1) and is_tuple(res) do
    parent = self()

    {pid, ref} =
      spawn_monitor(fn ->
        try do
          fun.(elem(res, 1))
        catch
          :exit, {:shutdown, int} when is_integer(int) ->
            send parent, {self, {:shutdown, int}}
            exit({:shutdown, int})
          :exit, reason
              when reason == :normal
              when reason == :shutdown
              when tuple_size(reason) == 2 and elem(reason, 0) == :shutdown ->
            send parent, {self, {:shutdown, 0}}
            exit(reason)
          kind, reason ->
            stack = System.stacktrace
            print_error(kind, reason, stack)
            send parent, {self, {:shutdown, 1}}
            exit(to_exit(kind, reason, stack))
        else
          _ ->
            send parent, {self, res}
        end
      end)

    receive do
      {^pid, res} ->
        :erlang.demonitor(ref, [:flush])
        res
      {:DOWN, ^ref, _, _, other} ->
        print_error({:EXIT, pid}, other, [])
        {:shutdown, 1}
    end
  end

  defp to_exit(:throw, reason, stack), do: {{:nocatch, reason}, stack}
  defp to_exit(:error, reason, stack), do: {reason, stack}
  defp to_exit(:exit, reason, _stack), do: reason

  defp shared_option?(list, config, callback) do
    case parse_shared(list, config) do
      {[h | hs], _} when h == hd(list) ->
        new_config = %{config | errors: ["#{h} : Unknown option" | config.errors]}
        callback.(hs, new_config)
      {new_list, new_config} ->
        callback.(new_list, new_config)
    end
  end

  defp print_error(kind, reason, trace) do
    IO.puts :stderr, Exception.format(kind, reason, prune_stacktrace(trace))
  end

  @elixir_internals [:elixir, :elixir_exp, :elixir_compiler, :elixir_module, :elixir_clauses,
                     :elixir_translator, :elixir_expand, :elixir_lexical, :elixir_exp_clauses,
                     :elixir_def, :elixir_map]

  defp prune_stacktrace([{mod, _, _, _} | t]) when mod in @elixir_internals do
    prune_stacktrace(t)
  end

  defp prune_stacktrace([{__MODULE__, :wrapper, 1, _} | _]) do
    []
  end

  defp prune_stacktrace([h | t]) do
    [h | prune_stacktrace(t)]
  end

  defp prune_stacktrace([]) do
    []
  end

  # Parse shared options

  defp parse_shared([opt | _t], _config) when opt in ["-v", "--version"] do
    if function_exported?(IEx, :started?, 0) and IEx.started? do
      IO.puts "IEx " <> System.build_info[:build]
    else
      IO.puts :erlang.system_info(:system_version)
      IO.puts "Elixir " <> System.build_info[:build]
    end

    System.halt 0
  end

  defp parse_shared(["-pa", h | t], config) do
    paths = expand_code_path(h)
    Enum.each(paths, &:code.add_patha/1)
    parse_shared t, %{config | pa: config.pa ++ paths}
  end

  defp parse_shared(["-pz", h | t], config) do
    paths = expand_code_path(h)
    Enum.each(paths, &:code.add_pathz/1)
    parse_shared t, %{config | pz: config.pz ++ paths}
  end

  defp parse_shared(["--app", h | t], config) do
    parse_shared t, %{config | commands: [{:app, h} | config.commands]}
  end

  defp parse_shared(["--no-halt" | t], config) do
    parse_shared t, %{config | halt: false}
  end

  defp parse_shared(["-e", h | t], config) do
    parse_shared t, %{config | commands: [{:eval, h} | config.commands]}
  end

  defp parse_shared(["-r", h | t], config) do
    parse_shared t, %{config | commands: [{:require, h} | config.commands]}
  end

  defp parse_shared(["-pr", h | t], config) do
    parse_shared t, %{config | commands: [{:parallel_require, h} | config.commands]}
  end

  defp parse_shared([erl, _ | t], config) when erl in ["--erl", "--sname", "--name", "--cookie", "--logger-otp-reports", "--logger-sasl-reports"] do
    parse_shared t, config
  end

  defp parse_shared([erl | t], config) when erl in ["--detached", "--hidden", "--werl"] do
    parse_shared t, config
  end

  defp parse_shared(list, config) do
    {list, config}
  end

  defp expand_code_path(path) do
    path = Path.expand(path)
    case Path.wildcard(path) do
      []   -> [to_charlist(path)]
      list -> Enum.map(list, &to_charlist/1)
    end
  end

  # Process init options

  defp parse_argv(["--" | t], config) do
    {config, t}
  end

  defp parse_argv(["+elixirc" | t], config) do
    parse_compiler t, config
  end

  defp parse_argv(["+iex" | t], config) do
    parse_iex t, config
  end

  defp parse_argv(["-S", h | t], config) do
    {%{config | commands: [{:script, h} | config.commands]}, t}
  end

  defp parse_argv([h | t] = list, config) do
    case h do
      "-" <> _ ->
        shared_option? list, config, &parse_argv(&1, &2)
      _ ->
        if Keyword.has_key?(config.commands, :eval) do
          {config, list}
        else
          {%{config | commands: [{:file, h} | config.commands]}, t}
        end
    end
  end

  defp parse_argv([], config) do
    {config, []}
  end

  # Parse compiler options

  defp parse_compiler(["--" | t], config) do
    {config, t}
  end

  defp parse_compiler(["-o", h | t], config) do
    parse_compiler t, %{config | output: h}
  end

  defp parse_compiler(["--no-docs" | t], config) do
    parse_compiler t, %{config | compiler_options: [{:docs, false} | config.compiler_options]}
  end

  defp parse_compiler(["--no-debug-info" | t], config) do
    parse_compiler t, %{config | compiler_options: [{:debug_info, false} | config.compiler_options]}
  end

  defp parse_compiler(["--ignore-module-conflict" | t], config) do
    parse_compiler t, %{config | compiler_options: [{:ignore_module_conflict, true} | config.compiler_options]}
  end

  defp parse_compiler(["--warnings-as-errors" | t], config) do
    parse_compiler t, %{config | compiler_options: [{:warnings_as_errors, true} | config.compiler_options]}
  end

  defp parse_compiler(["--verbose" | t], config) do
    parse_compiler t, %{config | verbose_compile: true}
  end

  defp parse_compiler([h | t] = list, config) do
    case h do
      "-" <> _ ->
        shared_option? list, config, &parse_compiler(&1, &2)
      _ ->
        pattern = if File.dir?(h), do: "#{h}/**/*.ex", else: h
        parse_compiler t, %{config | compile: [pattern | config.compile]}
    end
  end

  defp parse_compiler([], config) do
    {%{config | commands: [{:compile, config.compile} | config.commands]}, []}
  end

  # Parse IEx options

  defp parse_iex(["--" | t], config) do
    {config, t}
  end

  # This clause is here so that Kernel.CLI does not
  # error out with "unknown option"
  defp parse_iex(["--dot-iex", _ | t], config) do
    parse_iex t, config
  end

  defp parse_iex([opt, _ | t], config) when opt in ["--remsh"] do
    parse_iex t, config
  end

  defp parse_iex(["-S", h | t], config) do
    {%{config | commands: [{:script, h} | config.commands]}, t}
  end

  defp parse_iex([h | t] = list, config) do
    case h do
      "-" <> _ ->
        shared_option? list, config, &parse_iex(&1, &2)
      _ ->
        {%{config | commands: [{:file, h} | config.commands]}, t}
    end
  end

  defp parse_iex([], config) do
    {config, []}
  end

  # Process commands

  defp process_command({:cookie, h}, _config) do
    if Node.alive? do
      wrapper fn -> Node.set_cookie(String.to_atom(h)) end
    else
      {:error, "--cookie : Cannot set cookie if the node is not alive (set --name or --sname)"}
    end
  end

  defp process_command({:eval, expr}, _config) when is_binary(expr) do
    wrapper fn -> Code.eval_string(expr, []) end
  end

  defp process_command({:app, app}, _config) when is_binary(app) do
    case Application.ensure_all_started(String.to_atom(app)) do
      {:error, {app, reason}} ->
        {:error, "--app : Could not start application #{app}: " <>
          Application.format_error(reason)}
      {:ok, _} ->
        :ok
    end
  end

  defp process_command({:script, file}, _config) when is_binary(file) do
    if exec = find_elixir_executable(file) do
      wrapper fn -> Code.require_file(exec) end
    else
      {:error, "-S : Could not find executable #{file}"}
    end
  end

  defp process_command({:file, file}, _config) when is_binary(file) do
    if File.regular?(file) do
      wrapper fn -> Code.require_file(file) end
    else
      {:error, "No file named #{file}"}
    end
  end

  defp process_command({:require, pattern}, _config) when is_binary(pattern) do
    files = filter_patterns(pattern)

    if files != [] do
      wrapper fn -> Enum.map files, &Code.require_file(&1) end
    else
      {:error, "-r : No files matched pattern #{pattern}"}
    end
  end

  defp process_command({:parallel_require, pattern}, _config) when is_binary(pattern) do
    files = filter_patterns(pattern)

    if files != [] do
      wrapper fn -> Kernel.ParallelRequire.files(files) end
    else
      {:error, "-pr : No files matched pattern #{pattern}"}
    end
  end

  defp process_command({:compile, patterns}, config) do
    # If ensuring the dir returns an error no files will be found.
    _ = :filelib.ensure_dir(:filename.join(config.output, "."))

    case filter_multiple_patterns(patterns) do
      {:ok, []} ->
        {:error, "No files matched provided patterns"}
      {:ok, files} ->
        wrapper fn ->
          Code.compiler_options(config.compiler_options)
          opts =
            if config.verbose_compile do
              [each_long_compilation: &IO.puts("Compiling #{&1} (it's taking more than 5s)")]
            else
              []
            end
          Kernel.ParallelCompiler.files_to_path(files, config.output, opts)
        end
      {:missing, missing} ->
        {:error, "No files matched pattern(s) #{Enum.join(missing, ",")}"}
    end
  end

  defp filter_patterns(pattern) do
    pattern
    |> Path.wildcard
    |> :lists.usort
    |> Enum.filter(&File.regular?/1)
  end

  defp filter_multiple_patterns(patterns) do
    matched_files = Enum.map patterns, fn(pattern) ->
      case filter_patterns(pattern) do
        []    -> {:missing, pattern}
        files -> {:ok, files}
      end
    end

    files = Enum.filter_map matched_files,
       fn(match) -> elem(match, 0) == :ok end,
       &elem(&1, 1)

    missing_patterns = Enum.filter_map matched_files,
       fn(match) -> elem(match, 0) == :missing end,
       &elem(&1, 1)

    if missing_patterns == [] do
      {:ok, :lists.usort(Enum.concat(files))}
    else
      {:missing, :lists.usort(missing_patterns)}
    end
  end

  defp wrapper(fun) do
    _ = fun.()
    :ok
  end

  defp find_elixir_executable(file) do
    if exec = System.find_executable(file) do
      # If we are on Windows, the executable is going to be
      # a .bat file that must be in the same directory as
      # the actual Elixir executable.
      case :os.type() do
        {:win32, _} ->
          base = Path.rootname(exec)
          if File.regular?(base), do: base, else: exec
        _ ->
          exec
      end
    end
  end
end
