import math


def TranslateMatrix(pt):
    M = [[0] * 4 for i in range(4)]
    for i in range(4):
        M[i][i] = 1.0
    for i in range(3):
        M[3][i] = pt[i]
    return M


def ProjectionMatrix(near=10, far=1000.0, fov_h=1.7, fov_v=1.4):
    """Setup projection matrix with given distance to near and far planes
    and fields of view in radians"""
    # Matrices are considered to be M[row][col]
    # Use DirectX convention, so need to do rowvec*Matrix to transform
    w = 1. / math.tan(fov_h * 0.5)
    h = 1. / math.tan(fov_v * 0.5)
    Q = far / (far - near)
    M = [[0] * 4 for i in range(4)]
    M[0][0] = w
    M[1][1] = h
    M[2][2] = Q
    M[3][2] = -Q * near
    M[2][3] = 1
    return M


def vec_sub(A, B):
    return [a - b for a, b in zip(A, B)]


def vec_dot(A, B):
    return sum(a * b for a, b in zip(A, B))


def vec_cross(a, b):
    return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]


def vec_normal(A):
    n = math.sqrt(sum(a ** 2 for a in A)) + 0.0001
    return [a / n for a in A]


def LookAtMatrix(at, eye, up=[0, 0, 1], reflect=False):
    """Define a matrix of an eye looking at"""
    # If reflect, then reflect in plane -20.0 (water depth)
    if reflect:
        depth = -20.0  # Shallower to avoid edge effects
        eye[2] = 2 * depth - eye[2]
        at[2] = 2 * depth - at[2]
    zaxis = vec_normal(vec_sub(at, eye))
    xaxis = vec_normal(vec_cross(up, zaxis))
    yaxis = vec_cross(zaxis, xaxis)
    xaxis.append(-vec_dot(xaxis, eye))
    yaxis.append(-vec_dot(yaxis, eye))
    zaxis.append(-vec_dot(zaxis, eye))
    z = [0, 0, 0, 1.0]
    return [[xaxis[a], yaxis[a], zaxis[a], z[a]] for a in range(4)]


def BillboardMatrix():
    """Define a matrix that copies x,y and sets z to 0.9"""
    return [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.9, 1.0]]


def mat_mult(A, B):
    return [[sum(A[i][j] * B[j][k] for j in range(4)) for k in range(4)] for i in range(4)]


def mat_transpose(A):
    return [[A[k][i] for k in range(4)] for i in range(4)]


def vec_mat_mult(A, B):
    return [sum(A[j] * B[j][k] for j in range(4)) for k in range(4)]


class View(object):
    """The view holds the perspective transformations for the current view.
    Call lookAt to set the camera.
    Call begin_matrix to start a new view based on this perspective, then translate or rotate to set up transform.
    Can use view.V to access the matrix representing the current transform"""

    def lookAt(self, at, eye):
        """Set up view matrix to look from eye to at including perspective"""
        self.L = LookAtMatrix(at, eye)
        self.P = ProjectionMatrix()
        self.M = mat_mult(self.L, self.P)  # Apply transform/rotation first, then shift into perspective space
        self.L_reflect = LookAtMatrix(at, eye, reflect=True)
        self.M_reflect = mat_mult(self.L_reflect, self.P)

    def begin_matrix(self):
        self.V = [row[:] for row in self.M]

    def translate(self, pt):
        """Move an object to the given location"""
        V = self.V
        V[3] = [sum(pt[j] * V[j][i] for j in xrange(3)) + V[3][i] for i in xrange(4)]

    def rotate(self, angle):
        """Rotate an object by an angle in degrees"""
        c = math.cos(angle * 3.1415 / 180.0)
        s = math.sin(angle * 3.1415 / 180.0)
        M = [[c, s, 0, 0], [-s, c, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
        self.V = mat_mult(M, self.V)
