##############################################################################
#
# Copyright (c) 2007 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Page Drawing Related Element Processing

$Id: canvas.py 74195 2007-04-16 22:41:24Z srichter $
"""
__docformat__ = "reStructuredText"
import zope.interface
import reportlab.pdfgen.canvas
from z3c.rml import attr, directive, interfaces, occurence, stylesheet
from z3c.rml import chart, flowable, form, page


class IShape(interfaces.IRMLDirectiveSignature):
    """A shape to be drawn on the canvas."""

    x = attr.Measurement(
        title=u'X-Coordinate',
        description=(u'The X-coordinate of the lower-left position of the '
                     u'shape.'),
        required=True)

    y = attr.Measurement(
        title=u'Y-Coordinate',
        description=(u'The Y-coordinate of the lower-left position of the '
                     u'shape.'),
        required=True)

    fill = attr.Boolean(
        title=u'Fill',
        description=u'A flag to specify whether the shape should be filled.',
        required=False)

    stroke = attr.Boolean(
        title=u'Stroke',
        description=(u"A flag to specify whether the shape's outline should "
                     u"be drawn."),
        required=False)


class CanvasRMLDirective(directive.RMLDirective):
    callable = None
    attrMapping = None

    def process(self):
        kwargs = dict(self.getAttributeValues(attrMapping=self.attrMapping))
        canvas = attr.getManager(self, interfaces.ICanvasManager).canvas
        getattr(canvas, self.callable)(**kwargs)


class IDrawString(interfaces.IRMLDirectiveSignature):
    """Draws a simple string (left aligned) onto the canvas at the specified
    location."""

    x = attr.Measurement(
        title=u'X-Coordinate',
        description=(u'The X-coordinate of the lower-left position of the '
                     u'string.'),
        required=True)

    y = attr.Measurement(
        title=u'Y-Coordinate',
        description=(u'The Y-coordinate of the lower-left position of the '
                     u'string.'),
        required=True)

    text = attr.TextNode(
        title=u'Text',
        description=(u'The string/text that is put onto the canvas.'),
        required=True)

class DrawString(CanvasRMLDirective):
    signature = IDrawString
    callable = 'drawString'

class IDrawRightString(IDrawString):
    """Draws a simple string (right aligned) onto the canvas at the specified
    location."""

class DrawRightString(DrawString):
    signature = IDrawRightString
    callable = 'drawRightString'


class IDrawCenteredString(IDrawString):
    """Draws a simple string (centered aligned) onto the canvas at the specified
    location."""

class DrawCenteredString(DrawString):
    signature = IDrawCenteredString
    callable = 'drawCentredString'


class IDrawAlignedString(IDrawString):
    """Draws a simple string (aligned to the pivot character) onto the canvas
    at the specified location."""

    pivotChar = attr.Text(
        title=u'Text',
        description=(u'The string/text that is put onto the canvas.'),
        min_length=1,
        max_length=1,
        default=u'.',
        required=True)

class DrawAlignedString(DrawString):
    signature = IDrawAlignedString
    callable = 'drawAlignedString'


class IEllipse(IShape):
    """Draws an ellipse on the canvas."""

    width = attr.Measurement(
        title=u'Width',
        description=u'The width of the ellipse.',
        required=True)

    height = attr.Measurement(
        title=u'Height',
        description=u'The height of the ellipse.',
        required=True)

class Ellipse(CanvasRMLDirective):
    signature = IEllipse
    callable = 'ellipse'
    attrMapping = {'x': 'x1', 'y': 'y1'}

    def process(self):
        kwargs = dict(self.getAttributeValues(attrMapping=self.attrMapping))
        canvas = attr.getManager(self, interfaces.ICanvasManager).canvas
        # Convert width and height to end locations
        kwargs['x2'] = kwargs['x1'] + kwargs['width']
        del kwargs['width']
        kwargs['y2'] = kwargs['y1'] + kwargs['height']
        del kwargs['height']
        getattr(canvas, self.callable)(**kwargs)


class ICircle(IShape):
    """Draws a circle on the canvas."""

    radius = attr.Measurement(
        title=u'Radius',
        description=u'The radius of the circle.',
        required=True)

class Circle(CanvasRMLDirective):
    signature = ICircle
    callable = 'circle'
    attrMapping = {'x': 'x_cen', 'y': 'y_cen', 'radius': 'r'}


class IRectangle(IShape):
    """Draws an ellipse on the canvas."""

    width = attr.Measurement(
        title=u'Width',
        description=u'The width of the rectangle.',
        required=True)

    height = attr.Measurement(
        title=u'Height',
        description=u'The height of the rectangle.',
        required=True)

    round = attr.Measurement(
        title=u'Corner Radius',
        description=u'The radius of the rounded corners.',
        required=False)

class Rectangle(CanvasRMLDirective):
    signature = IRectangle
    callable = 'rect'
    attrMapping = {'round': 'radius'}

    def process(self):
        if 'round' in self.element.keys():
            self.callable = 'roundRect'
        super(Rectangle, self).process()


class IGrid(interfaces.IRMLDirectiveSignature):
    """A shape to be drawn on the canvas."""

    xs = attr.Sequence(
        title=u'X-Coordinates',
        description=(u'A sequence x-coordinates that represent the vertical '
                     u'line positions.'),
        value_type=attr.Measurement(),
        required=True)

    ys = attr.Sequence(
        title=u'Y-Coordinates',
        description=(u'A sequence y-coordinates that represent the horizontal '
                     u'line positions.'),
        value_type=attr.Measurement(),
        required=True)


class Grid(CanvasRMLDirective):
    signature = IGrid
    callable = 'grid'
    attrMapping = {'xs': 'xlist', 'ys': 'ylist'}


class ILines(interfaces.IRMLDirectiveSignature):
    """A path of connected lines drawn on the canvas."""

    linelist = attr.TextNodeGrid(
        title=u'Line List',
        description=(u'A list of lines coordinates to draw.'),
        value_type=attr.Measurement(),
        columns=4,
        required=True)

class Lines(CanvasRMLDirective):
    signature = ILines
    callable = 'lines'


class ICurves(interfaces.IRMLDirectiveSignature):
    """A path of connected bezier curves drawn on the canvas."""

    curvelist = attr.TextNodeGrid(
        title=u'Curve List',
        description=(u'A list of curve coordinates to draw.'),
        value_type=attr.Measurement(),
        columns=8,
        required=True)

class Curves(CanvasRMLDirective):
    signature = ICurves
    callable = 'bezier'

    def process(self):
        argset = self.getAttributeValues(valuesOnly=True)[0]
        canvas = attr.getManager(self, interfaces.ICanvasManager).canvas
        for args in argset:
            getattr(canvas, self.callable)(*args)


class IImage(interfaces.IRMLDirectiveSignature):
    """Draws an external image on the canvas."""

    file = attr.Image(
        title=u'File',
        description=(u'Reference to the external file of the iamge.'),
        required=True)

    x = attr.Measurement(
        title=u'X-Coordinate',
        description=(u'The X-coordinate of the lower-left position of the '
                     u'shape.'),
        required=True)

    y = attr.Measurement(
        title=u'Y-Coordinate',
        description=(u'The Y-coordinate of the lower-left position of the '
                     u'shape.'),
        required=True)

    width = attr.Measurement(
        title=u'Width',
        description=u'The width of the image.',
        required=False)

    height = attr.Measurement(
        title=u'Height',
        description=u'The height of the image.',
        required=False)

    showBoundary = attr.Boolean(
        title=u'Show Boundary',
        description=(u'A flag determining whether a border should be drawn '
                     u'around the image.'),
        default=False,
        required=False)

    preserveAspectRatio = attr.Boolean(
        title=u'Preserve Aspect Ratio',
        description=(u"A flag determining whether the image's aspect ration "
                     u"should be conserved under any circumstances."),
        default=False,
        required=False)

class Image(CanvasRMLDirective):
    signature = IImage
    callable = 'drawImage'
    attrMapping = {'file': 'image'}

    def process(self):
        kwargs = dict(self.getAttributeValues(attrMapping=self.attrMapping))
        preserve = kwargs.pop('preserveAspectRatio')
        show = kwargs.pop('showBoundary')

        if preserve:
            imgX, imgY = kwargs['image'].getSize()

            # Scale image correctly, if width and/or height were specified
            if 'width' in kwargs and 'height' not in kwargs:
                kwargs['height'] = imgY * kwargs['width'] / imgX
            elif 'height' in kwargs and 'width' not in kwargs:
                kwargs['width'] = imgX * kwargs['height'] / imgY
            elif 'width' in kwargs and 'height' in kwargs:
                if float(kwargs['width'])/kwargs['height'] > float(imgX)/imgY:
                    kwargs['width'] = imgX * kwargs['height'] / imgY
                else:
                    kwargs['height'] = imgY * kwargs['width'] / imgX

        canvas = attr.getManager(self, interfaces.ICanvasManager).canvas
        getattr(canvas, self.callable)(**kwargs)

        if show:
            width = kwargs.get('width', kwargs['image'].getSize()[0])
            height = kwargs.get('height', kwargs['image'].getSize()[1])
            canvas.rect(kwargs['x'], kwargs['y'], width, height)


class IPlace(interfaces.IRMLDirectiveSignature):
    """Draws a set of flowables on the canvas within a given region."""

    x = attr.Measurement(
        title=u'X-Coordinate',
        description=(u'The X-coordinate of the lower-left position of the '
                     u'place.'),
        required=True)

    y = attr.Measurement(
        title=u'Y-Coordinate',
        description=(u'The Y-coordinate of the lower-left position of the '
                     u'place.'),
        required=True)

    width = attr.Measurement(
        title=u'Width',
        description=u'The width of the place.',
        required=False)

    height = attr.Measurement(
        title=u'Height',
        description=u'The height of the place.',
        required=False)

class Place(CanvasRMLDirective):
    signature = IPlace

    def process(self):
        x, y, width, height = self.getAttributeValues(
            select=('x', 'y', 'width', 'height'), valuesOnly=True)
        y += height

        flows = flowable.Flow(self.element, self.parent)
        flows.process()

        canvas = attr.getManager(self, interfaces.ICanvasManager).canvas
        for flow in flows.flow:
            flowWidth, flowHeight = flow.wrap(width, height)
            if flowWidth <= width and flowHeight <= height:
                y -= flowHeight
                flow.drawOn(canvas, x, y)
                height -= flowHeight
            else:
                raise ValueError("Not enough space")


class IParam(interfaces.IRMLDirectiveSignature):
    """Sets one paramter for the text annotation."""

    name = attr.String(
        title=u'Name',
        description=u'The name of the paramter.',
        required=True)

    value = attr.TextNode(
        title=u'Value',
        description=(u'The parameter value.'),
        required=True)

class Param(directive.RMLDirective):
    signature = IParam

    def process(self):
        args = dict(self.getAttributeValues())
        self.parent.params[args['name']] = args['value']


class ITextAnnotation(interfaces.IRMLDirectiveSignature):
    """Writes a low-level text annotation into the PDF."""
    occurence.containing(
        occurence.ZeroOrMore('param', IParam))

    contents = attr.FirstLevelTextNode(
        title=u'Contents',
        description=u'The PDF commands that are inserted as annotation.',
        required=True)

class TextAnnotation(CanvasRMLDirective):
    signature = ITextAnnotation
    factories = {'param': Param}

    paramTypes = {'escape': attr.Integer()}

    def process(self):
        contents = self.getAttributeValues(valuesOnly=True)[0]
        self.params = {}
        self.processSubDirectives()
        for name, type in self.paramTypes.items():
            if name in self.params:
                bound = type.bind(self)
                self.params[name] = bound.fromUnicode(self.params[name])
        canvas = attr.getManager(self, interfaces.ICanvasManager).canvas
        canvas.textAnnotation(contents, **self.params)


class IMoveTo(interfaces.IRMLDirectiveSignature):
    """Move the path cursor to the specified location."""

    position = attr.TextNodeSequence(
        title=u'Position',
        description=u'Position to which the path pointer is moved to.',
        value_type=attr.Measurement(),
        min_length=2,
        max_length=2,
        required=True)

class MoveTo(directive.RMLDirective):
    signature = IMoveTo

    def process(self):
        args = self.getAttributeValues(valuesOnly=True)
        self.parent.path.moveTo(*args[0])


class ICurveTo(interfaces.IRMLDirectiveSignature):
    """Create a bezier curve from the current location to the specified one."""

    curvelist = attr.TextNodeGrid(
        title=u'Curve Specification',
        description=u'Describes the end position and the curve properties.',
        value_type=attr.Measurement(),
        columns=6,
        required=True)

class CurveTo(directive.RMLDirective):
    signature = ICurveTo

    def process(self):
        argset = self.getAttributeValues(valuesOnly=True)[0]
        for args in argset:
            self.parent.path.curveTo(*args)

class ICurvesTo(ICurveTo):
    pass
directive.DeprecatedDirective(
    ICurvesTo,
    'Available for ReportLab RML compatibility. Please use the "curveto" '
    'directive instead.')


class IPath(IShape):
    """Create a line path."""
    occurence.containing(
        occurence.ZeroOrMore('moveto', IMoveTo),
        occurence.ZeroOrMore('curveto', ICurveTo),
        occurence.ZeroOrMore('curvesto', ICurvesTo),
        )

    points = attr.TextNodeGrid(
        title=u'Points',
        description=(u'A list of coordinate points that define th path.'),
        value_type=attr.Measurement(),
        columns=2,
        required=True)

    close = attr.Boolean(
        title=u'Close Path',
        description=(u"A flag specifying whether the path should be closed."),
        default=False,
        required=False)

class Path(CanvasRMLDirective):
    signature = IPath
    factories = {
        'moveto': MoveTo,
        'curveto': CurveTo,
        'curvesto': CurveTo
        }

    def processPoints(self, text):
        if text.strip() == '':
            return
        bound = self.signature['points'].bind(self)
        for coords in bound.fromUnicode(text):
            self.path.lineTo(*coords)

    def process(self):
        kwargs = dict(self.getAttributeValues(ignore=('points',)))

        # Start the path and set the cursor to the start location.
        canvas = attr.getManager(self, interfaces.ICanvasManager).canvas
        self.path = canvas.beginPath()
        self.path.moveTo(kwargs.pop('x'), kwargs.pop('y'))

        # Process the text before the first sub-directive.
        if self.element.text is not None:
            self.processPoints(self.element.text)
        # Handle each sub-directive.
        for directive in self.element.getchildren():
            if directive.tag in self.factories:
                self.factories[directive.tag](directive, self).process()
            # If there is more text after sub-directive, process it.
            if directive.tail is not None:
                self.processPoints(directive.tail)

        if kwargs.pop('close', False):
            self.path.close()

        canvas.drawPath(self.path, **kwargs)


class IFill(interfaces.IRMLDirectiveSignature):
    """Set the fill color."""

    color = attr.Color(
        title=u'Color',
        description=(u'The color value to be set.'),
        required=True)

class Fill(CanvasRMLDirective):
    signature = IFill
    callable = 'setFillColor'
    attrMapping = {'color': 'aColor'}


class IStroke(interfaces.IRMLDirectiveSignature):
    """Set the stroke/line color."""

    color = attr.Color(
        title=u'Color',
        description=(u'The color value to be set.'),
        required=True)

class Stroke(CanvasRMLDirective):
    signature = IStroke
    callable = 'setStrokeColor'
    attrMapping = {'color': 'aColor'}


class ISetFont(interfaces.IRMLDirectiveSignature):
    """Set the font name and/or size."""

    name = attr.String(
        title=u'Font Name',
        description=(u'The name of the font as it was registered.'),
        required=True)

    size = attr.Measurement(
        title=u'Size',
        description=(u'The font size.'),
        required=True)

    leading = attr.Measurement(
        title=u'Leading',
        description=(u'The font leading.'),
        required=False)

class SetFont(CanvasRMLDirective):
    signature = ISetFont
    callable = 'setFont'
    attrMapping = {'name': 'psfontname'}


class IScale(interfaces.IRMLDirectiveSignature):
    """Scale the drawing using x and y scaling factors."""

    sx = attr.Float(
        title=u'X-Scaling-Factor',
        description=(u'The scaling factor applied on x-coordinates.'),
        required=True)

    sy = attr.Float(
        title=u'Y-Scaling-Factor',
        description=(u'The scaling factor applied on y-coordinates.'),
        required=True)

class Scale(CanvasRMLDirective):
    signature = IScale
    callable = 'scale'
    attrMapping = {'sx': 'x', 'sy': 'y'}


class ITranslate(interfaces.IRMLDirectiveSignature):
    """Translate the drawing coordinates by the specified x and y offset."""

    dx = attr.Measurement(
        title=u'X-Offset',
        description=(u'The amount to move the drawing to the right.'),
        required=True)

    dy = attr.Measurement(
        title=u'Y-Offset',
        description=(u'The amount to move the drawing upward.'),
        required=True)

class Translate(CanvasRMLDirective):
    signature = ITranslate
    callable = 'translate'


class IRotate(interfaces.IRMLDirectiveSignature):
    """Rotate the drawing counterclockwise."""

    degrees = attr.Measurement(
        title=u'Angle',
        description=(u'The angle in degrees.'),
        required=True)

class Rotate(CanvasRMLDirective):
    signature = IRotate
    callable = 'rotate'
    attrMapping = {'degrees': 'theta'}


class ISkew(interfaces.IRMLDirectiveSignature):
    """Skew the drawing."""

    alpha = attr.Measurement(
        title=u'Alpha',
        description=(u'The amount to skew the drawing in the horizontal.'),
        required=True)

    beta = attr.Measurement(
        title=u'Beta',
        description=(u'The amount to skew the drawing in the vertical.'),
        required=True)

class Skew(CanvasRMLDirective):
    signature = ISkew
    callable = 'skew'


class ITransform(interfaces.IRMLDirectiveSignature):
    """A full 2-D matrix transformation"""

    matrix = attr.TextNodeSequence(
        title=u'Matrix',
        description=u'The transformation matrix.',
        value_type=attr.Float(),
        min_length=6,
        max_length=6,
        required=True)

class Transform(CanvasRMLDirective):
    signature = ITransform

    def process(self):
        args = self.getAttributeValues(valuesOnly=True)
        canvas = attr.getManager(self, interfaces.ICanvasManager).canvas
        canvas.transform(*args[0])


class ILineMode(interfaces.IRMLDirectiveSignature):
    """Set the line mode for the following graphics elements."""

    width = attr.Measurement(
        title=u'Width',
        description=(u'The line width.'),
        required=False)

    dash = attr.Sequence(
        title=u'Dash-Pattern',
        description=(u'The dash-pattern of a line.'),
        value_type=attr.Measurement(),
        required=False)

    miterLimit = attr.Measurement(
        title=u'Miter Limit',
        description=(u'The ???.'),
        required=False)

    join = attr.Choice(
        title=u'Join',
        description=u'The way lines are joined together.',
        choices=interfaces.JOIN_CHOICES,
        required=False)

    cap = attr.Choice(
        title=u'Cap',
        description=u'The cap is the desciption of how the line-endings look.',
        choices=interfaces.CAP_CHOICES,
        required=False)

class LineMode(CanvasRMLDirective):
    signature = ILineMode

    def process(self):
        kw = dict(self.getAttributeValues())
        canvas = attr.getManager(self, interfaces.ICanvasManager).canvas
        if 'width' in kw:
            canvas.setLineWidth(kw['width'])
        if 'join' in kw:
            canvas.setLineJoin(kw['join'])
        if 'cap' in kw:
            canvas.setLineCap(kw['cap'])
        if 'miterLimit' in kw:
            canvas.setMiterLimit(kw['miterLimit'])
        if 'dash' in kw:
            canvas.setDash(kw['dash'])


class IDrawing(interfaces.IRMLDirectiveSignature):
    """A container directive for all directives that draw directly on the
    cnavas."""
    occurence.containing(
        occurence.ZeroOrMore('drawString', IDrawString),
        occurence.ZeroOrMore('drawRightString', IDrawRightString),
        occurence.ZeroOrMore('drawCenteredString', IDrawCenteredString),
        occurence.ZeroOrMore('drawCentredString', IDrawCenteredString),
        occurence.ZeroOrMore('drawAlignedString', IDrawAlignedString),
        # Drawing Operations
        occurence.ZeroOrMore('ellipse', IEllipse),
        occurence.ZeroOrMore('circle', ICircle),
        occurence.ZeroOrMore('rect', IRectangle),
        occurence.ZeroOrMore('grid', IGrid),
        occurence.ZeroOrMore('lines', ILines),
        occurence.ZeroOrMore('curves', ICurves),
        occurence.ZeroOrMore('image', IImage),
        occurence.ZeroOrMore('place', IPlace),
        occurence.ZeroOrMore('textAnnotation', ITextAnnotation),
        occurence.ZeroOrMore('path', IPath),
        # State Change Operations
        occurence.ZeroOrMore('fill', IFill),
        occurence.ZeroOrMore('stroke', IStroke),
        occurence.ZeroOrMore('setFont', ISetFont),
        occurence.ZeroOrMore('scale', IScale),
        occurence.ZeroOrMore('translate', ITranslate),
        occurence.ZeroOrMore('rotate', IRotate),
        occurence.ZeroOrMore('skew', ISkew),
        occurence.ZeroOrMore('transform', ITransform),
        occurence.ZeroOrMore('lineMode', ILineMode),
        # Form Field Elements
        occurence.ZeroOrMore('barCode', form.IBarCode),
        occurence.ZeroOrMore('textField', form.ITextField),
        occurence.ZeroOrMore('buttonField', form.IButtonField),
        occurence.ZeroOrMore('selectField', form.ISelectField),
        # Charts
        occurence.ZeroOrMore('barChart', chart.IBarChart),
        occurence.ZeroOrMore('barChart3D', chart.IBarChart3D),
        occurence.ZeroOrMore('linePlot', chart.ILinePlot),
        occurence.ZeroOrMore('pieChart', chart.IPieChart),
        occurence.ZeroOrMore('pieChart3D', chart.IPieChart3D),
        occurence.ZeroOrMore('spiderChart', chart.ISpiderChart),
        )

class Drawing(directive.RMLDirective):
    signature = IDrawing

    factories = {
        'drawString': DrawString,
        'drawRightString': DrawRightString,
        'drawCenteredString': DrawCenteredString,
        'drawCentredString': DrawCenteredString,
        'drawAlignedString': DrawAlignedString,
        # Drawing Operations
        'ellipse': Ellipse,
        'circle': Circle,
        'rect': Rectangle,
        'grid': Grid,
        'lines': Lines,
        'curves': Curves,
        'image': Image,
        'place': Place,
        'textAnnotation': TextAnnotation,
        'path': Path,
        # Form Field Elements
        'barCode': form.BarCode,
        'textField': form.TextField,
        'buttonField': form.ButtonField,
        'selectField': form.SelectField,
        # State Change Operations
        'fill': Fill,
        'stroke': Stroke,
        'setFont': SetFont,
        'scale': Scale,
        'translate': Translate,
        'rotate': Rotate,
        'skew': Skew,
        'transform': Transform,
        'lineMode': LineMode,
        # Charts
        'barChart': chart.BarChart,
        'barChart3D': chart.BarChart3D,
        'linePlot': chart.LinePlot,
        'pieChart': chart.PieChart,
        'pieChart3D': chart.PieChart3D,
        'spiderChart': chart.SpiderChart
        }


class IPageDrawing(IDrawing):
    """Draws directly on the content of one page's canvas. Every call of this
    directive creates a new page."""

    occurence.containing(
        #'mergePage': IMergePage,
        *IDrawing.getTaggedValue('directives'))

class PageDrawing(Drawing):
    signature = IDrawing

    factories = Drawing.factories.copy()
    factories.update({
        'mergePage': page.MergePage
        })

    def process(self):
        super(Drawing, self).process()
        canvas = attr.getManager(self, interfaces.ICanvasManager).canvas
        canvas.showPage()


class IPageInfo(interfaces.IRMLDirectiveSignature):
    """Set's up page-global settings."""

    pageSize = attr.PageSize(
        title=u'Page Size',
        description=(u'The page size of all pages within this document.'),
        required=True)

class PageInfo(CanvasRMLDirective):
    signature=IPageInfo
    callable = 'setPageSize'
    attrMapping = {'pageSize': 'size'}
