#Copyright ReportLab Europe Ltd. 2000-2017
#see license.txt for license details
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/textlabels.py
__version__='3.3.0'

from reportlab.lib import colors
from reportlab.lib.utils import simpleSplit
from reportlab.lib.geomutils import normalizeTRBL
from reportlab.lib.validators import isNumber, isNumberOrNone, OneOf, isColorOrNone, isString, \
        isTextAnchor, isBoxAnchor, isBoolean, NoneOr, isInstanceOf, isNoneOrString, isNoneOrCallable, \
        isSubclassOf, EitherOr, isListOfNumbers
from reportlab.lib.attrmap import *
from reportlab.pdfbase.pdfmetrics import stringWidth, getAscentDescent
from reportlab.graphics.shapes import Drawing, Group, Circle, Rect, String, STATE_DEFAULTS
from reportlab.graphics.widgetbase import Widget, PropHolder
from reportlab.graphics.shapes import DirectDraw
from reportlab.platypus import XPreformatted, Flowable
from reportlab.lib.styles import ParagraphStyle, PropertySet
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER
_ta2al = dict(start=TA_LEFT,end=TA_RIGHT,middle=TA_CENTER)
from ..utils import text2Path as _text2Path   #here for continuity

_A2BA=  {
        'x': {0:'n', 45:'ne', 90:'e', 135:'se', 180:'s', 225:'sw', 270:'w', 315: 'nw', -45: 'nw'},
        'y': {0:'e', 45:'se', 90:'s', 135:'sw', 180:'w', 225:'nw', 270:'n', 315: 'ne', -45: 'ne'},
        }

try:
    from rlextra.graphics.canvasadapter import DirectDrawFlowable
except ImportError:
    DirectDrawFlowable = None

_BA2TA={'w':'start','nw':'start','sw':'start','e':'end', 'ne': 'end', 'se':'end', 'n':'middle','s':'middle','c':'middle'}
class Label(Widget):
    """A text label to attach to something else, such as a chart axis.

    This allows you to specify an offset, angle and many anchor
    properties relative to the label's origin.  It allows, for example,
    angled multiline axis labels.
    """
    # fairly straight port of Robin Becker's textbox.py to new widgets
    # framework.

    _attrMap = AttrMap(
        x = AttrMapValue(isNumber,desc=''),
        y = AttrMapValue(isNumber,desc=''),
        dx = AttrMapValue(isNumber,desc='delta x - offset'),
        dy = AttrMapValue(isNumber,desc='delta y - offset'),
        angle = AttrMapValue(isNumber,desc='angle of label: default (0), 90 is vertical, 180 is upside down, etc'),
        boxAnchor = AttrMapValue(isBoxAnchor,desc='anchoring point of the label'),
        boxStrokeColor = AttrMapValue(isColorOrNone,desc='border color of the box'),
        boxStrokeWidth = AttrMapValue(isNumber,desc='border width'),
        boxFillColor = AttrMapValue(isColorOrNone,desc='the filling color of the box'),
        boxTarget = AttrMapValue(OneOf('normal','anti','lo','hi'),desc="one of ('normal','anti','lo','hi')"),
        boxRx = AttrMapValue(isNumber,desc='box corner x radius'),
        boxRy = AttrMapValue(isNumber,desc='box corner y radius'),
        fillColor = AttrMapValue(isColorOrNone,desc='label text color'),
        strokeColor = AttrMapValue(isColorOrNone,desc='label text border color'),
        strokeWidth = AttrMapValue(isNumber,desc='label text border width'),
        text = AttrMapValue(isString,desc='the actual text to display'),
        fontName = AttrMapValue(isString,desc='the name of the font used'),
        fontSize = AttrMapValue(isNumber,desc='the size of the font'),
        leading = AttrMapValue(isNumberOrNone,desc=''),
        width = AttrMapValue(isNumberOrNone,desc='the width of the label'),
        maxWidth = AttrMapValue(isNumberOrNone,desc='maximum width the label can grow to'),
        height = AttrMapValue(isNumberOrNone,desc='the height of the text'),
        textAnchor = AttrMapValue(isTextAnchor,desc='the anchoring point of the text inside the label'),
        visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
        topPadding = AttrMapValue(isNumber,desc='padding at top of box'),
        leftPadding = AttrMapValue(isNumber,desc='padding at left of box'),
        rightPadding = AttrMapValue(isNumber,desc='padding at right of box'),
        bottomPadding = AttrMapValue(isNumber,desc='padding at bottom of box'),
        padding = AttrMapValue(EitherOr((isNumberOrNone,isListOfNumbers)),'TRBL css like padding'),
        useAscentDescent = AttrMapValue(isBoolean,desc="If True then the font's Ascent & Descent will be used to compute default heights and baseline."),
        customDrawChanger = AttrMapValue(isNoneOrCallable,desc="An instance of CustomDrawChanger to modify the behavior at draw time", _advancedUsage=1),
        ddf = AttrMapValue(NoneOr(isSubclassOf(DirectDraw),'NoneOrDirectDraw'),desc="A DirectDrawFlowable instance", _advancedUsage=1),
        ddfKlass = AttrMapValue(NoneOr(isSubclassOf(Flowable),'NoneOrDirectDraw'),desc="A Flowable class for direct drawing (default is XPreformatted", _advancedUsage=1),
        ddfStyle = AttrMapValue(NoneOr((isSubclassOf(PropertySet),isInstanceOf(PropertySet))),desc="A style or style class for a ddfKlass or None", _advancedUsage=1),
        )

    def __init__(self,**kw):
        self._setKeywords(**kw)
        self._setKeywords(
                _text = 'Multi-Line\nString',
                boxAnchor = 'c',
                angle = 0,
                x = 0,
                y = 0,
                dx = 0,
                dy = 0,
                topPadding = 0,
                leftPadding = 0,
                rightPadding = 0,
                bottomPadding = 0,
                boxStrokeWidth = 0.5,
                boxStrokeColor = None,
                boxTarget = 'normal',
                boxRx = 0,
                boxRy = 0,
                strokeColor = None,
                boxFillColor = None,
                leading = None,
                width = None,
                maxWidth = None,
                height = None,
                fillColor = STATE_DEFAULTS['fillColor'],
                fontName = STATE_DEFAULTS['fontName'],
                fontSize = STATE_DEFAULTS['fontSize'],
                strokeWidth = 0.1,
                textAnchor = 'start',
                visible = 1,
                useAscentDescent = False,
                ddf = DirectDrawFlowable,
                ddfKlass = getattr(self.__class__,'ddfKlass',None),
                ddfStyle = getattr(self.__class__,'ddfStyle',None),
                )

    @property
    def padding(self):
        p = self.topPadding, self.rightPadding, self.bottomPadding, self.leftPadding
        n = len(set(p))
        if n==1: return p[0]
        elif n==2 and p[0]==p[2] and p[1]==p[3]: return p[:2]
        elif n==3 and p[1]==p[3]: return p[:3]
        return p

    @padding.setter
    def padding(self,p):
        self.topPadding, self.rightPadding, self.bottomPadding, self.leftPadding = normalizeTRBL(p)

    def setText(self, text):
        """Set the text property.  May contain embedded newline characters.
        Called by the containing chart or axis."""
        self._text = text


    def setOrigin(self, x, y):
        """Set the origin.  This would be the tick mark or bar top relative to
        which it is defined.  Called by the containing chart or axis."""
        self.x = x
        self.y = y


    def demo(self):
        """This shows a label positioned with its top right corner
        at the top centre of the drawing, and rotated 45 degrees."""

        d = Drawing(200, 100)

        # mark the origin of the label
        d.add(Circle(100,90, 5, fillColor=colors.green))

        lab = Label()
        lab.setOrigin(100,90)
        lab.boxAnchor = 'ne'
        lab.angle = 45
        lab.dx = 0
        lab.dy = -20
        lab.boxStrokeColor = colors.green
        lab.setText('Another\nMulti-Line\nString')
        d.add(lab)

        return d

    def _getBoxAnchor(self):
        '''hook for allowing special box anchor effects'''
        ba = self.boxAnchor
        if ba in ('autox', 'autoy'):
            angle = self.angle
            na = (int((angle%360)/45.)*45)%360
            if not (na % 90): # we have a right angle case
                da = (angle - na) % 360
                if abs(da)>5:
                    na = na + (da>0 and 45 or -45)
            ba = _A2BA[ba[-1]][na]
        return ba

    def _getBaseLineRatio(self):
        if self.useAscentDescent:
            self._ascent, self._descent = getAscentDescent(self.fontName,self.fontSize)
            self._baselineRatio = self._ascent/(self._ascent-self._descent)
        else:
            self._baselineRatio = 1/1.2

    def _computeSizeEnd(self,objH):
        self._height = self.height or (objH + self.topPadding + self.bottomPadding)
        self._ewidth = (self._width-self.leftPadding-self.rightPadding)
        self._eheight = (self._height-self.topPadding-self.bottomPadding)
        boxAnchor = self._getBoxAnchor()
        if boxAnchor in ['n','ne','nw']:
            self._top = -self.topPadding
        elif boxAnchor in ['s','sw','se']:
            self._top = self._height-self.topPadding
        else:
            self._top = 0.5*self._eheight
        self._bottom = self._top - self._eheight

        if boxAnchor in ['ne','e','se']:
            self._left = self.leftPadding - self._width
        elif boxAnchor in ['nw','w','sw']:
            self._left = self.leftPadding
        else:
            self._left = -self._ewidth*0.5
        self._right = self._left+self._ewidth

    def computeSize(self):
        # the thing will draw in its own coordinate system
        ddfKlass = getattr(self,'ddfKlass',None)
        if not ddfKlass:
            self._lineWidths = []
            self._lines = simpleSplit(self._text,self.fontName,self.fontSize,self.maxWidth)
            if not self.width:
                self._width = self.leftPadding+self.rightPadding
                if self._lines:
                    self._lineWidths = [stringWidth(line,self.fontName,self.fontSize) for line in self._lines]
                    self._width += max(self._lineWidths)
            else:
                self._width = self.width
            self._getBaseLineRatio()
            if self.leading:
                self._leading = self.leading
            elif self.useAscentDescent:
                self._leading = self._ascent - self._descent
            else:
                self._leading = self.fontSize*1.2
            objH = self._leading*len(self._lines)
        else:
            if self.ddf is None:
                raise RuntimeError('DirectDrawFlowable class is not available you need the rlextra package as well as reportlab')
            sty = dict(
                    name='xlabel-generated',
                    fontName=self.fontName,
                    fontSize=self.fontSize,
                    fillColor=self.fillColor,
                    strokeColor=self.strokeColor,
                    )

            if not self.ddfStyle:
                sty = ParagraphStyle(**sty)
            elif isinstance(self.ddfStyle,PropertySet):
                sty = self.ddfStyle.clone(**sty)
            elif isinstance(self.ddfStyle,type) and issubclass(self.ddfStyle,PropertySet):
                sty = self.ddfStyle(**sty)
            else:
                raise ValueError(f'ddfStyle has invalid type {type(self.ddfStyle)}')

            self._style = sty
            self._getBaseLineRatio()
            if self.useAscentDescent:
                sty.autoLeading = True
                sty.leading = self._ascent - self._descent
            else:
                sty.leading = self.leading if self.leading else self.fontSize*1.2
            self._leading = sty.leading
            ta = self._getTextAnchor()

            aW = self.maxWidth or 0x7fffffff
            if ta!='start':
                sty.alignment = TA_LEFT
                obj = ddfKlass(self._text,style=sty)
                _, objH = obj.wrap(aW,0x7fffffff)
                aW = self.maxWidth or obj._width_max
            sty.alignment = _ta2al[ta]
            self._ddfObj = obj = ddfKlass(self._text,style=sty)
            _, objH = obj.wrap(aW,0x7fffffff)

            if not self.width:
                self._width = self.leftPadding+self.rightPadding
                self._width += obj._width_max
            else:
                self._width = self.width
        self._computeSizeEnd(objH)

    def _getTextAnchor(self):
        '''This can be overridden to allow special effects'''
        ta = self.textAnchor
        if ta=='boxauto': ta = _BA2TA[self._getBoxAnchor()]
        return ta

    def _rawDraw(self):
        _text = self._text
        self._text = _text or ''
        self.computeSize()
        self._text = _text
        g = Group()
        g.translate(self.x + self.dx, self.y + self.dy)
        g.rotate(self.angle)

        ddfKlass = getattr(self,'ddfKlass',None)
        if ddfKlass:
            x = self._left
        else:
            y = self._top - self._leading*self._baselineRatio
            textAnchor = self._getTextAnchor()
            if textAnchor == 'start':
                x = self._left
            elif textAnchor == 'middle':
                x = self._left + self._ewidth*0.5
            else:
                x = self._right

        # paint box behind text just in case they
        # fill it
        if self.boxFillColor or (self.boxStrokeColor and self.boxStrokeWidth):
            g.add(Rect( self._left-self.leftPadding,
                        self._bottom-self.bottomPadding,
                        self._width,
                        self._height,
                        strokeColor=self.boxStrokeColor,
                        strokeWidth=self.boxStrokeWidth,
                        fillColor=self.boxFillColor,
                        rx=self.boxRx, 
                        ry=self.boxRy, 
                        ))

        if ddfKlass:
            g1 = Group()
            g1.translate(x,self._top-self._eheight)
            g1.add(self.ddf(self._ddfObj))
            g.add(g1)
        else:
            fillColor, fontName, fontSize = self.fillColor, self.fontName, self.fontSize
            strokeColor, strokeWidth, leading = self.strokeColor, self.strokeWidth, self._leading
            svgAttrs=getattr(self,'_svgAttrs',{})
            if strokeColor:
                for line in self._lines:
                    s = _text2Path(line, x, y, fontName, fontSize, textAnchor)
                    s.fillColor = fillColor
                    s.strokeColor = strokeColor
                    s.strokeWidth = strokeWidth
                    g.add(s)
                    y -= leading
            else:
                for line in self._lines:
                    s = String(x, y, line, _svgAttrs=svgAttrs)
                    s.textAnchor = textAnchor
                    s.fontName = fontName
                    s.fontSize = fontSize
                    s.fillColor = fillColor
                    g.add(s)
                    y -= leading

        return g

    def draw(self):
        customDrawChanger = getattr(self,'customDrawChanger',None)
        if customDrawChanger:
            customDrawChanger(True,self)
            try:
                return self._rawDraw()
            finally:
                customDrawChanger(False,self)
        else:
            return self._rawDraw()

class LabelDecorator:
    _attrMap = AttrMap(
        x = AttrMapValue(isNumberOrNone,desc=''),
        y = AttrMapValue(isNumberOrNone,desc=''),
        dx = AttrMapValue(isNumberOrNone,desc=''),
        dy = AttrMapValue(isNumberOrNone,desc=''),
        angle = AttrMapValue(isNumberOrNone,desc=''),
        boxAnchor = AttrMapValue(isBoxAnchor,desc=''),
        boxStrokeColor = AttrMapValue(isColorOrNone,desc=''),
        boxStrokeWidth = AttrMapValue(isNumberOrNone,desc=''),
        boxFillColor = AttrMapValue(isColorOrNone,desc=''),
        fillColor = AttrMapValue(isColorOrNone,desc=''),
        strokeColor = AttrMapValue(isColorOrNone,desc=''),
        strokeWidth = AttrMapValue(isNumberOrNone),desc='',
        fontName = AttrMapValue(isNoneOrString,desc=''),
        fontSize = AttrMapValue(isNumberOrNone,desc=''),
        leading = AttrMapValue(isNumberOrNone,desc=''),
        width = AttrMapValue(isNumberOrNone,desc=''),
        maxWidth = AttrMapValue(isNumberOrNone,desc=''),
        height = AttrMapValue(isNumberOrNone,desc=''),
        textAnchor = AttrMapValue(isTextAnchor,desc=''),
        visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
        )

    def __init__(self):
        self.textAnchor = 'start'
        self.boxAnchor = 'w'
        for a in self._attrMap.keys():
            if not hasattr(self,a): setattr(self,a,None)

    def decorate(self,l,L):
        chart,g,rowNo,colNo,x,y,width,height,x00,y00,x0,y0 = l._callOutInfo
        L.setText(chart.categoryAxis.categoryNames[colNo])
        g.add(L)

    def __call__(self,l):
        L = Label()
        for a,v in self.__dict__.items():
            if v is None: v = getattr(l,a,None)
            setattr(L,a,v)
        self.decorate(l,L)

isOffsetMode=OneOf('high','low','bar','axis')
class LabelOffset(PropHolder):
    _attrMap = AttrMap(
                posMode = AttrMapValue(isOffsetMode,desc="Where to base +ve offset"),
                pos = AttrMapValue(isNumber,desc='Value for positive elements'),
                negMode = AttrMapValue(isOffsetMode,desc="Where to base -ve offset"),
                neg = AttrMapValue(isNumber,desc='Value for negative elements'),
                )
    def __init__(self):
        self.posMode=self.negMode='axis'
        self.pos = self.neg = 0

    def _getValue(self, chart, val):
        flipXY = chart._flipXY
        A = chart.categoryAxis
        jA = A.joinAxis
        if val>=0:
            mode = self.posMode
            delta = self.pos
        else:
            mode = self.negMode
            delta = self.neg
        if flipXY:
            v = A._x
        else:
            v = A._y
        if jA:
            if flipXY:
                _v = jA._x
            else:
                _v = jA._y
            if mode=='high':
                v = _v + jA._length
            elif mode=='low':
                v = _v
            elif mode=='bar':
                v = _v+val
        return v+delta

NoneOrInstanceOfLabelOffset=NoneOr(isInstanceOf(LabelOffset))

class PMVLabel(Label):
    _attrMap = AttrMap(
        BASE=Label,
        )

    def __init__(self, **kwds):
        Label.__init__(self, **kwds)
        self._pmv = 0

    def _getBoxAnchor(self):
        a = Label._getBoxAnchor(self)
        if self._pmv<0: a = {'nw':'se','n':'s','ne':'sw','w':'e','c':'c','e':'w','sw':'ne','s':'n','se':'nw'}[a]
        return a

    def _getTextAnchor(self):
        a = Label._getTextAnchor(self)
        if self._pmv<0: a = {'start':'end', 'middle':'middle', 'end':'start'}[a]
        return a

class BarChartLabel(PMVLabel):
    """
    An extended Label allowing for nudging, lines visibility etc
    """
    _attrMap = AttrMap(
        BASE=PMVLabel,
        lineStrokeWidth = AttrMapValue(isNumberOrNone, desc="Non-zero for a drawn line"),
        lineStrokeColor = AttrMapValue(isColorOrNone, desc="Color for a drawn line"),
        fixedEnd = AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw ends +/-"),
        fixedStart = AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw starts +/-"),
        nudge = AttrMapValue(isNumber, desc="Non-zero sign dependent nudge"),
        boxTarget = AttrMapValue(OneOf('normal','anti','lo','hi','mid'),desc="one of ('normal','anti','lo','hi','mid')"),
        )

    def __init__(self, **kwds):
        PMVLabel.__init__(self, **kwds)
        self.lineStrokeWidth = 0
        self.lineStrokeColor = None
        self.fixedStart = self.fixedEnd = None
        self.nudge = 0

class NA_Label(BarChartLabel):
    """
    An extended Label allowing for nudging, lines visibility etc
    """
    _attrMap = AttrMap(
        BASE=BarChartLabel,
        text = AttrMapValue(isNoneOrString, desc="Text to be used for N/A values"),
        )
    def __init__(self):
        BarChartLabel.__init__(self)
        self.text = 'n/a'
NoneOrInstanceOfNA_Label=NoneOr(isInstanceOf(NA_Label))

from reportlab.graphics.charts.utils import CustomDrawChanger
class RedNegativeChanger(CustomDrawChanger):
    def __init__(self,fillColor=colors.red):
        CustomDrawChanger.__init__(self)
        self.fillColor = fillColor
    def _changer(self,obj):
        R = {}
        if obj._text.startswith('-'):
            R['fillColor'] = obj.fillColor
            obj.fillColor = self.fillColor
        return R

class XLabel(Label):
    '''like label but uses XPreFormatted/Paragraph to draw the _text'''
    _attrMap = AttrMap(BASE=Label,
            )
    def __init__(self,*args,**kwds):
        Label.__init__(self,*args,**kwds)
        self.ddfKlass = kwds.pop('ddfKlass',XPreformatted)
        self.ddf = kwds.pop('directDrawClass',self.ddf)

    if False:
        def __init__(self,*args,**kwds):
            self._flowableClass = kwds.pop('flowableClass',XPreformatted)
            ddf = kwds.pop('directDrawClass',DirectDrawFlowable)
            if ddf is None:
                raise RuntimeError('DirectDrawFlowable class is not available you need the rlextra package as well as reportlab')
            self._ddf = ddf
            Label.__init__(self,*args,**kwds)
        def computeSize(self):
            # the thing will draw in its own coordinate system
            self._lineWidths = []
            sty = self._style = ParagraphStyle('xlabel-generated',
                    fontName=self.fontName,
                    fontSize=self.fontSize,
                    fillColor=self.fillColor,
                    strokeColor=self.strokeColor,
                    )
            self._getBaseLineRatio()
            if self.useAscentDescent:
                sty.autoLeading = True
                sty.leading = self._ascent - self._descent
            else:
                sty.leading = self.leading if self.leading else self.fontSize*1.2
            self._leading = sty.leading
            ta = self._getTextAnchor()
            aW = self.maxWidth or 0x7fffffff
            if ta!='start':
                sty.alignment = TA_LEFT
                obj = self._flowableClass(self._text,style=sty)
                _, objH = obj.wrap(aW,0x7fffffff)
                aW = self.maxWidth or obj._width_max
            sty.alignment = _ta2al[ta]
            self._obj = obj = self._flowableClass(self._text,style=sty)
            _, objH = obj.wrap(aW,0x7fffffff)

            if not self.width:
                self._width = self.leftPadding+self.rightPadding
                self._width += self._obj._width_max
            else:
                self._width = self.width
            self._computeSizeEnd(objH)

        def _rawDraw(self):
            _text = self._text
            self._text = _text or ''
            self.computeSize()
            self._text = _text
            g = Group()
            g.translate(self.x + self.dx, self.y + self.dy)
            g.rotate(self.angle)

            x = self._left

            # paint box behind text just in case they
            # fill it
            if self.boxFillColor or (self.boxStrokeColor and self.boxStrokeWidth):
                g.add(Rect( self._left-self.leftPadding,
                            self._bottom-self.bottomPadding,
                            self._width,
                            self._height,
                            strokeColor=self.boxStrokeColor,
                            strokeWidth=self.boxStrokeWidth,
                            fillColor=self.boxFillColor)
                            )
            g1 = Group()
            g1.translate(x,self._top-self._eheight)
            g1.add(self._ddf(self._obj))
            g.add(g1)
            return g
