#! /usr/bin/env python3
# -*- coding: utf-8 -*-

# from https://robertvandeneynde.be/

from math import sin, cos, degrees, radians, tan

import numpy
import numpy as np
from numpy import array, matrix, linalg
from numpy import float32
farray = float32

def fvec(*args):
    return farray(args)

def sind(x):
    return sin(radians(x))

def cosd(x):
    return cos(radians(x))

def tand(x):
    return tan(radians(x))

def polar(*args):
    """
    polar(pi/6) -> (cos(pi/6), sin(pi/6)) = vec2(0.86602539, 0.5)
    polar(5, pi/6) -> (5 * cos(pi/6), 5 * sin(pi/6)) = vec2(4.33012676, 2.5)
    """
    if len(args) == 2:
        r,t = args
    elif len(args) == 1:
        r,t = 1, args[0]
    else:
        raise TypeError('Accept 1 or 2 arguments')
    
    return r * vec2(cos(t), sin(t))

def polard(*args):
    """
    polard(30) -> (cosd(30), sind(30)) = vec2(0.86602539, 0.5)
    polard(5, 30) -> (5 * cosd(30), 5 * sind(30)) = vec2(4.33012676, 2.5)
    """
    if len(args) == 2:
        r,t = args
    elif len(args) == 1:
        r,t = 1, args[0]
    else:
        raise TypeError('Accept 1 or 2 arguments')
    
    return r * polar(radians(t))

NAME_TO_INT = {
    'x': 0,
    'y': 1,
    'z': 2,
    'w': 3,
    'r': 0,
    'g': 1,
    'b': 2,
    'a': 3,
}

def readvec(V, names):
    """
    Returns numpy.array from values of vector V depending on arguments:
    readvec((1,2,3), 'xy') -> farray((1,2))
    readvec((1,2,3), 'rg') -> farray((1,2))
    readvec((1,2,3), 'zx') -> farray((3,1))
    readvec((1,2,3), 'zz') -> farray((3,3))
    
    If the funcoperators lib is installed, I'd suggest defining functions like xy
    (1,2,3)|xy -> farray((1,2))
    
    Given:
    xy = postfix(lambda vec: readvec(vec, 'xy'))
    """
    return farray([
        V[NAME_TO_INT[x]]
        for i,x in enumerate(names)])

def vec_from_kwargs(size, **kwargs):
    V = farray((0,) * size)
    for x,v in list(kwargs.items()):
        if len(x) == 1:
            V[NAME_TO_INT[x]] = v
        else:
            it = iter(v)
            for c in x:
                V[NAME_TO_INT[c]] = next(it)
    return V

def vec_from_args(size, *args):
    if len(args) == 0:
        return farray((0,) * size)
    if len(args) == 1 and not hasattr(args[0], '__iter__'):
        return farray((args[0],) * size)
    L = []
    for a in args:
        if hasattr(a, '__iter__'):
            L.extend(a)
        else:
            L.append(a)
    if not len(L) == size:
        raise TypeError('Accept {} arguments'.format(size))
    return farray(L)

def vec2(*args, **kwargs):
    """
    vec2(1,2) -> farray((1.0, 2.0))
    vec2(5) -> farray((5.0, 5.0))
    vec2(y=6, x=1) -> farray((1.0, 6.0))
    """
    if args and kwargs:
        raise TypeError('vec2 accept either args or kwargs, not both')
    elif kwargs:
        return vec_from_kwargs(2, **kwargs)
    else:
        return vec_from_args(2, *args)

def vec3(*args, **kwargs):
    """
    Returns numpy.array depending on arguments:
    vec3() -> (0,0,0)
    vec3(5) -> (5,5,5)
    vec3(1,2,3) -> (1,2,3)
    vec3((1,2,3)) -> (1,2,3)
    vec3((1,2),3) -> (1,2,3)
    vec3(1,(2,3)) -> (1,2,3)
    vec3(x=1,y=2,z=3) -> (1,2,3)
    vec3(r=1,g=2,b=3) -> (1,2,3)
    vec3(xy=(1,2),z=3) -> (1,2,3)
    vec3(x=1,yz=(2,3)) -> (1,2,3)
    vec3(y=2,xz=(1,3)) -> (1,2,3)
    vec3(xyz=(1,2,3)) -> (1,2,3)
    """
    if args and kwargs:
        raise TypeError('vec3 accept either args or kwargs, not both')
    elif kwargs:
        return vec_from_kwargs(3, **kwargs)
    else:
        return vec_from_args(3, *args)

def vec4(*args, **kwargs):
    """
    vec4(5) -> (5,5,5,5)
    vec4(1,2,3,4) -> (1,2,3,4)
    vec4(xy=(1,2), z=3, w=4) -> (1,2,3,4)
    vec4((1,2,3), 4) -> (1,2,3,4)
    ...
    """
    if args and kwargs:
        raise TypeError('vec3 accept either args or kwargs, not both')
    elif kwargs:
        return vec_from_kwargs(4, **kwargs)
    else:
        return vec_from_args(4, *args)

def normalized(v):
    return v / linalg.norm(v)

def PerspectiveMatrix(fovy, aspect, zNear, zFar):
    f = 1.0 / tan(radians(fovy) / 2.0)
    return farray([
            [f/aspect, 0, 0, 0],
            [0, f, 0, 0],
            [0, 0, 1.0 * (zFar + zNear) / (zNear - zFar), 2.0 * zFar * zNear / (zNear - zFar)],
            [0,0,-1,0]
        ])

def TranslationMatrix(*args):
    """
    TranslationMatrix(x, y, z)
    TranslationMatrix((x, y, z))
    TranslationMatrix(x, y) # z = 0
    """
    if len(args) == 3:
        tx,ty,tz = args
    elif len(args) == 2:
        (tx,ty),tz = args, 0
    elif len(args) == 1:
        tx,ty,tz = args[0]
    else:
        raise TypeError("Accept 1, 2 or 3 arguments")
        
    return farray([
            [1, 0, 0, tx],
            [0, 1, 0, ty],
            [0, 0, 1, tz],
            [0, 0, 0, 1]
        ])

def LookAtMatrix(*args):
    """
    LookAtMatrix((camera_x, camera_y, camera_z), (target_x, target_y, target_z), (up_x, up_y, up_z))
    LookAtMatrix(camera_x, camera_y, camera_z, target_x, target_y, target_z, up_x, up_y, up_z)
    """
    if len(args) == 3:
        e,c,ur = args
    elif len(args) == 9:
        e,c,ur = args[:3], args[3:6], args[6:]
    else:
        raise TypeError("Accept 3 or 9 arguments")
    e,c,ur = array(e), array(c), array(ur)
    
    U = normalized(ur)
    f = normalized(c - e)
    s = numpy.cross(f, U)
    u = numpy.cross(normalized(s), f)
    
    return farray([
        [ s[0],  s[1],  s[2], 0],
        [ u[0],  u[1],  u[2], 0],
        [-f[0], -f[1], -f[2], 0],
        [    0,     0,     0, 1],
    ]).dot(
        TranslationMatrix(-e)
    )

class Axis:
    X = 0
    Y = 1
    Z = 2

def AxisRotationMatrix(angle, axis=Axis.Z):
    if angle % 90 == 0:
        x = angle % 360
        c = 1 if x == 0 else -1 if x == 180 else 0
        s = 1 if x == 90 else -1 if x == 270 else 0
    else:
        t = radians(angle)
        c = cos(t)
        s = sin(t)
    
    return farray([
        [c, s, 0, 0],
        [-s, c, 0, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 1]
    ] if axis == 2 else [
        [c, 0, s, 0],
        [0, 1, 0, 0],
        [-s, 0, c, 0],
        [0, 0, 0, 1]
    ] if axis == 1 else [
        [1, 0, 0, 0],
        [0, c, -s, 0],
        [0, s, c, 0],
        [0, 0, 0, 1]
    ])


def RotationMatrix(angle, axe):
    """Rotation matrix for angle in degree around any axe
    RotationMatrix(30, (0,0,1))
    """
    x, y, z = normalized(axe)

    if angle % 90 == 0:
        a = angle % 360
        c = 1 if a == 0 else -1 if a == 180 else 0
        s = 1 if a == 90 else -1 if a == 270 else 0
    else:
        t = radians(angle)
        c = cos(t)
        s = sin(t)

    k = 1 - c

    # Rodriguez rotation formula
    return farray([
        [x * x * k + c, x * y * k - z * s, x * z * k + y * s, 0],
        [y * x * k + z * s, y * y * k + c, y * z * k - x * s, 0],
        [x * z * k - y * s, y * z * k + x * s, z * z * k + c, 0],
        [0, 0, 0, 1]
    ])


def ScaleMatrix(kx, ky=None, kz=None):
    """ScaleMatrix
    ScaleMatrix(2) = ScaleMatrix(2,2,2)
    ScaleMatrix(1,2,3)
    """
    if ky is None:
        ky = kx
    if kz is None:
        kz = kx
    return farray([
        [kx, 0, 0, 0],
        [0, ky, 0, 0],
        [0, 0, kz, 0],
        [0, 0, 0, 1]
    ])

def IdentityMatrix():
    """IdentityMatrix
    IdentityMatrix()
    """
    return farray([
        [1, 0, 0, 0],
        [0, 1, 0, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 1]
    ])
