# -*- encoding: utf-8 -*-
#
#
# Copyright (C) 2002-2011 Jörg Lehmann <joerg@pyx-project.org>
# Copyright (C) 2002-2011 André Wobst <wobsta@pyx-project.org>
#
# This file is part of PyX (https://pyx-project.org/).
#
# PyX 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.
#
# PyX 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 PyX; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

import logging, os, string, tempfile
from . import baseclasses, bbox, config, unit, trafo, pswriter

logger = logging.getLogger("pyx")

# PostScript-procedure definitions (cf. 5002.EPSF_Spec_v3.0.pdf)
# with important correction in EndEPSF:
#   end operator is missing in the spec!

_BeginEPSF = pswriter.PSdefinition("BeginEPSF", b"""{
  /b4_Inc_state save def
  /dict_count countdictstack def
  /op_count count 1 sub def
  userdict begin
  /showpage { } def
  0 setgray 0 setlinecap
  1 setlinewidth 0 setlinejoin
  10 setmiterlimit [ ] 0 setdash newpath
  /languagelevel where
  {pop languagelevel
  1 ne
    {false setstrokeadjust false setoverprint
    } if
  } if
} bind""")

_EndEPSF = pswriter.PSdefinition("EndEPSF", b"""{
  end
  count op_count sub {pop} repeat
  countdictstack dict_count sub {end} repeat
  b4_Inc_state restore
} bind""")


class linefilereader:
    """a line by line file reader

    This line by line file reader allows for '\n', '\r' and
    '\r\n' as line separation characters. Line separation
    characters are not modified (binary mode). It implements
    a readline, a read and a close method similar to a regular
    file object."""

    # note: '\n\r' is not considered to be a linebreak as its documented
    #       in the DSC spec #5001, while '\n\r' *is* a *single* linebreak
    #       according to the EPSF spec #5002

    def __init__(self, file, typicallinelen=257):
        """Opens the file filename for reading.

        typicallinelen defines the default buffer increase
        to find the next linebreak."""

        # note: The maximal line size in an EPS is 255 plus the
        #       linebreak characters. However, we also handle
        #       lines longer than that.
        self.file = file
        self.buffer = b""
        self.typicallinelen = typicallinelen

    def read(self, count=None, EOFmsg="unexpected end of file"):
        """read bytes from the file

        count is the number of bytes to be read when set. Then count
        is unset, the rest of the file is returned. EOFmsg is used
        to raise a IOError, when the end of the file is reached while
        reading count bytes or when the rest of the file is empty when
        count is unset. When EOFmsg is set to None, less than the
        requested number of bytes might be returned."""
        if count is not None:
            if count > len(self.buffer):
                self.buffer += self.file.read(count - len(self.buffer))
            if EOFmsg is not None and len(self.buffer) < count:
                raise IOError(EOFmsg)
            result = self.buffer[:count]
            self.buffer = self.buffer[count:]
            return result
        else:
            self.buffer += self.file.read()
            if EOFmsg is not None and not len(self.buffer):
                raise IOError(EOFmsg)
            result = self.buffer
            self.buffer = ""
            return result

    def readline(self, EOFmsg="unexpected end of file"):
        """reads a line from the file

        Lines are separated by '\n', '\r' or '\r\n'. The line separation
        strings are included in the return value. The last line might not
        end with an line separation string. Reading beyond the file generates
        an IOError with the EOFmsg message. When EOFmsg is None, an empty
        string is returned when reading beyond the end of the file."""
        EOF = 0
        while True:
            crpos = self.buffer.find(b"\r")
            nlpos = self.buffer.find(b"\n")
            if nlpos == -1 and (crpos == -1 or crpos == len(self.buffer) - 1) and not EOF:
                newbuffer = self.file.read(self.typicallinelen)
                if not len(newbuffer):
                    EOF = 1
                self.buffer += newbuffer
            else:
                eol = len(self.buffer)
                if not eol and EOFmsg is not None:
                    raise IOError(EOFmsg)
                if nlpos != -1:
                    eol = nlpos + 1
                if crpos != -1 and (nlpos == -1 or crpos < nlpos - 1):
                    eol = crpos + 1
                result = self.buffer[:eol]
                self.buffer = self.buffer[eol:]
                return result


def _readbbox(file):
    """returns bounding box of EPS file filename"""

    file = linefilereader(file)

    # check the %! header comment
    if not file.readline().startswith(b"%!"):
        raise IOError("file doesn't start with a '%!' header comment")

    bboxatend = 0
    # parse the header (use the first BoundingBox)
    while True:
        line = file.readline()
        if not line:
            break
        if line.startswith(b"%%BoundingBox:") and not bboxatend:
            values = line.split(b":", 1)[1].split()
            if values == ["(atend)"]:
                bboxatend = 1
            else:
                if len(values) != 4:
                    raise IOError("invalid number of bounding box values")
                return bbox.bbox_pt(*list(map(int, values)))
        elif (line.rstrip() == b"%%EndComments" or
              (len(line) >= 2 and chr(line[0]) != "%" and chr(line[1]) not in string.whitespace)):
            # implicit end of comments section
            break
    if not bboxatend:
        raise IOError("no bounding box information found")

    # parse the body
    nesting = 0 # allow for nested documents
    while True:
        line = file.readline()
        if line.startswith(b"%%BeginData:"):
            values = line.split(":", 1)[1].split()
            if len(values) > 3:
                raise IOError("invalid number of arguments")
            if len(values) == 3:
                if values[2] == b"Lines":
                    for i in range(int(values[0])):
                        file.readline()
                elif values[2] != b"Bytes":
                    raise IOError("invalid bytesorlines-value")
                else:
                    file.read(int(values[0]))
            else:
                file.read(int(values[0]))
            line = file.readline()
            # ignore tailing whitespace/newline for binary data
            if (len(values) < 3 or values[2] != "Lines") and not len(line.strip()):
                line = file.readline()
            if line.rstrip() != b"%%EndData":
                raise IOError("missing EndData")
        elif line.startswith(b"%%BeginBinary:"):
            file.read(int(line.split(":", 1)[1]))
            line = file.readline()
            # ignore tailing whitespace/newline
            if not len(line.strip()):
                line = file.readline()
            if line.rstrip() != b"%%EndBinary":
                raise IOError("missing EndBinary")
        elif line.startswith(b"%%BeginDocument:"):
            nesting += 1
        elif line.rstrip() == b"%%EndDocument":
            if nesting < 1:
                raise IOError("unmatched EndDocument")
            nesting -= 1
        elif not nesting and line.rstrip() == b"%%Trailer":
            break

    usebbox = None
    # parse the trailer (use the last BoundingBox)
    line = True
    while line:
        line = file.readline(EOFmsg=None)
        if line.startswith("%%BoundingBox:"):
            values = line.split(b":", 1)[1].split()
            if len(values) != 4:
                raise IOError("invalid number of bounding box values")
            usebbox = bbox.bbox_pt(*list(map(int, values)))
    if not usebbox:
        raise IOError("missing bounding box information in document trailer")
    return usebbox


class epsfile(baseclasses.canvasitem):

    """class for epsfiles"""

    def __init__(self,
                 x, y, filename,
                 width=None, height=None, scale=None, align="bl",
                 clip=1, translatebbox=1, bbox=None,
                 kpsearch=0):
        """inserts epsfile

        Object for an EPS file named filename at position (x,y). Width, height,
        scale and aligment can be adjusted by the corresponding parameters. If
        clip is set, the result gets clipped to the bbox of the EPS file. If
        translatebbox is not set, the EPS graphics is not translated to the
        corresponding origin. If bbox is not None, it overrides the bounding
        box in the epsfile itself. If kpsearch is set then filename is searched
        using the kpathsea library.
        """

        self.x_pt = unit.topt(x)
        self.y_pt = unit.topt(y)
        self.filename = filename
        self.kpsearch = kpsearch
        if bbox:
            self.mybbox = bbox
        else:
            with self.open() as epsfile:
                self.mybbox = _readbbox(epsfile)

        # determine scaling in x and y direction
        self.scalex = self.scaley = scale

        if width is not None or height is not None:
            if scale is not None:
                raise ValueError("cannot set both width and/or height and scale simultaneously")
            if height is not None:
                self.scaley = unit.topt(height)/(self.mybbox.ury_pt-self.mybbox.lly_pt)
            if width is not None:
                self.scalex = unit.topt(width)/(self.mybbox.urx_pt-self.mybbox.llx_pt)

            if self.scalex is None:
                self.scalex = self.scaley
            if self.scaley is None:
                self.scaley = self.scalex

        # set the actual width and height of the eps file (after a
        # possible scaling)
        self.width_pt  = self.mybbox.urx_pt-self.mybbox.llx_pt
        if self.scalex:
            self.width_pt *= self.scalex

        self.height_pt = self.mybbox.ury_pt-self.mybbox.lly_pt
        if self.scaley:
            self.height_pt *= self.scaley

        # take alignment into account
        self.align       = align
        if self.align[0]=="b":
            pass
        elif self.align[0]=="c":
            self.y_pt -= self.height_pt/2.0
        elif self.align[0]=="t":
            self.y_pt -= self.height_pt
        else:
            raise ValueError("vertical alignment can only be b (bottom), c (center), or t (top)")

        if self.align[1]=="l":
            pass
        elif self.align[1]=="c":
            self.x_pt -= self.width_pt/2.0
        elif self.align[1]=="r":
            self.x_pt -= self.width_pt
        else:
            raise ValueError("horizontal alignment can only be l (left), c (center), or r (right)")

        self.clip = clip
        self.translatebbox = translatebbox

        self.trafo = trafo.translate_pt(self.x_pt, self.y_pt)

        if self.scalex is not None:
            self.trafo = self.trafo * trafo.scale_pt(self.scalex, self.scaley)

        if translatebbox:
            self.trafo = self.trafo * trafo.translate_pt(-self.mybbox.llx_pt, -self.mybbox.lly_pt)

    def open(self):
        if self.kpsearch:
            return config.open(self.filename, [config.format.pict])
        else:
            return open(self.filename, "rb")

    def bbox(self):
        return self.mybbox.transformed(self.trafo)

    def processPS(self, file, writer, context, registry, bbox):
        registry.add(_BeginEPSF)
        registry.add(_EndEPSF)
        bbox += self.bbox()

        file.write("BeginEPSF\n")

        if self.clip:
            llx_pt, lly_pt, urx_pt, ury_pt = self.mybbox.transformed(self.trafo).highrestuple_pt()
            file.write("%g %g %g %g rectclip\n" % (llx_pt, lly_pt, urx_pt-llx_pt, ury_pt-lly_pt))

        self.trafo.processPS(file, writer, context, registry)

        file.write("%%%%BeginDocument: %s\n" % self.filename)

        with self.open() as epsfile:
            file.write_bytes(epsfile.read())

        file.write("%%EndDocument\n")
        file.write("EndEPSF\n")

    def processPDF(self, file, writer, context, registry, bbox):
        logger.warning("EPS file is included as a bitmap created using pipeGS")
        from pyx import bitmap, canvas
        from PIL import Image
        c = canvas.canvas()
        c.insert(self)
        i = Image.open(c.pipeGS(device="pngalpha", resolution=600))
        i.load()
        b = bitmap.bitmap_pt(self.bbox().llx_pt, self.bbox().lly_pt, i)
        # we slightly shift the bitmap to re-center it, as the bitmap might contain some additional border
        # unfortunately we need to construct another bitmap instance for that ...
        b = bitmap.bitmap_pt(self.bbox().llx_pt + 0.5*(self.bbox().width_pt()-b.bbox().width_pt()),
                             self.bbox().lly_pt + 0.5*(self.bbox().height_pt()-b.bbox().height_pt()), i)
        b.processPDF(file, writer, context, registry, bbox)
