lib/matrix_reloaded/matrix.ex

defmodule MatrixReloaded.Matrix do
  @moduledoc """
  Provides a set of functions to work with matrices.

  Don't forget, numbering of row and column starts from `0` and goes
  to `m - 1` and `n - 1` where `{m, n}` is dimension (size) of matrix.
  """

  alias MatrixReloaded.Vector

  @type t :: [Vector.t()]
  @type dimension :: {pos_integer, pos_integer} | pos_integer
  @type index :: {non_neg_integer, non_neg_integer}
  @type submatrix :: number | Vector.t() | t()

  @doc """
  Creates a new matrix of the specified size. In case of positive number you get
  a squared matrix, for tuple `{m, n}` you get a rectangular matrix. For negative
  values you get an error message. All elements of the matrix are filled with the
  default value 0. This value can be changed.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ## Examples

      iex> MatrixReloaded.Matrix.new(3)
      {:ok, [[0, 0, 0], [0, 0, 0], [0, 0, 0]]}

      iex> MatrixReloaded.Matrix.new({2, 3}, -10)
      {:ok, [[-10, -10, -10], [-10, -10, -10]]}

  """
  @spec new(dimension, number) :: Result.t(String.t(), t())
  def new(dimension, val \\ 0)

  def new(dim, val) when is_tuple(dim) do
    dim
    |> is_dimension_ok?()
    |> Result.map(fn {rows, cols} ->
      for(
        _r <- 1..rows,
        do: make_row(cols, val)
      )
    end)
  end

  def new(dim, val) do
    dim
    |> is_dimension_ok?()
    |> Result.map(fn row ->
      for(
        _r <- 1..row,
        do: make_row(row, val)
      )
    end)
  end

  @doc """
  Summation of two matrices. Sizes (dimensions) of both matrices must be same.
  Otherwise you get an error message.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ## Examples

      iex> mat1 = {:ok, [[1, 2, 3], [4, 5, 6], [7, 8, 9]]}
      iex> mat2 = MatrixReloaded.Matrix.new(3,1)
      iex> Result.and_then_x([mat1, mat2], &MatrixReloaded.Matrix.add(&1, &2))
      {:ok,
        [
          [2, 3, 4],
          [5, 6, 7],
          [8, 9, 10]
        ]
      }

  """
  @spec add(t(), t()) :: Result.t(String.t(), t())
  def add(matrix1, matrix2) do
    {rs1, cs1} = size(matrix1)
    {rs2, cs2} = size(matrix2)

    if rs1 == rs2 and cs1 == cs2 do
      matrix1
      |> Enum.zip(matrix2)
      |> Enum.map(fn {row1, row2} ->
        Vector.add(row1, row2)
      end)
      |> Result.product()
    else
      Result.error("Sizes (dimensions) of both matrices must be same!")
    end
  end

  @doc """
  Subtraction of two matrices. Sizes (dimensions) of both matrices must be same.
  Otherwise you get an error message.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ## Examples

      iex> mat1 = {:ok, [[1, 2, 3], [4, 5, 6], [7, 8, 9]]}
      iex> mat2 = MatrixReloaded.Matrix.new(3,1)
      iex> Result.and_then_x([mat1, mat2], &MatrixReloaded.Matrix.sub(&1, &2))
      {:ok,
        [
          [0, 1, 2],
          [3, 4, 5],
          [6, 7, 8]
        ]
      }

  """
  @spec sub(t(), t()) :: Result.t(String.t(), t())
  def sub(matrix1, matrix2) do
    {rs1, cs1} = size(matrix1)
    {rs2, cs2} = size(matrix2)

    if rs1 == rs2 and cs1 == cs2 do
      matrix1
      |> Enum.zip(matrix2)
      |> Enum.map(fn {row1, row2} ->
        Vector.sub(row1, row2)
      end)
      |> Result.product()
    else
      Result.error("Sizes (dimensions) of both matrices must be same!")
    end
  end

  @doc """
  Product of two matrices. If matrix `A` has a size `n × p` and matrix `B` has
  a size `p × m` then their matrix product `A*B` is matrix of size `n × m`.
  Otherwise you get an error message.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ## Examples

      iex> mat1 = {:ok, [[1, 2], [3, 4], [5, 6], [7, 8]]}
      iex> mat2 = {:ok, [[1, 2 ,3], [4, 5, 6]]}
      iex> Result.and_then_x([mat1, mat2], &MatrixReloaded.Matrix.product(&1, &2))
      {:ok,
        [
          [9, 12, 15],
          [19, 26, 33],
          [29, 40, 51],
          [39, 54, 69]
        ]
      }

  """
  @spec product(t(), t()) :: Result.t(String.t(), t())
  def product(matrix1, matrix2) do
    {_rs1, cs1} = size(matrix1)
    {rs2, _cs2} = size(matrix2)

    if cs1 == rs2 do
      matrix1
      |> Enum.map(fn row1 ->
        matrix2
        |> transpose()
        |> Enum.map(fn row2 -> Vector.dot(row1, row2) end)
      end)
      |> Enum.map(&Result.product(&1))
      |> Result.product()
    else
      Result.error("Column size of first matrix must be same as row size of second matrix!")
    end
  end

  @doc """
  Schur product (or the Hadamard product) of two matrices. It produces another
  matrix where each element `i, j` is the product of elements `i, j` of the
  original two matrices. Sizes (dimensions) of both matrices must be same.
  Otherwise you get an error message.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ## Examples

      iex> mat1 = {:ok, [[1, 2, 3], [5, 6, 7]]}
      iex> mat2 = {:ok, [[1, 2 ,3], [4, 5, 6]]}
      iex> Result.and_then_x([mat1, mat2], &MatrixReloaded.Matrix.schur_product(&1, &2))
      {:ok,
        [
          [1, 4, 9],
          [20, 30, 42]
        ]
      }

  """
  @spec schur_product(t(), t()) :: Result.t(String.t(), t())
  def schur_product(matrix1, matrix2) do
    {rs1, cs1} = size(matrix1)
    {rs2, cs2} = size(matrix2)

    if rs1 == rs2 and cs1 == cs2 do
      matrix1
      |> Enum.zip(matrix2)
      |> Enum.map(fn {row1, row2} -> Vector.inner_product(row1, row2) end)
      |> Result.product()
    else
      Result.error(
        "Dimension of matrix {#{rs1}, #{cs1}} is not same as dimension of matrix {#{rs2}, #{cs2}}!"
      )
    end
  end

  @doc """
  Updates the matrix by given a submatrix. The position of submatrix inside
  matrix is given by index `{row_num, col_num}` and dimension of submatrix.
  Size of submatrix must be less than or equal to size of matrix. Otherwise
  you get an error message. The values of indices start from `0` to `matrix row size - 1`.
  Similarly for `col` size.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ##  Example:

      iex> mat = MatrixReloaded.Matrix.new(4)
      iex> mat |> Result.and_then(&MatrixReloaded.Matrix.update(&1, [[1,2],[3,4]], {1,2}))
      {:ok,
        [
          [0, 0, 0, 0],
          [0, 0, 1, 2],
          [0, 0, 3, 4],
          [0, 0, 0, 0]
        ]
      }

  """
  @spec update(t(), submatrix, index) :: Result.t(String.t(), t())
  def update(matrix, submatrix, index) do
    matrix
    |> is_index_ok?(index)
    |> Result.and_then(
      &is_submatrix_smaller_than_matrix?(&1, size(matrix), size(submatrix), :update)
    )
    |> Result.and_then(&is_submatrix_in_matrix?(&1, size(matrix), size(submatrix), index))
    |> Result.map(&make_update(&1, submatrix, index))
  end

  @doc """
  Updates the matrix by given a number. The position of element in matrix
  which you want to change is given by tuple `{row_num, col_num}`.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ##  Example:

      iex> mat = MatrixReloaded.Matrix.new(3)
      iex> mat |> Result.and_then(&MatrixReloaded.Matrix.update_element(&1, -1, {1, 1}))
      {:ok,
        [
          [0, 0, 0],
          [0, -1, 0],
          [0, 0, 0]
        ]
      }

  """
  @spec update_element(t(), number, index) :: Result.t(String.t(), t())
  def update_element(matrix, el, index) when is_number(el) do
    matrix
    |> is_index_ok?(index)
    |> Result.and_then(&is_element_in_matrix?(&1, size(matrix), index))
    |> Result.map(&make_update(&1, [[el]], index))
  end

  @doc """
  Updates row in the matrix by given a row vector (list) of numbers. The row which
  you want to change is given by tuple `{row_num, col_num}`. Both values are non
  negative integers.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ##  Example:
      iex> {:ok, mat} = MatrixReloaded.Matrix.new(4)
      iex> MatrixReloaded.Matrix.update_row(mat, [1, 2, 3], {3, 1})
      {:ok,
        [
          [0, 0, 0, 0],
          [0, 0, 0, 0],
          [0, 0, 0, 0],
          [0, 1, 2, 3]
        ]
      }

  """
  @spec update_row(t(), Vector.t(), index) :: Result.t(String.t(), t())
  def update_row(matrix, row, index) do
    matrix
    |> is_index_ok?(index)
    |> Result.and_then(&is_row_ok?(&1, row))
    |> Result.and_then(&is_row_size_smaller_than_rows_of_matrix?(&1, size(matrix), length(row)))
    |> Result.and_then(&is_row_in_matrix?(&1, size(matrix), length(row), index))
    |> Result.map(&make_update(&1, [row], index))
  end

  @doc """
  Updates column in the matrix by given a column vector. The column which you
  want to change is given by tuple `{row_num, col_num}`. Both values are non
  negative integers.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ##  Example:

      iex> {:ok, mat} = MatrixReloaded.Matrix.new(4)
      iex> MatrixReloaded.Matrix.update_col(mat, [[1], [2], [3]], {0, 1})
      {:ok,
        [
          [0, 1, 0, 0],
          [0, 2, 0, 0],
          [0, 3, 0, 0],
          [0, 0, 0, 0]
        ]
      }

  """
  @spec update_col(t(), Vector.column(), index) :: Result.t(String.t(), t())
  def update_col(matrix, [hd | _] = submatrix, index)
      when is_list(submatrix) and length(hd) == 1 do
    update(matrix, submatrix, index)
  end

  @doc """
  Updates the matrix by given a submatrices. The positions (or locations) of these
  submatrices are given by list of indices. Index of the individual submatrices is
  tuple of two numbers. These two numbers are number row and number column of matrix
  where the submatrices will be located. All submatrices must have same size (dimension).

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ##  Example:

      iex> mat = MatrixReloaded.Matrix.new(5)
      iex> sub_mat = MatrixReloaded.Matrix.new(2,1)
      iex> positions = [{0,0}, {3, 3}]
      iex> [mat, sub_mat] |> Result.and_then_x(&MatrixReloaded.Matrix.update_map(&1, &2, positions))
      {:ok,
        [
          [1, 1, 0, 0, 0],
          [1, 1, 0, 0, 0],
          [0, 0, 0, 0, 0],
          [0, 0, 0, 1, 1],
          [0, 0, 0, 1, 1]
        ]
      }

  """
  @spec update_map(t(), submatrix, list(index)) :: Result.t(String.t(), t())
  def update_map(matrix, submatrix, position_indices) do
    Enum.reduce(position_indices, {:ok, matrix}, fn position, acc ->
      Result.and_then(acc, &update(&1, submatrix, position))
    end)
  end

  @doc """
  Gets a submatrix from the matrix. By index you can select a submatrix. Dimension of
  submatrix is given by positive number (result then will be a square matrix) or tuple
  of two positive numbers (you get then a rectangular matrix).

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ##  Example:

      iex> mat = [[0, 0, 0, 0], [0, 0, 1, 2], [0, 0, 3, 4], [0, 0, 0, 0]]
      iex> MatrixReloaded.Matrix.get_submatrix(mat, {1, 2}, 2)
      {:ok,
        [
          [1, 2],
          [3, 4]
        ]
      }

      iex> mat = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 1, 2, 3], [0, 4, 5, 6]]
      iex> MatrixReloaded.Matrix.get_submatrix(mat, {2, 1}, {3, 3})
      {:ok,
        [
          [1, 2, 3],
          [4, 5, 6]
        ]
      }

  """
  @spec get_submatrix(t(), index, dimension) :: Result.t(String.t(), t())
  def get_submatrix(matrix, index, dimension) do
    dim_sub = dimension_of_submatrix(index, dimension)

    matrix
    |> is_index_ok?(index)
    |> Result.and_then(&is_submatrix_smaller_than_matrix?(&1, size(matrix), dim_sub, :get))
    |> Result.and_then(&is_submatrix_in_matrix?(&1, size(matrix), index, dim_sub, :get))
    |> Result.map(&make_get_submatrix(&1, index, dim_sub))
  end

  @doc """
  Gets an element from the matrix. By index you can select an element.

  Returns result, it means either tuple of `{:ok, number}` or `{:error, "msg"}`.

  ##  Example:

      iex> mat = [[0, 0, 0, 0], [0, 0, 1, 2], [0, 0, 3, 4], [0, 0, 0, 0]]
      iex> MatrixReloaded.Matrix.get_element(mat, {2, 2})
      {:ok, 3}

  """
  @spec get_element(t(), index) :: Result.t(String.t(), number)
  def get_element(matrix, index) when is_tuple(index) do
    dim_sub = dimension_of_submatrix(index, 1)

    matrix
    |> is_element_in_matrix?(size(matrix), index, :get)
    |> Result.map(&make_get_submatrix(&1, index, dim_sub))
    |> Result.map(fn el -> el |> hd |> hd end)
  end

  @doc """
  Gets a whole row from the matrix. By row number you can select the row which
  you want.

  Returns result, it means either tuple of `{:ok, number}` or `{:error, "msg"}`.

  ##  Example:

      iex> mat = [[0, 0, 0, 0], [0, 0, 1, 2], [0, 0, 3, 4], [0, 0, 0, 0]]
      iex> MatrixReloaded.Matrix.get_row(mat, 1)
      {:ok, [0, 0, 1, 2]}

  """
  @spec get_row(t(), non_neg_integer) :: Result.t(String.t(), Vector.t())
  def get_row(matrix, row_num) do
    {rs, cs} = size(matrix)

    matrix
    |> is_non_neg_integer?(row_num)
    |> Result.and_then(&is_row_num_at_matrix?(&1, {rs, cs}, row_num))
    |> Result.map(&make_get_submatrix(&1, {row_num, 0}, {row_num, cs}))
    |> Result.map(&hd(&1))
  end

  @doc """
  Gets a part row from the matrix. By index and positive number you can select
  the row and elements which you want.

  Returns result, it means either tuple of `{:ok, number}` or `{:error, "msg"}`.

  ##  Example:

      iex> mat = [[0, 0, 0, 0], [0, 0, 1, 2], [0, 0, 3, 4], [0, 0, 0, 0]]
      iex> MatrixReloaded.Matrix.get_row(mat, {2, 1}, 2)
      {:ok, [0, 3]}

  """
  @spec get_row(t(), index, non_neg_integer) :: Result.t(String.t(), Vector.t())
  def get_row(matrix, {row_num, _} = index, num_of_el) do
    {rs, cs} = size(matrix)

    matrix
    |> is_index_ok?(index)
    |> Result.and_then(&is_positive_integer?(&1, num_of_el))
    |> Result.and_then(&is_row_num_at_matrix?(&1, {rs, cs}, row_num))
    |> Result.map(&make_get_submatrix(&1, index, {row_num, num_of_el}))
    |> Result.map(&hd(&1))
  end

  @doc """
  Gets a whole column from the matrix. By column number you can select the column
  which you want.

  Returns result, it means either tuple of `{:ok, number}` or `{:error, "msg"}`.

  ##  Example:

      iex> mat = [[0, 0, 0, 0], [0, 0, 1, 2], [0, 0, 3, 4], [0, 0, 0, 0]]
      iex> MatrixReloaded.Matrix.get_col(mat, 3)
      {:ok, [[0], [2], [4], [0]]}

  """
  @spec get_col(t(), non_neg_integer) :: Result.t(String.t(), Vector.column())
  def get_col(matrix, col_num) do
    {rs, cs} = size(matrix)

    matrix
    |> is_non_neg_integer?(col_num)
    |> Result.map(&transpose/1)
    |> Result.and_then(&is_row_num_at_matrix?(&1, {rs, cs}, col_num, :column))
    |> Result.map(&make_get_submatrix(&1, {col_num, 0}, {col_num, cs}))
    |> Result.map(&hd(&1))
    |> Result.map(&Vector.transpose(&1))
  end

  @doc """
  Gets a part column from the matrix. By index and positive number you can select
  the column and elements which you want.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ##  Example:

      iex> mat = [[0, 0, 0, 0], [0, 0, 1, 2], [0, 0, 3, 4], [0, 0, 0, 0]]
      iex> MatrixReloaded.Matrix.get_col(mat, {1, 2}, 2)
      {:ok, [[1], [3]]}

  """
  @spec get_col(t(), index, non_neg_integer) :: Result.t(String.t(), Vector.column())
  def get_col(matrix, {row_num, col_num} = index, num_of_el) do
    {rs, cs} = size(matrix)

    matrix
    |> is_index_ok?(index)
    |> Result.and_then(&is_positive_integer?(&1, num_of_el))
    |> Result.map(&transpose/1)
    |> Result.and_then(&is_row_num_at_matrix?(&1, {cs, rs}, col_num, :column))
    |> Result.map(&make_get_submatrix(&1, {col_num, row_num}, {col_num, num_of_el}))
    |> Result.map(&hd(&1))
    |> Result.map(&Vector.transpose(&1))
  end

  @doc """
  Creates a square diagonal matrix with the elements of vector on the main diagonal
  or on lower/upper bidiagonal if diagonal number `k` is `k < 0` or `0 < k`.
  This number `k` must be integer.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ##  Example:
      iex> MatrixReloaded.Matrix.diag([1, 2, 3])
      {:ok,
        [
          [1, 0, 0],
          [0, 2, 0],
          [0, 0, 3]
        ]
      }
      iex> MatrixReloaded.Matrix.diag([1, 2, 3], 1)
      {:ok,
        [
          [0, 1, 0, 0],
          [0, 0, 2, 0],
          [0, 0, 0, 3],
          [0, 0, 0, 0]
        ]
      }

  """
  @spec diag(Vector.t(), integer()) :: Result.t(String.t(), t())
  def diag(vector, k \\ 0)

  def diag(vector, k) when is_list(vector) and is_integer(k) and 0 <= k do
    len = length(vector)

    if k <= len do
      0..(len - 1)
      |> Enum.reduce(new(len + k), fn i, acc ->
        acc |> Result.and_then(&update_element(&1, Enum.at(vector, i), {i, i + k}))
      end)
    else
      Result.error("Length of upper bidiagonal must be less or equal to length of vector!")
    end
  end

  def diag(vector, k) when is_list(vector) and is_integer(k) and k < 0 do
    len = length(vector)

    if abs(k) <= len do
      0..(len - 1)
      |> Enum.reduce(new(len - k), fn i, acc ->
        acc |> Result.and_then(&update_element(&1, Enum.at(vector, i), {i - k, i}))
      end)
    else
      Result.error("Length of lower bidiagonal must be less or equal to length of vector!")
    end
  end

  @doc """
  Transpose of matrix.

  ##  Example:

      iex> mat = [[1,2,3], [4,5,6], [7,8,9]]
      iex> MatrixReloaded.Matrix.transpose(mat)
      [
        [1, 4, 7],
        [2, 5, 8],
        [3, 6, 9]
      ]

  """
  @spec transpose(t()) :: t()
  def transpose(matrix) do
    matrix
    |> make_transpose()
  end

  @doc """
  Flip columns of matrix in the left-right direction (i.e. about a vertical axis).

  ##  Example:

      iex> mat = [[1,2,3], [4,5,6], [7,8,9]]
      iex> MatrixReloaded.Matrix.flip_lr(mat)
      [
        [3, 2, 1],
        [6, 5, 4],
        [9, 8, 7]
      ]

  """
  @spec flip_lr(t()) :: t()
  def flip_lr(matrix) do
    matrix
    |> Enum.map(fn row -> Enum.reverse(row) end)
  end

  @doc """
  Flip rows of matrix in the up-down direction (i.e. about a horizontal axis).

  ##  Example:

      iex> mat = [[1,2,3], [4,5,6], [7,8,9]]
      iex> MatrixReloaded.Matrix.flip_ud(mat)
      [
        [7, 8, 9],
        [4, 5, 6],
        [1, 2, 3]
      ]

  """
  @spec flip_ud(t()) :: t()
  def flip_ud(matrix) do
    matrix
    |> Enum.reverse()
  end

  @doc """
  Drops the row or list of rows from the matrix. The row number (or row numbers)
  must be positive integer.

  Returns matrix.

  ##  Example:
      iex> mat = [[0, 0, 0, 0], [0, 0, 1, 2], [0, 0, 3, 4], [0, 0, 0, 0]]
      iex> MatrixReloaded.Matrix.drop_row(mat, 2)
      {:ok,
        [
          [0, 0, 0, 0],
          [0, 0, 1, 2],
          [0, 0, 0, 0]
        ]
      }

      iex> mat = [[0, 0, 0, 0], [0, 0, 1, 2], [0, 0, 3, 4], [0, 0, 0, 0]]
      iex> MatrixReloaded.Matrix.drop_row(mat, [0, 3])
      {:ok,
        [
          [0, 0, 1, 2],
          [0, 0, 3, 4]
        ]
      }

  """
  @spec drop_row(t(), non_neg_integer | [non_neg_integer]) :: Result.t(String.t(), t())
  def drop_row(matrix, rows) when is_list(rows) do
    matrix
    |> is_all_row_numbers_ok?(rows)
    |> Result.and_then(&make_drop_rows(&1, rows))
  end

  def drop_row(matrix, row) do
    matrix
    |> is_non_neg_integer?(row)
    |> Result.and_then(&make_drop_row(&1, row))
  end

  @doc """
  Drops the column or list of columns from the matrix. The column number
  (or column numbers) must be positive integer.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ##  Example:
      iex> mat = [[0, 0, 0, 0], [0, 0, 1, 2], [0, 0, 3, 4], [0, 0, 0, 0]]
      iex> MatrixReloaded.Matrix.drop_col(mat, 2)
      {:ok,
        [
          [0, 0, 0],
          [0, 0, 2],
          [0, 0, 4],
          [0, 0, 0]
        ]
      }

      iex> mat = [[0, 0, 0, 0], [0, 0, 1, 2], [0, 0, 3, 4], [0, 0, 0, 0]]
      iex> MatrixReloaded.Matrix.drop_col(mat, [0, 1])
      {:ok,
        [
          [0, 0],
          [1, 2],
          [3, 4],
          [0, 0]
        ]
      }

  """
  @spec drop_col(t(), non_neg_integer | [non_neg_integer]) :: Result.t(String.t(), t())
  def drop_col(matrix, cols) when is_list(cols) do
    matrix
    |> transpose()
    |> is_all_row_numbers_ok?(cols)
    |> Result.and_then(&make_drop_rows(&1, cols, :column))
    |> Result.map(&transpose/1)
  end

  def drop_col(matrix, col) do
    matrix
    |> transpose()
    |> is_non_neg_integer?(col)
    |> Result.and_then(&make_drop_row(&1, col, :column))
    |> Result.map(&transpose/1)
  end

  @doc """
  Concatenate matrices horizontally. Both matrices must have same a row dimension.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ##  Example:
      iex> mat1 = MatrixReloaded.Matrix.diag([1, 1, 1])
      iex> mat2 = MatrixReloaded.Matrix.diag([2, 2, 2])
      iex> Result.and_then_x([mat1, mat2], &MatrixReloaded.Matrix.concat_row(&1, &2))
      {:ok,
        [
          [1, 0, 0, 2, 0, 0],
          [0, 1, 0, 0, 2, 0],
          [0, 0, 1, 0, 0, 2]
        ]
      }

  """
  @spec concat_row(t(), t()) :: Result.t(String.t(), t())
  def concat_row(matrix1, matrix2) do
    {rs1, _cs1} = size(matrix1)
    {rs2, _cs2} = size(matrix2)

    if rs1 == rs2 do
      matrix1
      |> Enum.zip(matrix2)
      |> Enum.map(fn {r1, r2} -> Enum.concat(r1, r2) end)
      |> Result.ok()
    else
      Result.error("Matrices have different row dimensions. Must be same!")
    end
  end

  @doc """
  Concatenate matrices vertically. Both matrices must have same a column dimension.

  Returns result, it means either tuple of `{:ok, matrix}` or `{:error, "msg"}`.

  ##  Example:
      iex> mat1 = MatrixReloaded.Matrix.diag([1, 1, 1])
      iex> mat2 = MatrixReloaded.Matrix.diag([2, 2, 2])
      iex> Result.and_then_x([mat1, mat2], &MatrixReloaded.Matrix.concat_col(&1, &2))
      {:ok,
        [
          [1, 0, 0],
          [0, 1, 0],
          [0, 0, 1],
          [2, 0, 0],
          [0, 2, 0],
          [0, 0, 2]
        ]
      }

  """
  @spec concat_col(t(), t()) :: Result.t(String.t(), t())
  def concat_col(matrix1, matrix2) do
    {_rs1, cs1} = size(matrix1)
    {_rs2, cs2} = size(matrix2)

    if cs1 == cs2 do
      matrix1
      |> Enum.concat(matrix2)
      |> Result.ok()
    else
      Result.error("Matrices have different column dimensions. Must be same!")
    end
  end

  @doc """
  Reshape vector or matrix. The `row` and `col` numbers must be positive number.
  By the `row` or `col` number you can change shape of matrix, respectively create
  new from vector.

  Returns result, it means either tuple of `{:ok, vector | matrix}` or `{:error, "msg"}`.

  ## Example:
      iex> 1..10 |> Enum.to_list |> MatrixReloaded.Matrix.reshape(5, 2)
      {:ok,
        [
          [1, 2],
          [3, 4],
          [5, 6],
          [7, 8],
          [9, 10]
        ]
      }

      iex> MatrixReloaded.Matrix.new({3,4}) |> Result.map(&MatrixReloaded.Matrix.reshape(&1, 2, 6))
      {:ok,
        [
          [0, 0, 0, 0, 0, 0,],
          [0, 0, 0, 0, 0, 0,]
        ]
      }

  """
  @spec reshape(Vector.t() | t(), pos_integer(), pos_integer()) ::
          Result.t(String.t(), Vector.t()) | Result.t(String.t(), t())
  def reshape([el | _] = vector, row, col)
      when is_list(vector) and is_number(el) and
             is_integer(row) and row > 0 and is_integer(col) and col == 1 do
    vector
    |> transpose()
  end

  def reshape([el | _] = vector, row, col)
      when is_list(vector) and is_number(el) and
             is_integer(row) and row == 1 and is_integer(col) and col > 0 do
    vector
  end

  def reshape([el | _] = vector, row, col)
      when is_list(vector) and is_number(el) and
             is_integer(row) and row > 0 and is_integer(col) and col > 0 do
    vector
    |> is_reshapeable?(row, col)
    |> Result.map(&Enum.chunk_every(&1, col))
  end

  def reshape([r | _] = matrix, row, col)
      when is_list(matrix) and is_list(r) and
             is_integer(row) and row == 1 and is_integer(col) and col > 0 do
    matrix
    |> is_reshapeable?(row, col)
    |> Result.and_then(&List.flatten(&1))
  end

  def reshape([r | _] = matrix, row, col)
      when is_list(matrix) and is_list(r) and
             is_integer(row) and row > 0 and is_integer(col) and col > 0 do
    matrix
    |> is_reshapeable?(row, col)
    |> Result.map(&List.flatten(&1))
    |> Result.and_then(&Enum.chunk_every(&1, col))
  end

  def reshape(_matrix, row, col) when row < 2 and col < 2 do
    Result.error("'row' and 'col' number must be positive integer number greater than 0!")
  end

  @doc """
  The size (dimensions) of the matrix.

  Returns tuple of {row_size, col_size}.

  ## Example:

      iex> MatrixReloaded.Matrix.new({3,4}) |> Result.map(&MatrixReloaded.Matrix.size(&1))
      {:ok, {3, 4}}

  """
  @spec size(t()) :: {pos_integer, pos_integer}
  def size(matrix), do: {length(matrix), length(List.first(matrix))}

  defp make_row(0, _val), do: []
  defp make_row(n, val), do: [val] ++ make_row(n - 1, val)

  defp make_update(matrix, submatrix, {from_row, from_col}) do
    {to_row, to_col} = size(submatrix)

    matrix
    |> Enum.with_index()
    |> Enum.map(fn {row, i} ->
      if i in from_row..(from_row + to_row - 1) do
        row
        |> Enum.with_index()
        |> Enum.map(fn {val, j} ->
          if j in from_col..(from_col + to_col - 1) do
            submatrix |> Enum.at(i - from_row) |> Enum.at(j - from_col)
          else
            val
          end
        end)
      else
        row
      end
    end)
  end

  defp make_get_submatrix(matrix, {from_row, from_col}, {to_row, to_col}) do
    matrix
    |> Enum.with_index()
    |> Enum.filter(fn {_row, i} ->
      i in from_row..(from_row + to_row - 1)
    end)
    |> Enum.map(fn {row, _i} ->
      row
      |> Enum.with_index()
      |> Enum.filter(fn {_col, j} ->
        j in from_col..(from_col + to_col - 1)
      end)
      |> Enum.map(fn {val, _j} -> val end)
    end)
  end

  defp make_drop_rows(matrix, rows, vec \\ :row) do
    {row_size, col_size} = size(matrix)

    if length(rows) < row_size do
      row_help =
        0..(length(rows) - 1)
        |> Enum.to_list()

      rows
      |> Vector.sub(row_help)
      |> Result.and_then(
        &Enum.reduce(&1, {:ok, matrix}, fn r, acc ->
          acc |> Result.and_then(fn a -> drop_row(a, r) end)
        end)
      )
    else
      Result.error(
        "It is not possible drop all the #{Atom.to_string(vec)}s from matrix! Matrix has dimensions {#{row_size}, #{col_size}}."
      )
    end
  end

  defp make_drop_row(matrix, row, vec \\ "row") do
    {row_size, _col_size} = size(matrix)

    if row < row_size do
      matrix
      |> List.delete_at(row)
      |> Result.ok()
    else
      Result.error(
        "It is not possible drop the #{vec} #{row} from matrix! Numbering of #{vec}s begins from 0 to (matrix #{vec} size - 1)."
      )
    end
  end

  defp make_transpose([[] | _]), do: []

  defp make_transpose(matrix) do
    [Enum.map(matrix, &hd/1) | make_transpose(Enum.map(matrix, &tl/1))]
  end

  defp is_submatrix_smaller_than_matrix?(
         matrix,
         {rs_mat, cs_mat},
         {rs_sub, cs_sub},
         _method
       )
       when rs_sub < rs_mat and cs_sub < cs_mat do
    Result.ok(matrix)
  end

  defp is_submatrix_smaller_than_matrix?(
         _matrix,
         _size_mat,
         _size_sub,
         :update
       ) do
    Result.error(
      "You can not update the matrix. Size of submatrix is same or bigger than size of matrix!"
    )
  end

  defp is_submatrix_smaller_than_matrix?(
         _matrix,
         _size_mat,
         _size_sub,
         :get
       ) do
    Result.error(
      "You can not get the submatrix. Size of submatrix is same or bigger than size of matrix!"
    )
  end

  defp is_row_size_smaller_than_rows_of_matrix?(
         matrix,
         size_mat,
         size_row,
         method \\ :update
       )

  defp is_row_size_smaller_than_rows_of_matrix?(
         matrix,
         {_rs_mat, cs_mat},
         size_r,
         _method
       )
       when size_r <= cs_mat do
    Result.ok(matrix)
  end

  defp is_row_size_smaller_than_rows_of_matrix?(
         _matrix,
         _size_mat,
         _size_row,
         method
       ) do
    Result.error(
      "You can not #{Atom.to_string(method)} the matrix. Size of row is bigger than row size of matrix!"
    )
  end

  defp is_element_in_matrix?(
         matrix,
         size_mat,
         index,
         method \\ :update
       )

  defp is_element_in_matrix?(
         matrix,
         {rs_mat, cs_mat},
         {from_row, from_col},
         _method
       )
       when from_row < rs_mat and from_col < cs_mat do
    Result.ok(matrix)
  end

  defp is_element_in_matrix?(
         _matrix,
         _size_mat,
         {from_row, from_col},
         method
       ) do
    Result.error(
      "You can not #{Atom.to_string(method)} the matrix on given position {#{from_row}, #{from_col}}. The element is outside of matrix!"
    )
  end

  defp is_submatrix_in_matrix?(
         matrix,
         size_mat,
         size_sub,
         index,
         method \\ :update
       )

  defp is_submatrix_in_matrix?(
         matrix,
         {rs_mat, cs_mat},
         {to_row, to_col},
         {from_row, from_col},
         _method
       )
       when from_row + to_row - 1 < rs_mat and from_col + to_col - 1 < cs_mat do
    Result.ok(matrix)
  end

  defp is_submatrix_in_matrix?(
         _matrix,
         _size_mat,
         _size_sub,
         {from_row, from_col},
         method
       ) do
    Result.error(
      "You can not #{Atom.to_string(method)} the matrix on given position {#{from_row}, #{from_col}}. The submatrix is outside of matrix!"
    )
  end

  defp is_row_in_matrix?(
         matrix,
         size_mat,
         size_row,
         index,
         method \\ :update
       )

  defp is_row_in_matrix?(
         matrix,
         {rs_mat, cs_mat},
         s_row,
         {from_row, from_col},
         _method
       )
       when from_row <= rs_mat and from_col + s_row <= cs_mat do
    Result.ok(matrix)
  end

  defp is_row_in_matrix?(
         _matrix,
         _size_mat,
         _size_row,
         {from_row, from_col},
         method
       ) do
    Result.error(
      "You can not #{Atom.to_string(method)} row in the matrix on given position {#{from_row}, #{from_col}}. A part of row is outside of matrix!"
    )
  end

  defp is_row_num_at_matrix?(matrix, size_mat, row_num, vec \\ :row)

  defp is_row_num_at_matrix?(matrix, {rs_mat, _cs_mat}, row_num, _vec)
       when row_num < rs_mat do
    Result.ok(matrix)
  end

  defp is_row_num_at_matrix?(
         _matrix,
         _size_mat,
         row_num,
         vec
       ) do
    Result.error(
      "You can not get #{Atom.to_string(vec)} from the matrix. The #{Atom.to_string(vec)} number #{row_num} is outside of matrix!"
    )
  end

  defp dimension_of_submatrix({from_row, from_col}, {to_row, to_col} = dimension)
       when is_tuple(dimension) do
    {to_row - from_row + 1, to_col - from_col + 1}
  end

  defp dimension_of_submatrix(_index, dimension) do
    {dimension, dimension}
  end

  defp is_row_ok?(matrix, [hd | _] = row) when is_list(row) and is_number(hd) do
    Result.ok(matrix)
  end

  defp is_row_ok?(_matrix, _row) do
    Result.error("Input row (or column) vector must be only list of numbers!")
  end

  defp is_all_row_numbers_ok?(matrix, row_list, vec \\ :row) do
    is_all_ok? =
      row_list
      |> Enum.map(fn r -> 0 <= r end)
      |> Enum.find_index(fn r -> r == false end)

    if is_all_ok? == nil do
      Result.ok(matrix)
    else
      Result.error("List of #{Atom.to_string(vec)} numbers must be greater or equal to zero!")
    end
  end

  defp is_dimension_ok?({rows, cols} = tpl)
       when tuple_size(tpl) == 2 and is_integer(rows) and rows > 0 and is_integer(cols) and
              cols > 0 do
    Result.ok(tpl)
  end

  defp is_dimension_ok?({rows, cols}) do
    Result.error(
      "The size {#{rows}, #{cols}} of matrix must be in the form {m, n} where m, n are positive integers!"
    )
  end

  defp is_dimension_ok?(dim)
       when is_integer(dim) and 0 < dim do
    Result.ok(dim)
  end

  defp is_dimension_ok?(_dim) do
    Result.error("Dimension of squared matrix must be positive integer!")
  end

  defp is_index_ok?(matrix, ind)
       when is_tuple(ind) and tuple_size(ind) == 2 and is_integer(elem(ind, 0)) and
              0 <= elem(ind, 0) and is_integer(elem(ind, 1)) and 0 <= elem(ind, 1) do
    Result.ok(matrix)
  end

  defp is_index_ok?(_matrix, _index) do
    Result.error("The index must be in the form {m, n} where 0 <= m and 0 <= n !")
  end

  defp is_non_neg_integer?(matrix, num) when is_integer(num) and 0 <= num do
    Result.ok(matrix)
  end

  defp is_non_neg_integer?(_matrix, num) when is_number(num) do
    Result.error("The integer number must be greater or equal to zero!")
  end

  defp is_positive_integer?(matrix, num) when is_integer(num) and 0 < num do
    Result.ok(matrix)
  end

  defp is_positive_integer?(_matrix, num) when is_number(num) do
    Result.error("The integer number must be positive, i.e. n > 0 !")
  end

  defp is_reshapeable?([el | _] = vector, row, col)
       when is_list(vector) and is_number(el) and length(vector) == row * col do
    Result.ok(vector)
  end

  defp is_reshapeable?([el | _] = vector, _row, _col)
       when is_list(vector) and is_number(el) do
    Result.error(
      "It is not possible to reshape vector! The numbers of element of vector must be equal row * col."
    )
  end

  defp is_reshapeable?([r | _] = matrix, row, col)
       when is_list(matrix) and is_list(r) do
    {rs, cs} = size(matrix)

    if row * col == rs * cs do
      Result.ok(matrix)
    else
      Result.error(
        "It is not possible to reshape matrix! The numbers of element of matrix must be equal row * col."
      )
    end
  end
end