# -*- coding: utf-8 -*-

from copy import copy
import glob
import os
from os.path import abspath, dirname
import sys
from urllib.request import urlretrieve

from PIL import Image as PILImage
from reportlab.platypus.flowables import Image, Flowable
from reportlab.lib.units import cm, inch

from .log import log, nodeid

try:
    from .svgimage import SVGImage
except ImportError:
    # svglib may optionally not be installed, which causes this error
    SVGImage = None

# find base path
if hasattr(sys, 'frozen'):
    PATH = abspath(dirname(sys.executable))
else:
    PATH = abspath(dirname(__file__))

missing = os.path.join(PATH, 'images', 'image-missing.jpg')


def defaultimage(
    filename,
    width=None,
    height=None,
    kind='direct',
    mask='auto',
    lazy=1,
    srcinfo=None,
):
    """Get default image backend.

    We have multiple image backends, including the stock ReportLab one.  This
    wrapper around the ReportLab one allows us to pass the client ``RstToPdf``
    object and the URI into all our backends, which they can use (or not) as
    necessary.
    """
    return Image(filename, width, height, kind, mask, lazy)


class MyImage(Flowable):
    """A Image subclass that can:

    1. Take a ``percentage_of_container`` kind, which resizes it on ``wrap(``
       to use a percentage of the container's width.
    2. Take vector formats and instantiates the right "backend" flowable.
    """

    warned = False

    @classmethod
    def support_warning(cls):
        if cls.warned or PILImage:
            return
        cls.warned = True
        log.warning(
            'Support for images other than JPG is now limited. Please install '
            'Pillow.'
        )

    @staticmethod
    def split_uri(uri):
        """Split provided URI.

        A really minimalistic split -- doesn't cope with http:, etc.  HOWEVER,
        it tries to do so in a fashion that allows a clueless user to have
        ``#`` inside his filename without screwing anything up.
        """
        basename, extra = os.path.splitext(uri)
        extra = extra.split('#', 1) + ['']
        fname = basename + extra[0]
        extension = extra[0][1:].lower()
        options = extra[1]
        return fname, extension, options

    def __init__(
        self,
        filename,
        width=None,
        height=None,
        kind='direct',
        mask='auto',
        lazy=1,
        client=None,
        target=None,
    ):
        # Client is mandatory.  Perhaps move it farther up if we refactor
        assert client is not None
        self.__kind = kind

        # Maximum page height is used when resizing in wrap() as we can't render an image across a page boundary
        self.max_page_height = 999999

        if filename.split("://")[0].lower() in ('http', 'ftp', 'https'):
            try:
                filename2, _ = urlretrieve(filename)
                if filename != filename2:
                    client.to_unlink.append(filename2)
                    filename = filename2
            except IOError:
                filename = missing
        self.filename, self._backend = self.get_backend(filename, client)
        srcinfo = client, self.filename

        if kind == 'percentage_of_container':
            self.image = self._backend(
                self.filename, width, height, 'direct', mask, lazy, srcinfo
            )
            self.image.drawWidth = width
            self.image.drawHeight = height
            self.__width = width
            self.__height = height
        else:
            self.image = self._backend(
                self.filename, width, height, kind, mask, lazy, srcinfo
            )
        self.__ratio = float(self.image.imageWidth) / self.image.imageHeight
        self.__wrappedonce = False
        self.target = target

    @classmethod
    def raster(self, filename, client):
        """Convert image to raster image.

        Takes a filename and converts it to a raster image reportlab can
        process.
        """

        if not os.path.exists(filename):
            log.error('Missing image file: %s', filename)
            return missing

        try:
            # First try to rasterize using the suggested backend
            backend = self.get_backend(filename, client)[1]
            return backend.raster(filename, client)
        except Exception:
            pass

        # Last resort: try everything

        if PILImage:  # See if pil can process it
            try:
                PILImage.open(filename)
                return filename
            except Exception:
                # Can't read it
                pass

        # PIL can't, so we can't
        self.support_warning()
        log.error('Could not load image: %s', filename)
        return missing

    @classmethod
    def get_backend(self, uri, client):
        """Get backend for an image.

        Given the filename of an image, returns ``(fname, backend)``, where
        ``fname`` is the filename to be used (could be the same as filename, or
        something different if the image had to be converted or is missing),
        and ``backend`` is an ``Image`` class that can handle ``fname``.

        If ``uri`` ends with '.*' then the returned filename will be the best
        quality supported at the moment.

        That means: SVG > PNG > JPG > GIF
        """

        backend = defaultimage

        # Extract all the information from the URI
        filename, extension, options = self.split_uri(uri)

        if '*' in filename:
            preferred = ['gif', 'jpg', 'png', 'svg']

            # Find out what images are available
            available = glob.glob(filename)
            cfn = available[0]
            cv = -10
            for fn in available:
                ext = fn.split('.')[-1]
                if ext in preferred:
                    v = preferred.index(ext)
                else:
                    v = -1
                if v > cv:
                    cv = v
                    cfn = fn

            # cfn should have our favourite type of those available
            filename = cfn
            extension = cfn.split('.')[-1]
            uri = filename

        # If the image doesn't exist, we use a 'missing' image
        if not os.path.exists(filename):
            log.error('Missing image file: %s', filename)
            filename = missing

            return filename, backend

        if extension in ['svg', 'svgz']:
            if SVGImage is not None:
                log.info('Backend for %s is SVGImage', filename)
                backend = SVGImage
            else:
                log.error('SVG image support requires svglib: %s', filename)
                filename = missing
        elif extension in ['pdf']:
            log.error("PDF images are not supported")
            filename = missing
        elif extension != 'jpg' and not PILImage:
            # No way to make this work
            log.error(
                '%s image support requires Pillow: %s',
                extension.upper(),
                filename,
            )
            filename = missing

        return filename, backend

    @classmethod
    def size_for_node(self, node, client):
        """Get size for image node.

        Given a docutils image node, returns the size the image should have in
        the PDF document, and what "kind" of size that is. That involves lots
        of guesswork.
        """

        uri = str(node.get('uri'))
        if uri.split('://')[0].lower() not in ('http', 'ftp', 'https'):
            uri = os.path.join(client.basedir, uri)
        else:
            uri, _ = urlretrieve(uri)
            client.to_unlink.append(uri)

        srcinfo = client, uri
        # Extract all the information from the URI
        imgname, extension, options = self.split_uri(uri)

        if not os.path.isfile(imgname):
            imgname = missing

        scale = float(node.get('scale', 100)) / 100

        # Figuring out the size to display of an image is ... annoying.
        # If the user provides a size with a unit, it's simple, adjustUnits
        # will return it in points and we're done.

        # However, often the unit wil be "%" (specially if it's meant for
        # HTML originally. In which case, we will use a percentage of
        # the containing frame.

        # Find the image size in pixels:
        kind = 'direct'
        xdpi, ydpi = client.styles.def_dpi, client.styles.def_dpi
        extension = imgname.split('.')[-1].lower()
        if extension in ['svg', 'svgz']:
            if not SVGImage:
                raise RuntimeError(
                    'Documentation uses SVG image but svglib is not installed.'
                )

            iw, ih = SVGImage(imgname, srcinfo=srcinfo).wrap(0, 0)
            # These are in pt, so convert to px
            iw = iw * xdpi / 72
            ih = ih * ydpi / 72
        else:
            keeptrying = True

            if PILImage:
                try:
                    img = PILImage.open(imgname)
                    img.load()
                    iw, ih = img.size
                    xdpi, ydpi = img.info.get('dpi', (xdpi, ydpi))
                    keeptrying = False
                except IOError:  # PIL throws this when it's a broken/unknown image
                    pass

            if keeptrying:
                if extension not in ['jpg', 'jpeg']:
                    log.error(
                        "The image (%s, %s) is broken or in an unknown format",
                        imgname,
                        nodeid(node),
                    )
                    raise ValueError
                else:
                    # Can be handled by reportlab
                    log.warning(
                        "Can't figure out size of the image (%s, %s). Install PIL for better results.",
                        imgname,
                        nodeid(node),
                    )
                    iw = 1000
                    ih = 1000

        # Try to get the print resolution from the image itself via PIL.
        # If it fails, assume a DPI of 300, which is pretty much made up,
        # and then a 100% size would be iw*inch/300, so we pass
        # that as the second parameter to adjustUnits
        #
        # Some say the default DPI should be 72. That would mean
        # the largest printable image in A4 paper would be something
        # like 480x640. That would be awful.
        #

        w = node.get('width')
        h = node.get('height')
        if h is None and w is None:  # Nothing specified
            # Guess from iw, ih
            log.debug(
                "Using image %s without specifying size."
                "Calculating based on image size at %ddpi [%s]",
                imgname,
                xdpi,
                nodeid(node),
            )
            w = iw * inch / xdpi
            h = ih * inch / ydpi
        elif w is not None:
            # Node specifies only w
            # In this particular case, we want the default unit
            # to be pixels so we work like rst2html
            if w[-1] == '%':
                kind = 'percentage_of_container'
                w = int(w[:-1])
            else:
                # This uses default DPI setting because we
                # are not using the image's "natural size"
                # this is what LaTeX does, according to the
                # docutils mailing list discussion
                w = client.styles.adjustUnits(w, client.styles.tw, default_unit='px')

            if h is None:
                # h is set from w with right aspect ratio
                h = w * ih / iw
            else:
                h = client.styles.adjustUnits(h, ih * inch / ydpi, default_unit='px')
        elif h is not None and w is None:
            if h[-1] != '%':
                h = client.styles.adjustUnits(h, ih * inch / ydpi, default_unit='px')

                # w is set from h with right aspect ratio
                w = h * iw / ih
            else:
                log.error(
                    'Setting height as a percentage does **not** work. '
                    'ignoring height parameter [%s]',
                    nodeid(node),
                )
                # Set both from image data
                w = iw * inch / xdpi
                h = ih * inch / ydpi

        # Apply scale factor
        w = w * scale
        h = h * scale

        # And now we have this probably completely bogus size!
        log.info(
            'Image %s size calculated: %fcm by %fcm [%s]',
            imgname,
            w / cm,
            h / cm,
            nodeid(node),
        )

        return w, h, kind

    def _restrictSize(self, aW, aH):
        return self.image._restrictSize(aW, aH)

    def _unRestrictSize(self, aW, aH):
        return self.image._unRestrictSize(aW, aH)

    def __deepcopy__(self, *whatever):
        # ImageCore class is not deep copyable.  Stop the copy at this
        # class.  If you remove this, re-test for issue #126.
        return copy(self)

    def wrap(self, availWidth, availHeight):
        if self.max_page_height < availHeight:
            # We can't render an image across a page boundary, so this is the tallest we can be
            availHeight = self.max_page_height

        if self.__kind == 'percentage_of_container':
            w, h = self.__width, self.__height
            if not w:
                log.warning(
                    'Scaling image as % of container with w unset. '
                    'This should not happen, setting to 100'
                )
                w = 100
            scale = w / 100.0
            w = availWidth * scale
            h = w / self.__ratio
            self.image.drawWidth, self.image.drawHeight = w, h
            return w, h
        else:
            if self.image.drawHeight > availHeight:
                if not getattr(self, '_atTop', True):
                    return self.image.wrap(availWidth, availHeight)
                else:
                    # It's the first thing in the frame, probably
                    # Wrapping it will not make it work, so we
                    # adjust by height
                    # FIXME get rst file info (line number)
                    # here for better error message
                    log.warning(
                        'Image %s is too tall for the frame, rescaling', self.filename
                    )
                    self.image.drawHeight = availHeight
                    self.image.drawWidth = availHeight * self.__ratio
            elif self.image.drawWidth > availWidth:
                log.warning(
                    'Image %s is too wide for the frame, rescaling', self.filename
                )
                self.image.drawWidth = availWidth
                self.image.drawHeight = availWidth / self.__ratio
            return self.image.wrap(availWidth, availHeight)

    def drawOn(self, canv, x, y, _sW=0):
        if self.target:
            offset = 0
            if self.image.hAlign == 'CENTER':
                offset = _sW / 2.0
            elif self.image.hAlign == 'RIGHT':
                offset = _sW
            canv.linkURL(
                self.target,
                (
                    x + offset,
                    y,
                    x + offset + self.image.drawWidth,
                    y + self.image.drawHeight,
                ),
                relative=True,
                # thickness = 3,
            )
        return self.image.drawOn(canv, x, y, _sW)
