#
# Copyright (c) 2002, 2003 Art Haas
#
# This file is part of PythonCAD.
#
# PythonCAD 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 2 of the License, or
# (at your option) any later version.
#
# PythonCAD 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 PythonCAD; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#
# basic text functionality
#

import math
import types

import Generic.color
import Generic.util
import Generic.entity

#
# common constant for style and weight
#

NORMAL = 0

#
# styles
#

OBLIQUE = 1
ITALIC = 2

#
# weights
#

LIGHT = 1
BOLD = 2
HEAVY = 3

class TextFormat(Generic.entity.Entity):
    """A class for describing text string properties.

A TextFormat object has the following attributes:

family: The font family
style: The font style
size: The font size
weight: The font weight
color: The font color

A TextFormat object has the following methods:

{get/set}Family(): Get/Set the font family.
{get/set}Size(): Get/Set the font size.
{get/set}Style(): Get/Set the font style.
{get/set}Weight(): Get/Set the font weight.
{get/set}Color(): Get/Set the font color.
    """
    messages = {
        'fontfamily_changed' : True,
        'fontsize_changed' : True,
        'fontstyle_changed' : True,
        'fontweight_changed' : True,
        'fontcolor_changed' : True,
        }
    #
    # default text color
    #

    __defcolor = Generic.color.Color(0xffffff)

    def __init__(self, family='Sans', size=12, style=NORMAL, weight=NORMAL,
                 color=None):
        """Initialize a TextFormat object.

ts = TextFormat([family, size, style, weight, color])

Each argument has a default value:

family: Sans
size: 12
style: NORMAL
weight: NORMAL
color: A default color - #ffffff
        """
        _family = family
        if not isinstance(_family, types.StringTypes):
            raise TypeError, "Invalid font family name: " + str(_family)
        _size = size
        if not isinstance(_size, int):
            _size = int(size)
        if _size < 1:
            raise ValueError, "Invalid text size: %d" % _size
        _style = style
        if (_style != NORMAL and
            _style != OBLIQUE and
            _style != ITALIC):
            raise ValueError, "Invalid text style: " + str(style)
        _weight = weight
        if (_weight != NORMAL and
            _weight != LIGHT and
            _weight != BOLD and
            _weight != HEAVY):
            raise ValueError, "Invalid text weight: " + str(weight)
        _color = color
        if _color is None:
            _color = TextFormat.__defcolor
        if not isinstance(color, Generic.color.Color):
            _color = Generic.color.Color(color)
        Generic.entity.Entity.__init__(self)
        self.__family = _family
        self.__size = _size
        self.__fstyle = _style
        self.__weight = _weight
        self.__color = _color

    def __eq__(self, tf):
        """Compare to TextFormats for equality.
        """
        if not isinstance(tf, TextFormat):
            raise TypeError, "Invalid TextFormat: " + str(tf)
        _val = False
        if (self.__family == tf.getFamily() and
            self.__size == tf.getSize() and
            self.__fstyle == tf.getStyle() and
            self.__weight == tf.getWeight() and
            self.__color == tf.getColor()):
            _val = True
        return _val

    def __ne__(self, tf):
        """Compare to TextFormats for inequality.
        """
        if not isinstance(tf, TextFormat):
            raise TypeError, "Invalid TextFormat: " + str(tf)
        _val = True
        if (self.__family == tf.getFamily() and
            self.__size == tf.getSize() and
            self.__fstyle == tf.getStyle() and
            self.__weight == tf.getWeight() and
            self.__color == tf.getColor()):
            _val = False
        return _val

    def getFamily(self):
        """Return the font family.

getFamily()
        """
        return self.__family

    def setFamily(self, family): # what should default be?
        """Set the font family.

setFamily(family)
        """
        if self.isLocked():
            raise RuntimeError, "Setting family not allowed - object locked."
        if not isinstance(family, str):
            raise TypeError, "Invalid font family name: " + str(family)
        _f = self.__family
        if _f != family:
            self.__family = family
            self.sendMessage('fontfamily_changed', _f)
            self.modified()

    family = property(getFamily, setFamily, None, "Text font family")

    def getSize(self):
        """Return the font size.

getSize()
        """
        return self.__size

    def setSize(self, size=12):
        """Set the font size.

setSize([size])

The argument "size" should be an integer value
greater than 0. If this method is called without
arguments, the font size defaults to 12.
        """
        if self.isLocked():
            raise RuntimeError, "Setting size not allowed - object locked."
        _size = size
        if not isinstance(_size, int):
            _size = int(size)
        if _size < 1:
            raise ValueError, "Invalid text size: %d" % _size
        _s = self.__size
        if _s != _size:
            self.__size = _size
            self.sendMessage('fontsize_changed', _s)
            self.modified()

    size = property(getSize, setSize, None, "Text font size")

    def getStyle(self):
        """Return the font style.

getStyle()
        """
        return self.__fstyle

    def setStyle(self, style=NORMAL):
        """Set the font style.

setStyle([style])

The argument "style" should be one of the following:

NORMAL
OBLIQUE
ITALIC

If the method is invoked without an argument, the font
style is set to NORMAL.
        """
        if self.isLocked():
            raise RuntimeError, "Setting style not allowed - object locked."
        _style = style
        if (_style != NORMAL and
            _style != OBLIQUE and
            _style != ITALIC):
            raise ValueError, "Invalid text style: " + str(style)
        _s = self.__fstyle
        if _s != _style:
            self.__fstyle = _style
            self.sendMessage('fontstyle_changed', _s)
            self.modified()

    style = property(getStyle, setStyle, None, "Text font style")

    def getWeight(self):
        """Return the font weight.

getWeight()
        """
        return self.__weight

    def setWeight(self, weight=NORMAL):
        """Set the font weight.

setWeight([weight])

The argument "weight" should be one of the following:

NORMAL
LIGHT
BOLD
HEAVY

If the method is called without an argument, the weight
is set to NORMAL.
        """
        if self.isLocked():
            raise RuntimeError, "Setting weight not allowed - object locked."
        _weight = weight
        if (_weight != NORMAL and
            _weight != LIGHT and
            _weight != BOLD and
            _weight != HEAVY):
            raise ValueError, "Invalid text weight: " + str(weight)
        _w = self.__weight
        if _w != _weight:
            self.__weight = _weight
            self.sendMessage('fontweight_changed', _w)
            self.modified()

    weight = property(getWeight, setWeight, None, "Text font weight")

    def getColor(self):
        """Return the font color.

getColor()
        """
        return self.__color

    def setColor(self, color=None):
        """Set the font color.

setColor([color])

The argument "color" should be a Color object. If the method
is called without an argument, the font color is set to the
default text color.
        """
        if self.isLocked():
            raise RuntimeError, "Setting color not allowed - object locked."
        _color = color
        if _color is None:
            _color = TextFormat.__color
        if not isinstance(_color, Generic.color.Color):
            _color = Generic.color.Color(color)
        _c = self.__color
        if _c != _color:
            self.__color = _color
            self.sendMessage('fontcolor_changed', _c)
            self.modified()

    color = property(getColor, setColor, None, "Text color")

    def sendsMessage(self, m):
        if m in TextFormat.messages:
            return True
        return Generic.entity.Entity.sendsMessage(self, m)

def font_style_string(style):
    """Return a text string for the font style.

font_style_string(style)
    """
    if style == NORMAL:
        _str = 'normal'
    elif style == OBLIQUE:
        _str = 'oblique'
    elif style == ITALIC:
        _str = 'italic'
    else:
        raise ValueError, "Unknown font style: " + str(style)
    return _str

def font_weight_string(weight):
    """Return a text string for the font weight.

font_weight_string(weight)
    """
    if weight == NORMAL:
        _str = 'normal'
    elif weight == LIGHT:
        _str = 'light'
    elif weight == BOLD:
        _str = 'bold'
    elif weight == HEAVY:
        _str = 'heavy'
    else:
        raise ValueError, "Unknown font weight: " + str(weight)
    return _str

#
# line justification options
#

ALIGN_LEFT = 0
ALIGN_CENTER = 1
ALIGN_RIGHT = 2

class TextStyle(TextFormat):
    """A class for storing named TextFormat objects.

The TextStyle class is an extension of the TextFormat class
with the a 'name' used for referencing the properties stored
in an instance of this class. As this class is derived from
the TextFormat class, it shares all the attributes and methods
of that class, except once the values in the object are set
they cannot be changed.

The TextStyle class has the following extra attributes:

name: The name of the TextStyle object.

The TextStyle class has th following extra methods:

{get/set}Name(): Get/Set the name of the TextStyle.
{get/set}Alignment(): Get/Set the line justification.
    """

    #
    # default text color
    #

    __defcolor = Generic.color.Color(0xffffff)

    def __init__(self, name, family="Sans", size=12,
                 style=NORMAL, weight=NORMAL, color=None):
        if not isinstance(name, types.StringTypes):
            raise ValueError, "Invalid name: " + str(name)
        self.__name = None
        _name = name
        if isinstance(_name, str):
            _name = unicode(name)
        _color = color
        if _color is None:
            _color = TextStyle.__defcolor
        self.__initialized = False
        TextFormat.__init__(self, family, size, style, weight, _color)
        self.__name = _name
        self.__alignment = ALIGN_LEFT
        self._finalize()

    def _finalize(self):
        self.__initialized = True

    def __eq__(self, ts):
        """Test two TextStyles for equality.
        """
        if not isinstance(ts, TextStyle):
            raise TypeError, "Invalid TextStyle: " + str(ts)
        _val = False
        if (self.__name == ts.getName() and
            self.__alignment == ts.getAlignment() and
            TextFormat.__eq__(self, ts)):
            _val = True
        return _val

    def __ne__(self, ts):
        """Test two TextStyles for inequality.
        """
        if not isinstance(ts, TextStyle):
            raise TypeError, "Invalid TextStyle: " + str(ts)
        _val = True
        if (self.__name == ts.getName() and
            self.__alignment == ts.getAlignment() and
            TextFormat.__eq__(self, ts)):
            _val = False
        return _val
#
# override TextFormat setXXXX() methods so the values in the
# TextStyle cannot be changed (easily)
#
    def setFamily(self, family):
        if not self.__initialized:
            TextFormat.setFamily(self, family)

    family = property(TextFormat.getFamily, setFamily, None, "Style family.")

    def setColor(self, color):
        if not self.__initialized:
            TextFormat.setColor(self, color)

    color = property(TextFormat.getColor, setColor, None, "Style color.")

    def setWeight(self, weight):
        if not self.__initialized:
            TextFormat.setWeight(self, weight)

    weight = property(TextFormat.getWeight, setWeight, None, "Style weight.")

    def setStyle(self, style):
        if not self.__initialized:
            TextFormat.setStyle(self, style)

    weight = property(TextFormat.getStyle, setStyle, None, "Style font style.")

    def setSize(self, size):
        if not self.__initialized:
            TextFormat.setSize(self, size)

    size = property(TextFormat.getSize, setSize, None, "Style font size.")

    def getName(self):
        """Retrieve the name of the TextStyle.

getName()
        """
        return self.__name

    def setName(self, name):
        """Set the name of the TextStyle.

setName(name)

The argument "name" should be a string or unicode string.
        """
        if not isinstance(name, types.StringTypes):
            raise ValueError, "Invalid name: " + str(name)
        _name = name
        if isinstance(_name, str):
            _name = unicode(name)
        self.__name = _name

    name = property(getName, setName, None, "TextStyle name.")


    def setAlignment(self, opt):
        """Set left, center, or right line justification.

setAlignment(opt)

The argument "opt" should be one of

Text.ALIGN_LEFT
Text.ALIGN_CENTER
Text.ALIGN_RIGHT
        """
        if (opt != ALIGN_LEFT and
            opt != ALIGN_CENTER and
            opt != ALIGN_RIGHT):
            raise ValueError, "Invalid justification: " + str(opt)
        self.__alignment = opt

    def getAlignment(self):
        """Return the line justification setting.

getAlignment()
        """
        return self.__alignment
#
# the font_prop_map and parse_font() should be moved as
# they are GTK specific ...
#

font_prop_map = {
    'Oblique' : 'style',
    'Italic' : 'style',
    'Ultra-Light' : 'weight',
    'Light' : 'weight',
    'Medium' : 'weight',
    'Semi-Bold' : 'weight',
    'Bold' : 'weight',
    'Ultra-Bold' : 'weight',
    'Heavy' : 'weight',
    'Ultra-Condensed' : 'stretch',
    'Extra-Condensed' : 'stretch',
    'Condensed' : 'stretch',
    'Semi-Condensed' : 'stretch',
    'Semi-Expanded' : 'stretch',
    'Expanded' : 'stretch',
    'Extra-Expanded' : 'stretch',
    'Ultra-Expanded' : 'stretch',
    }

def parse_font(fontstr):
    _size = 12
    _weight = NORMAL
    _style = NORMAL
    _stretch = NORMAL
    _family = 'Sans'
    if fontstr != '':
        _fontlist = fontstr.split()
        _fontlist.reverse()
        if _fontlist[0].isdigit():
            _sz = _fontlist.pop(0)
            _size = int(_sz)
        while (_fontlist[0] in font_prop_map):
            _prop = _fontlist.pop(0)
            _item = font_prop_map[_prop]
            # print "prop: " + _prop
            # print "item: " + _item
            if _item == 'style':
                if _prop == 'Oblique':
                    _style = 1
                elif _prop == 'Italic':
                    _style = 2
                else:
                    _style = NORMAL # default
            elif _item == 'weight':
                if (_prop == 'Ultra-Light' or
                    _prop == 'Light' or
                    _prop == 'Medium'):
                    _weight = 1
                elif (_prop == 'Semi-Bold' or
                      _prop == 'Bold'):
                    _weight = 2
                elif (_prop == 'Ultra-Bold' or
                      _prop == 'Heavy'):
                    _weight = 3
                else:
                    _weight = NORMAL
            elif _item == 'stretch':
                _stretch = _prop # fixme - add stretching bits
            else:
                raise ValueError, "Unknown font property: " + _item
        _fontlist.reverse()
        if len(_fontlist):
            _family = ' '.join(_fontlist)
    return (_family, _style, _weight, _stretch, _size)

#
# The TextBlock class defines the routines used to store unicode text
# objects in an image.
#

class TextBlock(TextFormat):
    """A class for storing text in an image

The TextBlock class is derived from the TextFormat class, so it
shares the methods and attributes of that class. In addtion
to the methods in the base class, a TextBlock has the following
addtional methods:

{get/set}Style(): Get/Set the TextStyle used by the TextBlock.
applyStyle(): Set the TextBlock properties to those in the style.
{get/set}Location(): Get/Set the Location of the TextBlock.
{get/set}Angle(): Get/Set the inclination angle of the TextBlock.
    """

    messages = {
    'style_changed' : True,
    'angle_changed' : True,
    'text_changed' : True,
    'moved' : True
    }
    
    def __init__(self, text, style):
        TextFormat.__init__(self) # just get defaults ...
        _text = text
        if not isinstance(_text, unicode):
            _text = unicode(text)
        if not isinstance(style, TextStyle):
            raise TypeError, "Invalid TextStyle: " + str(style)
        self.__text = _text
        self.__location = None
        self.__angle = 0.0
        self.__textstyle = style

    def __eq__(self, tb):
        if not isinstance(tb, TextBlock):
            raise TypeError, "Invalid TextBlock: " + str(tb)
        _stext = self.__text
        _sl = self.__location
        _tbtext = tb.getText()
        _tbl = tb.getLocation()
        _val = False
        if _stext == _tbtext:
            if _sl is None and _tbl is None:
                _val = True
            elif _sl is not None and _tbl is not None:
                _sx, _sy = _sl
                _tx, _ty = _tbl
                if abs(_sx - _tx) < 1e-10 and abs(_sy - _ty) < 1e-10:
                    _val = True
        return _val

    def __ne__(self, tb):
        if not isinstance(tb, TextBlock):
            raise TypeError, "Invalid TextBlock: " + str(tb)
        _stext = self.__text
        _sl = self.__location
        _tbtext = tb.getText()
        _tbl = tb.getLocation()
        _val = True
        if _stext == _tbtext:
            if _sl is None and _tbl is None:
                _val = False
            elif _sl is not None and _tbl is not None:
                _sx, _sy = _sl
                _tx, _ty = _tbl
                if abs(_sx - _tx) < 1e-10 and abs(_sy - _ty) < 1e-10:
                    _val = False
        return _val

    def getText(self):
        """Retrieve the text stored in the TextBlock.

getText()
        """
        return self.__text

    def setText(self, text):
        """Set the text stored in the TextBlock.

setText(text)

The argument 'text' is converted to a unicode object if
it is not one already.
        """
        _text = text
        if not isinstance(_text, unicode):
            _text = unicode(text)
        _t = self.__text
        if _t != _text:
            self.__text = _text
            self.sendMessage('text_changed', _t)
            self.modified()

    def getTextStyle(self):
        """Get the style used by the TextBlock.

getTextStyle()

This method returns a TextStyle object.
        """
        return self.__textstyle

    def setTextStyle(self, style):
        """Set the style used by the TextBlock.

setTextStyle(style)

The argument "style" must be a TextStyle object.
        """
        if not isinstance(style, TextStyle):
            raise TypeError, "Invalid TextStyle: " + str(style)
        _s = self.__textstyle
        if _s != style:
            self.__textstyle = style
            self.applyStyle()
            self.sendMessage('style_changed', _s)
            self.modified()

    def applyStyle(self):
        """Set the TextBlock properties to those in the TextStyle.

applyStyle()

Invoking this method will set all the TextBlock properties to those
defined in the TextStyle.
        """
        if self.__textstyle is None:
            raise ValueError, "Undefined style"
        _style = self.__textstyle
        self.setFamily(_style.getFamily())
        self.setWeight(_style.getWeight())
        self.setStyle(_style.getStyle())
        self.setSize(_style.getSize())
        self.setColor(_style.getColor())

    def getLocation(self):
        """Get the location where the text block is located.

getLocation()

This method returns a tuple giving the x/y coordinates of the
block location, or None if no location has been set.
        """
        return self.__location

    def setLocation(self, x, y):
        """Set the location where the text block should be placed.

setLocation(x, y)

The arguments x and y should both be floats.
        """
        _x, _y = Generic.util.make_coords(x, y)
        _loc = self.__location
        _sx = _x + 1.0 # this guarantees the location will be set
        _sy = _y + 1.0 # so does this ...
        if _loc is not None:
            _sx, _sy = _loc
        if abs(_x - _sx) > 1e-10 or abs(_y - _sy) > 1e-10:
            self.__location = (_x, _y)
            self.sendMessage('moved', _x, _y)
            self.modified()

    def getAngle(self):
        """Return the angle at which the text block is drawn.

getAngle()

This method returns an angle -360.0 < angle < 360.0.
        """
        return self.__angle

    def setAngle(self, angle):
        """Set the angle at which the text block should be drawn.

setAngle(angle)

        """
        _angle = angle
        if not isinstance(_angle, float):
            _angle = float(angle)
        if _angle > 360.0 or _angle < -360.0:
            _angle = math.fmod(value, 360.0)
        _a = self.__angle
        if abs(_a - _angle) > 1e-10:
            self.__angle = _angle
            self.sendMessage('angle_changed', _a)
            self.modified()

    def sendsMessage(self, m):
        if m in TextBlock.messages:
            return True
        return TextFormat.sendsMessage(self, m)
