# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import io
import math
import os
import uuid
from random import Random
from urllib.parse import urljoin, quote

try:
    import PIL
    from PIL import Image, ImageDraw, ImageFont
except ImportError:
    PIL = None

from trytond.config import config
from trytond.model import ModelSQL, fields, Unique
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.wsgi import Base64Converter

from .resource import ResourceMixin

if config.getboolean('database', 'avatar_filestore', default=False):
    file_id = 'image_id'
    store_prefix = config.get('database', 'avatar_prefix', default=None)
else:
    file_id = None
    store_prefix = None
URL_BASE = config.get('web', 'avatar_base', default='')
FONT = os.path.join(os.path.dirname(__file__), 'fonts', 'karla.ttf')


class ImageMixin:
    __slots__ = ()
    image = fields.Binary(
        "Image", file_id=file_id, store_prefix=store_prefix)
    image_id = fields.Char("Image ID", readonly=True)


class Avatar(ImageMixin, ResourceMixin, ModelSQL):
    "Avatar"
    __name__ = 'ir.avatar'

    uuid = fields.Char("UUID", required=True)
    cache = fields.One2Many('ir.avatar.cache', 'avatar', "Cache")

    @classmethod
    def __setup__(cls):
        super().__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('resource_unique', Unique(t, t.resource),
                'ir.msg_avatar_resource_unique'),
            ]

    @classmethod
    def default_uuid(cls):
        return uuid.uuid4().hex

    @classmethod
    def create(cls, vlist):
        vlist = [v.copy() for v in vlist]
        for values in vlist:
            values.setdefault('uuid', cls.default_uuid())
        return super().create(vlist)

    @classmethod
    def write(cls, *args):
        avatars = sum(args[0:None:2], [])
        super().write(*args)
        cls.clear_cache(avatars)

    @property
    def url(self):
        if self.image_id or self.image:
            return urljoin(
                URL_BASE, quote('/avatar/%(database)s/%(uuid)s' % {
                        'database': Base64Converter(None).to_url(
                            Transaction().database.name),
                        'uuid': self.uuid,
                        }))

    def get(self, size=64):
        size = min((
                2 ** math.ceil(math.log2(size)),
                10 * math.ceil(size / 10) if size <= 100
                else 50 * math.ceil(size / 50)))
        if not (0 < size <= 2048):
            raise ValueError("Invalid size")
        for avatar in self.cache:
            if avatar.size == size:
                return avatar.image
        if not self.image:
            return None
        if PIL:
            with Transaction().new_transaction():
                cache = self._store_cache(size, self._resize(size))
                # Save cache only if record is already committed
                if self.__class__.search([('id', '=', self.id)]):
                    cache.save()
                return cache.image
        else:
            return self.image

    @classmethod
    def convert(cls, image, **_params):
        if not PIL or not image:
            return image
        data = io.BytesIO()
        img = Image.open(io.BytesIO(image))
        width, height = img.size
        size = min(width, height)
        img = img.crop((
                (width - size) // 2,
                (height - size) // 2,
                (width + size) // 2,
                (height + size) // 2))
        if size > 2048:
            img = img.resize((2048, 2048))
        if img.mode in {'RGBA', 'P'}:
            img = img.convert('RGB')
        img.save(data, format='jpeg', optimize=True, **_params)
        return data.getvalue()

    def _resize(self, size=64, **_params):
        if not PIL:
            return self.image
        data = io.BytesIO()
        img = Image.open(io.BytesIO(self.image))
        img = img.resize((size, size))
        img.save(data, format='jpeg', optimize=True, **_params)
        return data.getvalue()

    def _store_cache(self, size, image):
        pool = Pool()
        Cache = pool.get('ir.avatar.cache')
        return Cache(
            avatar=self,
            image=image,
            size=size)

    @classmethod
    def clear_cache(cls, avatars):
        pool = Pool()
        Cache = pool.get('ir.avatar.cache')
        caches = [c for a in avatars for c in a.cache]
        Cache.delete(caches)


class AvatarCache(ImageMixin, ModelSQL):
    "Avatar Cache"
    __name__ = 'ir.avatar.cache'

    avatar = fields.Many2One(
        'ir.avatar', "Avatar", required=True, ondelete='CASCADE')
    size = fields.Integer(
        "Size", required=True,
        domain=[
            ('size', '>', 0),
            ('size', '<=', 2048),
            ])

    @classmethod
    def __setup__(cls):
        super().__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('size_unique', Unique(t, t.avatar, t.size),
                'ir.msg_avatar_size_unique'),
            ]
        cls._order.append(('size', 'ASC'))


def generate(size, string):
    if not PIL:
        return

    def background_color(string):
        random = Random(string)
        r = v = b = 255
        # Skip too bright color
        while r + v + b > 255 * 2:
            r = random.randint(0, 255)
            v = random.randint(0, 255)
            b = random.randint(0, 255)
        return r, v, b

    try:
        font = ImageFont.truetype(FONT, size=int(0.65 * size))
    except ImportError:
        return
    white = (255, 255, 255)
    image = Image.new('RGB', (size, size), background_color(string))
    draw = ImageDraw.Draw(image)
    letter = string[0].upper() if string else ''
    draw.text(
        (size / 2, size / 2), letter, fill=white, font=font, anchor='mm')
    data = io.BytesIO()
    image.save(data, format='jpeg', optimize=True)
    return data.getvalue()
