import logging

from furl import furl

from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from mayan.apps.converter.classes import ConverterBase
from mayan.apps.converter.models import LayerTransformation
from mayan.apps.converter.settings import setting_image_generation_timeout
from mayan.apps.converter.transformations import BaseTransformation
from mayan.apps.file_caching.models import CachePartitionFile
from mayan.apps.lock_manager.backends.base import LockingBackend

from ..managers import DocumentFilePageManager, ValidDocumentFilePageManager

from .document_file_models import DocumentFile
from .mixins import PagedModelMixin

__all__ = ('DocumentFilePage', 'DocumentFilePageSearchResult')
logger = logging.getLogger(name=__name__)


class DocumentFilePage(PagedModelMixin, models.Model):
    """
    Model that describes a document file page
    """
    _paged_model_parent_field = 'document_file'

    document_file = models.ForeignKey(
        on_delete=models.CASCADE, related_name='file_pages', to=DocumentFile,
        verbose_name=_('Document file')
    )
    page_number = models.PositiveIntegerField(
        db_index=True, default=1, editable=False,
        verbose_name=_('Page number')
    )

    objects = DocumentFilePageManager()
    valid = ValidDocumentFilePageManager()

    class Meta:
        ordering = ('page_number',)
        verbose_name = _('Document file page')
        verbose_name_plural = _('Document file pages')

    def __str__(self):
        return self.get_label()

    @cached_property
    def cache_partition(self):
        partition, created = self.document_file.cache.partitions.get_or_create(
            name=self.uuid
        )
        return partition

    def delete(self, *args, **kwargs):
        self.cache_partition.delete()
        super().delete(*args, **kwargs)

    def generate_image(
        self, user=None, _acquire_lock=True,
        transformation_instance_list=None, maximum_layer_order=None
    ):
        combined_transformation_list = self.get_combined_transformation_list(
            maximum_layer_order=maximum_layer_order,
            transformation_instance_list=transformation_instance_list,
            user=user
        )
        combined_cache_filename = self.get_combined_cache_filename(
            _combined_transformation_list=combined_transformation_list
        )

        logger.debug(
            'transformations cache filename: %s', combined_cache_filename
        )

        try:
            if _acquire_lock:
                lock_name = self.get_lock_name(
                    _combined_cache_filename=combined_cache_filename
                )
                lock = LockingBackend.get_backend().acquire_lock(
                    name=lock_name,
                    timeout=setting_image_generation_timeout.value
                )
        except Exception:
            raise
        else:
            # Second try block to release the lock even on fatal errors inside
            # the block.
            try:
                try:
                    self.cache_partition.get_file(
                        filename=combined_cache_filename
                    )
                except CachePartitionFile.DoesNotExist:
                    logger.debug(
                        'transformations cache file "%s" not found', combined_cache_filename
                    )
                    image = self.get_image(
                        transformation_instance_list=combined_transformation_list
                    )
                    with self.cache_partition.create_file(filename=combined_cache_filename) as file_object:
                        file_object.write(image.getvalue())
                else:
                    logger.debug(
                        'transformations cache file "%s" found', combined_cache_filename
                    )

                return combined_cache_filename
            finally:
                if _acquire_lock:
                    lock.release()

    def get_absolute_url(self):
        return reverse(
            viewname='documents:document_file_page_view', kwargs={
                'document_file_page_id': self.pk
            }
        )

    def get_api_image_url(
        self, maximum_layer_order=None, transformation_instance_list=None,
        user=None
    ):
        """
        Create an unique URL combining:
        - the page's image URL
        - the interactive argument
        - a hash from the server side and interactive transformations
        The purpose of this unique URL is to allow client side caching
        if document page images.
        """
        transformation_instance_list = transformation_instance_list or ()

        transformations_hash = BaseTransformation.combine(
            transformations=self.get_combined_transformation_list(
                maximum_layer_order=maximum_layer_order,
                transformation_instance_list=transformation_instance_list,
                user=user
            )
        )

        final_url = furl()
        final_url.path = reverse(
            viewname='rest_api:documentfilepage-image', kwargs={
                'document_id': self.document_file.document_id,
                'document_file_id': self.document_file_id,
                'document_file_page_id': self.pk
            }
        )
        # Remove leading '?' character.
        final_url.query = BaseTransformation.list_as_query_string(
            transformation_instance_list=transformation_instance_list
        )[1:]
        final_url.args['_hash'] = transformations_hash

        if maximum_layer_order is not None:
            final_url.args['maximum_layer_order'] = maximum_layer_order

        return final_url.tostr()

    def get_combined_cache_filename(
        self, maximum_layer_order=None, transformation_instance_list=None,
        user=None, _combined_transformation_list=None
    ):
        combined_transformation_list = _combined_transformation_list or self.get_combined_transformation_list(
            maximum_layer_order=maximum_layer_order,
            transformation_instance_list=transformation_instance_list,
            user=user
        )
        return BaseTransformation.combine(
            transformations=combined_transformation_list
        )

    def get_combined_transformation_list(
        self, maximum_layer_order=None, transformation_instance_list=None,
        user=None
    ):
        """
        Return a list of transformation containing the server side
        transformations for this object as well as transformations
        created from the arguments as transient interactive transformation.
        """
        result = []

        # Stored transformations first.
        result.extend(
            LayerTransformation.objects.get_for_object(
                as_classes=True, maximum_layer_order=maximum_layer_order,
                obj=self, user=user
            )
        )

        # Interactive transformations second.
        result.extend(transformation_instance_list or [])

        return result

    def get_image(self, transformation_instance_list=None):
        cache_filename = 'base_image'
        logger.debug('Page cache filename: %s', cache_filename)

        try:
            cache_file = self.cache_partition.get_file(filename=cache_filename)
        except CachePartitionFile.DoesNotExist:
            logger.debug('Page cache file "%s" not found', cache_filename)

            try:
                with self.document_file.get_intermediate_file() as file_object:
                    converter = ConverterBase.get_converter_class()(
                        file_object=file_object
                    )
                    converter.seek_page(page_number=self.page_number - 1)

                    page_image = converter.get_page()

                    # Since open "wb+" doesn't create files, create it
                    # explicitly.
                    with self.cache_partition.create_file(filename=cache_filename) as file_object:
                        file_object.write(page_image.getvalue())

                    # Apply runtime transformations.
                    for transformation in transformation_instance_list or ():
                        converter.transform(transformation=transformation)

                    return converter.get_page()
            except Exception as exception:
                logger.error(
                    'Error creating document file page cache file from '
                    'document file intermediate file. Expected file named '
                    '"%s" failed to be created; %s', cache_filename,
                    exception, exc_info=True
                )
                raise
        else:
            logger.debug('Page cache file "%s" found', cache_filename)

            with cache_file.open() as file_object:
                converter = ConverterBase.get_converter_class()(
                    file_object=file_object
                )

                converter.seek_page(page_number=0)

                # This code is also repeated below to allow using a context
                # manager with cache_file.open and close it automatically.
                # Apply runtime transformations.
                for transformation in transformation_instance_list or ():
                    converter.transform(transformation=transformation)

                return converter.get_page()

    def get_label(self):
        return _(
            '%(document_file)s - page %(page_num)d of %(total_pages)d'
        ) % {
            'document_file': self.document_file,
            'page_num': self.page_number,
            'total_pages': self.get_pages_last_number() or 1
        }
    get_label.short_description = _('Label')

    def get_lock_name(
        self, _combined_cache_filename=None, maximum_layer_order=None,
        transformation_instance_list=None, user=None
    ):
        if _combined_cache_filename:
            combined_cache_filename = _combined_cache_filename
        else:
            combined_cache_filename = self.get_combined_cache_filename(
                maximum_layer_order=maximum_layer_order,
                transformation_instance_list=transformation_instance_list,
                user=user
            )

        return 'document_file_page_generate_image_{}_{}'.format(
            self.pk, combined_cache_filename
        )

    @property
    def is_in_trash(self):
        return self.document_file.document.is_in_trash

    def natural_key(self):
        return (self.page_number, self.document_file.natural_key())
    natural_key.dependencies = ['documents.DocumentFile']

    @property
    def uuid(self):
        """
        Make cache UUID a mix of file ID and page ID to avoid using stale
        images.
        """
        return '{}-{}'.format(self.document_file.uuid, self.pk)


class DocumentFilePageSearchResult(DocumentFilePage):
    class Meta:
        ordering = ('document_file__document', 'page_number')
        proxy = True
        verbose_name = _('Document file page')
        verbose_name_plural = _('Document file pages')

import logging

from furl import furl

from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from mayan.apps.converter.classes import ConverterBase
from mayan.apps.converter.models import LayerTransformation
from mayan.apps.converter.settings import setting_image_generation_timeout
from mayan.apps.converter.transformations import BaseTransformation
from mayan.apps.file_caching.models import CachePartitionFile
from mayan.apps.lock_manager.backends.base import LockingBackend

from ..managers import DocumentFilePageManager, ValidDocumentFilePageManager

from .document_file_models import DocumentFile
from .mixins import PagedModelMixin

__all__ = ('DocumentFilePage', 'DocumentFilePageSearchResult')
logger = logging.getLogger(name=__name__)


class DocumentFilePage(PagedModelMixin, models.Model):
    """
    Model that describes a document file page
    """
    _paged_model_parent_field = 'document_file'

    document_file = models.ForeignKey(
        on_delete=models.CASCADE, related_name='file_pages', to=DocumentFile,
        verbose_name=_('Document file')
    )
    page_number = models.PositiveIntegerField(
        db_index=True, default=1, editable=False,
        verbose_name=_('Page number')
    )

    objects = DocumentFilePageManager()
    valid = ValidDocumentFilePageManager()

    class Meta:
        ordering = ('page_number',)
        verbose_name = _('Document file page')
        verbose_name_plural = _('Document file pages')

    def __str__(self):
        return self.get_label()

    @cached_property
    def cache_partition(self):
        partition, created = self.document_file.cache.partitions.get_or_create(
            name=self.uuid
        )
        return partition

    def delete(self, *args, **kwargs):
        self.cache_partition.delete()
        super().delete(*args, **kwargs)

    def generate_image(
        self, user=None, _acquire_lock=True,
        transformation_instance_list=None, maximum_layer_order=None
    ):
        combined_transformation_list = self.get_combined_transformation_list(
            maximum_layer_order=maximum_layer_order,
            transformation_instance_list=transformation_instance_list,
            user=user
        )
        combined_cache_filename = self.get_combined_cache_filename(
            _combined_transformation_list=combined_transformation_list
        )

        logger.debug(
            'transformations cache filename: %s', combined_cache_filename
        )

        try:
            if _acquire_lock:
                lock_name = self.get_lock_name(
                    _combined_cache_filename=combined_cache_filename
                )
                lock = LockingBackend.get_backend().acquire_lock(
                    name=lock_name,
                    timeout=setting_image_generation_timeout.value
                )
        except Exception:
            raise
        else:
            # Second try block to release the lock even on fatal errors inside
            # the block.
            try:
                try:
                    self.cache_partition.get_file(
                        filename=combined_cache_filename
                    )
                except CachePartitionFile.DoesNotExist:
                    logger.debug(
                        'transformations cache file "%s" not found', combined_cache_filename
                    )
                    image = self.get_image(
                        transformation_instance_list=combined_transformation_list
                    )
                    with self.cache_partition.create_file(filename=combined_cache_filename) as file_object:
                        file_object.write(image.getvalue())
                else:
                    logger.debug(
                        'transformations cache file "%s" found', combined_cache_filename
                    )

                return combined_cache_filename
            finally:
                if _acquire_lock:
                    lock.release()

    def get_absolute_url(self):
        return reverse(
            viewname='documents:document_file_page_view', kwargs={
                'document_file_page_id': self.pk
            }
        )

    def get_api_image_url(
        self, maximum_layer_order=None, transformation_instance_list=None,
        user=None
    ):
        """
        Create an unique URL combining:
        - the page's image URL
        - the interactive argument
        - a hash from the server side and interactive transformations
        The purpose of this unique URL is to allow client side caching
        if document page images.
        """
        transformation_instance_list = transformation_instance_list or ()

        transformations_hash = BaseTransformation.combine(
            transformations=self.get_combined_transformation_list(
                maximum_layer_order=maximum_layer_order,
                transformation_instance_list=transformation_instance_list,
                user=user
            )
        )

        final_url = furl()
        final_url.path = reverse(
            viewname='rest_api:documentfilepage-image', kwargs={
                'document_id': self.document_file.document_id,
                'document_file_id': self.document_file_id,
                'document_file_page_id': self.pk
            }
        )
        # Remove leading '?' character.
        final_url.query = BaseTransformation.list_as_query_string(
            transformation_instance_list=transformation_instance_list
        )[1:]
        final_url.args['_hash'] = transformations_hash

        if maximum_layer_order is not None:
            final_url.args['maximum_layer_order'] = maximum_layer_order

        return final_url.tostr()

    def get_combined_cache_filename(
        self, maximum_layer_order=None, transformation_instance_list=None,
        user=None, _combined_transformation_list=None
    ):
        combined_transformation_list = _combined_transformation_list or self.get_combined_transformation_list(
            maximum_layer_order=maximum_layer_order,
            transformation_instance_list=transformation_instance_list,
            user=user
        )
        return BaseTransformation.combine(
            transformations=combined_transformation_list
        )

    def get_combined_transformation_list(
        self, maximum_layer_order=None, transformation_instance_list=None,
        user=None
    ):
        """
        Return a list of transformation containing the server side
        transformations for this object as well as transformations
        created from the arguments as transient interactive transformation.
        """
        result = []

        # Stored transformations first.
        result.extend(
            LayerTransformation.objects.get_for_object(
                as_classes=True, maximum_layer_order=maximum_layer_order,
                obj=self, user=user
            )
        )

        # Interactive transformations second.
        result.extend(transformation_instance_list or [])

        return result

    def get_image(self, transformation_instance_list=None):
        cache_filename = 'base_image'
        logger.debug('Page cache filename: %s', cache_filename)

        try:
            cache_file = self.cache_partition.get_file(filename=cache_filename)
        except CachePartitionFile.DoesNotExist:
            logger.debug('Page cache file "%s" not found', cache_filename)

            try:
                with self.document_file.get_intermediate_file() as file_object:
                    converter = ConverterBase.get_converter_class()(
                        file_object=file_object
                    )
                    converter.seek_page(page_number=self.page_number - 1)

                    page_image = converter.get_page()

                    # Since open "wb+" doesn't create files, create it
                    # explicitly.
                    with self.cache_partition.create_file(filename=cache_filename) as file_object:
                        file_object.write(page_image.getvalue())

                    # Apply runtime transformations.
                    for transformation in transformation_instance_list or ():
                        converter.transform(transformation=transformation)

                    return converter.get_page()
            except Exception as exception:
                logger.error(
                    'Error creating document file page cache file from '
                    'document file intermediate file. Expected file named '
                    '"%s" failed to be created; %s', cache_filename,
                    exception, exc_info=True
                )
                raise
        else:
            logger.debug('Page cache file "%s" found', cache_filename)

            with cache_file.open() as file_object:
                converter = ConverterBase.get_converter_class()(
                    file_object=file_object
                )

                converter.seek_page(page_number=0)

                # This code is also repeated below to allow using a context
                # manager with cache_file.open and close it automatically.
                # Apply runtime transformations.
                for transformation in transformation_instance_list or ():
                    converter.transform(transformation=transformation)

                return converter.get_page()

    def get_label(self):
        return _(
            '%(document_file)s - page %(page_num)d of %(total_pages)d'
        ) % {
            'document_file': self.document_file,
            'page_num': self.page_number,
            'total_pages': self.get_pages_last_number() or 1
        }
    get_label.short_description = _('Label')

    def get_lock_name(
        self, _combined_cache_filename=None, maximum_layer_order=None,
        transformation_instance_list=None, user=None
    ):
        if _combined_cache_filename:
            combined_cache_filename = _combined_cache_filename
        else:
            combined_cache_filename = self.get_combined_cache_filename(
                maximum_layer_order=maximum_layer_order,
                transformation_instance_list=transformation_instance_list,
                user=user
            )

        return 'document_file_page_generate_image_{}_{}'.format(
            self.pk, combined_cache_filename
        )

    @property
    def is_in_trash(self):
        return self.document_file.document.is_in_trash

    def natural_key(self):
        return (self.page_number, self.document_file.natural_key())
    natural_key.dependencies = ['documents.DocumentFile']

    @property
    def uuid(self):
        """
        Make cache UUID a mix of file ID and page ID to avoid using stale
        images.
        """
        return '{}-{}'.format(self.document_file.uuid, self.pk)


class DocumentFilePageSearchResult(DocumentFilePage):
    class Meta:
        ordering = ('document_file__document', 'page_number')
        proxy = True
        verbose_name = _('Document file page')
        verbose_name_plural = _('Document file pages')

Model that describes a document file page # Second try block to release the lock even on fatal errors inside # the block. Create an unique URL combining:         - the page's image URL         - the interactive argument         - a hash from the server side and interactive transformations         The purpose of this unique URL is to allow client side caching         if document page images. # Remove leading '?' character. Return a list of transformation containing the server side         transformations for this object as well as transformations         created from the arguments as transient interactive transformation. # Stored transformations first. # Interactive transformations second. # Since open "wb+" doesn't create files, create it # explicitly. # Apply runtime transformations. # This code is also repeated below to allow using a context # manager with cache_file.open and close it automatically. # Apply runtime transformations. Make cache UUID a mix of file ID and page ID to avoid using stale         images.