# Miscellaneous functions
# Copyright (C) 2019, 2020  Nguyễn Gia Phong
# Copyright (C) 2021  Ngô Ngọc Đức Huy
# Copyright (C) 2021  Nguyễn Trường Giang
#
# 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__ = ['color', 'twelve', 'nine', 'indexify', 'load_model', 'mapidgen',
           'mapgen', 'neighbors', 'mirror', 'normalized', 'rot33', 'placeable']

from functools import reduce
from importlib.resources import open_binary, path
from itertools import (chain, combinations_with_replacement,
                       permutations, product)
from logging import CRITICAL, getLogger
from operator import add, attrgetter
from random import choices, shuffle
from typing import Iterator, List, Tuple

import numpy
from numpy.linalg import norm
from pyrr import matrix33
from pywavefront import Wavefront

getLogger('pywavefront').setLevel(CRITICAL)

OXY_FRONT = numpy.float32([[0, 0, 0], [1, 0, 0], [1, 1, 0],
                           [1, 1, 0], [0, 1, 0], [0, 0, 0]])
OYZ_FRONT = numpy.float32([[0, 0, 0], [0, 1, 0], [0, 1, 1],
                           [0, 1, 1], [0, 0, 1], [0, 0, 0]])
OZX_FRONT = numpy.float32([[0, 0, 0], [1, 0, 1], [1, 0, 0],
                          [1, 0, 1], [0, 0, 0], [0, 0, 1]])
OXY_BACK = numpy.float32([[0, 0, 0], [1, 1, 0], [1, 0, 0],
                          [1, 1, 0], [0, 0, 0], [0, 1, 0]])
OYZ_BACK = numpy.float32([[0, 0, 0], [0, 1, 1], [0, 1, 0],
                          [0, 1, 1], [0, 0, 0], [0, 0, 1]])
OZX_BACK = 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(open_binary('axuy.assets', 'map.npy'))
COLORS = tuple(map(numpy.float32, permutations((1.0, 0.5, 0.0))))


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:
            face = OXY_BACK if occupied else OXY_FRONT
            vertices.extend(i+j for i, j in product(neighbors(x, y, z), face))
        if space[x-1][y][z] ^ occupied:
            face = OYZ_BACK if occupied else OYZ_FRONT
            vertices.extend(i+j for i, j in product(neighbors(x, y, z), face))
        if space[x][y-1][z] ^ occupied:
            face = OZX_BACK if occupied else OZX_FRONT
            vertices.extend(i+j for i, j in product(neighbors(x, y, z), face))
    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)


def load_model(filename: str) -> Tuple[numpy.float32, numpy.int32]:
    """Return the model's vertex and index array."""
    with path('axuy.assets', filename) as obj:
        model = Wavefront(obj, collect_faces=True)
    vertices = numpy.float32(model.vertices)
    vertices = vertices - numpy.mean(vertices, 0)
    indices = numpy.int32(reduce(
        add, map(attrgetter('faces'), model.mesh_list)))
    return vertices / max(map(norm, vertices)), indices
