from PIL import Image
from django.db.models.fields.files import ImageField, ImageFieldFile
from django.core.files.base import ContentFile
import os
import math
import re
import colorsys

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

class PushPinImageFieldFile(ImageFieldFile):
    def hex_to_rgb(self, value):
        value = value.lstrip('#')
        lv = len(value)
        
        return tuple(int(value[i:i+lv/3], 16) for i in range(0, lv, lv/3))
    
    def max_size(self, width, height):
        ratio = float(width) / height

        if width > self.field.max_width:
            width = self.field.max_width
            height = int(math.floor(width / ratio))
        if height > self.field.max_height:
            height = self.field.max_height
            width = int(math.floor(height * ratio))        
        
        return width, height
        
    def crop_to_fixed(self, img, width, height):
        """
        Returns a scaled down and cropped version of the image based on the provided size values.
        """
        ratio = float(img.size[0]) / img.size[1]
        
        w = img.size[0]
        h = img.size[1]
        
        if (float(w) / width) >= (float(h) / height):
            new_h = height
            new_w = int(round(height * ratio))
        else:
            new_w = width
            new_h = int(round(width / ratio))
            
        img = img.resize((new_w, new_h), Image.ANTIALIAS)
        
        if (new_w, new_h) != (width, height):  # crop needed
            if new_w > width:
                start_x = int(round(float(new_w - width) / 2))
                img = img.crop((start_x, 0, start_x + width, new_h))
            elif new_w < width:
                img = img.resize((width, new_h), Image.ANTIALIAS)

            if new_h > height:
                start_y = int(round(float(new_h - height) / 2))
                img = img.crop((0, start_y, new_w, start_y + height))
            elif new_h < height:
                img = img.resize((new_w, height), Image.ANTIALIAS)

        return img
        
    def save(self, name, content, save=True):
        push_pin_im = Image.open(self.field.push_pin_path)

        new_content = StringIO()
        content.file.seek(0)
        
        in_image = Image.open(content.file)
        
        # if fixed size given, max will be ignored
        if self.field.fixed_width > 0 and self.field.fixed_height > 0:
            in_width, in_height = self.field.fixed_width, self.field.fixed_height
        else: # get adjusted height and width values
            in_width, in_height = self.max_size(in_image.size[0], in_image.size[1])

        # adjust further based on borders and stroke
        new_in_height = in_height - (self.field.border[0] * 2)
        new_in_width = in_width - (self.field.border[0] * 2)

        # scale or crop the input image
        if self.field.fixed_width or self.field.fixed_height:
            scaled_in_image = self.crop_to_fixed(in_image, new_in_width, new_in_height)
        else:
            scaled_in_image = in_image.resize((new_in_width, new_in_height), Image.ANTIALIAS)

        new_im = Image.new('RGB', (in_width, in_height))

        # outer stroke
        if self.field.outer_border_stroke[0]:
            new_im.paste(self.hex_to_rgb(self.field.outer_border_stroke[1]), (0, 0, in_width, in_height))

        # the main border
        if self.field.border[0]:
            new_im.paste(self.hex_to_rgb(self.field.border[1]), (self.field.outer_border_stroke[0], self.field.outer_border_stroke[0],
                                            in_width - self.field.outer_border_stroke[0],
                                            in_height - self.field.outer_border_stroke[0]
                                            ))

        offset_x = float(in_width - new_in_width) / 2
        offset_y = float(in_height - new_in_height) / 2

        more_offset_x = 0
        if offset_x % 1:
            more_offset_x = 1

        more_offset_y = 0
        if offset_y % 1:
            more_offset_y = 1

        offset_x = int(math.floor(offset_x))
        offset_y = int(math.floor(offset_y))

        # inner stroke
        if self.field.inner_border_stroke[0]:
            new_im.paste(self.hex_to_rgb(self.field.inner_border_stroke[1]), (
                                            offset_x - self.field.inner_border_stroke[0],
                                            offset_y - self.field.inner_border_stroke[0],
                                            in_width - (offset_x + more_offset_x) + self.field.inner_border_stroke[0],
                                            in_height - (offset_y + more_offset_y) + self.field.inner_border_stroke[0]
                                            ))

        # paste the scaled-down and centered input image
        new_im.paste(scaled_in_image, (
                            offset_x,
                            offset_y,
                            offset_x + new_in_width,
                            offset_y + new_in_height
                            ))

        # colorize the push pin
        push_pin_im = push_pin_im.convert('RGBA')
        pixdata = push_pin_im.load()
        rgb_push_pin = self.hex_to_rgb(self.field.push_pin_color)
        push_pin_hsv = colorsys.rgb_to_hsv(float(rgb_push_pin[0])/255, float(rgb_push_pin[1])/255,
                                            float(rgb_push_pin[2])/255)
        push_pin_hue = push_pin_hsv[0]
        push_pin_s = push_pin_hsv[1]

        highest_s = list(colorsys.rgb_to_hsv(float(pixdata[16, 20][0])/255, float(pixdata[16, 20][1])/255,
                                            float(pixdata[16, 20][2])/255))[1]
        s_offset = push_pin_s - highest_s

        for y in xrange(push_pin_im.size[1]):
            for x in xrange(push_pin_im.size[0]):
                hsv = list(colorsys.rgb_to_hsv(float(pixdata[x, y][0])/255, float(pixdata[x, y][1])/255,
                                            float(pixdata[x, y][2])/255))
                if hsv[0]>0:
                    hsv[0] = push_pin_hue
                    hsv[1] += s_offset
                    new_rgb = colorsys.hsv_to_rgb(hsv[0], hsv[1], hsv[2])
                    pixdata[x, y] = (int(round(new_rgb[0] * 255)), int(round(new_rgb[1] * 255)), int(round(new_rgb[2] * 255)), pixdata[x, y][3])
            
        # paste in the push pin
        pin_x = int(round(float(in_width - push_pin_im.size[1]) / 2)) + 8
        new_im.paste(push_pin_im, (
                            pin_x,
                            1,
                            pin_x + push_pin_im.size[0],
                            1 + push_pin_im.size[1]
                            ), push_pin_im)

        new_im.save(new_content,  format=self.field.format, quality=100)

        new_content = ContentFile(new_content.getvalue())

        super(PushPinImageFieldFile, self).save(name, new_content, save)
        
class PushPinImageField(ImageField):
    attr_class = PushPinImageFieldFile
    
    @staticmethod
    def is_valid_hex_color(value):
        if not value.startswith('#') or len(value) != 7 or not re.match('^[A-F0-9]*$', value[1:7].upper()):
            return False
        else:
            return True

    def check_color(self, value, field):
        if not PushPinImageField.is_valid_hex_color(value):
            raise ValueError('The color value for the %s field must contain a valid 6 digit hex color value' \
                            ' and must start with a hash(#) character. Short-hand notation (e.g. #FFF) is' \
                            ' not acceptable.' % field)    

    def __init__(self, push_pin_path, max_width=150, max_height=100, fixed_width=0, fixed_height=0,
                push_pin_color='#ffff33', format='PNG', border=(0, '#ffffff'), inner_border_stroke=(0,'#000000'),
                outer_border_stroke=(0, '#000000'), *args, **kwargs):
        self.check_color(push_pin_color, 'push_pin_color')
        self.check_color(border[1], 'border')
        self.check_color(inner_border_stroke[1], 'inner_border_stroke')
        self.check_color(outer_border_stroke[1], 'outer_border_stroke')
        if inner_border_stroke[0] + outer_border_stroke[0] > border[0]:
            raise ValueError('The sum of the sizes of the innner_border_stroke and outer_border_stroke' \
                            ' cannot exceed the size of the border.')
                            
        self.push_pin_path = push_pin_path
        self.max_width = max_width
        self.max_height = max_height
        self.fixed_width = fixed_width
        self.fixed_height = fixed_height
        self.push_pin_color = push_pin_color
        self.format = format
        self.border = border
        self.inner_border_stroke = inner_border_stroke
        self.outer_border_stroke = outer_border_stroke

        super(PushPinImageField, self).__init__(*args, **kwargs)