from typing import Iterable, Any, Optional, List, Union, Tuple


class Matrix:
    """
    # noqa: E501
    Poor man's implementation of a DataFrame/Matrix

    This is used to calculate similarities between HPO sets
    and is surprisingly much faster than using pandas DataFrames

    .. note::

        Pandas::

            ===== COMPARING SETS ======
            23806489 function calls (23770661 primitive calls) in 19.705 seconds

            ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            ....
            9900    0.267    0.000   19.106    0.002 set.py:318(similarity)
            9900    1.124    0.000   14.330    0.001 set.py:477(_sim_score)
            ....

        Matrix::

            ===== COMPARING SETS ======
            12870433 function calls in 6.642 seconds

            ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            ....
            9900    0.048    0.000    6.424    0.001 set.py:316(similarity)
            9900    0.928    0.000    5.112    0.001 set.py:432(_sim_score)
            ....

    .. warning::

        This `Matrix` should not be used as a public interface.
        It's only used internally for calculations.

    Parameters
    ----------
    rows: int
        The number of rows in the Matrix
    cols: int
        The number of columns in the Matrix
    data: list of values, default ``None``
        A list with values to fill the Matrix.

    Attributes
    ----------
    n_rows: int
        The number of rows in the Matrix

    n_cols: int
        The number of columns in the Matrix

    rows: iterator
        Iterator over all rows

        **Example:** ::

            print(matrix)

            >>    ||   0|   1|   2|   3|
            >> =========================
            >> 0  ||  11|  12|  13|  14|
            >> 1  ||  21|  22|  23|  24|
            >> 2  ||  31|  32|  33|  34|

            for row in matrix.rows:
                print(row)

            >> [11, 12, 13, 14]
            >> [21, 22, 23, 24]
            >> [31, 32, 33, 34]

    columns: iterator
        Iterator over all columns

        **Example:** ::

            print(matrix)

            >>    ||   0|   1|   2|   3|
            >> =========================
            >> 0  ||  11|  12|  13|  14|
            >> 1  ||  21|  22|  23|  24|
            >> 2  ||  31|  32|  33|  34|

            for col in matrix.columns:
                print(col)

            >> [11, 21, 31]
            >> [12, 22, 32]
            >> [13, 23, 33]
            >> [14, 24, 34]

    """
    def __init__(
        self,
        rows: int,
        cols: int,
        data: Optional[List[Any]] = None
    ):
        self.n_rows = rows
        self.n_cols = cols
        self._data: List[Any]
        if data is None:
            self._data = [None] * rows * cols
        elif len(data) == rows * cols:
            self._data = data
        else:
            raise RuntimeError('Wrong number of data items in `data`')

    def __getitem__(self, key: Tuple[Optional[int], Optional[int]]) -> Any:
        idx = self._get_key_indicies(key)
        return self._data[idx]

    def __setitem__(
        self,
        key: Tuple[Optional[int], Optional[int]],
        val: Any
    ) -> None:
        idx = self._get_key_indicies(key)

        if isinstance(idx, int) or len(self._data[idx]) == len(val):
            self._data[idx] = val
        else:
            raise ValueError('Different length of matrix subset and values')

    def _get_key_indicies(
        self,
        key: Tuple[Optional[int], Optional[int]]
    ) -> Union[int, slice]:
        row = key[0]
        col = key[1]

        if row is None and isinstance(col, int):
            # Return one column
            return slice(
                col,
                self.n_rows * (self.n_cols) + col,
                self.n_cols
            )

        if col is None and isinstance(row, int):
            # Return one row
            return slice(
                row * self.n_cols,
                row * self.n_cols + self.n_cols,
                1
            )

        if isinstance(row, int) and isinstance(col, int):
            if row > self.n_rows - 1:
                raise RuntimeError('Invalid row number: {}'.format(row))
            if col > self.n_cols - 1:
                raise RuntimeError('Invalid column number: {}'.format(col))
            return row * self.n_cols + col

        raise RuntimeError('Invalid arguments for Matrix subset')

    @property
    def rows(self) -> Iterable[Any]:
        for x in range(self.n_rows):
            yield self[x, None]

    @property
    def columns(self) -> Iterable[Any]:
        for x in range(self.n_cols):
            yield self[None, x]

    def __str__(self) -> str:
        maxlength = max(
            [len(str(self.n_cols))] +
            [len(str(x)) for x in self._data]
        ) + 2
        idxlength = len(str(self.n_rows)) + 2

        s = ''

        s += '{}||'.format(''.rjust(idxlength))
        s += ''.join([
            '{}|'.format(str(x).rjust(maxlength))
            for x in range(self.n_cols)
        ])
        s += '\n' + '=' * len(s)

        for idx, item in enumerate(self._data):
            if idx % self.n_cols == 0:
                s += '\n{}||'.format(
                    str(int(idx/self.n_cols)).ljust(idxlength)
                )
            s += '{}|'.format(str(item).rjust(maxlength))

        return s

from typing import Iterable, Any, Optional, List, Union, Tuple


class Matrix:
    """
    # noqa: E501
    Poor man's implementation of a DataFrame/Matrix

    This is used to calculate similarities between HPO sets
    and is surprisingly much faster than using pandas DataFrames

    .. note::

        Pandas::

            ===== COMPARING SETS ======
            23806489 function calls (23770661 primitive calls) in 19.705 seconds

            ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            ....
            9900    0.267    0.000   19.106    0.002 set.py:318(similarity)
            9900    1.124    0.000   14.330    0.001 set.py:477(_sim_score)
            ....

        Matrix::

            ===== COMPARING SETS ======
            12870433 function calls in 6.642 seconds

            ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            ....
            9900    0.048    0.000    6.424    0.001 set.py:316(similarity)
            9900    0.928    0.000    5.112    0.001 set.py:432(_sim_score)
            ....

    .. warning::

        This `Matrix` should not be used as a public interface.
        It's only used internally for calculations.

    Parameters
    ----------
    rows: int
        The number of rows in the Matrix
    cols: int
        The number of columns in the Matrix
    data: list of values, default ``None``
        A list with values to fill the Matrix.

    Attributes
    ----------
    n_rows: int
        The number of rows in the Matrix

    n_cols: int
        The number of columns in the Matrix

    rows: iterator
        Iterator over all rows

        **Example:** ::

            print(matrix)

            >>    ||   0|   1|   2|   3|
            >> =========================
            >> 0  ||  11|  12|  13|  14|
            >> 1  ||  21|  22|  23|  24|
            >> 2  ||  31|  32|  33|  34|

            for row in matrix.rows:
                print(row)

            >> [11, 12, 13, 14]
            >> [21, 22, 23, 24]
            >> [31, 32, 33, 34]

    columns: iterator
        Iterator over all columns

        **Example:** ::

            print(matrix)

            >>    ||   0|   1|   2|   3|
            >> =========================
            >> 0  ||  11|  12|  13|  14|
            >> 1  ||  21|  22|  23|  24|
            >> 2  ||  31|  32|  33|  34|

            for col in matrix.columns:
                print(col)

            >> [11, 21, 31]
            >> [12, 22, 32]
            >> [13, 23, 33]
            >> [14, 24, 34]

    """
    def __init__(
        self,
        rows: int,
        cols: int,
        data: Optional[List[Any]] = None
    ):
        self.n_rows = rows
        self.n_cols = cols
        self._data: List[Any]
        if data is None:
            self._data = [None] * rows * cols
        elif len(data) == rows * cols:
            self._data = data
        else:
            raise RuntimeError('Wrong number of data items in `data`')

    def __getitem__(self, key: Tuple[Optional[int], Optional[int]]) -> Any:
        idx = self._get_key_indicies(key)
        return self._data[idx]

    def __setitem__(
        self,
        key: Tuple[Optional[int], Optional[int]],
        val: Any
    ) -> None:
        idx = self._get_key_indicies(key)

        if isinstance(idx, int) or len(self._data[idx]) == len(val):
            self._data[idx] = val
        else:
            raise ValueError('Different length of matrix subset and values')

    def _get_key_indicies(
        self,
        key: Tuple[Optional[int], Optional[int]]
    ) -> Union[int, slice]:
        row = key[0]
        col = key[1]

        if row is None and isinstance(col, int):
            # Return one column
            return slice(
                col,
                self.n_rows * (self.n_cols) + col,
                self.n_cols
            )

        if col is None and isinstance(row, int):
            # Return one row
            return slice(
                row * self.n_cols,
                row * self.n_cols + self.n_cols,
                1
            )

        if isinstance(row, int) and isinstance(col, int):
            if row > self.n_rows - 1:
                raise RuntimeError('Invalid row number: {}'.format(row))
            if col > self.n_cols - 1:
                raise RuntimeError('Invalid column number: {}'.format(col))
            return row * self.n_cols + col

        raise RuntimeError('Invalid arguments for Matrix subset')

    @property
    def rows(self) -> Iterable[Any]:
        for x in range(self.n_rows):
            yield self[x, None]

    @property
    def columns(self) -> Iterable[Any]:
        for x in range(self.n_cols):
            yield self[None, x]

    def __str__(self) -> str:
        maxlength = max(
            [len(str(self.n_cols))] +
            [len(str(x)) for x in self._data]
        ) + 2
        idxlength = len(str(self.n_rows)) + 2

        s = ''

        s += '{}||'.format(''.rjust(idxlength))
        s += ''.join([
            '{}|'.format(str(x).rjust(maxlength))
            for x in range(self.n_cols)
        ])
        s += '\n' + '=' * len(s)

        for idx, item in enumerate(self._data):
            if idx % self.n_cols == 0:
                s += '\n{}||'.format(
                    str(int(idx/self.n_cols)).ljust(idxlength)
                )
            s += '{}|'.format(str(item).rjust(maxlength))

        return s

# noqa: E501     Poor man's implementation of a DataFrame/Matrix      This is used to calculate similarities between HPO sets     and is surprisingly much faster than using pandas DataFrames      .. note::          Pandas::              ===== COMPARING SETS ======             23806489 function calls (23770661 primitive calls) in 19.705 seconds              ncalls  tottime  percall  cumtime  percall filename:lineno(function)             ....             9900    0.267    0.000   19.106    0.002 set.py:318(similarity)             9900    1.124    0.000   14.330    0.001 set.py:477(_sim_score)             ....          Matrix::              ===== COMPARING SETS ======             12870433 function calls in 6.642 seconds              ncalls  tottime  percall  cumtime  percall filename:lineno(function)             ....             9900    0.048    0.000    6.424    0.001 set.py:316(similarity)             9900    0.928    0.000    5.112    0.001 set.py:432(_sim_score)             ....      .. warning::          This `Matrix` should not be used as a public interface.         It's only used internally for calculations.      Parameters     ----------     rows: int         The number of rows in the Matrix     cols: int         The number of columns in the Matrix     data: list of values, default ``None``         A list with values to fill the Matrix.      Attributes     ----------     n_rows: int         The number of rows in the Matrix      n_cols: int         The number of columns in the Matrix      rows: iterator         Iterator over all rows          **Example:** ::              print(matrix)              >>    ||   0|   1|   2|   3|             >> =========================             >> 0  ||  11|  12|  13|  14|             >> 1  ||  21|  22|  23|  24|             >> 2  ||  31|  32|  33|  34|              for row in matrix.rows:                 print(row)              >> [11, 12, 13, 14]             >> [21, 22, 23, 24]             >> [31, 32, 33, 34]      columns: iterator         Iterator over all columns          **Example:** ::              print(matrix)              >>    ||   0|   1|   2|   3|             >> =========================             >> 0  ||  11|  12|  13|  14|             >> 1  ||  21|  22|  23|  24|             >> 2  ||  31|  32|  33|  34|              for col in matrix.columns:                 print(col)              >> [11, 21, 31]             >> [12, 22, 32]             >> [13, 23, 33]             >> [14, 24, 34] # Return one column # Return one row