defmodule NextLS.DocumentSymbol do
  @moduledoc false

  alias GenLSP.Enumerations.SymbolKind
  alias GenLSP.Structures.DocumentSymbol
  alias GenLSP.Structures.Position
  alias GenLSP.Structures.Range

  @spec fetch(text :: String.t()) :: list(DocumentSymbol.t())
  def fetch(text) do
    ast =
      case NextLS.Parser.parse(
             text,
             # we set the literal encoder so that we can know when atoms and strings start and end
             # this makes it useful for knowing the exact locations of struct field definitions
             literal_encoder: fn literal, meta ->
               if is_atom(literal) or is_binary(literal) do
                 {:ok, {:__literal__, meta, [literal]}}
               else
                 {:ok, literal}
               end
             end,
             unescape: false,
             token_metadata: true,
             columns: true
           ) do
        {:error, ast, _errors} ->
          ast

        {:error, _} ->
          raise "Failed to parse!"

        {:ok, ast} ->
          ast
      end

    for %DocumentSymbol{} = ds <- List.wrap(walker(ast, nil)) do
      ds
    end
  end

  defp walker([{{:__literal__, _, [:do]}, {_, _, _exprs} = ast}], mod) do
    walker(ast, mod)
  end

  defp walker({:__block__, _, exprs}, mod) do
    for expr <- exprs, sym = walker(expr, mod), sym != nil do
      sym
    end
  end

  defp walker({:defmodule, meta, [name | children]}, _mod) do
    name = Macro.to_string(unliteral(name))

    %DocumentSymbol{
      name: name,
      kind: SymbolKind.module(),
      children: List.flatten(for(child <- children, sym = walker(child, name), sym != nil, do: sym)),
      range: %Range{
        start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
        end: %Position{line: meta[:end][:line] - 1, character: meta[:end][:column] - 1}
      },
      selection_range: %Range{
        start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
        end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
      }
    }
  end

  defp walker({:describe, meta, [name | children]}, mod) do
    name = String.replace("describe " <> Macro.to_string(unliteral(name)), "\n", "")

    %DocumentSymbol{
      name: name,
      kind: SymbolKind.class(),
      children: List.flatten(for(child <- children, sym = walker(child, mod), sym != nil, do: sym)),
      range: %Range{
        start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
        end: %Position{line: meta[:end][:line] - 1, character: meta[:end][:column] - 1}
      },
      selection_range: %Range{
        start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
        end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
      }
    }
  end

  defp walker({:defstruct, meta, [fields]}, mod) do
    fields =
      for field <- fields do
        {name, start_line, start_column} =
          case field do
            {:__literal__, meta, [name]} ->
              start_line = meta[:line] - 1
              start_column = meta[:column] - 1
              name = Macro.to_string(name)

              {name, start_line, start_column}

            {{:__literal__, meta, [name]}, default} ->
              start_line = meta[:line] - 1
              start_column = meta[:column] - 1
              name = to_string(name) <> ": " <> Macro.to_string(unliteral(default))

              {name, start_line, start_column}
          end

        %DocumentSymbol{
          name: name,
          children: [],
          kind: SymbolKind.field(),
          range: %Range{
            start: %Position{
              line: start_line,
              character: start_column
            },
            end: %Position{
              line: start_line,
              character: start_column + String.length(name)
            }
          },
          selection_range: %Range{
            start: %Position{line: start_line, character: start_column},
            end: %Position{line: start_line, character: start_column}
          }
        }
      end

    %DocumentSymbol{
      name: "%#{mod}{}",
      children: fields,
      kind: elixir_kind_to_lsp_kind(:defstruct),
      range: %Range{
        start: %Position{
          line: meta[:line] - 1,
          character: meta[:column] - 1
        },
        end: %Position{
          line: (meta[:end_of_expression][:line] || meta[:line]) - 1,
          character: (meta[:end_of_expression][:column] || meta[:column]) - 1
        }
      },
      selection_range: %Range{
        start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
        end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
      }
    }
  end

  defp walker({:@, meta, [{_name, _, value}]} = attribute, _) when length(value) > 0 do
    %DocumentSymbol{
      name: attribute |> unliteral() |> Macro.to_string() |> String.replace("\n", ""),
      children: [],
      kind: elixir_kind_to_lsp_kind(:@),
      range: %Range{
        start: %Position{
          line: meta[:line] - 1,
          character: meta[:column] - 1
        },
        end: %Position{
          line: (meta[:end_of_expression] || meta)[:line] - 1,
          character: (meta[:end_of_expression] || meta)[:column] - 1
        }
      },
      selection_range: %Range{
        start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
        end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
      }
    }
  end

  defp walker({type, meta, [name | _children]}, _) when type in [:test, :feature, :property] do
    %DocumentSymbol{
      name: String.replace("#{type} #{Macro.to_string(unliteral(name))}", "\n", ""),
      children: [],
      kind: SymbolKind.constructor(),
      range: %Range{
        start: %Position{
          line: meta[:line] - 1,
          character: meta[:column] - 1
        },
        end: %Position{
          line: (meta[:end] || meta[:end_of_expression] || meta)[:line] - 1,
          character: (meta[:end] || meta[:end_of_expression] || meta)[:column] - 1
        }
      },
      selection_range: %Range{
        start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
        end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
      }
    }
  end

  defp walker({type, meta, [name | _children]}, _) when type in [:def, :defp, :defmacro, :defmacro] do
    %DocumentSymbol{
      name: String.replace("#{type} #{name |> unliteral() |> Macro.to_string()}", "\n", ""),
      children: [],
      kind: elixir_kind_to_lsp_kind(type),
      range: %Range{
        start: %Position{
          line: meta[:line] - 1,
          character: meta[:column] - 1
        },
        end: %Position{
          line: (meta[:end] || meta[:end_of_expression] || meta)[:line] - 1,
          character: (meta[:end] || meta[:end_of_expression] || meta)[:column] - 1
        }
      },
      selection_range: %Range{
        start: %Position{line: meta[:line] - 1, character: meta[:column] - 1},
        end: %Position{line: meta[:line] - 1, character: meta[:column] - 1}
      }
    }
  end

  defp walker(_ast, _) do
    nil
  end

  defp unliteral(ast) do
    Macro.prewalk(ast, fn
      {:__literal__, _, [literal]} ->
        literal

      node ->
        node
    end)
  end

  defp elixir_kind_to_lsp_kind(:defstruct), do: SymbolKind.struct()
  defp elixir_kind_to_lsp_kind(:@), do: SymbolKind.property()

  defp elixir_kind_to_lsp_kind(kind) when kind in [:def, :defp, :defmacro, :defmacrop, :test, :describe],
    do: SymbolKind.function()
end
