defmodule QRCode.Placement do
  @moduledoc """
  A patterns are a non-data element of the QR code that is required
  by the QR code specification, such as the three finder patterns in
  the corners of the QR code matrix.

  It contains function patterns (finder patterns, timing patterns,
  separators, alignment patterns) and reserved areas (format
  information area, version information area).

  0,1 ... Endcoding data
  2 ... Finders
  3 ... Separators
  4 ... Alignments
  5 ... Reserved areas
  6 ... Timing
  7 ... Dark module
  """

  alias MatrixReloaded.{Matrix, Vector}
  alias QRCode.QR
  import QRCode.QR, only: [version: 1]

  @locations [
    {14, [26, 46, 66]},
    {15, [26, 48, 70]},
    {16, [26, 50, 74]},
    {17, [30, 54, 78]},
    {18, [30, 56, 82]},
    {19, [30, 58, 86]},
    {20, [34, 62, 90]},
    {21, [28, 50, 72, 94]},
    {22, [26, 50, 74, 98]},
    {23, [30, 54, 78, 102]},
    {24, [28, 54, 80, 106]},
    {25, [32, 58, 84, 110]},
    {26, [30, 58, 86, 114]},
    {27, [34, 62, 90, 118]},
    {28, [26, 50, 74, 98, 122]},
    {29, [30, 54, 78, 102, 126]},
    {30, [26, 52, 78, 104, 130]},
    {31, [30, 56, 82, 108, 134]},
    {32, [34, 60, 86, 112, 138]},
    {33, [30, 58, 86, 114, 142]},
    {34, [34, 62, 90, 118, 146]},
    {35, [30, 54, 78, 102, 126, 150]},
    {36, [24, 50, 76, 102, 128, 154]},
    {37, [28, 54, 80, 106, 132, 158]},
    {38, [32, 58, 84, 110, 136, 162]},
    {39, [26, 54, 82, 110, 138, 166]},
    {40, [30, 58, 86, 114, 142, 170]}
  ]

  @finder Matrix.new(7, 2) |> elem(1)
  @separator Vector.row(8, 3)
  @alignment Matrix.new(5, 4) |> elem(1)
  @reserved_area 5
  @timing 6
  @dark_module 7

  @correct_finder [Matrix.new(7, 1), Matrix.new(5)]
                  |> Result.and_then_x(&Matrix.update(&1, &2, {1, 1}))
                  |> (fn mat -> [mat, Matrix.new(3, 1)] end).()
                  |> Result.and_then_x(&Matrix.update(&1, &2, {2, 2}))
                  |> elem(1)

  @correct_alignment [Matrix.new(5, 1), Matrix.new(3)]
                     |> Result.and_then_x(&Matrix.update(&1, &2, {1, 1}))
                     |> Result.and_then(&Matrix.update_element(&1, 1, {2, 2}))
                     |> elem(1)

  @correct_separator Vector.row(8)

  @spec put_patterns(QR.t()) :: Result.t(String.t(), QR.t())
  def put_patterns(%QR{version: version, message: message} = qr) when version(version) do
    size = (version - 1) * 4 + 21

    size
    |> Matrix.new()
    |> Result.and_then(&add_finders(&1, version, @finder))
    |> Result.and_then(&add_separators(&1, version, @separator))
    |> Result.and_then(&add_reserved_areas(&1, version, @reserved_area))
    |> Result.and_then(&add_timings(&1, version, @timing))
    |> Result.and_then(&add_alignments(&1, version, @alignment))
    |> Result.and_then(&add_dark_module(&1, version, @dark_module))
    |> Result.map(&fill_matrix_by_message(&1, size, message))
    |> Result.map(fn matrix -> %{qr | matrix: matrix} end)
  end

  @spec replace_placeholders(QR.t()) :: Result.t(String.t(), QR.t())
  def replace_placeholders(%QR{matrix: matrix, version: version} = qr) when version(version) do
    matrix
    |> add_finders(version)
    |> Result.and_then(&add_separators(&1, version))
    |> Result.and_then(&add_reserved_areas(&1, version))
    |> Result.and_then(&add_timings(&1, version))
    |> Result.and_then(&add_alignments(&1, version))
    |> Result.and_then(&add_dark_module(&1, version))
    |> Result.map(fn matrix -> %{qr | matrix: matrix} end)
  end

  @spec add_finders(Matrix.t(), QR.version(), Matrix.t()) :: Result.t(String.t(), Matrix.t())
  def add_finders(matrix, version, finder \\ @correct_finder) do
    matrix
    |> Matrix.update(finder, {0, 0})
    |> Result.and_then(&Matrix.update(&1, finder, {0, 4 * version + 10}))
    |> Result.and_then(&Matrix.update(&1, finder, {4 * version + 10, 0}))
  end

  @spec add_separators(Matrix.t(), QR.version(), Vector.t()) :: Result.t(String.t(), Matrix.t())
  def add_separators(matrix, version, row \\ @correct_separator) do
    col = Vector.transpose(row)

    matrix
    |> Matrix.update_row(row, {7, 0})
    |> Result.and_then(&Matrix.update_row(&1, row, {7, 4 * version + 9}))
    |> Result.and_then(&Matrix.update_row(&1, row, {4 * version + 9, 0}))
    |> Result.and_then(&Matrix.update_col(&1, col, {0, 7}))
    |> Result.and_then(&Matrix.update_col(&1, col, {0, 4 * version + 9}))
    |> Result.and_then(&Matrix.update_col(&1, col, {4 * version + 9, 7}))
  end

  @spec add_reserved_areas(Matrix.t(), QR.version(), non_neg_integer()) ::
          Result.t(String.t(), Matrix.t())
  def add_reserved_areas(matrix, version, val \\ 0)

  def add_reserved_areas(matrix, version, val) when version < 7 do
    add_reserve_fia(matrix, version, val)
  end

  def add_reserved_areas(matrix, version, val) do
    matrix
    |> add_reserve_fia(version, val)
    |> add_reserve_via(version, val)
  end

  @spec add_timings(Matrix.t(), QR.version()) :: Result.t(String.t(), Matrix.t())
  def add_timings(matrix, version) do
    row = get_timing_row(version)

    [row |> Result.and_then(&Matrix.update_row(matrix, &1, {6, 8})), row]
    |> Result.and_then_x(&Matrix.update_col(&1, Vector.transpose(&2), {8, 6}))
  end

  @spec add_timings(Matrix.t(), QR.version(), pos_integer()) :: Result.t(String.t(), Matrix.t())
  def add_timings(matrix, version, val) do
    size = 4 * version + 1
    row = size |> Vector.row(val)

    matrix
    |> Matrix.update_row(row, {6, 8})
    |> Result.and_then(&Matrix.update_col(&1, Vector.transpose(row), {8, 6}))
  end

  @spec add_alignments(Matrix.t(), QR.version(), Matrix.t()) :: Result.t(String.t(), Matrix.t())
  def add_alignments(matrix, version, alignment \\ @correct_alignment)
  def add_alignments(matrix, 1, _alignment), do: Result.ok(matrix)

  def add_alignments(matrix, version, alignment) when version < 7 do
    Matrix.update(matrix, alignment, {4 * version + 8, 4 * version + 8})
  end

  def add_alignments(matrix, version, alignment) when version < 14 do
    Matrix.update_map(matrix, alignment, get_all_positions([2 * version + 8, 4 * version + 10]))
  end

  def add_alignments(matrix, version, alignment) do
    positions =
      version
      |> find_positions()
      |> get_all_positions()

    Matrix.update_map(matrix, alignment, positions)
  end

  @spec add_dark_module(Matrix.t(), QR.version(), pos_integer()) ::
          Result.t(String.t(), Matrix.t())
  def add_dark_module(matrix, version, val \\ 1) do
    Matrix.update_element(matrix, val, {4 * version + 9, 8})
  end

  defp fill_matrix_by_message(matrix, size, message) do
    (size - 1)..7//-1
    |> Enum.take_every(2)
    |> Enum.concat([5, 3, 1])
    |> Enum.map_reduce({matrix, message}, fn col, acc ->
      {col, make_fill(acc, [col, col - 1])}
    end)
    |> Kernel.elem(1)
    |> Kernel.elem(0)
  end

  defp make_fill({matrix, acc_message}, cols) do
    matrix
    |> Matrix.flip_ud()
    |> Enum.map_reduce(acc_message, fn row, acc_msg ->
      fill_row(row, acc_msg, cols)
    end)
  end

  defp fill_row(row, acc_msg, cols) do
    Enum.reduce(cols, {row, acc_msg}, fn col, {row, msg} ->
      if Enum.at(row, col) == 0 do
        <<cw::size(1), rest::bitstring>> = msg

        {List.update_at(row, col, fn _ -> cw end), rest}
      else
        {row, msg}
      end
    end)
  end

  defp reserved_area(val) do
    {6, 3} |> Matrix.new(val) |> elem(1)
  end

  defp add_reserve_fia(matrix, version, val) do
    row_left = Vector.row(6, val) ++ [0] ++ [val, val]
    row_right = Vector.row(8, val)
    col_top = Vector.transpose(row_left)
    col_bottom = Vector.col(7, val)

    matrix
    |> Matrix.update_row(row_left, {8, 0})
    |> Result.and_then(&Matrix.update_row(&1, row_right, {8, 4 * version + 9}))
    |> Result.and_then(&Matrix.update_col(&1, col_top, {0, 8}))
    |> Result.and_then(&Matrix.update_col(&1, col_bottom, {4 * version + 10, 8}))
  end

  defp add_reserve_via(matrix, version, val) do
    transp = val |> reserved_area() |> Matrix.transpose()

    matrix
    |> Result.and_then(&Matrix.update(&1, reserved_area(val), {0, 4 * version + 6}))
    |> Result.and_then(&Matrix.update(&1, transp, {4 * version + 6, 0}))
  end

  defp find_positions(version) do
    Enum.reduce_while(@locations, version, fn {ver, list_center}, acc ->
      if version == ver do
        {:halt, list_center}
      else
        {:cont, acc}
      end
    end)
  end

  defp generate_positions(list) do
    for x <- list, y <- list, do: {x, y}
  end

  defp generate_positions(list, :horizontal) do
    for x <- [6], y <- Enum.drop(list, -1), do: {x, y}
  end

  defp generate_positions(list, :vertical) do
    for x <- Enum.drop(list, -1), y <- [6], do: {x, y}
  end

  defp get_positions(version) when version < 14 do
    [2 * version + 8, 4 * version + 10]
  end

  defp get_positions(version) do
    find_positions(version)
  end

  defp get_all_positions(list) do
    list
    |> generate_positions()
    |> Kernel.++(generate_positions(list, :horizontal))
    |> Kernel.++(generate_positions(list, :vertical))
    |> Enum.map(fn {row_pos, col_pos} -> {row_pos - 2, col_pos - 2} end)
  end

  defp get_timing_row(version) when version < 7 do
    size = 4 * version + 1

    size
    |> Vector.row()
    |> Vector.alternate_seq(1)
    |> Result.ok()
  end

  defp get_timing_row(version) do
    size = 4 * version + 1

    positions =
      version
      |> get_positions()
      |> generate_positions(:horizontal)
      |> Enum.map(fn {_row_pos, col_pos} -> col_pos - 10 end)

    size
    |> Vector.row()
    |> Vector.alternate_seq(1)
    |> Vector.update_map(
      [0, 0, 0, 0, 0],
      positions
    )
  end
end
