defmodule Credo.CLI.Command.Suggest.Output.Default do
  @moduledoc false

  alias Credo.CLI.Filename
  alias Credo.CLI.Output
  alias Credo.CLI.Output.Summary
  alias Credo.CLI.Output.UI
  alias Credo.CLI.Sorter
  alias Credo.Execution
  alias Credo.Issue
  alias Credo.SourceFile

  @category_starting_order [:design, :readability, :refactor]
  @category_ending_order [:warning, :consistency, :custom, :unknown]
  @category_colors [
    design: :olive,
    readability: :blue,
    refactor: :yellow,
    warning: :red,
    consistency: :cyan
  ]
  @category_titles [
    design: "Software Design",
    readability: "Code Readability",
    refactor: "Refactoring opportunities",
    warning: "Warnings - please take a look",
    consistency: "Consistency"
  ]
  @many_source_files 60
  @per_category 5
  @indent 8

  @doc "Called before the analysis is run."
  def print_before_info(_source_files, %Execution{format: "short"}), do: nil

  def print_before_info(source_files, exec) do
    case Enum.count(source_files) do
      0 ->
        UI.puts("No files found!")

      1 ->
        UI.puts("Checking 1 source file ...")

      count ->
        UI.puts("Checking #{count} source files#{checking_suffix(count)} ...")
    end

    Output.print_skipped_checks(exec)
  end

  defp checking_suffix(count) when count > @many_source_files do
    " (this might take a while)"
  end

  defp checking_suffix(_), do: ""

  @doc "Called after the analysis has run."
  def print_after_info(source_files, exec, time_load, time_run) do
    print_issue_list_by_category(source_files, exec, time_load, time_run)
    print_summary(source_files, exec, time_load, time_run)
  end

  defp print_issue_list_by_category(
         _source_files,
         %Execution{format: "short"},
         _time_load,
         _time_run
       ),
       do: nil

  defp print_issue_list_by_category(source_files, exec, _time_load, _time_run) do
    term_width = Output.term_columns()
    source_file_map = Enum.into(source_files, %{}, &{&1.filename, &1})
    issues = Execution.get_issues(exec)
    categories = issues |> Enum.map(& &1.category) |> Enum.uniq()

    issue_map =
      Enum.into(categories, %{}, fn category ->
        {category, issues |> Enum.filter(&(&1.category == category))}
      end)

    categories
    |> Sorter.ensure(@category_starting_order, @category_ending_order)
    |> Enum.each(fn category ->
      print_issues_for_category(
        category,
        issue_map[category],
        source_file_map,
        exec,
        term_width
      )
    end)
  end

  defp print_summary(source_files, exec, time_load, time_run) do
    Summary.print(source_files, exec, time_load, time_run)
  end

  defp print_issues_for_category(
         _category,
         nil,
         _source_file_map,
         _exec,
         _term_width
       ) do
    nil
  end

  defp print_issues_for_category(
         category,
         issues,
         source_file_map,
         exec,
         term_width
       ) do
    color = @category_colors[category] || :magenta
    title = @category_titles[category] || "Category: #{category}"

    UI.puts()

    [
      :bright,
      "#{color}_background" |> String.to_atom(),
      color,
      " ",
      Output.foreground_color(color),
      :normal,
      " #{title}" |> String.pad_trailing(term_width - 1)
    ]
    |> UI.puts()

    color
    |> UI.edge()
    |> UI.puts()

    print_issues(issues, source_file_map, exec, term_width)

    if Enum.count(issues) > per_category(exec) do
      not_shown = Enum.count(issues) - per_category(exec)

      [
        UI.edge(color),
        :faint,
        " ...  (#{not_shown} more, use `--all` to show them)"
      ]
      |> UI.puts()
    end
  end

  defp print_issues(issues, source_file_map, exec, term_width) do
    count = per_category(exec)

    issues
    |> Enum.sort_by(fn issue ->
      {issue.priority, issue.severity, issue.filename, issue.line_no}
    end)
    |> Enum.reverse()
    |> Enum.take(count)
    |> do_print_issues(source_file_map, exec, term_width)
  end

  defp per_category(%Execution{all: true}), do: 1_000_000
  defp per_category(%Execution{all: false}), do: @per_category

  defp do_print_issues(
         issues,
         source_file_map,
         %Execution{format: _} = exec,
         term_width
       ) do
    Enum.each(issues, fn %Issue{filename: filename} = issue ->
      source_file = source_file_map[filename]

      do_print_issue(issue, source_file, exec, term_width)
    end)
  end

  defp do_print_issue(
         %Issue{
           check: check,
           message: message,
           filename: filename,
           priority: priority
         } = issue,
         source_file,
         %Execution{format: _, verbose: verbose} = exec,
         term_width
       ) do
    outer_color = Output.check_color(issue)
    inner_color = Output.issue_color(issue)
    message_color = outer_color
    filename_color = :default_color

    tag_style =
      if outer_color == inner_color do
        :faint
      else
        :bright
      end

    message =
      if verbose do
        message <> " [" <> inspect(check) <> "]"
      else
        message
      end

    message
    |> UI.wrap_at(term_width - @indent)
    |> print_issue_message(
      check,
      outer_color,
      message_color,
      tag_style,
      priority
    )

    [
      UI.edge(outer_color, @indent),
      filename_color,
      :faint,
      filename |> to_string,
      :default_color,
      :faint,
      Filename.pos_suffix(issue.line_no, issue.column),
      :conceal,
      " #",
      :reset,
      :faint,
      "(#{issue.scope})"
    ]
    |> UI.puts()

    if exec.verbose do
      print_issue_line(issue, source_file, inner_color, outer_color, term_width)

      UI.puts_edge([outer_color, :faint])
    end
  end

  defp print_issue_message(
         [first_line | other_lines],
         check,
         outer_color,
         message_color,
         tag_style,
         priority
       ) do
    [
      UI.edge(outer_color),
      outer_color,
      tag_style,
      Output.check_tag(check.category()),
      " ",
      priority |> Output.priority_arrow(),
      :normal,
      message_color,
      " ",
      first_line
    ]
    |> UI.puts()

    other_lines
    |> Enum.each(&print_issue_message(&1, outer_color, message_color))
  end

  defp print_issue_message("", _outer_color, _message_color) do
  end

  defp print_issue_message(message, outer_color, message_color) do
    [
      UI.edge(outer_color),
      outer_color,
      String.duplicate(" ", @indent - 3),
      :normal,
      message_color,
      " ",
      message
    ]
    |> UI.puts()
  end

  defp print_issue_line(
         %Issue{line_no: nil},
         _source_file,
         _inner_color,
         _outer_color,
         _term_width
       ) do
    nil
  end

  defp print_issue_line(
         %Issue{} = issue,
         source_file,
         inner_color,
         outer_color,
         term_width
       ) do
    raw_line = SourceFile.line_at(source_file, issue.line_no)
    line = String.trim(raw_line)

    [outer_color, :faint]
    |> UI.edge()
    |> UI.puts()

    [
      UI.edge([outer_color, :faint]),
      :cyan,
      :faint,
      String.duplicate(" ", @indent - 2),
      UI.truncate(line, term_width - @indent)
    ]
    |> UI.puts()

    print_issue_trigger_marker(issue, raw_line, inner_color, outer_color)
  end

  defp print_issue_trigger_marker(
         %Issue{column: nil},
         _line,
         _inner_color,
         _outer_color
       ) do
    nil
  end

  defp print_issue_trigger_marker(
         %Issue{} = issue,
         line,
         inner_color,
         outer_color
       ) do
    offset = String.length(line) - String.length(String.trim(line))

    # column is one-based
    x = max(issue.column - offset - 1, 0)

    w =
      case issue.trigger do
        nil -> 1
        atom -> atom |> to_string |> String.length()
      end

    [
      UI.edge([outer_color, :faint], @indent),
      inner_color,
      String.duplicate(" ", x),
      :faint,
      String.duplicate("^", w)
    ]
    |> UI.puts()
  end
end
