#!/usr/bin/env python


# We'll start with the notion of a vertex

# Because we're moving vertices around, their coordinates are inherently
# mutable but we need references to them to build up larger structures so we're
# going to use a class containing a 3-tuple for the current coordinates

class Vertex:
    def __init__(self, x, y, z):
        self.coordinates = (x, y, z)

v1 = Vertex(0, 0, 0)
v2 = Vertex(0, 1, 0)


# Next we define a distance function that takes two vertices

from math import sqrt


def distance(vertex_1, vertex_2):
    return sqrt(sum(
        (c1 - c2) ** 2
        for c1, c2 in zip(vertex_1.coordinates, vertex_2.coordinates)
    ))


assert distance(v1, v2) == 1.0


# An Edge pairs two vertices together

class Edge:
    def __init__(self, vertex_1, vertex_2):
        self.vertex_1 = vertex_1
        self.vertex_2 = vertex_2

    def length(self):
        return distance(self.vertex_1, self.vertex_2)


e1 = Edge(v1, v2)

assert e1.length() == 1.0


# We want a test for collinearity. To do that, we'll first introduce the notion
# of a 3-tuple "vector", the conversion to spherical coordinates to get a
# "direction" and then finally a test for whether two vectors are
# parallel or not

def vector(vertex_1, vertex_2):
    return tuple(
        c1 - c2
        for c1, c2 in zip(vertex_1.coordinates, vertex_2.coordinates)
    )

# (we could rewrite "distance" above to use this)

from math import acos, atan2


def direction(vector):
    r = sqrt(sum(c ** 2 for c in vector))
    theta = acos(vector[2] / r)
    phi = atan2(vector[1], vector[0])
    return (theta, phi)


assert vector(v1, v2) == (0, -1, 0)
assert direction(vector(v1, v2)) == (1.5707963267948966, -1.5707963267948966)


def collinear(*vertices):
    d = direction(vector(vertices[0], vertices[1]))
    for vertex in vertices[2:]:
        if direction(vector(vertices[0], vertex)) != d:
            return False
    return True


v3 = Vertex(0, 2, 0)
v4 = Vertex(1, 0, 0)

assert collinear(v1, v2, v3) is True
assert collinear(v1, v2, v4) is False


# Before moving on to coplanarity, I'm going to take a moment to try
# reimplementing the collinearity test using Cayley-Menger determinants

# simplifying the 4x4 Cayley-Menger determinant for determining whether three
# elements are "straight", we get:
#
# det = AB^2 - 2 AB AC + AC^2 - 2 AB BC - 2 AC BC + BC^2
#
# where AB, AC and BC are the square of the distance

def straight(A, B, C):
    AB = distance(A, B) ** 2
    AC = distance(A, C) ** 2
    BC = distance(B, C) ** 2

    cayley_menger_det = (
        AB * AB + AC * AC + BC * BC
        - 2 * AB * AC - 2 * AB * BC - 2 * AC * BC
    )

    return cayley_menger_det == 0


assert straight(v1, v2, v3) is True
assert straight(v1, v2, v4) is False


# The simplification of the 5x5 is:
#
# det = -2 AB AC BC + 2 AB AD BC + 2 AC AD BC - 2 AD^2 BC - 2 AD BC^2 +
# 2 AB AC BD - 2 AC^2 BD - 2 AB AD BD + 2 AC AD BD + 2 AC BC BD +
# 2 AD BC BD - 2 AC BD^2 - 2 AB^2 CD + 2 AB AC CD + 2 AB AD CD -
# 2 AC AD CD + 2 AB BC CD + 2 AD BC CD + 2 AB BD CD + 2 AC BD CD -
# 2 BC BD CD - 2 AB CD^2

# where AB, AC, AD, BC, BD, CD are squares of the distance

# Because taking a square root in distance() then squaring again can lead to
# imprecision which results in a determinant very close but not quite zero
# when it really should be zero, we'll use a square_distance function:


def square_distance(vertex_1, vertex_2):
    return sum(
        (c1 - c2) ** 2
        for c1, c2 in zip(vertex_1.coordinates, vertex_2.coordinates)
    )


def plane(A, B, C, D):
    AB = square_distance(A, B)
    AC = square_distance(A, C)
    AD = square_distance(A, D)
    BC = square_distance(B, C)
    BD = square_distance(B, D)
    CD = square_distance(C, D)

    cayley_menger_det = (
        - 2 * AB * AB * CD
        - 2 * AB * AC * BC
        + 2 * AB * AC * BD
        + 2 * AB * AC * CD
        + 2 * AB * AD * BC
        - 2 * AB * AD * BD
        + 2 * AB * AD * CD
        + 2 * AB * BC * CD
        + 2 * AB * BD * CD
        - 2 * AB * CD * CD

        - 2 * AC * AC * BD
        + 2 * AC * AD * BC
        + 2 * AC * AD * BD
        - 2 * AC * AD * CD
        + 2 * AC * BC * BD
        - 2 * AC * BD * BD
        + 2 * AC * BD * CD

        - 2 * AD * AD * BC
        - 2 * AD * BC * BC
        + 2 * AD * BC * BD
        + 2 * AD * BC * CD

        - 2 * BC * BD * CD
    )
    return cayley_menger_det == 0


assert plane(v1, v2, v3, v4) is True


# We can now rewrite straight as:

def better_straight(A, B, C):
    AB = square_distance(A, B)
    AC = square_distance(A, C)
    BC = square_distance(B, C)

    cayley_menger_det = (
        AB * AB + AC * AC + BC * BC
        - 2 * AB * AC - 2 * AB * BC - 2 * AC * BC
    )

    return cayley_menger_det == 0


# to model folding, we're going to want to know the foot of an altitude from
# a Vertex to an Edge.

def foot_of_altitude(vertex, edge):
    x1, y1, z1 = edge.vertex_1.coordinates
    x2, y2, z2 = edge.vertex_2.coordinates
    x3, y3, z3 = vertex.coordinates

    t = 1. * (
        (x1 - x2) * (x1 - x3) + (y1 - y2) * (y1 - y3) + (z1 - z2) * (z1 - z3)
    ) / ((x1 - x2) ** 2 + (y1 - y2) ** 2 + (z1 - z2) ** 2)

    return Vertex(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t, z1 + (z2 - z1) * t)


assert foot_of_altitude(
    Vertex(1, 2, 0), Edge(Vertex(0, 0, 0,), Vertex(2, 0, 0))
).coordinates == (1, 0, 0)


# to keep things rational, going to rewrite that using fractions.Fraction

from fractions import Fraction


def rational_foot_of_altitude(vertex, edge):
    x1, y1, z1 = edge.vertex_1.coordinates
    x2, y2, z2 = edge.vertex_2.coordinates
    x3, y3, z3 = vertex.coordinates

    t = Fraction(
        (x1 - x2) * (x1 - x3) + (y1 - y2) * (y1 - y3) + (z1 - z2) * (z1 - z3),
        (x1 - x2) ** 2 + (y1 - y2) ** 2 + (z1 - z2) ** 2
    )

    return Vertex(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t, z1 + (z2 - z1) * t)


assert rational_foot_of_altitude(
    Vertex(1, 2, 0), Edge(Vertex(0, 0, 0,), Vertex(2, 0, 0))
).coordinates == (1, 0, 0)


def norm_vector(edge):
    l = Fraction(edge.length())
    return tuple(
        Fraction(c1 - c2, l)
        for c1, c2 in zip(edge.vertex_1.coordinates, edge.vertex_2.coordinates)
    )


# if we have a point C and the line AB and we calculate the foot of the
# altitude from C to AB to be D, then what we now want to do is define a circle
# axis AB, centre D, radius |CD|

class Circle:
    def __init__(self, axis, altitude):
        assert altitude.vertex_1.coordinates == axis.vertex_1.coordinates
        self.center = axis.vertex_1
        self.axis = axis
        self.altitude = altitude

    def parameterization(self, m):
        # the rational parametrization of a unit circle at the origin in 2D is:
        # [(1-m^2)/(1+m^2), 2m/(1+m^2)]
        radius = Fraction(self.altitude.length())
        local_x = radius * Fraction(1 - m ** 2, 1 + m ** 2)
        local_y = radius * Fraction(2 * m, 1 + m ** 2)

        u_x, u_y, u_z = norm_vector(self.altitude)
        w_x, w_y, w_z = norm_vector(self.axis)

        v_x = w_y * u_z - w_z * u_y
        v_y = w_z * u_x - w_x * u_z
        v_z = w_x * u_y - w_y * u_x

        c_x, c_y, c_z = self.center.coordinates

        # now we need to transform that with c + xru + yrv
        global_x = c_x + local_x * u_x + local_y * v_x
        global_y = c_y + local_x * u_y + local_y * v_y
        global_z = c_z + local_x * u_z + local_y * v_z

        return Vertex(global_x, global_y, global_z)


axis = Edge(Vertex(1, 0, 0), Vertex(2, 0, 0))
altitude = Edge(Vertex(1, 0, 0), Vertex(1, 2, 0))

circle = Circle(axis, altitude)

for m in [0, Fraction(1, 10), Fraction(1, 2), 1]:
    print m,
    print circle.parameterization(m).coordinates
