"""
    cairocffi.matrix
    ~~~~~~~~~~~~~~~~

    Transformation matrices.

    :copyright: Copyright 2013-2019 by Simon Sapin
    :license: BSD, see LICENSE for details.

"""

from . import _check_status, cairo, ffi


class Matrix(object):
    """A 2D transformation matrix.

    Matrices are used throughout cairo to convert between
    different coordinate spaces.
    A :class:`Matrix` holds an affine transformation,
    such as a scale, rotation, shear, or a combination of these.
    The transformation of a point (x,y) is given by::

        x_new = xx * x + xy * y + x0
        y_new = yx * x + yy * y + y0

    The current transformation matrix of a :class:`Context`,
    represented as a :class:`Matrix`,
    defines the transformation from user-space coordinates
    to device-space coordinates.
    See :meth:`Context.get_matrix` and :meth:`Context.set_matrix`.

    The default values produce an identity matrix.

    Matrices can be compared with ``m1 == m2`` and ``m2 != m2``
    as well as multiplied with ``m3 = m1 * m2``.

    """
    def __init__(self, xx=1, yx=0, xy=0, yy=1, x0=0, y0=0):
        self._pointer = ffi.new('cairo_matrix_t *')
        cairo.cairo_matrix_init(self._pointer, xx, yx, xy, yy, x0, y0)

    @classmethod
    def init_rotate(cls, radians):
        """Return a new :class:`Matrix` for a transformation
        that rotates by ``radians``.

        :type radians: float
        :param radians:
            Angle of rotation, in radians.
            The direction of rotation is defined such that
            positive angles rotate in the direction
            from the positive X axis toward the positive Y axis.
            With the default axis orientation of cairo,
            positive angles rotate in a clockwise direction.

        """
        result = cls()
        cairo.cairo_matrix_init_rotate(result._pointer, radians)
        return result

    def as_tuple(self):
        """Return all of the matrix’s components.

        :returns: A ``(xx, yx, xy, yy, x0, y0)`` tuple of floats.

        """
        ptr = self._pointer
        return (ptr.xx, ptr.yx, ptr.xy, ptr.yy, ptr.x0, ptr.y0)

    def copy(self):
        """Return a new copy of this matrix."""
        return type(self)(*self.as_tuple())

    def __getitem__(self, index):
        return getattr(
            self._pointer, ('xx', 'yx', 'xy', 'yy', 'x0', 'y0')[index])

    def __iter__(self):
        return iter(self.as_tuple())

    def __eq__(self, other):
        return self.as_tuple() == other.as_tuple()

    def __ne__(self, other):
        return self.as_tuple() != other.as_tuple()

    def __repr__(self):
        class_ = type(self)
        return '%s(%g, %g, %g, %g, %g, %g)' % (
            (class_.__name__, *self.as_tuple()))

    def multiply(self, other):
        """Multiply with another matrix
        and return the result as a new :class:`Matrix` object.
        Same as ``self * other``.

        """
        res = Matrix()
        cairo.cairo_matrix_multiply(
            res._pointer, self._pointer, other._pointer)
        return res

    __mul__ = multiply

    def translate(self, tx, ty):
        """Applies a translation by ``tx``, ``ty``
        to the transformation in this matrix.

        The effect of the new transformation is to
        first translate the coordinates by ``tx`` and ``ty``,
        then apply the original transformation to the coordinates.

        .. note::
            This changes the matrix in-place.

        :param tx: Amount to translate in the X direction.
        :param ty: Amount to translate in the Y direction.
        :type tx: float
        :type ty: float

        """
        cairo.cairo_matrix_translate(self._pointer, tx, ty)

    def scale(self, sx, sy=None):
        """Applies scaling by ``sx``, ``sy``
        to the transformation in this matrix.

        The effect of the new transformation is to
        first scale the coordinates by ``sx`` and ``sy``,
        then apply the original transformation to the coordinates.

        If ``sy`` is omitted, it is the same as ``sx``
        so that scaling preserves aspect ratios.

        .. note::
            This changes the matrix in-place.

        :param sx: Scale factor in the X direction.
        :param sy: Scale factor in the Y direction.
        :type sx: float
        :type sy: float

        """
        if sy is None:
            sy = sx
        cairo.cairo_matrix_scale(self._pointer, sx, sy)

    def rotate(self, radians):
        """Applies a rotation by ``radians``
        to the transformation in this matrix.

        The effect of the new transformation is to
        first rotate the coordinates by ``radians``,
        then apply the original transformation to the coordinates.

        .. note::
            This changes the matrix in-place.

        :type radians: float
        :param radians:
            Angle of rotation, in radians.
            The direction of rotation is defined such that positive angles
            rotate in the direction from the positive X axis
            toward the positive Y axis.
            With the default axis orientation of cairo,
            positive angles rotate in a clockwise direction.

        """
        cairo.cairo_matrix_rotate(self._pointer, radians)

    def invert(self):
        """Changes matrix to be the inverse of its original value.
        Not all transformation matrices have inverses;
        if the matrix collapses points together (it is degenerate),
        then it has no inverse and this function will fail.

        .. note::
            This changes the matrix in-place.

        :raises: :exc:`CairoError` on degenerate matrices.

        """
        _check_status(cairo.cairo_matrix_invert(self._pointer))

    def inverted(self):
        """Return the inverse of this matrix. See :meth:`invert`.

        :raises: :exc:`CairoError` on degenerate matrices.
        :returns: A new :class:`Matrix` object.

        """
        matrix = self.copy()
        matrix.invert()
        return matrix

    def transform_point(self, x, y):
        """Transforms the point ``(x, y)`` by this matrix.

        :param x: X position.
        :param y: Y position.
        :type x: float
        :type y: float
        :returns: A ``(new_x, new_y)`` tuple of floats.

        """
        xy = ffi.new('double[2]', [x, y])
        cairo.cairo_matrix_transform_point(self._pointer, xy + 0, xy + 1)
        return tuple(xy)

    def transform_distance(self, dx, dy):
        """Transforms the distance vector ``(dx, dy)`` by this matrix.
        This is similar to :meth:`transform_point`
        except that the translation components of the transformation
        are ignored.
        The calculation of the returned vector is as follows::

            dx2 = dx1 * xx + dy1 * xy
            dy2 = dx1 * yx + dy1 * yy

        Affine transformations are position invariant,
        so the same vector always transforms to the same vector.
        If ``(x1, y1)`` transforms to ``(x2, y2)``
        then ``(x1 + dx1, y1 + dy1)`` will transform
        to ``(x1 + dx2, y1 + dy2)`` for all values of ``x1`` and ``x2``.

        :param dx: X component of a distance vector.
        :param dy: Y component of a distance vector.
        :type dx: float
        :type dy: float
        :returns: A ``(new_dx, new_dy)`` tuple of floats.

        """
        xy = ffi.new('double[2]', [dx, dy])
        cairo.cairo_matrix_transform_distance(self._pointer, xy + 0, xy + 1)
        return tuple(xy)

    def _component_property(name):  # noqa: N805
        return property(
            lambda self: getattr(self._pointer, name),
            lambda self, value: setattr(self._pointer, name, value),
            doc='Read-write attribute access to a single float component.')

    xx = _component_property('xx')
    yx = _component_property('yx')
    xy = _component_property('xy')
    yy = _component_property('yy')
    x0 = _component_property('x0')
    y0 = _component_property('y0')
    del _component_property
