defmodule Raxol.Terminal.Buffer.CharEditor do
  @moduledoc """
  Manages terminal character editing operations.
  """

  alias Raxol.Terminal.Cell

  @doc """
  Inserts a character at the current position.
  """
  def insert_char(%Cell{} = cell, char) when is_binary(char) do
    %{cell | char: char}
  end

  @doc """
  Deletes a character at the current position.
  """
  def delete_char(%Cell{} = cell) do
    %{cell | char: " "}
  end

  @doc """
  Replaces a character at the current position.
  """
  def replace_char(%Cell{} = cell, char) when is_binary(char) do
    %{cell | char: char}
  end

  @doc """
  Inserts a string of characters.
  """
  def insert_string(%Cell{} = cell, string) when is_binary(string) do
    case String.length(string) do
      0 -> cell
      1 -> insert_char(cell, string)
      _ -> %{cell | char: string}
    end
  end

  @doc """
  Deletes a string of characters.
  """
  def delete_string(%Cell{} = cell, length)
      when is_integer(length) and length > 0 do
    case length do
      1 -> delete_char(cell)
      _ -> %{cell | char: " "}
    end
  end

  @doc """
  Replaces a string of characters.
  """
  def replace_string(%Cell{} = cell, string) when is_binary(string) do
    case String.length(string) do
      0 -> cell
      1 -> replace_char(cell, string)
      _ -> %{cell | char: string}
    end
  end

  @doc """
  Checks if a character is a control character.
  """
  def control_char?(char) when is_binary(char) do
    case String.to_charlist(char) do
      [c] when c < 32 or c == 127 -> true
      _ -> false
    end
  end

  @doc """
  Checks if a character is a printable character.
  """
  def printable_char?(char) when is_binary(char) do
    case String.to_charlist(char) do
      [c] when c >= 32 and c != 127 -> true
      _ -> false
    end
  end

  @doc """
  Checks if a character is a whitespace character.
  """
  def whitespace_char?(char) when is_binary(char) do
    char in [" ", "\t", "\n", "\r"]
  end

  @doc """
  Gets the width of a character.
  """
  def char_width(char) when is_binary(char) do
    case String.to_charlist(char) do
      [c] when c < 32 or c == 127 -> 0
      [c] when c < 128 -> 1
      _ -> 2
    end
  end

  @doc """
  Gets the width of a string.
  """
  def string_width(string) when is_binary(string) do
    string
    |> String.to_charlist()
    |> Enum.map(&char_width/1)
    |> Enum.sum()
  end

  @doc """
  Determines the content length of a line (number of non-blank characters).
  """
  def content_length(line) do
    line
    |> Enum.take_while(fn cell -> cell.char != " " end)
    |> length()
  end

  @doc """
  Truncates a line to the specified content length, padding with blank cells if needed.
  """
  def truncate_to_content_length(line, content_length) do
    case length(line) <= content_length do
      true ->
        # Pad with blank cells if line is shorter than content length
        padding = List.duplicate(Cell.new(" "), content_length - length(line))
        line ++ padding

      false ->
        # Truncate to content length
        Enum.take(line, content_length)
    end
  end

  defp pad_or_truncate_line(line, width) when length(line) < width do
    # Use the style of the last cell if present, else defaults
    last_cell = List.last(line) || Cell.new(" ")
    style = Map.get(last_cell, :style)
    len = length(line)

    # Padding cells should always have dirty: false since they don't represent changes
    padding =
      Enum.map(1..(width - len), fn _ ->
        %Cell{
          char: " ",
          style: style,
          dirty: false,
          wide_placeholder: false
        }
      end)

    line ++ padding
  end

  defp pad_or_truncate_line(line, width) when length(line) > width do
    Enum.take(line, width)
  end

  defp pad_or_truncate_line(line, _width), do: line

  @doc """
  Inserts a specified number of characters at the given position.
  Characters to the right of the insertion point are shifted right.
  Characters shifted off the end of the line are discarded.

  ## Parameters

  * `buffer` - The screen buffer to modify
  * `row` - The row to insert characters in
  * `col` - The column to start inserting at
  * `count` - The number of characters to insert

  ## Returns

  The updated screen buffer.
  """
  @spec insert_chars(
          Raxol.Terminal.ScreenBuffer.t(),
          non_neg_integer(),
          non_neg_integer(),
          non_neg_integer()
        ) :: Raxol.Terminal.ScreenBuffer.t()

  def insert_chars(buffer, row, col, count) do
    insert_chars_if_valid(buffer, row, col, count)
  end

  defp can_insert_at_position?(buffer, row, col, count) do
    case buffer.cells do
      nil ->
        # Return false if cells is nil
        false

      cells ->
        col + count <= buffer.width and
          col <= content_length(Enum.at(cells, row))
    end
  end

  defp valid_insert_params?(buffer, row, col, count) do
    is_struct(buffer) and
      is_integer(row) and is_integer(col) and is_integer(count) and
      row >= 0 and col >= 0 and count > 0 and
      row < buffer.height and col < buffer.width
  end

  @doc """
  Inserts a specified number of characters at the given position.
  Characters to the right of the insertion point are shifted right.
  Characters shifted off the end of the line are discarded.
  Uses the provided default style for new characters.

  ## Parameters

  * `buffer` - The screen buffer to modify
  * `row` - The row to insert characters in
  * `col` - The column to start inserting at
  * `count` - The number of characters to insert
  * `default_style` - The style to apply to new characters

  ## Returns

  The updated screen buffer.

  ## Examples

      iex> buffer = ScreenBuffer.new(80, 24)
      iex> style = %{fg: :red, bg: :blue}
      iex> buffer = CharEditor.insert_characters(buffer, 0, 0, 5, style)
      iex> CharEditor.get_char(buffer, 0, 0)
      " "
  """
  @spec insert_characters(
          Raxol.Terminal.ScreenBuffer.t(),
          non_neg_integer(),
          non_neg_integer(),
          non_neg_integer(),
          Raxol.Terminal.ANSI.TextFormatting.text_style()
        ) :: Raxol.Terminal.ScreenBuffer.t()
  def insert_characters(buffer, row, col, count, default_style)
      when row >= 0 and col >= 0 and count > 0 do
    insert_chars_in_bounds(buffer, row, col, count, default_style)
  end

  @doc """
  Inserts characters into a line at the specified position.

  ## Parameters

  * `line` - The line to modify
  * `col` - The column to start inserting at
  * `count` - The number of characters to insert
  * `default_style` - The style to apply to new characters

  ## Returns

  The updated line with inserted characters.

  ## Examples

      iex> line = List.duplicate(%Cell{}, 10)
      iex> style = %{fg: :red, bg: :blue}
      iex> new_line = CharEditor.insert_into_line(line, 5, 3, style)
      iex> length(new_line)
      10
  """
  @spec insert_into_line(
          list(Cell.t()),
          non_neg_integer(),
          non_neg_integer(),
          Raxol.Terminal.ANSI.TextFormatting.text_style()
        ) :: list(Cell.t())
  def insert_into_line(line, col, count, default_style) do
    {left_part, right_part} = Enum.split(line, col)
    # Inserted blanks are not dirty (for consistency with delete_from_line)
    blank_cell = %Cell{
      char: " ",
      style: default_style,
      dirty: false,
      wide_placeholder: false
    }

    blank_cells = List.duplicate(blank_cell, count)
    shifted_right = Enum.take(right_part, length(line) - col - count)
    result = left_part ++ blank_cells ++ shifted_right
    pad_or_truncate_line(result, length(line))
  end

  @doc """
  Deletes a specified number of characters starting from the given position.
  Characters to the right of the deleted characters are shifted left.
  Blank characters are added at the end of the line.

  ## Parameters

  * `buffer` - The screen buffer to modify
  * `row` - The row to delete characters from
  * `col` - The column to start deleting from
  * `count` - The number of characters to delete

  ## Returns

  The updated screen buffer.
  """
  @spec delete_chars(
          Raxol.Terminal.ScreenBuffer.t(),
          non_neg_integer(),
          non_neg_integer(),
          non_neg_integer()
        ) :: Raxol.Terminal.ScreenBuffer.t()
  def delete_chars(buffer, row, col, count) do
    case valid_delete_params?(buffer, row, col, count) do
      true -> delete_characters(buffer, row, col, count, buffer.default_style)
      false -> buffer
    end
  end

  defp valid_delete_params?(buffer, row, col, count) do
    is_struct(buffer) and
      is_integer(row) and is_integer(col) and is_integer(count) and
      row >= 0 and col >= 0 and count > 0 and
      row < buffer.height and col < buffer.width
  end

  @doc """
  Deletes a specified number of characters starting from the given position.
  Characters to the right of the deleted characters are shifted left.
  Blank characters are added at the end of the line with the specified style.

  ## Parameters

  * `buffer` - The screen buffer to modify
  * `row` - The row to delete characters from
  * `col` - The column to start deleting from
  * `count` - The number of characters to delete
  * `default_style` - The style to apply to new blank characters

  ## Returns

  The updated screen buffer.
  """
  @spec delete_characters(
          Raxol.Terminal.ScreenBuffer.t(),
          non_neg_integer(),
          non_neg_integer(),
          non_neg_integer(),
          Raxol.Terminal.ANSI.TextFormatting.text_style()
        ) :: Raxol.Terminal.ScreenBuffer.t()
  def delete_characters(buffer, row, col, count, default_style)
      when row >= 0 and col >= 0 and count > 0 do
    delete_chars_in_bounds(buffer, row, col, count, default_style)
  end

  @doc """
  Deletes characters from a line at the specified position.

  ## Parameters

  * `line` - The line to modify
  * `col` - The column to start deleting from
  * `count` - The number of characters to delete
  * `default_style` - The style to apply to new blank characters

  ## Returns

  The updated line with deleted characters replaced by blanks.

  ## Examples

      iex> line = List.duplicate(%Cell{}, 10)
      iex> style = %{fg: :red, bg: :blue}
      iex> new_line = CharEditor.delete_from_line(line, 5, 3, style)
      iex> length(new_line)
      10
  """
  @spec delete_from_line(
          list(Cell.t()),
          non_neg_integer(),
          non_neg_integer(),
          Raxol.Terminal.ANSI.TextFormatting.text_style()
        ) :: list(Cell.t())
  def delete_from_line(line, col, count, default_style) do
    line_length = length(line)
    left_part = Enum.take(line, col)
    right_part = Enum.drop(line, col + count)

    blank_cell = %Cell{
      char: " ",
      style: default_style,
      dirty: false,
      wide_placeholder: false
    }

    blanks_needed = line_length - length(left_part) - length(right_part)
    blank_cells = List.duplicate(blank_cell, blanks_needed)
    result = left_part ++ right_part ++ blank_cells
    Enum.take(result, line_length)
  end

  @doc """
  Writes a character at the specified position in the buffer.
  """
  def write_char(buffer, row, col, char) when is_binary(char) do
    write_char_if_in_bounds(buffer, row, col, char)
  end

  @doc """
  Writes a string at the specified position in the buffer.
  """
  def write_string(buffer, row, col, string) when is_binary(string) do
    # Check if position is valid
    write_string_if_valid_position(buffer, row, col, string)
  end

  defp update_line_with_chars_wide_aware(line, col, chars, width) do
    Enum.reduce(Enum.with_index(chars), {line, col}, fn {char, _index},
                                                        {acc_line, current_col} ->
      process_char_in_line(acc_line, current_col, char, width)
    end)
    |> elem(0)
  end

  defp process_char_in_line(line, col, char, width) do
    process_char_if_fits(line, col, char, width)
  end

  def update_line_with_string(line, col, string, width) do
    chars = String.graphemes(string)

    Enum.reduce(Enum.with_index(chars), line, fn {char, index}, acc_line ->
      pos = col + index

      replace_char_if_in_width(acc_line, pos, char, width)
    end)
  end

  @doc """
  Erases a specified number of characters starting from the given position.
  Characters to the right of the erased characters are shifted left.
  Blank characters are added at the end of the line.

  ## Parameters

  * `buffer` - The screen buffer to modify
  * `row` - The row to erase characters from
  * `col` - The column to start erasing from
  * `count` - The number of characters to erase

  ## Returns

  The updated screen buffer.
  """
  @spec erase_chars(
          Raxol.Terminal.ScreenBuffer.t(),
          non_neg_integer(),
          non_neg_integer(),
          non_neg_integer()
        ) :: Raxol.Terminal.ScreenBuffer.t()
  def erase_chars(buffer, row, col, count) do
    case valid_erase_params?(buffer, row, col, count) do
      true -> erase_chars_in_buffer(buffer, row, col, count)
      false -> buffer
    end
  end

  @doc """
  Erases a specified number of characters starting from the given position with a specific style.
  Characters to the right of the erased characters are shifted left.
  Blank characters are added at the end of the line with the specified style.

  ## Parameters

  * `buffer` - The screen buffer to modify
  * `row` - The row to erase characters from
  * `col` - The column to start erasing from
  * `count` - The number of characters to erase
  * `style` - The style to apply to new blank characters

  ## Returns

  The updated screen buffer.
  """
  @spec erase_chars(
          Raxol.Terminal.ScreenBuffer.t(),
          non_neg_integer(),
          non_neg_integer(),
          non_neg_integer(),
          Raxol.Terminal.ANSI.TextFormatting.text_style()
        ) :: Raxol.Terminal.ScreenBuffer.t()
  def erase_chars(buffer, row, col, count, style) do
    case valid_erase_params?(buffer, row, col, count) do
      true -> erase_chars_in_buffer_with_style(buffer, row, col, count, style)
      false -> buffer
    end
  end

  defp valid_erase_params?(buffer, row, col, count) do
    is_struct(buffer) and
      is_integer(row) and is_integer(col) and is_integer(count) and
      row >= 0 and col >= 0 and count > 0 and
      row < buffer.height and col < buffer.width
  end

  defp erase_chars_in_buffer_with_style(buffer, row, col, count, style) do
    line = Enum.at(buffer.cells, row)

    updated_line =
      erase_chars_in_line_with_style(line, col, count, buffer.width, style)

    cells = List.replace_at(buffer.cells, row, updated_line)
    %{buffer | cells: cells}
  end

  defp erase_chars_in_line_with_style(line, col, count, width, style) do
    {left, _} = Enum.split(line, col)
    right = Enum.slice(line, col + count, width - (col + count))

    new_line = left ++ right
    padding_needed = max(0, width - length(new_line))

    (new_line ++ List.duplicate(Cell.new(" ", style), padding_needed))
    |> Enum.take(width)
  end

  defp erase_chars_in_buffer(buffer, row, col, count) do
    line = Enum.at(buffer.cells, row)

    updated_line =
      erase_chars_in_line(line, col, count, buffer.width, buffer.default_style)

    cells = List.replace_at(buffer.cells, row, updated_line)
    %{buffer | cells: cells}
  end

  defp erase_chars_in_line(line, col, count, width, default_style) do
    {left, _} = Enum.split(line, col)
    right = Enum.slice(line, col + count, width - (col + count))

    new_line = left ++ right
    padding_needed = max(0, width - length(new_line))

    (new_line ++ List.duplicate(Cell.new(" ", default_style), padding_needed))
    |> Enum.take(width)
  end

  def replace_chars(buffer, row, col, string, style \\ nil) do
    replace_chars_if_valid_params(buffer, row, col, string, style)
  end

  defp valid_replace_params?(buffer, row, col, string) do
    is_struct(buffer) and
      is_integer(row) and is_integer(col) and is_binary(string) and
      row >= 0 and col >= 0 and
      row < buffer.height and col < buffer.width
  end

  defp replace_chars_in_buffer(buffer, row, col, string, style) do
    line = Enum.at(buffer.cells, row)
    updated_line = replace_chars_in_line(line, col, string, style, buffer.width)
    cells = List.replace_at(buffer.cells, row, updated_line)
    %{buffer | cells: cells}
  end

  defp replace_chars_in_line(line, col, string, style, width) do
    line_len = length(line)
    max_replace = max(0, line_len - col)
    chars = String.graphemes(string) |> Enum.take(max_replace)
    rep_len = length(chars)

    {left, right} = split_line_for_replacement(line, col, rep_len, line_len)
    rep_cells = create_replacement_cells(line, col, chars, style)

    new_line = left ++ rep_cells ++ right
    pad_or_truncate_line(new_line, width)
  end

  defp split_line_for_replacement(line, col, rep_len, line_len) do
    {left, _} = Enum.split(line, col)

    right =
      get_right_part_after_replacement(line, col, rep_len, line_len)

    {left, right}
  end

  defp create_replacement_cells(line, col, chars, style) do
    Enum.with_index(chars)
    |> Enum.map(fn {c, i} ->
      orig_cell = Enum.at(line, col + i)
      update_cell_with_style(orig_cell, c, style)
    end)
  end

  defp update_cell_with_style(orig_cell, char, style) do
    case style do
      nil -> %{orig_cell | char: char, dirty: true}
      s -> %{orig_cell | char: char, style: s, dirty: true}
    end
  end

  ## Helper functions for refactored if statements

  defp insert_chars_if_valid(buffer, row, col, count) do
    case valid_insert_params?(buffer, row, col, count) and
           can_insert_at_position?(buffer, row, col, count) do
      true -> insert_characters(buffer, row, col, count, buffer.default_style)
      false -> buffer
    end
  end

  defp insert_chars_in_bounds(buffer, row, col, _count, _default_style)
       when row >= buffer.height or col >= buffer.width do
    buffer
  end

  defp insert_chars_in_bounds(buffer, row, col, count, default_style) do
    case buffer.cells do
      nil ->
        # Return buffer unchanged if cells is nil
        buffer

      cells ->
        new_cells =
          List.replace_at(
            cells,
            row,
            insert_into_line(Enum.at(cells, row), col, count, default_style)
          )

        %{buffer | cells: new_cells}
    end
  end

  defp delete_chars_in_bounds(buffer, row, col, _count, _default_style)
       when row >= buffer.height or col >= buffer.width do
    buffer
  end

  defp delete_chars_in_bounds(buffer, row, col, count, default_style) do
    case buffer.cells do
      nil ->
        # Return buffer unchanged if cells is nil
        buffer

      cells ->
        new_cells =
          List.replace_at(
            cells,
            row,
            delete_from_line(Enum.at(cells, row), col, count, default_style)
          )

        %{buffer | cells: new_cells}
    end
  end

  defp write_char_if_in_bounds(buffer, row, col, char)
       when row >= 0 and row < buffer.height and col >= 0 and col < buffer.width do
    line = Enum.at(buffer.cells, row)
    updated_line = List.replace_at(line, col, Cell.new(char))
    cells = List.replace_at(buffer.cells, row, updated_line)
    %{buffer | cells: cells}
  end

  defp write_char_if_in_bounds(buffer, _row, _col, _char), do: buffer

  defp write_string_if_valid_position(buffer, row, col, _string)
       when row < 0 or row >= buffer.height or col < 0 or col >= buffer.width do
    buffer
  end

  defp write_string_if_valid_position(buffer, row, col, string) do
    line = Enum.at(buffer.cells, row)
    chars = String.graphemes(string)

    # Only write if we have characters to write
    write_chars_if_present(buffer, line, row, col, chars)
  end

  defp write_chars_if_present(buffer, _line, _row, _col, []), do: buffer

  defp write_chars_if_present(buffer, line, row, col, chars) do
    updated_line =
      update_line_with_chars_wide_aware(line, col, chars, buffer.width)

    updated_line = pad_or_truncate_line(updated_line, buffer.width)
    cells = List.replace_at(buffer.cells, row, updated_line)
    %{buffer | cells: cells}
  end

  defp process_char_if_fits(line, col, char, width) do
    char_w = char_width(char)

    process_char_with_width_check(
      col < width and col + char_w <= width,
      line,
      col,
      char,
      char_w
    )
  end

  defp process_char_with_width_check(true, line, col, char, char_w) do
    updated_line =
      List.replace_at(line, col, %{Enum.at(line, col) | char: char, dirty: true})

    final_line = handle_wide_char_if_needed(updated_line, col, char, char_w)
    {final_line, col + char_w}
  end

  defp process_char_with_width_check(false, line, col, _char, _char_w),
    do: {line, col}

  defp handle_wide_char_if_needed(updated_line, col, _char, char_w)
       when char_w > 1 do
    List.replace_at(updated_line, col + 1, %{
      Enum.at(updated_line, col + 1)
      | wide_placeholder: true,
        dirty: true
    })
  end

  defp handle_wide_char_if_needed(updated_line, _col, _char, _char_w),
    do: updated_line

  defp replace_char_if_in_width(acc_line, pos, char, width) when pos < width do
    List.replace_at(acc_line, pos, Cell.new(char))
  end

  defp replace_char_if_in_width(acc_line, _pos, _char, _width), do: acc_line

  defp replace_chars_if_valid_params(buffer, row, col, string, style) do
    case valid_replace_params?(buffer, row, col, string) do
      true ->
        chars = String.graphemes(string)
        max_replace = max(0, buffer.width - col)
        truncated_chars = Enum.take(chars, max_replace)

        replace_truncated_chars_if_present(
          buffer,
          row,
          col,
          truncated_chars,
          style
        )

      false ->
        buffer
    end
  end

  defp replace_truncated_chars_if_present(buffer, _row, _col, [], _style),
    do: buffer

  defp replace_truncated_chars_if_present(
         buffer,
         row,
         col,
         truncated_chars,
         style
       ) do
    replace_chars_in_buffer(
      buffer,
      row,
      col,
      Enum.join(truncated_chars),
      style || buffer.default_style
    )
  end

  defp get_right_part_after_replacement(line, col, rep_len, line_len)
       when col + rep_len < line_len do
    Enum.slice(line, col + rep_len, line_len - (col + rep_len))
  end

  defp get_right_part_after_replacement(_line, _col, _rep_len, _line_len),
    do: []
end
