# Copyright Louis Paternault 2025
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# Yogroup=u should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

"""Writing orders"""

import functools
import itertools
import random as _random

DEFAULT_ORDER = "latin"

# There is probably a smart way to format those function in a single one, but,
# this one is clear, and I am lazy…

################################################################################
# Officially supported writing orders


def lrtb(n, width, height):
    """Left to right, top to bottom (english, french, etc. writing order)."""
    return n % width, (width * height - n - 1) // width


def lrbt(n, width, height):
    """Left to right, bottom to top"""
    # pylint: disable=unused-argument
    return n % width, n // width


def rltb(n, width, height):
    """Right to left, top to bottom (arabic writing order)."""
    return (width * height - n - 1) % width, (width * height - n - 1) // width


def rlbt(n, width, height):
    """Right to left, bottom to top."""
    return (width * height - n - 1) % width, n // width


def tblr(n, width, height):
    """Top to bottom, left to right."""
    return n // height, (width * height - n - 1) % height


def btlr(n, width, height):
    """Bottom to top, left to right"""
    # pylint: disable=unused-argument
    return n // height, n % height


def tbrl(n, width, height):
    """Top to bottom, right to left (japanese writing order)."""
    return (width * height - n - 1) // height, (width * height - n - 1) % height


def btrl(n, width, height):
    """Bottom to top, right to left ."""
    return (width * height - n - 1) // height, n % height


################################################################################
# Some aliases


def latin(*args, **kwargs):
    """Latin writing order (alias for `lrtb`)."""
    return lrtb(*args, **kwargs)


def japanese(*args, **kwargs):
    """Japanese writing order (alias for `tbrl`)."""
    return tbrl(*args, **kwargs)


def arabic(*args, **kwargs):
    """Arabic writing order (alias for `rltb`)."""
    return rltb(*args, **kwargs)


################################################################################
# Boustrophedon (experimental)
# This is useless, but not as useless as the spirals below!

# pylint: disable=invalid-name, no-else-return


def Btrl(n, width, height):
    """Boustrophedon, starting from the top right corner, and heading left."""
    if n // width % 2 == 0:
        return rltb(n, width, height)
    else:
        return lrtb(n, width, height)


def Btlr(n, width, height):
    """Boustrophedon, starting from the top left corner, and heading right."""
    if n // width % 2 == 0:
        return lrtb(n, width, height)
    else:
        return rltb(n, width, height)


def Bblr(n, width, height):
    """Boustrophedon, starting from the bottom left corner, and heading right."""
    if n // width % 2 == 0:
        return lrbt(n, width, height)
    else:
        return rlbt(n, width, height)


def Bblt(n, width, height):
    """Boustrophedon, starting from the bottom left corner, and heading top."""
    if n // height % 2 == 0:
        return btlr(n, width, height)
    else:
        return tblr(n, width, height)


def Bbrl(n, width, height):
    """Boustrophedon, starting from the bottom right corner, and heading left."""
    if n // width % 2 == 0:
        return rlbt(n, width, height)
    else:
        return lrbt(n, width, height)


def Bbrt(n, width, height):
    """Boustrophedon, starting from the bottom right corner, and heading top."""
    if n // height % 2 == 0:
        return btrl(n, width, height)
    else:
        return tbrl(n, width, height)


def Btlb(n, width, height):
    """Boustrophedon, starting from the top left corner, and heading bottom."""
    if n // height % 2 == 0:
        return tblr(n, width, height)
    else:
        return btlr(n, width, height)


def Btrb(n, width, height):
    """Boustrophedon, starting from the top right corner, and heading bottom."""
    if n // height % 2 == 0:
        return tbrl(n, width, height)
    else:
        return btrl(n, width, height)


################################################################################
# Spiral (experimental)
# Why did I even waste time coding this?


def _rotation(  # pylint: disable=too-many-return-statements, inconsistent-return-statements
    start: tuple[int, int], clockwise: bool
) -> tuple[tuple[int, int]]:
    """Return the list of four directions, that are to be cycled to rotate.

    For instance: right, bottom, left, top as coordinates of the direction:
    `((1, 0), (0, -1), (-1, 0), (0, 1))`

    :param start: Coordinates of the starting corner.
        Any non-zero value is interpreted as "as right/top as possible".
    :param clockwise: If `True`, rotate clockwise. Otherwise, rotate anticlockwise.
    """
    # If I was smarter, or less tired, or less lazy, I would have found a smarter way to do this.
    # But this is legible and clear, so who cares?
    if clockwise and start[0] == 0 and start[1] == 0:
        # Bottom left corner
        return ((0, 1), (1, 0), (0, -1), (-1, 0))
    if clockwise and start[0] != 0 and start[1] == 0:
        # Bottom right corner
        return ((-1, 0), (0, 1), (1, 0), (0, -1))
    if clockwise and start[0] == 0 and start[1] != 0:
        # Top left corner
        return ((1, 0), (0, -1), (-1, 0), (0, 1))
    if clockwise and start[0] != 0 and start[1] != 0:
        # Top right corner
        return ((0, -1), (-1, 0), (0, 1), (1, 0))
    if not clockwise and start[0] == 0 and start[1] == 0:
        # Bottom left corner
        return ((1, 0), (0, 1), (-1, 0), (0, -1))
    if not clockwise and start[0] != 0 and start[1] == 0:
        # Bottom right corner
        return ((0, 1), (-1, 0), (0, -1), (1, 0))
    if not clockwise and start[0] == 0 and start[1] != 0:
        # Top left corner
        return ((0, -1), (1, 0), (0, 1), (-1, 0))
    if not clockwise and start[0] != 0 and start[1] != 0:
        # Top right corner
        return ((-1, 0), (0, -1), (1, 0), (0, 1))


@functools.lru_cache(1)
def _spiral(
    size: tuple[int, int], start: tuple[int, int], clockwise: bool, inward: bool
) -> dict[int, tuple[int, int]]:
    """Return the mapping from integer to coordinates, in a spiral.

    Suppose `size == (4, 3)`. This function builds the following spiral:

    +--+--+--+--+
    | 0| 1| 2| 3|
    +--+--+--+--+
    | 9|10|11| 4|
    +--+--+--+--+
    | 8| 7| 6| 5|
    +--+--+--+--+

    And then returns the dictionary mapping integers (those you can see in the spiral)
    to coordinates (the coordinate of the number in the spiral).
    For instance, if the return value is stored in `spiral`, then:
    `spiral[10] == (2, 1)` and `spiral[1] == (1, 2)`.

    :param size: Size of the grid, as `(width, height)`.
    :param start: Initial (or last, with `inward=False`) position of the spiral, as coordinates.
        If those coordinates are not a corner of the grid, the result is undefined.
    :param clockwise: If `True`, rotate clockwise. Otherwise, rotate anticlockwise.
    :param inward: If `True`, the spiral starts from a corner, and spirals to the conter.
        If `False`, the spirals goes the opposite.
        In this latter case, the `start` argument is the *end* of the spiral, not the start.
    """
    # Build grid
    if not inward:
        clockwise = not clockwise
    grid = [[None] * size[1] for _ in range(size[0])]
    rotation = itertools.cycle(_rotation(start, clockwise))
    direction = next(rotation)
    for i in range(size[0] * size[1]):
        grid[start[0]][start[1]] = i
        try:
            if grid[start[0] + direction[0]][start[1] + direction[1]] is not None:
                direction = next(rotation)
        except IndexError:
            direction = next(rotation)
        start = (
            start[0] + direction[0],
            start[1] + direction[1],
        )
        # Debug: uncomment to display the grid
        # print("\n".join(str(line) for line in zip(*grid)))

    # Grid is built. Return a reverse-map of it:
    # given a number, we get its position in the grid.
    # pylint: disable=no-else-return
    if inward:
        return {
            grid[x][y]: (x, y) for x in range(len(grid)) for y in range(len(grid[x]))
        }
    else:
        # We "reverse" the grid numbers
        return {
            size[0] * size[1] - grid[x][y] - 1: (x, y)
            for x in range(len(grid))
            for y in range(len(grid[x]))
        }


def Stlci(n, width, height):
    """Spiral, starting from the top left corner, clockwise, inward."""
    return _spiral((width, height), (0, height - 1), clockwise=True, inward=True)[n]


def Stlco(n, width, height):
    """Spiral, starting from the top left corner, clockwise, outward."""
    return _spiral((width, height), (0, height - 1), clockwise=False, inward=False)[n]


def Sblai(n, width, height):
    """Spiral, starting from the bottom left corner, anticlockwise, inward."""
    return _spiral((width, height), (0, 0), clockwise=False, inward=True)[n]


def Sblao(n, width, height):
    """Spiral, starting from the bottom left corner, anticlockwise, outward."""
    return _spiral((width, height), (0, 0), clockwise=False, inward=False)[n]


def Sblci(n, width, height):
    """Spiral, starting from the bottom left corner, clockwise, inward."""
    return _spiral((width, height), (0, 0), clockwise=True, inward=True)[n]


def Sblco(n, width, height):
    """Spiral, starting from the bottom left corner, clockwise, outward."""
    return _spiral((width, height), (0, 0), clockwise=True, inward=False)[n]


def Sbrai(n, width, height):
    """Spiral, starting from the bottom right corner, anticlockwise, inward."""
    return _spiral((width, height), (width - 1, 0), clockwise=False, inward=True)[n]


def Sbrao(n, width, height):
    """Spiral, starting from the bottom right corner, anticlockwise, outward."""
    return _spiral((width, height), (width - 1, 0), clockwise=False, inward=False)[n]


def Sbrci(n, width, height):
    """Spiral, starting from the bottom right corner, clockwise, inward."""
    return _spiral((width, height), (width - 1, 0), clockwise=True, inward=True)[n]


def Sbrco(n, width, height):
    """Spiral, starting from the bottom right corner, clockwise, outward."""
    return _spiral((width, height), (width - 1, 0), clockwise=True, inward=False)[n]


def Stlai(n, width, height):
    """Spiral, starting from the top left corner, anticlockwise, inward."""
    return _spiral((width, height), (0, height - 1), clockwise=False, inward=True)[n]


def Stlao(n, width, height):
    """Spiral, starting from the top left corner, anticlockwise, outward."""
    return _spiral((width, height), (0, height - 1), clockwise=False, inward=False)[n]


def Strai(n, width, height):
    """Spiral, starting from the top right corner, anticlockwise, inward."""
    return _spiral(
        (width, height), (width - 1, height - 1), clockwise=False, inward=True
    )[n]


def Strao(n, width, height):
    """Spiral, starting from the top right corner, anticlockwise, outward."""
    return _spiral(
        (width, height), (width - 1, height - 1), clockwise=False, inward=False
    )[n]


def Strci(n, width, height):
    """Spiral, starting from the top right corner, clockwise, inward."""
    return _spiral(
        (width, height), (width - 1, height - 1), clockwise=True, inward=True
    )[n]


def Strco(n, width, height):
    """Spiral, starting from the top right corner, clockwise, outward."""
    return _spiral(
        (width, height), (width - 1, height - 1), clockwise=True, inward=False
    )[n]


# Random (easter egg)

SEED = _random.random()


@functools.lru_cache(1)
def _shuffle(size):
    """Return a randomly shuffled list of integers from 0 to `size-1`.

    This function is cached and deterministic.
    """
    _random.seed(f"{SEED} {size}")
    return _random.sample(range(size), k=size)


def random(n, width, height):
    """Random writing order"""
    # This cannot be totally random, otherwise some pages will overlap.
    return latin(_shuffle(width * height)[n], width, height)


ORDERS = [
    function.__name__
    for function in (
        # Officially supported
        btlr,
        btrl,
        lrbt,
        lrtb,
        rlbt,
        rltb,
        tblr,
        tbrl,
    )
]

ORDERS_ALIASES = [
    function.__name__
    for function in (
        # Aliases
        arabic,
        japanese,
        latin,
    )
]

ORDERS_EXPERIMENTAL = [
    function.__name__
    for function in (
        ## Boustrophédon (experimental)
        Bblr,
        Bblt,
        Bbrl,
        Bbrt,
        Btlb,
        Btlr,
        Btrb,
        Btrl,
        ## Spiral (experimental)
        Sblai,
        Sblao,
        Sblci,
        Sblco,
        Sbrai,
        Sbrao,
        Sbrci,
        Sbrco,
        Stlai,
        Stlao,
        Stlci,
        Stlco,
        Strai,
        Strao,
        Strci,
        Strco,
    )
]

ORDERS_EASTEREGGS = [function.__name__ for function in (random,)]

ORDERS_PUBLIC = ORDERS_ALIASES + ORDERS + ORDERS_EXPERIMENTAL
ORDERS_ALL = ORDERS_PUBLIC + ORDERS_EASTEREGGS
