from sympy.abc import x, y
import sympy
from sympy.polys.polytools import degree
import numpy as np

from .carry import Carry


def add(
    expansion: np.ndarray, carry: Carry, val: int = 1, center: tuple[int, int] = (0, 0)
):
    """
    Add `val` to a position in an `expansion` designated the `center`, then apply `carry`.
    """
    expansion[center] += val
    carry.apply(expansion)
    return expansion


def times(expansion: np.ndarray, carry: Carry, val: int):
    """
    Multiply all digits in an `expansion` by `val`, then apply `carry`.
    """
    expansion *= val
    carry.apply(expansion)
    return expansion


def poly_from_array(
    array: np.ndarray, center: tuple[int, int] = (0, 0), x_var=x, y_var=y
):
    """
    Convert a numpy array to a polynomial in two variables.
    Note that indices are row-major in Numpy, so center is in the form (y, x).
    """
    ret = 0
    for i, row in enumerate(array):
        for j, val in enumerate(row):
            ret += val * (x_var ** (i - center[1]) * y_var ** (j - center[0]))
    return ret


def poly_to_array(
    poly: sympy.Expr,
    alloc: np.ndarray | None = None,
    x_var=x,
    y_var=y
) -> np.ndarray:
    '''Convert a polynomial of 2 variables to a 2D array'''
    if isinstance(poly, (int, sympy.core.numbers.Zero)):
        if alloc is None:
            alloc = np.zeros((1, 1), dtype=np.int32)
            alloc[0,0] = int(poly)
            return alloc
        alloc.fill(0)
        alloc[0,0] = int(poly)
        return alloc

    coeffs = list(sorted(
        map(
            lambda z: (z, (degree(z[0], x_var), degree(z[0], y_var))),  # type: ignore
            poly.as_coefficients_dict().items()
        ),
        key=lambda z: sum(z[1])
    ))

    max_x, max_y = coeffs[-1][1]
    if alloc is None:
        alloc = np.zeros((max_x + max_y + 1, max_x + max_y + 1), dtype=np.int32)
    elif alloc.shape[0] < (max_x + 1) or alloc.shape[1] < (max_y + 1):
        raise ValueError("return array has insufficient size")
    else:
        alloc.fill(0)

    for (_, coeff), (x_deg, y_deg) in coeffs:
        alloc[y_deg, x_deg] = coeff
    return alloc


def _first_nonzero(arr: np.ndarray, axis: int):
    """
    Find the index of the first nonzero entry of `arr` along `axis`.
    Modified from https://stackoverflow.com/questions/47269390
    """
    mask = arr != 0
    return min(
        np.where(mask.any(axis=axis), mask.argmax(axis=axis), arr.shape[axis] + 1)
    )


def latex_polynumber(
    arr: np.ndarray, center: tuple[int, int] | None = None, show_zero: bool = False
):
    """
    Convert a 2D Numpy array into a LaTeX array representing a polynomial in two variables.
    Vertical lines and `\\hline`s are placed to indicate the transition between negative
        and nonnegative powers, similar to a typical fractional separator (decimal point).

    By default, this assumes the constant term is in position (0, 0) of the array.
    It may be alternatively supplied by `center`, which designates `arr[center]` as the constant term.
    Note that indices are row-major in Numpy, so `center` is in the form (y, x).

    `show_zero` defaults to False, which causes entries in `arr` which are 0 to not be drawn.
    When true, all 0 entries will be drawn.
    """
    upper_left = _first_nonzero(arr, 0), _first_nonzero(arr, 1)
    lower_right = (
        len(arr) - _first_nonzero(np.flip(arr, 0), 0) - 1,
        len(arr[1]) - _first_nonzero(np.flip(arr, 1), 1) - 1,
    )

    center_left, center_top = center or (0, 0)
    if center is not None:
        # this does not need offset, since we iterate over this range
        center_top = center[0]
        # but this does for the array environment argument
        center_left = center[1] - upper_left[1]

    num_columns = lower_right[1] - upper_left[1]
    column_layout = ("c" * center_left) + "|" + ("c" * (num_columns - center_left))

    # build array output
    ret = "\\begin{array}{" + column_layout + "}"
    for i in range(upper_left[0], lower_right[0] + 1):
        if i == center_top:
            ret += " \\hline "
        ret += " & ".join(
            [
                str(arr[i, j] if arr[i, j] != 0 or show_zero else "")
                for j in range(upper_left[1], lower_right[1] + 1)
            ]
        )
        # next row
        ret += " \\\\ "
    return ret + "\\end{array}"
