defmodule VenomousREPL do
  @moduledoc """
    # Venomous REPL 
    List of keywords:
            - Enter leading "%" in case of colliding keyword/module names
            - RETURN: Brings up all visible modules/functions
            - '(i)nputs': views inputs list
            - '(o)utputs': views outputs list
            - '(h)elp': views this
            - '(r)epeat': repeats last function with the same arguments
            - '(e)dit': edit arguments
            - 'pop': removes first item from the outputs
            - 'exit': exits the REPL

  """
  @compile if Mix.env() in [:test, :dev], do: :export_all
  @options ["exit", "o", "i", "h", "pop", "e", "r", "h"]
  # credo:disable-for-this-file Credo.Check.Warning.Dbg
  # credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity
  # credo:disable-for-this-file Credo.Check.Refactor.Nesting

  defp repl_input(n, inputs, outputs) do
    IO.gets("[Python REPL] Enter (arg #{n}): ")
    |> String.trim_trailing("\n")
    |> Code.eval_string(Keyword.merge(inputs, outputs: outputs))
    |> elem(0)
  end

  defp repl_args(nil, args, _n, _inputs, _outputs), do: args

  defp repl_args(arg, args, n, inputs, outputs) do
    repl_input(n, inputs, outputs)
    |> repl_args(args ++ [arg], n + 1, inputs, outputs)
  end

  defp repl_args(inputs, outputs) do
    repl_input(1, inputs, outputs)
    |> repl_args([], 2, inputs, outputs)
  end

  defp functions(module) do
    Venomous.SnakeArgs.from_params(:venomous, :module_functions, [module]) |> Venomous.python!()
  end

  defp modules() do
    Venomous.SnakeArgs.from_params(:venomous, :all_modules, []) |> Venomous.python!()
  end

  defp reload() do
    ["venomous", "reload", "serpent_watcher", "venomous_test"]
    |> Enum.each(&(Venomous.SnakeArgs.from_params(:reload, :reload, [&1]) |> Venomous.python!()))
  end

  defp sanitize_params(params) do
    params
    |> Enum.map_join("\n\n\t", fn param ->
      param
      |> Map.to_list()
      |> Enum.reverse()
      |> Enum.map_join("\n\t", &Enum.join(Tuple.to_list(&1), ": "))
    end)
  end

  defp sanitize_functions(functions) do
    functions
    |> Map.to_list()
    |> Enum.each(fn {func, params} ->
      IO.puts("#{func}()\n\t#{sanitize_params(params)}")
    end)
  end

  defp get_function(functions, "", module) do
    IO.puts("Available functions:\n")
    sanitize_functions(functions)
    get_function(functions, module)
  end

  defp get_function(functions, <<"%", func::binary>>, _module),
    do: {functions[func], String.to_atom(func)}

  defp get_function(functions, func, module) do
    if functions |> Map.keys() |> Enum.member?(func) do
      {functions[func], String.to_atom(func)}
    else
      IO.puts("#{func} wasn't found. Enter leading '%' to force.")
      get_function(functions, module)
    end
  end

  defp get_function(functions, module) do
    func =
      IO.gets("[Python REPL] Enter function name: ") |> String.trim_trailing("\n")

    get_function(functions, func, module)
  end

  defp get_function(module) do
    module
    |> to_string
    |> functions()
    |> get_function(module)
  end

  defp get_module(modules \\ modules()) do
    mod =
      IO.gets("[Python REPL] Enter module name or (h)elp: ") |> String.trim_trailing("\n")

    case mod do
      "" ->
        modules |> Enum.join(", ") |> IO.puts()
        get_module(modules)

      mod when mod in @options ->
        mod

      <<"%", mod::binary>> ->
        mod

      mod ->
        if Enum.member?(modules, mod) == false do
          IO.puts("#{mod} wasn't found. Enter leading '%' to force.")
          get_module(modules)
        else
          mod
        end
    end
  end

  def repl(opts \\ []) do
    inputs = Keyword.get(opts, :inputs, [])
    outputs = Keyword.get(opts, :outputs, [])
    previous_args = Keyword.get(opts, :previous_args)
    reload()
    mod = get_module()

    case mod do
      "exit" ->
        outputs

      "o" ->
        outputs |> dbg()
        repl(inputs: inputs, outputs: outputs, previous_args: previous_args)

      "i" ->
        inputs |> dbg()
        repl(inputs: inputs, outputs: outputs, previous_args: previous_args)

      "pop" ->
        outputs =
          case outputs do
            [] -> outputs
            [_ | outputs] -> outputs
          end

        dbg(outputs)
        repl(inputs: inputs, outputs: outputs, previous_args: previous_args)

      "e" ->
        case previous_args do
          %Venomous.SnakeArgs{args: args} = previous_args ->
            old_args =
              args
              |> Enum.with_index()
              |> Enum.reduce(Keyword.new(), fn {val, idx}, acc ->
                Keyword.merge(["e#{idx + 1}": val], acc)
              end)

            dbg(old_args)
            args = repl_args(Keyword.merge(old_args, inputs), outputs)
            previous_args = %Venomous.SnakeArgs{previous_args | args: args}

            outputs = [
              Venomous.python!(previous_args) |> dbg
              | outputs
            ]

            repl(inputs: inputs, outputs: outputs, previous_args: previous_args)

          _ ->
            repl(inputs: inputs, outputs: outputs, previous_args: previous_args)
        end

      "r" ->
        outputs =
          if previous_args != nil do
            previous =
              case outputs do
                [] -> outputs
                [_ | previous] -> previous
              end

            [Venomous.python!(previous_args) |> dbg | previous]
          else
            outputs
          end

        repl(inputs: inputs, outputs: outputs, previous_args: previous_args)

      "h" ->
        IO.puts("""
          List of keywords:
            - Enter leading "%" in case of colliding keyword/module names
            - RETURN: Brings up all visible modules/functions
            - '(i)nputs': views inputs list
            - '(o)utputs': views outputs list
            - '(h)elp': views this
            - '(r)epeat': repeats last function with the same arguments
            - '(e)dit': edit arguments
            - 'pop': removes first item from the outputs
            - 'exit': exits the REPL
        """)

        repl(inputs: inputs, outputs: outputs, previous_args: previous_args)

      mod ->
        {function_signature, func} = get_function(mod)
        dbg(function_signature)
        args = repl_args(inputs, outputs)
        params = Venomous.SnakeArgs.from_params(String.to_atom(mod), func, args)

        outputs = [
          Venomous.python!(params) |> dbg
          | outputs
        ]

        repl(inputs: inputs, outputs: outputs, previous_args: params)
    end
  end
end
