# This file is part of the Coral project.
# Please read the copyright notice in the LICENSE file.

#
# X11 selection handling in Coral.
#
# We want to support PRIMARY and CLIPBOARD selections
#
import coral
import qt
import qtcanvas
import re
DI=coral.lang.XMIDI20
import traceback

from coral.modeler.diagram import presentation

import os
import tempfile

from coral import log
logger=log.getLogger("xselection")

from coral.modeler.util import svg_painter

from coral.modeler.diagram import di_support

# RFC 2045, RFC 2046, RFC 3023 at least are the relevant documents
# for text and text/xml. The charset declaration is mandatory.
supported_outputs = [ "text/xml;charset=utf-8",   # XMI 2.0
                      "image/ppm",
                      "image/png",
                      "image/pbm",
                      "image/svg+xml",
                      "application/postscript",
                      "application/eps", # Unsure if this is official
                      "text/plain", # Technically this is BS but too much software depends on it
                      "text/plain;charset=utf-8", # Technically this is BS but too much software depends on it
                      ]

def parse_mimetype(mimetype):
    m = re.search("^(.*?)(\s*;.*)?$", mimetype)
    if not m:
        return None
    complete = m.group(1)
    complete = complete.lower()
    parameters = {}
    rest = m.group(2)
    if rest:
        rest = re.sub("^\s*;", "", rest)
        params = rest.split(";")
        for param_value in params:
            m = re.search("^(.*?)\s*=\s*(.*)\s*$", param_value)
            if m:
                p = m.group(1).lower()
                k = m.group(2)
                k = re.sub("^\"(.*)\"$", "\\1", k)
                # Special-case for charset
                if p == "charset":
                    k = k.lower()
                parameters[p] = k
    return complete, parameters

class XSelection(qt.QMimeSource):
    def __init__(self, oclCollection, isPrimary):
        #The collection shouldn't have two elements such that one
        #is the (transitive) parent of the other.
        qt.QMimeSource.__init__(self)
        self.oclCollection = oclCollection

        self.text_representation = ""
        self.pixmap = None
        self.postscript = None
        self.encapsulated_postscript = None
        self.svg = None

        self.isPrimary = isPrimary

        #
        # We advertise graphics iff we have XMI-DI elements
        # and at least one has a QT QCanvasItem and is in a canvas.
        #
        self.hasGraphics = False
        for e in oclCollection:
            if e.oclIsKindOf(DI.DiagramElement):
                p = e.widget.__getPyValue__()
                if p and ((isinstance(p, qtcanvas.QCanvasItem) and p.canvas()) or isinstance(p, qtcanvas.QCanvas)):
                    self.hasGraphics = True
                    break

        # If we are not primary, we should save immediately
        if not isPrimary:
            self.__save()

    def format(self, i):
        """format() tells X11 what output formats are supported by this
        QMimeSource."""
        if i < 0 or i >= len(supported_outputs):
            return None
        # Don't advertise graphics if we don't have it
        if not self.hasGraphics:
            while re.search("image", supported_outputs[i]):
                i += 1
                if i >= len(supported_outputs):
                    return None
        #logger.debug("Returning X11 support for " + str(supported_outputs[i]))
        return supported_outputs[i]

    def provides(self, mimetype):
        """provides() tells X11 if we can provide a certain mimetype."""
        # Don't advertise graphics if we don't have it
        mimetype = mimetype.lower()
        if not self.hasGraphics:
            if re.search("image", mimetype):
                logger.debug("Returning False for " + str(mimetype))
                return False

        if mimetype in supported_outputs:
            #logger.debug("Returning True for " + mimetype)
            return True

        complete, parameters = parse_mimetype(mimetype)
        # Parameters must match
        for i in supported_outputs:
            c_cand, p_cand = parse_mimetype(i)
            if c_cand != complete:
                continue
            for k in p_cand.keys():
                if parameters.has_key(k) and parameters[k] == p_cand[k]:
                    continue
                return False
            #logger.debug("Returning True for " + mimetype)
            return True
        logger.debug("Returning False for " + str(mimetype))
        return False
    
    def __render(self, painter, xmidi):
        # BUG wrong rendering order.. Read 8.9 in XMI-DI spec.

        # Also, XMI[DI] specifies a silly rendering order
        # We will always draw edges + children last.
        edges = []

        while 1:
            children = []
            for e in xmidi:
                if not e.isVisible:
                    continue
                if edges is not None and e.__type__() == DI.GraphEdge:
                    edges.append(e)
                    continue
                children.extend([c for c in e.contained])
                if e.__type__() == DI.Diagram:
                    continue
                widget = e.widget.__getPyValue__()
                if not widget:
                    continue
                # the widgets do save..restore themselves
                widget.drawShape(painter)
            if not children:
                children = edges
                edges = None
            if not children:
                break
            xmidi = children

    def __drawOnPaintDevice(self, painter, paintdevice, xmidi, minx, miny, xscale = 1.0, yscale = 1.0):
        painter.begin(paintdevice)
        drawpen = qt.QPen()
        try:
            assert painter.isActive()
    
            # Translate painter so that we don't have whitespace above
            # or to the left of us
            painter.translate(-minx, -miny)
    
            # Scale appropriately to device resolution
            if xscale != 1.0 or yscale != 1.0:
                painter.scale(xscale, yscale)
                # This sets a "thick" pen when e.g. saving to Postscript
                # otherwise lines are drawn at the thinnest possible
                # device-dependent thickness
                drawpen.setWidth(1)
                presentation.PEN_THICKNESS = 1
            painter.setPen(drawpen)
            self.__render(painter, xmidi)
        finally:
            presentation.PEN_THICKNESS = 0
            painter.end()

        # SVG...
        if paintdevice is None:
            return painter.txt()
    
    def __save(self):
        """Save the selected elements. Notice that for CLIPBOARD,
        we want to call this method immediately (so content cannot change),
        but for PRIMARY it should be called when the user pastes (since
        contents are allowed to change after selection but before paste)."""

        # BUG We could go through the collection and make sure its
        # elements are still valid (i.e. not orphan) ?
        # BUG what if the diagram has been closed?

        pass

        # We need to go through the collection and make sure that no
        # parent _and_ child node is being saved. This chokes the IO routines
        collection = coral.Sequence()
        h = {}
        for i in self.oclCollection:
            h[i] = True

        already_appended = {}
        for i in self.oclCollection:
            if already_appended.has_key(i):
                continue
            chain = coral.Element__getMyselfAndParents__(i)
            found = False
            for c in chain:
                if h.has_key(c) and c != i:
                    # Parent exists, ignore
                    found = True
                    break
            if not found:
                collection.append(i)
                already_appended[i] = True

        # Get text
        s = coral.saveDocument_str(collection, "2.0")
        self.text_representation = s

        if not self.hasGraphics:
            return

        # Get image
        xmidi = [ e for e in collection if e.__type__().__language__() == DI ]
        # If we have chosen a diagram, only snapshot it and nothing else
        # It's a bit of a hack since the textual representation isn't
        # the same as the image, but...
        for e in xmidi:
            if e.oclIsKindOf(DI.Diagram):
                xmidi = [ e ]
                break
            
        minx, miny, maxx, maxy = di_support.calculateXMIDIBoundingBox(xmidi)
        if maxx == minx and minx == 0 and maxy == miny and miny == 0:
            maxx = 50
            maxy = 50
            
        width = maxx- minx
        height = maxy - miny

        # PIXMAP
        pixmap = qt.QPixmap(width, height)
        pixmap.fill() # default white background. Hm, could be transparent..

        self.__drawOnPaintDevice(qt.QPainter(), pixmap, xmidi, minx, miny)
        self.pixmap = pixmap

        # POSTSCRIPT
        printer = qt.QPrinter()
        printer.setOutputToFile(True)
        printer.setColorMode(qt.QPrinter.Color)
        # BUG Magic scaling number, seems to produce good results
        # when compared with the default printing button. ???
        scale = printer.resolution() / 96.3

        fd, name = tempfile.mkstemp(prefix="coral-temporary-postscript")
        # Immediately close, we just want the name
        os.close(fd)
        printer.setOutputFileName(name)
        try:
            try:
                self.__drawOnPaintDevice(qt.QPainter(), printer, xmidi, minx, miny, xscale = scale, yscale = scale)
                # Slurp up the file into the postscript string
                self.postscript = ''.join(file(name).readlines())
                self.eps = self.__createEPS(name)
            finally:
                try:
                    os.unlink(name)
                except:
                    pass
        except RuntimeError:
            logger.error("Could not retrieve EPS bounding box! EPS output is not functional!")
            self.eps = ""

        # Don't die on exceptions, since the SVGPainter isn't
        # complete yet.
        try:
            # SVG
            self.svg = self.__drawOnPaintDevice(svg_painter.SVGPainter(),
                                                None,
                                                xmidi,
                                                minx, miny,
                                                xscale = scale, yscale = scale)
        except:
            logger.debug(unicode(traceback.print_exc()))
            pass

    def __createEPS(self, filename):
        """Creates an Encapsulated Postscript from the Postscript file
        names @filename. Requires the "gs" program to be in path. Requires
        black magic and chicken sacrifices to work. Unix-specific."""
        # Why oh why does gs output on standard error???
        s = "gs -dQUIET -dBATCH -dNOPAUSE -sDEVICE=bbox " + filename
        wfd, rfd, efd = os.popen3(s, "r")
        try:
            bbox = None
            # Note we must read the bounding box from the error fd !?
            for line in efd:
                if re.search("^%%BoundingBox", line):
                    bbox = line
            if not bbox:
                raise RuntimeError("Could not retrieve EPS bounding box!")
        finally:
            wfd.close()
            rfd.close()
            efd.close()

        eps = []
        f = file(filename)
        try:
            found_psadobe = False
            found_bbox = False
            for line in f:
                if not found_psadobe and re.search("%!PS-Adobe", line):
                    found_psadobe = True
                    line = "%!PS-Adobe-2.0 EPSF-1.2\n"
                elif not found_bbox and re.search("%%BoundingBox", line):
                    found_bbox = True
                    line = bbox
                elif re.search("^%%DocumentFonts:", line):
                    continue
                eps.append(line)
        finally:
            f.close()
        return ''.join(eps)

    def encodedData(self, mimetype):
        """Return the data as requested by the client."""
        logger.debug("In XSelection.encodedData " + str(mimetype))

        if not self.provides(mimetype):
            msg = "Mimetype " + str(mimetype) + " not supported by Coral!"
            raise RuntimeError(msg)

        complete, parameters = parse_mimetype(mimetype)

        # Save for the first time if we are Primary
        if self.isPrimary:
            self.__save()

        if complete.startswith("text/"):
            return self.text_representation

        if not self.hasGraphics:
            raise RuntimeError("No graphics in the selection!")

        primarytype, subtype = complete.split("/")
        if primarytype == "application":
            if subtype == "postscript":
                return self.postscript
            if subtype == "eps":
                return self.eps

        if primarytype == "image" and subtype == "svg+xml":
            return self.svg

        if primarytype == "image":
            # BUG don't use temporary file...
            #bytearray = qt.QByteArray()
            # QBuffer isn't in PyQt???
            #buffer = qt.QBuffer(bytearray)
            #buffer.oppen(qt.IO_WriteOnly)
            #self.pixmap.save(buffer, "PPM")
            sfx = "." + subtype
            fd, name = tempfile.mkstemp(suffix = sfx,
                                        prefix="coral-temporary-picture")
            # Immediately close, we just want the name
            os.close(fd)

            self.pixmap.save(name, subtype.upper())
            f = file(name, "rb")
            try:
                s = f.read()
            finally:
                f.close()
            os.unlink(name)
            return qt.QByteArray(s)
        assert 0



## Helper routines for menus

def providesImage(mode):
    """Returns the QT image type that is supported by current selection
    (primary or clipboard). Empty string if no image retrieval possible."""
    import coral.modeler.coralmodeler
    cb = coral.modeler.coralmodeler.CoralModeler.getInstance().getClipboard()
    assert mode == qt.QClipboard.Selection or mode == qt.QClipboard.Clipboard

    qt_support = qt.QImageIO.inputFormats()
    mimesource = cb.data(mode)
    i = 0
    while 1:
        format = mimesource.format(i)
        if not format:
            break
        i += 1
        m = re.search("^image/(.*)", format)
        if m:
            img_type = str(m.group(1)).upper()
            if img_type in qt_support:
                return img_type
    return ""

def providesModelData(mode):
    """Returns true if current selection provides model data. (well,
    text/xml. That's almost model data, right?."""
    import coral.modeler.coralmodeler
    cb = coral.modeler.coralmodeler.CoralModeler.getInstance().getClipboard()
    assert mode == qt.QClipboard.Selection or mode == qt.QClipboard.Clipboard
    i = 0
    d = cb.data(mode)
    while 1:
        format = d.format(i)
        if not format:
            break
        i += 1
        m = re.search("^text/xml", format)
        if m:
            return format
    return ""

def providesText(mode):
    """Returns true if current selection provides text."""
    import coral.modeler.coralmodeler
    cb = coral.modeler.coralmodeler.CoralModeler.getInstance().getClipboard()
    assert mode == qt.QClipboard.Selection or mode == qt.QClipboard.Clipboard
    mimesource = cb.data(mode)
    i = 0
    while 1:
        format = mimesource.format(i)
        if not format:
            break
        i += 1
        m = re.search("^text/plain", format)
        if m:
            return format
    return ""
