# -*- coding: utf-8 -*-
"""
$Id$

Copyright 2008-2009 Lode Leroy
Copyright 2010 Lars Kruse <devel@sumpfralle.de>

This file is part of PyCAM.

PyCAM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

PyCAM 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with PyCAM.  If not, see <http://www.gnu.org/licenses/>.
"""

from pycam.Geometry import TransformableContainer
from pycam.Geometry.Point import Point
from pycam.Geometry.Plane import Plane
from pycam.Geometry.utils import epsilon, sqrt
import math


try:
    import OpenGL.GL as GL
    import OpenGL.GLUT as GLUT
    GL_enabled = True
except ImportError:
    GL_enabled = False


class Line(TransformableContainer):
    id = 0

    def __init__(self, p1, p2):
        self.id = Line.id
        Line.id += 1
        self.p1 = p1
        self.p2 = p2
        self.reset_cache()

    @property
    def vector(self):
        if self._vector is None:
            self._vector = self.p2.sub(self.p1)
        return self._vector

    @property
    def dir(self):
        return self.vector.normalized()

    @property
    def len(self):
        return self.vector.norm

    @property
    def minx(self):
        if self._minx is None:
            self._minx = min(self.p1.x, self.p2.x)
        return self._minx

    @property
    def maxx(self):
        if self._maxx is None:
            self._maxx = max(self.p1.x, self.p2.x)
        return self._maxx

    @property
    def miny(self):
        if self._miny is None:
            self._miny = min(self.p1.y, self.p2.y)
        return self._miny

    @property
    def maxy(self):
        if self._maxy is None:
            self._maxy = max(self.p1.y, self.p2.y)
        return self._maxy

    @property
    def minz(self):
        if self._minz is None:
            self._minz = min(self.p1.z, self.p2.z)
        return self._minz

    @property
    def maxz(self):
        if self._maxz is None:
            self._maxz = max(self.p1.z, self.p2.z)
        return self._maxz

    def __repr__(self):
        return "Line<%g,%g,%g>-<%g,%g,%g>" % (self.p1.x, self.p1.y, self.p1.z,
                self.p2.x, self.p2.y, self.p2.z)

    def __cmp__(self, other):
        """ Two lines are equal if both pairs of points are at the same
        locations.
        Otherwise the result is based on the comparison of the first and then
        the second point.
        """
        if self.__class__ == other.__class__:
            if (self.p1 == other.p1) and (self.p2 == other.p2):
                return 0
            elif self.p1 != other.p1:
                return cmp(self.p1, other.p1)
            else:
                return cmp(self.p2, other.p2)
        else:
            return cmp(str(self), str(other))

    def next(self):
        yield self.p1
        yield self.p2

    def get_children_count(self):
        # a line always contains two points
        return 2

    def reset_cache(self):
        self._vector = None
        self._minx = None
        self._maxx = None
        self._miny = None
        self._maxy = None
        self._minz = None
        self._maxz = None

    def get_points(self):
        return (self.p1, self.p2)

    def point_with_length_multiply(self, l):
        return self.p1.add(self.dir.mul(l*self.len))

    def get_length_line(self, length):
        """ return a line with the same direction and the specified length
        """
        return Line(self.p1, self.p1.add(self.dir.mul(length)))

    def closest_point(self, p):
        v = self.dir
        if v is None:
            # for zero-length lines
            return self.p1
        l = self.p1.dot(v) - p.dot(v)
        return self.p1.sub(v.mul(l))

    def dist_to_point_sq(self, p):
        return p.sub(self.closest_point(p)).normsq

    def dist_to_point(self, p):
        return sqrt(self.dist_to_point_sq(p))
    
    def is_point_inside(self, p):
        if (p == self.p1) or (p == self.p2):
            # these conditions are not covered by the code below
            return True
        dir1 = p.sub(self.p1).normalized()
        dir2 = self.p2.sub(p).normalized()
        # True if the two parts of the line have the same direction or if the
        # point is self.p1 or self.p2.
        return (dir1 == dir2 == self.dir) or (dir1 is None) or (dir2 is None)

    def to_OpenGL(self, color=None, show_directions=False):
        if not GL_enabled:
            return
        if not color is None:
            GL.glColor4f(*color)
        GL.glBegin(GL.GL_LINES)
        GL.glVertex3f(self.p1.x, self.p1.y, self.p1.z)
        GL.glVertex3f(self.p2.x, self.p2.y, self.p2.z)
        GL.glEnd()
        # (optional) draw a cone for visualizing the direction of each line
        if show_directions and (self.len > 0):
            cone_radius = self.len / 30
            cone_length = self.len / 10
            GL.glPushMatrix()
            # move the cone to the middle of the line
            GL.glTranslatef((self.p1.x + self.p2.x) / 2,
                    (self.p1.y + self.p2.y) / 2,
                    (self.p1.z + self.p2.z) / 2)
            # rotate the cone according to the line direction
            # The cross product is a good rotation axis.
            cross = self.dir.cross(Point(0, 0, -1))
            if cross.norm != 0:
                # The line direction is not in line with the z axis.
                angle = math.asin(sqrt(self.dir.x ** 2 + self.dir.y ** 2))
                # convert from radians to degree
                angle = angle / math.pi * 180
                if self.dir.z < 0:
                    angle = 180 - angle
                GL.glRotatef(angle, cross.x, cross.y, cross.z)
            elif self.dir.z == -1:
                # The line goes down the z axis - turn it around.
                GL.glRotatef(180, 1, 0, 0)
            else:
                # The line goes up the z axis - nothing to be done.
                pass
            # center the cone
            GL.glTranslatef(0, 0, -cone_length / 2)
            # draw the cone
            GLUT.glutSolidCone(cone_radius, cone_length, 12, 1)
            GL.glPopMatrix()

    def get_intersection(self, line, infinite_lines=False):
        """ Get the point of intersection between two lines. Intersections
        outside the length of these lines are ignored.
        Returns (None, None) if no valid intersection was found.
        Otherwise the result is (CollisionPoint, distance). Distance is between
        0 and 1.
        """
        x1, x2, x3, x4 = self.p1, self.p2, line.p1, line.p2
        a = x2.sub(x1)
        b = x4.sub(x3)
        c = x3.sub(x1)
        # see http://mathworld.wolfram.com/Line-LineIntersection.html (24)
        try:
            factor = c.cross(b).dot(a.cross(b)) / a.cross(b).normsq
        except ZeroDivisionError:
            # lines are parallel
            # check if they are _one_ line
            if a.cross(c).norm != 0:
                # the lines are parallel with a distance
                return None, None
            # the lines are on one straight
            candidates = []
            if self.is_point_inside(x3):
                candidates.append((x3, c.norm / a.norm))
            elif self.is_point_inside(x4):
                candidates.append((x4, line.p2.sub(self.p1).norm / a.norm))
            elif line.is_point_inside(x1):
                candidates.append((x1, 0))
            elif line.is_point_inside(x2):
                candidates.append((x2, 1))
            else:
                return None, None
            # return the collision candidate with the lowest distance
            candidates.sort(key=lambda (cp, dist): dist)
            return candidates[0]
        if infinite_lines or (-epsilon <= factor <= 1 + epsilon):
            intersection = x1.add(a.mul(factor))
            # check if the intersection is between x3 and x4
            if infinite_lines:
                return intersection, factor
            elif (min(x3.x, x4.x) - epsilon <= intersection.x <= max(x3.x, x4.x) + epsilon) \
                    and (min(x3.y, x4.y) - epsilon <= intersection.y <= max(x3.y, x4.y) + epsilon) \
                    and (min(x3.z, x4.z) - epsilon <= intersection.z <= max(x3.z, x4.z) + epsilon):
                return intersection, factor
            else:
                # intersection outside of the length of line(x3, x4)
                return None, None
        else:
            # intersection outside of the length of line(x1, x2)
            return None, None

    def get_cropped_line(self, minx, maxx, miny, maxy, minz, maxz):
        if self.is_completely_inside(minx, maxx, miny, maxy, minz, maxz):
            return self
        elif self.is_completely_outside(minx, maxx, miny, maxy, minz, maxz):
            return None
        else:
            # the line needs to be cropped
            # generate the six planes of the cube for possible intersections
            minp = Point(minx, miny, minz)
            maxp = Point(maxx, maxy, maxz)
            planes = [
                    Plane(minp, Point(1, 0, 0)),
                    Plane(minp, Point(0, 1, 0)),
                    Plane(minp, Point(0, 0, 1)),
                    Plane(maxp, Point(1, 0, 0)),
                    Plane(maxp, Point(0, 1, 0)),
                    Plane(maxp, Point(0, 0, 1)),
            ]
            # calculate all intersections
            intersections = [plane.intersect_point(self.dir, self.p1)
                    for plane in planes]
            # remove all intersections outside the box and outside the line
            valid_intersections = [(cp, dist) for cp, dist in intersections
                    if cp and (-epsilon <= dist <= self.len + epsilon) and \
                            cp.is_inside(minx, maxx, miny, maxy, minz, maxz)]
            # sort the intersections according to their distance to self.p1
            valid_intersections.sort(
                    cmp=lambda (cp1, l1), (cp2, l2): cmp(l1, l2))
            # Check if p1 is within the box - otherwise use the closest
            # intersection. The check for "valid_intersections" is necessary
            # to prevent an IndexError due to floating point inaccuracies.
            if self.p1.is_inside(minx, maxx, miny, maxy, minz, maxz) \
                    or not valid_intersections:
                new_p1 = self.p1
            else:
                new_p1 = valid_intersections[0][0]
            # Check if p2 is within the box - otherwise use the intersection
            # most distant from p1.
            if self.p2.is_inside(minx, maxx, miny, maxy, minz, maxz) \
                    or not valid_intersections:
                new_p2 = self.p2
            else:
                new_p2 = valid_intersections[-1][0]
            if new_p1 == new_p2:
                # no real line
                return None
            else:
                return Line(new_p1, new_p2)

