# miscellaneous functions
# Copyright (C) 2019  Nguyễn Gia Phong
#
# This file is part of Axuy
#
# Axuy 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.
#
# Axuy 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.
#
# You should have received a copy of the GNU Affero General Public License
# along with Axuy.  If not, see <https://www.gnu.org/licenses/>.

__doc__ = 'Axuy miscellaneous functions'
__all__ = ['abspath', 'color', 'twelve', 'nine', 'indexify', 'mapidgen',
           'mapgen', 'neighbors', 'mirror', 'normalized', 'rot33', 'placeable']

from itertools import (chain, combinations_with_replacement,
                       permutations, product)
from random import choices, shuffle
from typing import Iterator, List, Tuple

import numpy
from numpy.linalg import norm
from pkg_resources import resource_filename
from pyrr import matrix33

OXY = numpy.float32([[0, 0, 0], [1, 0, 0], [1, 1, 0],
                     [1, 1, 0], [0, 1, 0], [0, 0, 0]])
OYZ = numpy.float32([[0, 0, 0], [0, 1, 0], [0, 1, 1],
                     [0, 1, 1], [0, 0, 1], [0, 0, 0]])
OZX = numpy.float32([[0, 0, 0], [1, 0, 0], [1, 0, 1],
                     [1, 0, 1], [0, 0, 1], [0, 0, 0]])
AXIS = numpy.float32([0, -1, 0])

NEIGHBORS = set(chain.from_iterable(
    map(permutations, combinations_with_replacement((-1, 0, 1), 3))))
# map.npy is generated by ../tools/mapgen
SPACE = numpy.load(resource_filename('axuy', 'map.npy'))
COLORS = tuple(map(numpy.float32, permutations((1.0, 0.5, 0.0))))


def abspath(resource_name) -> str:
    """Return a true filesystem path for the specified resource."""
    return resource_filename('axuy', resource_name)


def color(code, value) -> numpy.float32:
    """Return NumPy float32 array of RGB colors from color name."""
    return COLORS[code] * (value + 1) * 0.5


def twelve(x) -> int:
    """Shorthand for int(x % 12)."""
    return int(x % 12)


def nine(x) -> int:
    """Shorthand for int(x % 9)."""
    return int(x % 9)


def indexify(iterable) -> Tuple[int, int, int]:
    """Return a tuple of int to be used as indices of the map's space."""
    x, y, z = iterable
    return twelve(x), twelve(y), nine(y)


def mapidgen(replacement=False) -> List[int]:
    """Return a randomly generated map ID."""
    mapid = list(range(48))
    if replacement: return choices(mapid, k=48)
    shuffle(mapid)
    return mapid


def mapgen(mapid):
    """Return the NumPy array of shape (12, 12, 9) of bools
    generated from the given ID.
    """
    base = numpy.stack([SPACE[i] for i in mapid]).reshape(4, 4, 3, 3, 3, 3)
    space = numpy.zeros([12, 12, 9], dtype=bool)
    for (i, j, k, x, y, z), occupied in numpy.ndenumerate(base):
        if occupied: space[i*3 + x][j*3 + y][k*3 + z] = 1
    return space


def neighbors(x, y, z) -> Iterator[Tuple[float, float, float]]:
    """Return a generator of coordinates of images point (x, y, z)
    in neighbor universes.
    """
    for i, j, k in NEIGHBORS: yield x + i*12, y + j*12, z + k*9


def mirror(space) -> numpy.float32:
    """Return the vertices to render the mirrored space."""
    vertices = []
    for (x, y, z), occupied in numpy.ndenumerate(space):
        if space[x][y][z-1] ^ occupied:
            vertices.extend(i+j for i, j in product(neighbors(x, y, z), OXY))
        if space[x-1][y][z] ^ occupied:
            vertices.extend(i+j for i, j in product(neighbors(x, y, z), OYZ))
        if space[x][y-1][z] ^ occupied:
            vertices.extend(i+j for i, j in product(neighbors(x, y, z), OZX))
    return numpy.stack(vertices).astype(numpy.float32)


def normalized(*vector) -> numpy.float32:
    """Return normalized vector as a NumPy array of float32."""
    v = numpy.float32(vector)
    if not v.any(): return v
    return v / norm(v)


def placeable(space, x, y, z, r=0) -> bool:
    """Return whether a sphere of radius r
    can be placed at (x, y, z) in given space.
    """
    return not any(space[i][j][k] for i, j, k in product(
        {twelve(x-r), twelve(x), twelve(x+r)},
        {twelve(y-r), twelve(y), twelve(y+r)},
        {nine(z-r), nine(z), nine(z+r)}))


def rot33(magnitude, direction) -> numpy.float32:
    """Return the 3x3 matrix of float32 which rotates
    by the given magnitude and direction.
    """
    return matrix33.create_from_axis_rotation(
        AXIS.dot(matrix33.create_from_z_rotation(direction)),
        magnitude,
        dtype=numpy.float32)
