#------------------------------------------------------------------------------
# Copyright (c) 2010, Enthought, Inc
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in enthought/LICENSE.txt and may be redistributed only
# under the conditions described in the aforementioned license.  The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
# Thanks for using Enthought open source!
#------------------------------------------------------------------------------
""" This is the QPainter backend for kiva. """


# These are the symbols that a backend has to define.
__all__ = ["CompiledPath", "Font", "font_metrics_provider", "GraphicsContext"]

from functools import partial
from itertools import izip
import numpy as np
import warnings

# Major package imports.
from pyface.qt import QtCore, QtGui

# Local imports.
from arc_conversion import arc_to_tangent_points
from fonttools import Font
import constants

cap_style = {}
cap_style[constants.CAP_ROUND]  = QtCore.Qt.RoundCap
cap_style[constants.CAP_SQUARE] = QtCore.Qt.SquareCap
cap_style[constants.CAP_BUTT]   = QtCore.Qt.FlatCap

join_style = {}
join_style[constants.JOIN_ROUND] = QtCore.Qt.RoundJoin
join_style[constants.JOIN_BEVEL] = QtCore.Qt.BevelJoin
join_style[constants.JOIN_MITER] = QtCore.Qt.MiterJoin

draw_modes = {}
draw_modes[constants.FILL]            = QtCore.Qt.OddEvenFill
draw_modes[constants.EOF_FILL]        = QtCore.Qt.WindingFill
draw_modes[constants.STROKE]          = 0
draw_modes[constants.FILL_STROKE]     = QtCore.Qt.OddEvenFill
draw_modes[constants.EOF_FILL_STROKE] = QtCore.Qt.WindingFill

gradient_coord_modes = {}
gradient_coord_modes['userSpaceOnUse'] = QtGui.QGradient.LogicalMode
gradient_coord_modes['objectBoundingBox'] = QtGui.QGradient.ObjectBoundingMode

gradient_spread_modes = {}
gradient_spread_modes['pad'] = QtGui.QGradient.PadSpread
gradient_spread_modes['repeat'] = QtGui.QGradient.RepeatSpread
gradient_spread_modes['reflect'] = QtGui.QGradient.ReflectSpread


class GraphicsContext(object):
    """ Simple wrapper around a Qt QPainter object.
    """
    def __init__(self, size, *args, **kwargs):
        super(GraphicsContext, self).__init__()
        self._width = size[0]
        self._height = size[1]

        self.text_pos = [0.0, 0.0]
        self.text_transform = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)

        # create some sort of device context
        parent = kwargs.pop("parent", None)
        if parent is None:
            # no parent -> offscreen context
            self.qt_dc = QtGui.QPixmap(*size)
        else:
            # normal windowed context
            self.qt_dc = parent

        self.gc = QtGui.QPainter(self.qt_dc)
        self.path = CompiledPath()

        # flip y
        trans = QtGui.QTransform()
        trans.translate(0, size[1])
        trans.scale(1.0, -1.0)
        self.gc.setWorldTransform(trans)

        # enable antialiasing
        self.gc.setRenderHints(QtGui.QPainter.Antialiasing|QtGui.QPainter.TextAntialiasing,
                               True)
        # set the pen and brush to useful defaults
        self.gc.setPen(QtCore.Qt.black)
        self.gc.setBrush(QtGui.QBrush(QtCore.Qt.SolidPattern))

    def __del__(self):
        # stop the painter if needed
        if self.gc.isActive():
            self.gc.end()

    #----------------------------------------------------------------
    # Size info
    #----------------------------------------------------------------

    def height(self):
        """ Returns the height of the context.
        """
        return self._height

    def width(self):
        """ Returns the width of the context.
        """
        return self._width

    #----------------------------------------------------------------
    # Coordinate Transform Matrix Manipulation
    #----------------------------------------------------------------

    def scale_ctm(self, sx, sy):
        """ Set the coordinate system scale to the given values, (sx,sy).

            sx:float -- The new scale factor for the x axis
            sy:float -- The new scale factor for the y axis
        """
        self.gc.scale(sx, sy)

    def translate_ctm(self, tx, ty):
        """ Translate the coordinate system by the given value by (tx,ty)

            tx:float --  The distance to move in the x direction
            ty:float --   The distance to move in the y direction
        """
        self.gc.translate(tx, ty)

    def rotate_ctm(self, angle):
        """ Rotates the coordinate space for drawing by the given angle.

            angle:float -- the angle, in radians, to rotate the coordinate
                           system
        """
        self.gc.rotate(np.rad2deg(angle))

    def concat_ctm(self, transform):
        """ Concatenate the transform to current coordinate transform matrix.

            transform:affine_matrix -- the transform matrix to concatenate with
                                       the current coordinate matrix.
        """
        m11,m12,m21,m22,tx,ty = transform
        self.gc.setTransform(QtGui.QTransform(m11, m12, m21, m22, tx, ty), True)

    def get_ctm(self):
        """ Return the current coordinate transform matrix.
        """
        t = self.gc.transform()
        return (t.m11(), t.m12(), t.m21(), t.m22(), t.dx(), t.dy())

    #----------------------------------------------------------------
    # Save/Restore graphics state.
    #----------------------------------------------------------------

    def save_state(self):
        """ Save the current graphic's context state.

            This should always be paired with a restore_state
        """
        self.gc.save()

    def restore_state(self):
        """ Restore the previous graphics state.
        """
        self.gc.restore()


    #----------------------------------------------------------------
    # context manager interface
    #----------------------------------------------------------------

    def __enter__(self):
        self.save_state()

    def __exit__(self, type, value, traceback):
        self.restore_state()

    #----------------------------------------------------------------
    # Manipulate graphics state attributes.
    #----------------------------------------------------------------

    def set_antialias(self,value):
        """ Set/Unset antialiasing for bitmap graphics context.
        """
        self.gc.setRenderHints(QtGui.QPainter.Antialiasing|QtGui.QPainter.TextAntialiasing,
                               value)

    def set_line_width(self,width):
        """ Set the line width for drawing

            width:float -- The new width for lines in user space units.
        """
        pen = self.gc.pen()
        pen.setWidthF(width)
        self.gc.setPen(pen)

    def set_line_join(self,style):
        """ Set style for joining lines in a drawing.

            style:join_style -- The line joining style.  The available
                                styles are JOIN_ROUND, JOIN_BEVEL, JOIN_MITER.
        """
        try:
            sjoin = join_style[style]
        except KeyError:
            msg = "Invalid line join style.  See documentation for valid styles"
            raise ValueError, msg

        pen = self.gc.pen()
        pen.setJoinStyle(sjoin)
        self.gc.setPen(pen)

    def set_miter_limit(self,limit):
        """ Specifies limits on line lengths for mitering line joins.

            If line_join is set to miter joins, the limit specifies which
            line joins should actually be mitered.  If lines aren't mitered,
            they are joined with a bevel.  The line width is divided by
            the length of the miter.  If the result is greater than the
            limit, the bevel style is used.

            limit:float -- limit for mitering joins.
        """
        pen = self.gc.pen()
        pen.setMiterLimit(limit)
        self.gc.setPen(pen)

    def set_line_cap(self,style):
        """ Specify the style of endings to put on line ends.

            style:cap_style -- the line cap style to use. Available styles
                               are CAP_ROUND,CAP_BUTT,CAP_SQUARE
        """
        try:
            scap = cap_style[style]
        except KeyError:
            msg = "Invalid line cap style.  See documentation for valid styles"
            raise ValueError, msg

        pen = self.gc.pen()
        pen.setCapStyle(scap)
        self.gc.setPen(pen)

    def set_line_dash(self,lengths,phase=0):
        """

            lengths:float array -- An array of floating point values
                                   specifing the lengths of on/off painting
                                   pattern for lines.
            phase:float -- Specifies how many units into dash pattern
                           to start.  phase defaults to 0.
        """
        lengths = list(lengths) if lengths is not None else []
        pen = self.gc.pen()
        pen.setDashPattern(lengths)
        pen.setDashOffset(phase)
        self.gc.setPen(pen)

    def set_flatness(self,flatness):
        """ Not implemented

            It is device dependent and therefore not recommended by
            the PDF documentation.
        """
        raise NotImplementedError

    #----------------------------------------------------------------
    # Sending drawing data to a device
    #----------------------------------------------------------------

    def flush(self):
        """ Send all drawing data to the destination device.
        """
        pass

    def synchronize(self):
        """ Prepares drawing data to be updated on a destination device.
        """
        pass

    #----------------------------------------------------------------
    # Page Definitions
    #----------------------------------------------------------------

    def begin_page(self):
        """ Create a new page within the graphics context.
        """
        pass

    def end_page(self):
        """ End drawing in the current page of the graphics context.
        """
        pass

    #----------------------------------------------------------------
    # Building paths (contours that are drawn)
    #
    # + Currently, nothing is drawn as the path is built.  Instead, the
    #   instructions are stored and later drawn.  Should this be changed?
    #   We will likely draw to a buffer instead of directly to the canvas
    #   anyway.
    #
    #   Hmmm. No.  We have to keep the path around for storing as a
    #   clipping region and things like that.
    #
    # + I think we should keep the current_path_point hanging around.
    #
    #----------------------------------------------------------------

    def begin_path(self):
        """ Clear the current drawing path and begin a new one.
        """
        self.path = CompiledPath()

    def move_to(self,x,y):
        """ Start a new drawing subpath at place the current point at (x,y).
        """
        self.path.move_to(x,y)

    def line_to(self,x,y):
        """ Add a line from the current point to the given point (x,y).

            The current point is moved to (x,y).
        """
        self.path.line_to(x,y)

    def lines(self,points):
        """ Add a series of lines as a new subpath.

            Currently implemented by calling line_to a zillion times.

            Points is an Nx2 array of x,y pairs.
        """
        self.path.lines(points)

    def line_set(self, starts, ends):
        """ Draw multiple disjoint line segments.
        """
        for start, end in izip(starts, ends):
            self.path.path.moveTo(start[0], start[1])
            self.path.path.lineTo(end[0], end[1])

    def rect(self,x,y,sx,sy):
        """ Add a rectangle as a new subpath.
        """
        self.path.rect(x,y,sx,sy)

    def rects(self,rects):
        """ Add multiple rectangles as separate subpaths to the path.
        """
        self.path.rects(rects)

    def draw_rect(self, rect, mode=constants.FILL_STROKE):
        """ Draw a rect.
        """
        rect = QtCore.QRectF(*rect)
        if mode == constants.STROKE:
            save_brush = self.gc.brush()
            self.gc.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
            self.gc.drawRect(rect)
            self.gc.setBrush(save_brush)
        elif mode in [constants.FILL, constants.EOF_FILL]:
            self.gc.fillRect(rect, self.gc.brush())
        else:
            self.gc.fillRect(rect, self.gc.brush())
            self.gc.drawRect(rect)

    def add_path(self, path):
        """ Add a subpath to the current path.
        """
        self.path.add_path(path)

    def close_path(self):
        """ Close the path of the current subpath.
        """
        self.path.close_path()

    def curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y):
        """
        """
        self.path.curve_to(cp1x, cp1y, cp2x, cp2y, x, y)

    def quad_curve_to(self, cpx, cpy, x, y):
        """
        """
        self.path.quad_curve_to(cpx, cpy, x, y)

    def arc(self, x, y, radius, start_angle, end_angle, clockwise=False):
        """
        """
        self.path.arc(x, y, radius, start_angle, end_angle, clockwise)

    def arc_to(self, x1, y1, x2, y2, radius):
        """
        """
        self.path.arc_to(x1, y1, x2, y2, radius)

    #----------------------------------------------------------------
    # Getting infomration on paths
    #----------------------------------------------------------------

    def is_path_empty(self):
        """ Test to see if the current drawing path is empty
        """
        return self.path.is_empty()

    def get_path_current_point(self):
        """ Return the current point from the graphics context.
        """
        return self.path.get_current_point()

    def get_path_bounding_box(self):
        """ Return the bounding box for the current path object.
        """
        return self.path.get_bounding_box()

    #----------------------------------------------------------------
    # Clipping path manipulation
    #----------------------------------------------------------------

    def clip(self):
        """
        """
        self.gc.setClipPath(self.path.path)

    def even_odd_clip(self):
        """
        """
        self.gc.setClipPath(self.path.path, operation=QtCore.Qt.IntersectClip)

    def clip_to_rect(self, x, y, w, h):
        """ Clip context to the given rectangular region.

            Region should be a 4-tuple or a sequence.
        """
        self.gc.setClipRect(QtCore.QRectF(x,y,w,h), operation=QtCore.Qt.IntersectClip)

    def clip_to_rects(self, rects):
        """
        """
        # Create a region which is a union of all rects.
        clip_region = QtGui.QRegion()
        for rect in rects:
            clip_region = clip_region.unite(QtGui.QRegion(*rect))

        # Then intersect that region with the current clip region.
        self.gc.setClipRegion(clip_region, operation=QtCore.Qt.IntersectClip)

    #----------------------------------------------------------------
    # Color space manipulation
    #
    # I'm not sure we'll mess with these at all.  They seem to
    # be for setting the color system.  Hard coding to RGB or
    # RGBA for now sounds like a reasonable solution.
    #----------------------------------------------------------------

    def set_fill_color_space(self):
        """
        """
        msg = "set_fill_color_space not implemented on Qt yet."
        raise NotImplementedError, msg

    def set_stroke_color_space(self):
        """
        """
        msg = "set_stroke_color_space not implemented on Qt yet."
        raise NotImplementedError, msg

    def set_rendering_intent(self):
        """
        """
        msg = "set_rendering_intent not implemented on Qt yet."
        raise NotImplementedError, msg

    #----------------------------------------------------------------
    # Color manipulation
    #----------------------------------------------------------------

    def set_fill_color(self, color):
        """
        """
        r,g,b = color[:3]
        try:
            a = color[3]
        except IndexError:
            a = 1.0
        brush = self.gc.brush()
        brush.setColor(QtGui.QColor.fromRgbF(r,g,b,a))
        self.gc.setBrush(brush)

    def set_stroke_color(self, color):
        """
        """
        r,g,b = color[:3]
        try:
            a = color[3]
        except IndexError:
            a = 1.0
        pen = self.gc.pen()
        pen.setColor(QtGui.QColor.fromRgbF(r,g,b,a))
        self.gc.setPen(pen)

    def set_alpha(self, alpha):
        """
        """
        self.gc.setOpacity(alpha)

    #----------------------------------------------------------------
    # Gradients
    #----------------------------------------------------------------

    def _apply_gradient(self, grad, stops, spread_method, units):
        """ Configures a gradient object and sets it as the current brush.
        """
        grad.setSpread(gradient_spread_modes.get(spread_method,
                                                 QtGui.QGradient.PadSpread))
        grad.setCoordinateMode(gradient_coord_modes.get(units, QtGui.QGradient.LogicalMode))

        for stop in stops:
            grad.setColorAt(stop[0], QtGui.QColor.fromRgbF(*stop[1:]))

        self.gc.setBrush(QtGui.QBrush(grad))

    def linear_gradient(self, x1, y1, x2, y2, stops, spread_method,
                        units='userSpaceOnUse'):
        """ Sets a linear gradient as the current brush.
        """
        grad = QtGui.QLinearGradient(x1, y1,x2, y2)
        self._apply_gradient(grad, stops, spread_method, units)

    def radial_gradient(self, cx, cy, r, fx, fy, stops, spread_method,
                        units='userSpaceOnUse'):
        """ Sets a radial gradient as the current brush.
        """
        grad = QtGui.QRadialGradient(cx, cy, r, fx, fy)
        self._apply_gradient(grad, stops, spread_method, units)

    #----------------------------------------------------------------
    # Drawing Images
    #----------------------------------------------------------------

    def draw_image(self, img, rect=None):
        """
        img is either a N*M*3 or N*M*4 numpy array, or a Kiva image

        rect - a tuple (x,y,w,h)
        """
        from kiva import agg

        def copy_padded(array):
            """ Pad image width to a multiple of 4 pixels, and minimum dims of
                12x12. QImage is very particular about its data.
            """
            y,x,d = array.shape
            pad = lambda v: (4-(v%4))%4
            nx = max(x+pad(x),12)
            ny = max(y,12)
            if x == nx and y == ny:
                return array
            ret = np.zeros((ny,nx,d), dtype=np.uint8)
            ret[:y,:x] = array[:]
            return ret

        if type(img) == type(np.array([])):
            # Numeric array
            if img.shape[2]==3:
                format = QtGui.QImage.Format_RGB888
            elif img.shape[2]==4:
                format = QtGui.QImage.Format_RGB32
            width, height = img.shape[:2]
            copy_array = copy_padded(img)
            draw_img = QtGui.QImage(img.astype(np.uint8), copy_array.shape[1],
                                    height, format)
            pixmap = QtGui.QPixmap.fromImage(draw_img)
        elif isinstance(img, agg.GraphicsContextArray):
            converted_img = img.convert_pixel_format('bgra32', inplace=0)
            copy_array = copy_padded(converted_img.bmp_array)
            width, height = img.width(), img.height()
            draw_img = QtGui.QImage(copy_array.flatten(),
                                    copy_array.shape[1], height,
                                    QtGui.QImage.Format_RGB32)
            pixmap = QtGui.QPixmap.fromImage(draw_img)
        elif (isinstance(img, GraphicsContext) and
              isinstance(img.gc.device(), QtGui.QPixmap)):
            # An offscreen Qt kiva context
            pixmap = img.gc.device()
            width, height = pixmap.width(), pixmap.height()
        else:
            warnings.warn("Cannot render image of type '%r' into Qt4 context." % \
                    type(img))
            return

        # create a rect object to draw into
        if rect is None:
            dest_rect = QtCore.QRectF(0.0, 0.0, self.width(), self.height())
        else:
            dest_rect = QtCore.QRectF(*rect)

        # draw using the entire image's data
        source_rect = QtCore.QRectF(0.0, 0.0, width, height)

        flip_trans = QtGui.QTransform()
        flip_trans.scale(1.0, -1.0)
        pixmap = pixmap.transformed(flip_trans)

        # draw
        self.gc.drawPixmap(dest_rect, pixmap, source_rect)

    #----------------------------------------------------------------
    # Drawing Text
    #----------------------------------------------------------------

    def select_font(self, name, size, textEncoding):
        """ Set the font for the current graphics context.
        """
        self.gc.setFont(QtGui.QFont(name, size))

    def set_font(self, font):
        """ Set the font for the current graphics context.
        """
        self.select_font(font.face_name, font.size, None)

    def set_font_size(self, size):
        """
        """
        font = self.gc.font()
        font.setPointSizeF(size)
        self.gc.setFont(font)

    def set_character_spacing(self, spacing):
        """
        """
        font = self.gc.font()
        font.setLetterSpacing(QtGui.QFont.AbsoluteSpacing, spacing)
        self.gc.setFont(font)

    def set_text_drawing_mode(self):
        """
        """
        pass

    def set_text_position(self,x,y):
        """
        """
        self.text_pos = [x,y]

    def get_text_position(self):
        """
        """
        return self.text_pos

    def set_text_matrix(self,ttm):
        """
        """
        self.text_transform = ttm

    def get_text_matrix(self):
        """
        """
        return self.text_transform

    def show_text(self, text, point=None):
        """ Draw text on the device at current text position.

            This is also used for showing text at a particular point
            specified by x and y.
        """
        if point is None:
            pos = tuple(self.text_pos)
        else:
            pos = tuple(point)

        unflip_trans = QtGui.QTransform(*self.text_transform)
        unflip_trans.translate(0, self._height)
        unflip_trans.scale(1.0, -1.0)

        self.gc.save()
        self.gc.setTransform(unflip_trans, True)
        self.gc.drawText(QtCore.QPointF(pos[0], self._flip_y(pos[1])), text)
        self.gc.restore()

    def show_text_at_point(self, text, x, y):
        """ Draw text at some point (x,y).
        """
        self.show_text(text, (x,y))

    def show_glyphs(self):
        """
        """
        msg = "show_glyphs not implemented on Qt yet."
        raise NotImplementedError, msg

    def get_text_extent(self, text):
        """ Returns the bounding rect of the rendered text
        """
        fm = self.gc.fontMetrics()
        rect = fm.boundingRect(text)

        return rect.left(), -fm.descent(), rect.right(), fm.height()

    def get_full_text_extent(self, text):
        """ Backwards compatibility API over .get_text_extent() for Enable
        """
        x1, y1, x2, y2 = self.get_text_extent(text)

        return x2, y2, y1, x1

    #----------------------------------------------------------------
    # Painting paths (drawing and filling contours)
    #----------------------------------------------------------------

    def stroke_path(self):
        """
        """
        self.gc.strokePath(self.path.path, self.gc.pen())
        self.begin_path()

    def fill_path(self):
        """
        """
        self.gc.fillPath(self.path.path, self.gc.brush())
        self.begin_path()

    def eof_fill_path(self):
        """
        """
        self.path.setFillRule(QtCore.Qt.OddEvenFill)
        self.gc.fillPath(self.path.path, self.gc.brush())
        self.begin_path()

    def stroke_rect(self,rect):
        """
        """
        self.gc.drawRect(QtCore.QRectF(*rect))

    def stroke_rect_with_width(self,rect,width):
        """
        """
        save_pen = self.gc.pen()
        draw_pen = QtGui.QPen(save_pen)
        draw_pen.setWidthF(width)

        self.gc.setPen(draw_pen)
        self.stroke_rect(rect)
        self.gc.setPen(save_pen)

    def fill_rect(self,rect):
        """
        """
        self.gc.fillRect(QtCore.QRectF(*rect), self.gc.brush())

    def fill_rects(self):
        """
        """
        msg = "fill_rects not implemented on Qt yet."
        raise NotImplementedError, msg

    def clear_rect(self, rect):
        """
        """
        self.gc.eraseRect(QtCore.QRectF(*rect))

    def clear(self, clear_color=(1.0,1.0,1.0,1.0)):
        """
        """
        if len(clear_color) == 4:
            r,g,b,a = clear_color
        else:
            r,g,b = clear_color
            a = 1.0
        self.gc.setBackground(QtGui.QBrush(QtGui.QColor.fromRgbF(r,g,b,a)))
        self.gc.eraseRect(QtCore.QRectF(0,0,self.width(),self.height()))

    def draw_path(self, mode=constants.FILL_STROKE):
        """ Walk through all the drawing subpaths and draw each element.

            Each subpath is drawn separately.
        """
        if mode == constants.STROKE:
            self.stroke_path()
        elif mode in [constants.FILL, constants.EOF_FILL]:
            mode = draw_modes[mode]
            self.path.path.setFillRule(mode)
            self.fill_path()
        else:
            mode = draw_modes[mode]
            self.path.path.setFillRule(mode)
            self.gc.drawPath(self.path.path)
        self.begin_path()

    def get_empty_path(self):
        """ Return a path object that can be built up and then reused.
        """
        return CompiledPath()

    def draw_path_at_points(self, points, path, mode=constants.FILL_STROKE):
        # set up drawing state and function
        if mode == constants.STROKE:
            draw_func = partial(self.gc.strokePath, path.path, self.gc.pen())
        elif mode in [constants.FILL, constants.EOF_FILL]:
            mode = draw_modes[mode]
            path.path.setFillRule(mode)
            draw_func = partial(self.gc.fillPath, path.path, self.gc.brush())
        else:
            mode = draw_modes[mode]
            path.path.setFillRule(mode)
            draw_func = partial(self.gc.drawPath, path.path)

        for point in points:
            x, y = point
            self.gc.save()
            self.gc.translate(x, y)
            draw_func()
            self.gc.restore()

    def _flip_y(self, y):
        "Converts between a Kiva and a Qt y coordinate"
        return self._height - y - 1

    def save(self, filename, file_format=None):
        """ Save the contents of the context to a file
        """
        if isinstance(self.qt_dc, QtGui.QPixmap):
            self.qt_dc.save(filename, format=file_format)
        else:
            msg = "save not implemented for window contexts."
            raise NotImplementedError, msg


class CompiledPath(object):
    def __init__(self):
        self.path = QtGui.QPainterPath()

    def begin_path(self):
        return

    def move_to(self, x, y):
        self.path.moveTo(x, y)

    def arc(self, x, y, r, start_angle, end_angle, clockwise=False):
        sweep_angle = end_angle-start_angle if not clockwise else start_angle-end_angle
        self.path.moveTo(x, y)
        self.path.arcTo(QtCore.QRectF(x-r, y-r, r*2, r*2),
                        np.rad2deg(start_angle), np.rad2deg(sweep_angle))

    def arc_to(self, x1, y1, x2, y2, r):
        # get the current pen position
        current_point = self.get_current_point()

        # Get the two points on the curve where it touches the line segments
        t1, t2 = arc_to_tangent_points(current_point, (x1,y1), (x2,y2), r)

        # draw!
        self.path.lineTo(*t1)
        self.path.quadTo(x1,y1,*t2)
        self.path.lineTo(x2,y2)

    def line_to(self, x, y):
        self.path.lineTo(x, y)

    def lines(self, points):
        self.path.moveTo(points[0][0],points[0][1])
        for x,y in points[1:]:
            self.path.lineTo(x,y)

    def curve_to(self, cx1, cy1, cx2, cy2, x, y):
        self.path.cubicTo(cx1, cy1, cx2, cy2, x, y)

    def quad_curve_to(self, cx, cy, x, y):
        self.path.quadTo(cx, cy, x, y)

    def rect(self, x, y, sx, sy):
        self.path.addRect(x, y, sx, sy)

    def rects(self, rects):
        for x,y,sx,sy in rects:
            self.path.addRect(x,y,sx,sy)

    def add_path(self, other_path):
        if isinstance(other_path, CompiledPath):
            self.path.addPath(other_path.path)

    def close_path(self):
        self.path.closeSubpath()

    def is_empty(self):
        return self.path.isEmpty()

    def get_current_point(self):
        point = self.path.currentPosition()
        return point.x(), point.y()

    def get_bounding_box(self):
        rect = self.path.boundingRect()
        return rect.x(), rect.y(), rect.width(), rect.height()


def font_metrics_provider():
    """ Creates an object to be used for querying font metrics.
    """
    return GraphicsContext((1,1))

