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

@author:		Martí Congost
@contact:		marti.congost@whads.com
@organization:	Whads/Accent SL
@since:			February 2010
"""
from __future__ import with_statement
import os
from subprocess import Popen
from tempfile import mkdtemp
from shutil import rmtree
from threading import Lock
from cStringIO import StringIO
from mimetypes import guess_type
from time import mktime, strptime
from httplib import HTTPConnection
from urlparse import urlsplit
from urllib import urlopen
import rfc822
import cherrypy
from cherrypy.lib.cptools import validate_since
from cherrypy.lib.static import serve_file
import Image
import ImageEnhance
import ImageFilter
from cocktail.modeling import (
    cached_getter,
    abstractmethod,
    OrderedDict,
    ListWrapper
)
from cocktail import schema
from cocktail.controllers import (
    context,
    get_parameter,
    Location
)
from woost.models import (
    Item,
    Publishable,
    File,
    URI,
    User,
    get_current_user,
    ReadPermission
)
from woost.controllers.basecmscontroller import BaseCMSController


class ImagesController(BaseCMSController):

    default_format = "PNG"

    formats_by_mime_type = {
        "image/jpeg": "JPEG",
        "image/png": "PNG",
        "image/gif": "GIF",
        "image/tiff": "TIFF"
    }

    mime_types_by_format = dict(
        (v, k)
        for k, v in formats_by_mime_type.iteritems()
    )

    def __call__(self, id, *processing, **kwargs):
        
        # Get the requested item or content type, validate access
        item = None

        try:
            id = int(id)
        except ValueError:
            for cls in Item.schema_tree():
                if cls.full_name == id:
                    item = cls
                    is_type = True
                    break
        else:
            item = Item.get_instance(int(id))
            is_type = False

        if item is None:
            raise cherrypy.NotFound()

        if not is_type:
            get_current_user().require_permission(ReadPermission, target = item)

        # Determine the rendering format
        format = kwargs.pop("format", None)
        if format:
            format = format.upper()

        # Find a content renderer that can handle the requested item
        renderer = None
        kind = kwargs.pop("kind", "icon" if is_type else "image").split(",")
        is_icon = False
        
        if "image" in kind:
            if is_type:
                raise ValueError(
                    "Can't set 'kind' to 'image' when rendering a content type"
                )

            for renderer in content_renderers:
                if renderer.can_render(item):
                    break
            else:
                renderer = None

        if renderer is None and "icon" in kind:
            for renderer in icon_renderers:
                if renderer.can_render(item):
                    is_icon = True
                    processing = []
                    break
            else:
                renderer = None

        if renderer is None:
            raise RuntimeError("No content renderer can render %s" % item)

        # Caching:
        cache_enabled_param = kwargs.pop("cache", "on")
        if cache_enabled_param == "on":
            cache_enabled = not is_icon
        elif cache_enabled_param == "off":
            cache_enabled = False
        else:
            raise ValueError(
                "Unknown value for the 'cache' parameter: %s"
                % cache_enabled_param
            )

        if cache_enabled:
        
            # Client side cache
            mtime = renderer.last_change_in_appearence(item)
            cherrypy.response.headers["Last-Modified"] = rfc822.formatdate(mtime)
            validate_since()

            # Server side cache            
            extension = format or self.default_format
            file_name = str(id)
            
            if processing:
                file_name += "-" + "-".join(processing)
            
            if kwargs:
                file_name += "?"
                for key in sorted(kwargs):
                    value = kwargs[key]
                    file_name += "%s=%s" % (key, value) 

            file_name = file_name.replace("/", "-") + "." + extension.lower()

            cached_file = os.path.join(
                context["cms"].application_path,
                "thumbnails",
                file_name
            )

            if os.path.exists(cached_file) \
            and os.stat(cached_file).st_mtime >= mtime:
                mime_type = self.mime_types_by_format[extension]
                cherrypy.response.headers["Content-Type"] = mime_type
                return serve_file(cached_file, mime_type)

        # Read rendering parameters
        params = {}
        rendering_schema = renderer.rendering_schema
        
        if rendering_schema:
            
            if not rendering_schema.name:
                raise ValueError("All rendering schemas must have a name")

            get_parameter(
                rendering_schema,
                target = params,
                prefix = rendering_schema.name + ".",
                implicit_booleans = False
            )

        # Render the image
        image = renderer.render(item, **params)

        # The renderer designated an image file as its return value: either
        # serve it unmodified, or load it into memory for further processing
        if isinstance(image, tuple):

            file_name, file_mime_type = image
            file_format = self.formats_by_mime_type[file_mime_type]

            # Image processing or re-encoding requested
            if processing or (format and format != file_format):
                image = Image.open(file_name)
                if not format:
                    format = file_format

            # Unmodified image requested; serve it straight away
            else:
                return serve_file(file_name, file_mime_type)

        # Image processing
        for processor in processing:
            pfunc, pargs, pkwargs = self._parse_processor(processor)
            image = pfunc(image, *pargs, **pkwargs)

        # Encode the resulting image
        buffer = StringIO()

        if not format:
            format = self.default_format

        if format == "JPEG" and image.mode != "RGB":
            image = image.convert("RGB")               

        # Store the resulting image in the server side cache
        if cache_enabled:
            image.save(cached_file, format)

        # Serve the image
        cherrypy.response.headers["Content-Type"] = \
            self.mime_types_by_format[format]
        image.save(buffer, format)
        return buffer.getvalue()

    def _parse_processor(self, processor_string):

        pos = processor_string.find("(")
        if pos == -1:
            proc_name = processor_string
        else:
            proc_name = processor_string[:pos]

        func = image_processors[proc_name]
        args = []
        kwargs = {}

        if pos != -1:
            if processor_string[-1] != ")":
                raise ValueError(
                    "Invalid image processor string: %s" % processor_string
                )

            for arg in processor_string[pos + 1:-1].split(","):
                parts = arg.split("=")
                
                # Positional parameters
                if len(parts) == 1:
                    value = self._parse_value(arg)
                    args.append(value)

                # Keyword parameters
                elif len(parts) == 2:
                    key, value = parts
                    value = self._parse_value(value)
                    kwargs[key] = value
                else:
                    raise ValueError(
                        "Invalid image processor string: %s" % processor_string
                    )

        return func, args, kwargs

    def _parse_value(self, value):

        if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'\""):
            return value[1:-1]

        if value in ("True", "False"):
            return bool(value)
        
        if "." in value:
            try:
                value = float(value)
            except ValueError:
                pass
            else:
                return value

        try:
            value = int(value)
        except ValueError:
            pass
        else:
            return value

        raise ValueError(
            "Invalid value for image processor parameter: %s" % value
        )

# Content renderers
#------------------------------------------------------------------------------

class ContentRenderersRegistry(ListWrapper):

    def __init__(self, items = None):
        ListWrapper.__init__(self, items)
        self.__lock = Lock()

    def register(self, renderer, after = None, before = None):
        """Add a new content renderer to the registry.

        By default, renderers are appended at the end of the registry. The
        L{after} or L{before} parameters can be used to specify the position in
        the registry where the renderer should be inserted. This can be
        important, since renderer are tried in registration order

        @param renderer: The renderer to register.
        @type renderer: L{ContentRenderer}

        @param after: 
        """

        with self.__lock:

            marker = after or before

            if marker is None:
                self._items.append(renderer)            
            else:
                if after and before:
                    raise ValueError(
                        "Can't register content renderer '%s' specifying both "
                        "'after' and 'before' parameters"
                        % renderer
                    )

                # Find the requested position
                if isinstance(marker, type):

                    # Find the first instance of the given type
                    for pos, registered_renderer in enumerate(self._items):
                        if isinstance(registered_renderer, marker):
                            break
                    else:
                        pos = -1
                else:
                    # Find an specific instance
                    try:
                        pos = self._items.index(marker)
                    except IndexError:
                        pos = -1

                if pos == -1:
                    raise IndexError(
                        "Trying to register content renderer '%s' %s %s, "
                        "which hasn't been registered yet"
                        % (renderer, "after" if after else "before", marker)
                    )

                if before:
                    self._items.insert(pos, renderer)
                else:
                    self._items.insert(pos + 1, renderer)  


class ContentRenderer(object):

    @abstractmethod
    def can_render(self, item):
        """Indicates if the renderer is able to render the given item.

        @param item: The item to evaluate.
        @type item: L{Item<woost.models.item.Item>}

        @return: True if the renderer claims to be able to render the item,
            False otherwise.
        @rtype: bool
        """

    @cached_getter
    def rendering_schema(self):
        """A schema describing additional parameters for the rendering process.
        @type: L{Schema<cocktail.schema.schema.Schema>}        
        """

    @abstractmethod
    def render(self, item, **kwargs):
        """Produces an image for the given item.

        @param item: The item to render.
        @type item: L{Item<woost.models.item.Item>}

        @param kwargs: Additional parameters that can modify the image produced
            by the renderer. These are implementation dependant, and should
            be described by L{rendering_schema}.

        @return: The image for the given item. If a suitable image file
            exists, the method should return a tuple consisting of a string
            pointing to its path and its MIME type. Otherwise, the method
            should craft the image in-memory and return it as an instance of
            the L{Image<Image.Image>} class.
        @rtype: tuple(str, str) or L{Image<Image.Image>}
        """

    def last_change_in_appearence(self, item):
        """Determines the last time an item was modified in a way that may
        alter its rendering.

        This method is mostly used to check wether cached images are still
        current.

        @param item: The item to evaluate.
        @type item: L{Item<woost.models.item.Item>}

        @return: The timestamp of the last change to the item that could alter
            the resulting image produced by the L{render} method of this
            renderer.
        @rtype: float
        """
        return mktime(item.last_update_time.timetuple())


class ImageFileRenderer(ContentRenderer):
    """A content renderer that handles image files."""

    def can_render(self, item):
        return isinstance(item, File) and item.resource_type == "image"

    def render(self, item):
        return (item.file_path, item.mime_type)

    def last_change_in_appearence(self, item):
        return os.stat(item.file_path).st_mtime


class ImageURIRenderer(ContentRenderer):

    def can_render(self, item):
        return isinstance(item, URI) and item.resource_type == "image"

    def get_item_uri(self, item):
        return item.uri

    def render(self, item):
        
        # Open the remote resource
        uri = self.get_item_uri(item)
        http_resource = urlopen(uri)

        # Wrap image data in a buffer
        # (the object returned by urlopen() doesn't support seek(), which is
        # required by PIL)
        buffer = StringIO()
        buffer.write(http_resource.read())
        buffer.seek(0)

        return Image.open(buffer)

    def last_change_in_appearence(self, item):
        uri = self.get_item_uri(item)
        urlparts = urlsplit(uri)
        host = urlparts[1]
        path = urlparts[2] + urlparts[3] + urlparts[4]
        http_conn = HTTPConnection(host)
        http_conn.request("HEAD", path)
        http_date = http_conn.getresponse().getheader("last-modified")
        http_conn.close()
        return mktime(strptime(http_date, "%a, %d %b %Y %H:%M:%S %Z"))


class HTMLRenderer(ContentRenderer):
    """A content renderer that handles XHTML/HTML pages."""

    mime_types = set([
        "text/html",
        "text/xhtml",
        "application/xhtml"
    ])

    def can_render(self, item):
        return (
            isinstance(item, Publishable)
            and item.mime_type in self.mime_types            
            and item.is_accessible(
                user = User.get_instance(qname = "woost.anonymous_user")
            )
        )

    @cached_getter
    def rendering_schema(self):
        return schema.Schema("html", members = [
            schema.Integer("window_width", min = 0),
            schema.Integer("window_height", min = 0)
        ])

    def render(self, item, window_width = None, window_height = None):

        temp_path = mkdtemp()

        try:
            temp_image_file = os.path.join(temp_path, "thumbnail.png")
            
            location = Location.get_current_host()
            location.path_info = context["cms"].uri(item)
            
            command = "python -m woost.models.renderurl %s %s" \
                % (unicode(location), temp_image_file)
            
            if window_width is not None:
                command += " --min-width %d" % window_width
            
            if window_height is not None:
                command += " --min-width %d" % window_height 

            Popen(command, shell = True).wait()
            return Image.open(temp_image_file)

        finally:
            rmtree(temp_path)


class IconRenderer(ContentRenderer):

    @cached_getter
    def rendering_schema(self):
        return schema.Schema("icon", members = [
            schema.Integer("size", default = 16)
        ])

    def can_render(self, item):
        return True

    def render(self, item, size):
        icon_file = context["cms"].icon_resolver.find_icon(item, size)
        return (icon_file, guess_type(icon_file)[0])


content_renderers = ContentRenderersRegistry()
content_renderers.register(ImageFileRenderer())
content_renderers.register(ImageURIRenderer())

try:
    import PyQt4.QtWebKit
except ImportError:
    pass
else:
    content_renderers.register(HTMLRenderer())

icon_renderers = ContentRenderersRegistry()
icon_renderers.register(IconRenderer())

# Image processors
#------------------------------------------------------------------------------
def resolve_px(value, size):
    if isinstance(value, float):
        value = int(size * value)
    elif value < 0:
        value = size + value
    return value

def resolve_color(value):

    if isinstance(value, basestring):

        if len(value) == 3:
            value = tuple(int(d * 2, 16) for d in value)
        elif len(value) == 6:
            value = (
                int(value[0:2], 16),
                int(value[2:4], 16),
                int(value[4:6], 16)
            )
        else:
            raise ValueError("Invalid color: " + value)

    return value

image_processors = OrderedDict()

def image_processor(func):
    image_processors[func.func_name] = func
    return image_processors

@image_processor
def thumbnail(image, width = None, height = None, filter = Image.ANTIALIAS):
    
    im_width, im_height = image.size

    if width is None:
        width = im_width

    if height is None:
        height = im_height

    image.thumbnail(
        (resolve_px(width, im_width),
         resolve_px(height, im_height)),
        filter
    )
    return image

@image_processor
def crop(image, x1, y1, x2, y2):

    width, height = image.size
    return image.crop((
        resolve_px(x1, width),
        resolve_px(y1, height),
        resolve_px(x2, width),
        resolve_px(y2, height)
    ))

@image_processor
def fill(image, width, height, crop = "center", filter = Image.ANTIALIAS):

    source_width, source_height = image.size
    source_ratio = float(source_width) / source_height
    target_ratio = float(width) / height

    if source_ratio > target_ratio:
        target_width = int(height * source_ratio)
        target_height = height
    else:
        target_width = width
        target_height = int(width * (1 / source_ratio))

    image = image.resize((target_width, target_height), filter)

    if crop:
        if crop == "center":
            offset_x = (target_width - width) / 2
            offset_y = (target_height - height) / 2
            image = image.crop((
                offset_x,
                offset_y,
                width + offset_x,
                height + offset_y
            ))
        else:
            raise ValueError("crop = %s not implemented" % crop)

    return image

@image_processor
def rotate(image, angle, expand = True, filter = Image.BICUBIC):
    return image.rotate(angle, filter, expand)

@image_processor
def color(image, level):
    return ImageEnhance.Color(image).enhance(level)

@image_processor
def brightness(image, level):
    return ImageEnhance.Brightness(image).enhance(level)

@image_processor
def contrast(image, level):
    return ImageEnhance.Contrast(image).enhance(level)

@image_processor
def sharpness(image, level):
    return ImageEnhance.Sharpness(image).enhance(level)

@image_processor
def frame(
    image,
    edge = 1,
    edge_color = (0,0,0),
    padding = 0,
    padding_color = (255,255,255)
):
    edge_color = resolve_color(edge_color)
    padding_color = resolve_color(padding_color)

    # Create the canvas
    width, height = image.size
    offset = edge + padding
    canvas = Image.new("RGBA", (width + offset * 2, height + offset * 2))    

    # Paint the border
    if edge:
        canvas.paste(edge_color, None)

    # Paint the padding color
    canvas.paste(
        padding_color,
        (edge,
         edge,
         width + offset * 2 - edge,
         height + offset * 2 - edge)
    )
    
    # Paste the original image over the frame
    canvas.paste(
        image,
        (offset, offset),
        image if image.mode in ("1", "L", "RGBA") else None
    )

    return canvas

@image_processor
def shadow(
    image,
    offset = 5,
    color = (70,70,70),
    padding = 8,
    iterations = 3):   
    
    # Create the backdrop image -- a box in the background colour with a 
    # shadow on it.
    total_width = image.size[0] + abs(offset) + 2 * padding
    total_height = image.size[1] + abs(offset) + 2 * padding
    back = Image.new("RGBA", (total_width, total_height))

    # Place the shadow, taking into account the offset from the image
    shadow_left = padding + max(offset, 0)
    shadow_top = padding + max(offset, 0)
    color = resolve_color(color)
    back.paste(color, [
        shadow_left,
        shadow_top,
        shadow_left + image.size[0], 
        shadow_top + image.size[1]
    ])

    # Apply the filter to blur the edges of the shadow.  Since a small kernel
    # is used, the filter must be applied repeatedly to get a decent blur.
    for n in range(iterations):
        back = back.filter(ImageFilter.BLUR)

    # Paste the input image onto the shadow backdrop
    image_left = padding - min(offset, 0)
    image_top = padding - min(offset, 0)
    back.paste(
        image,
        (image_left, image_top),
        image if image.mode in ("1", "L", "RGBA") else None
    )

    return back

