defmodule RefactorexLSP do
  use GenLSP

  alias GenLSP.Requests.{
    Initialize,
    CodeActionResolve,
    TextDocumentCodeAction,
    TextDocumentPrepareRename,
    TextDocumentRename,
    Shutdown
  }

  alias GenLSP.Notifications.{
    Exit,
    TextDocumentDidOpen,
    TextDocumentDidChange,
    TextDocumentDidClose
  }

  alias Refactorex.{
    NameCache,
    Parser,
    Refactor
  }

  alias __MODULE__.{
    Diff,
    Logger,
    Response
  }

  def start_link(args) do
    {args, opts} = Keyword.split(args, [])
    GenLSP.start_link(__MODULE__, args, opts)
  end

  @impl true
  def init(lsp, _args) do
    Logger.info("Server started")
    {:ok, assign(lsp, documents: %{})}
  end

  @impl true
  def handle_notification(%Exit{}, _lsp) do
    Logger.info("Server stopped")
    System.halt(0)
  end

  @impl true
  def handle_notification(%TextDocumentDidOpen{params: params}, lsp) do
    %{uri: uri, text: text} = params.text_document
    {:noreply, put_in(lsp.assigns.documents[uri], text)}
  end

  @impl true
  def handle_notification(%TextDocumentDidChange{params: params}, lsp) do
    %{uri: uri} = params.text_document
    [%{text: text}] = params.content_changes
    {:noreply, put_in(lsp.assigns.documents[uri], text)}
  end

  @impl true
  def handle_notification(%TextDocumentDidClose{params: params}, lsp) do
    %{uri: uri} = params.text_document
    {:noreply, put_in(lsp.assigns.documents[uri], "")}
  end

  @impl true
  def handle_notification(_, lsp), do: {:noreply, lsp}

  @impl true
  def handle_request(request, lsp) do
    try do
      case do_handle_request(request, lsp) do
        {:ok, reply} -> {:reply, reply, lsp}
        {:error, error} -> {:reply, Response.error(error), lsp}
      end
    rescue
      e ->
        Logger.error({e, __STACKTRACE__})
        {:reply, Response.error(:internal_error), lsp}
    end
  end

  def do_handle_request(%Initialize{}, lsp) do
    Logger.set_lsp(lsp)
    Logger.info("Client connected")
    {:ok, Response.initialize()}
  end

  def do_handle_request(%Shutdown{}, _lsp) do
    Logger.set_lsp(nil)
    Logger.info("Client disconnected")
    {:ok, nil}
  end

  def do_handle_request(%TextDocumentCodeAction{params: params}, lsp) do
    case params do
      %{
        context: %{trigger_kind: 1},
        text_document: %{uri: uri},
        range: range
      } ->
        original = lsp.assigns.documents[uri]

        case Parser.parse_inputs(original, range) do
          {:ok, zipper, selection_or_line} ->
            {
              :ok,
              zipper
              |> Refactor.available_refactorings(selection_or_line)
              |> Response.suggest_refactorings(uri, range)
            }

          {:error, :parse_error} ->
            {:ok, []}
        end

      _ ->
        {:ok, []}
    end
  end

  def do_handle_request(%CodeActionResolve{params: %{data: data}}, lsp) do
    %{module: module, uri: uri, range: range} = metadata = Parser.parse_metadata(data)

    original = lsp.assigns.documents[uri]
    NameCache.store_name(metadata[:new_name])

    with {:ok, zipper, selection_or_line} <- Parser.parse_inputs(original, range) do
      {
        :ok,
        zipper
        |> Refactor.refactor(selection_or_line, module)
        |> Diff.from_original(original)
        |> Response.perform_refactoring(uri)
      }
    end
  end

  def do_handle_request(%TextDocumentPrepareRename{params: params}, lsp) do
    %{text_document: %{uri: uri}, position: position} = params

    original = lsp.assigns.documents[uri]
    range = Parser.position_to_range(original, position)

    case Parser.parse_inputs(original, range) do
      {:ok, zipper, selection} ->
        cond do
          not Refactor.rename_available?(zipper, selection) ->
            {:ok, nil}

          match?({:@, _, _}, selection) ->
            # this is done so that the input placeholder
            # for RenameConstant doesn't include the @
            range = update_in(range.start.character, &(&1 + 1))
            {:ok, Response.send_rename_range(range)}

          true ->
            {:ok, Response.send_rename_range(range)}
        end

      {:error, :parse_error} ->
        {:ok, nil}
    end
  end

  def do_handle_request(%TextDocumentRename{params: params}, lsp) do
    %{
      text_document: %{uri: uri},
      position: position,
      new_name: new_name
    } = params

    original = lsp.assigns.documents[uri]
    range = Parser.position_to_range(original, position)

    with {:ok, zipper, selection} <- Parser.parse_inputs(original, range) do
      {
        :ok,
        zipper
        |> Refactor.rename(selection, new_name)
        |> Diff.from_original(original)
        |> Response.perform_renaming(uri)
      }
    end
  end
end
