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

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

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.utils import epsilon, sqrt, number
from pycam.Geometry import IDGenerator


def _is_near(x, y):
    return abs(x - y) < epsilon


class Point(IDGenerator):

    __slots__ = ["id", "x", "y", "z", "_norm", "_normsq"]

    def __init__(self, x, y, z):
        super(Point, self).__init__()
        self.x = number(x)
        self.y = number(y)
        self.z = number(z)
        self.reset_cache()

    @property
    def norm(self):
        if self._norm is None:
            self._norm = sqrt(self.normsq)
        return self._norm

    @property
    def normsq(self):
        if self._normsq is None:
            self._normsq = self.dot(self)
        return self._normsq

    def copy(self):
        return self.__class__(float(self.x), float(self.y), float(self.z))

    def __repr__(self):
        return "Point%d<%g,%g,%g>" % (self.id, self.x, self.y, self.z)

    def __cmp__(self, other):
        """ Two points are equal if all dimensions are identical.
        Otherwise the result is based on the individual x/y/z comparisons.
        """
        if self.__class__ == other.__class__:
            if (self.id == other.id) or \
                    ((_is_near(self.x, other.x)) and \
                        (_is_near(self.y, other.y)) and \
                        (_is_near(self.z, other.z))):
                return 0
            elif not _is_near(self.x, other.x):
                return cmp(self.x, other.x)
            elif not _is_near(self.y, other.y):
                return cmp(self.y, other.y)
            else:
                return cmp(self.z, other.z)
        else:
            return cmp(str(self), str(other))

    def transform_by_matrix(self, matrix, transformed_list=None, callback=None):
        # accept 3x4 matrices as well as 3x3 matrices
        offsets = []
        for column in matrix:
            if len(column) < 4:
                offsets.append(0)
            else:
                offsets.append(column[3])
        x = self.x * matrix[0][0] + self.y * matrix[0][1] \
                + self.z * matrix[0][2] + offsets[0]
        y = self.x * matrix[1][0] + self.y * matrix[1][1] \
                + self.z * matrix[1][2] + offsets[1]
        z = self.x * matrix[2][0] + self.y * matrix[2][1] \
                + self.z * matrix[2][2] + offsets[2]
        self.x = x
        self.y = y
        self.z = z
        if callback:
            callback()
        self.reset_cache()

    def reset_cache(self):
        self._norm = None
        self._normsq = None
        
    def mul(self, c):
        c = number(c)
        return Point(self.x * c, self.y * c, self.z * c)

    def div(self, c):
        c = number(c)
        return Point(self.x / c, self.y / c, self.z / c)

    def add(self, p):
        return Point(self.x + p.x, self.y + p.y, self.z + p.z)

    def sub(self, p):
        return Point(self.x - p.x, self.y - p.y, self.z - p.z)

    def dot(self, p):
        return self.x * p.x + self.y * p.y + self.z * p.z

    def cross(self, p):
        return Point(self.y * p.z - p.y * self.z, p.x * self.z - self.x * p.z,
                self.x * p.y - p.x * self.y)

    def normalized(self):
        n = self.norm
        if n == 0:
            return None
        else:
            return self.__class__(self.x / n, self.y / n, self.z / n)

    def is_inside(self, minx=None, maxx=None, miny=None, maxy=None, minz=None,
            maxz=None):
        return ((minx is None) or (minx - epsilon <= self.x)) \
                and ((maxx is None) or (self.x <= maxx + epsilon)) \
                and ((miny is None) or (miny - epsilon <= self.y)) \
                and ((maxy is None) or (self.y <= maxy + epsilon)) \
                and ((minz is None) or (minz - epsilon <= self.z)) \
                and ((maxz is None) or (self.z <= maxz + epsilon))

    def get_vector(self):
        return Vector(self.x, self.y, self.z)


class Vector(Point):
    """ The Vector class is similar to the Point class. The only difference
    is that vectors are not shifted during transformations. This feature
    is necessary for normals (e.g. of Triangles or Planes).
    """

    __slots__ = []

    def transform_by_matrix(self, matrix, transformed_list=None, callback=None):
        x = self.x * matrix[0][0] + self.y * matrix[0][1] \
                + self.z * matrix[0][2]
        y = self.x * matrix[1][0] + self.y * matrix[1][1] \
                + self.z * matrix[1][2]
        z = self.x * matrix[2][0] + self.y * matrix[2][1] \
                + self.z * matrix[2][2]
        self.x = x
        self.y = y
        self.z = z
        if callback:
            callback()
        self.reset_cache()

    def __repr__(self):
        return "Vector%d<%g,%g,%g>" % (self.id, self.x, self.y, self.z)