"""This module contains the models for the ``media`` app"""

import os
import shutil
import subprocess

from django.conf import settings
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from constance import config
from django_hashable_file_fields.hashers import with_default
from django_hashable_file_fields.models import HashableFileFieldsBaseModel

from mtp.discrete_apps.versionable.models import Dependency, PolymorphicModelForDependency

from mtp.apps.common.utils import upload_to_generator
from mtp.apps.sync.models import Syncable, SyncableForPolymorphicMixin
from mtp.apps.trainings.models import (
    Training,
    TrainingActivity,
    TrainingSession
)

from .fields import MimeTypeRestrictedFileField
from .managers import (
    DependencyMediaManager,
    DependencyPdfMediaManager,
    DependencyPdfMediaQuerySet,
    DependencyPresentationMediaManager,
    DependencyPresentationMediaQuerySet,
    DependencyVideoMediaManager,
    DependencyVideoMediaQuerySet,
    NoteQueryset,
)


class Media(Dependency,
            Syncable,
            SyncableForPolymorphicMixin,
            PolymorphicModelForDependency):
    """Base (non-abstract) Django model representing a media."""

    non_lockable_fields = Dependency.non_lockable_fields + Syncable.non_lockable_fields
    objects = DependencyMediaManager()


class VideoMedia(Media):
    """Media mean to store a video file

    Attributes
    ----------
    file: ``MimeTypeRestrictedFileField`` field
        The video file

    """

    file = MimeTypeRestrictedFileField(
        upload_to=upload_to_generator,
        verbose_name=_('Video file'),
        help_text=_('Only MP4 files (max 200Mo) are allowed.'),
        mime_types=['video/mp4'],
        max_size=209715200,  # 200MB
    )

    versionable_link = (Training, 'video__training')
    objects = DependencyVideoMediaManager.from_queryset(DependencyVideoMediaQuerySet)()


class PdfMedia(HashableFileFieldsBaseModel, Media):
    """Media mean to store a pdf file

    Attributes
    ----------
    file: ``MimeTypeRestrictedFileField`` field
        The pdf file
    file_hash: django ``CharField`` field
        The calculated hash of the ``file`` file.

    """

    file = MimeTypeRestrictedFileField(
        upload_to=upload_to_generator,
        verbose_name=_('Pdf file'),
        mime_types=['application/pdf'],
        help_text=_('Only PDF (max 10Mo) are allowed.'),
        max_size=10485760,  # 10MB
        blank=True,
    )
    file_hash = models.CharField(max_length=36, blank=True)

    objects = DependencyPdfMediaManager.from_queryset(DependencyPdfMediaQuerySet)()
    versionable_link = (Training, 'presentation_media__presentation__training')

    hashable_fields = [
        ('file', 'file_hash', with_default(settings.EMPTY_FILE_HASH)),
    ]

    IMAGES_FOLDER_PREFIX = 'images_'

    @cached_property
    def nb_pdf_pages(self):
        """Get the number of pages of the pdf file

        Returns
        -------
        int
            The number of the pages

        Raises
        ------
        ValueError
            If we can't correctly parse the output of the "pdfinfo" command

        """

        # How many pages do we have ?
        command = "pdfinfo %s | grep Pages: | awk '{print $2}'" % self.file.path
        output = subprocess.check_output(command, shell=True)

        try:
            return int(output)
        except ValueError:
            raise ValueError(
                'The "pdfinfo" command number of pages output is incorrect for the pdf media %s' % (
                    self.pk
                )
            )

    @cached_property
    def images_absolute_path(self):
        """Get the absolute path of the folder containing the images extracted from the pdf file

        Returns
        -------
        str
            The full path of the folder containing the images

        Notes
        -----
        The returned path may be non-existent

        """

        return os.path.normpath(
            os.path.join(
                settings.MEDIA_ROOT,
                settings.UPLOAD_TO[self._meta.object_name],
                '%(folder_prefix)s%(file_hash)s' % {
                    'folder_prefix': self.IMAGES_FOLDER_PREFIX,
                    'file_hash': self.file_hash
                }
            )
        )

    @cached_property
    def images_url_list(self):
        """Get the images urls list

        Returns
        -------
        list or None
            A list of images urls or None

        Examples
        --------
        >>> [
        ...     '/media_app/pdf/pdf_file_hash/file_hash-001.jpg',
        ...     '/media_app/pdf/pdf_file_hash/file_hash-002.jpg',
        >>> ]

        """

        image_urls = []

        if not os.path.isdir(self.images_absolute_path):
            return []

        # Build the url of the folder containing the pdf media
        # We take the full url and remove the last part containing the document name
        pdf_media_folder_url_path = '/'.join(
            self.file.url.split('/')[:-1]
        )

        # Build the images folder name
        images_folder_name = self.IMAGES_FOLDER_PREFIX + self.file_hash

        for image_name in os.listdir(self.images_absolute_path):
            image_urls.append(
                '%s/%s/%s' % (
                    pdf_media_folder_url_path,
                    images_folder_name,
                    image_name
                )
            )

        return sorted(image_urls)

    def save(self, *args, **kwargs):
        """Save the instance and launch the task to generate the images

        For the parameters, see ``django.db.models.Model``

        """

        super().save(*args, **kwargs)

        self.generate_images(async=True)

    def is_generate_images_needed(self):
        """Check if we should extract images from the pdf

        Returns
        -------
        tuple(bool, exception)
            A tuple with two entries:
            1. bool: ``True`` if generating the images is needed, ``False`` otherwise.
            2. ``None`` if the first entry is ``True``, and, if ``False``, an instance of an
               exception to know why images generation is not needed.

        """

        # The folder containing the images already exists
        if os.path.isdir(self.images_absolute_path):
            return False, FileExistsError(
                'existing-dir',
                'Folder already exists',
                self.images_absolute_path
            )

        # We got a file when we should get a directory (!)
        if os.path.isfile(self.images_absolute_path):
            return False, FileExistsError(
                'existing-file',
                'Path exists, but not a directory',
                self.images_absolute_path
            )

        return True, None

    def generate_images(self, async=False, force=False):
        """Generate images from the pdf file

        Parameters
        ----------
        async: bool, default to ``False``
            If ``True``, the images generation will be done asynchronously,
            otherwise it will be executed directly.
        force: bool, default to ``False``
            If ``True``, we generate the images even if the folder containing
            the images already exists.

        Returns
        -------
        str / AsyncResult
            If ``async`` is ``True``, will return an ``AsyncResult`` from celery.
            If it's ``False``, will return the absolute path of the folder with
            the generated images.

        """

        needed, reason = self.is_generate_images_needed()

        if not needed:
            # The folder already exists, and we are NOT asked to force the generation
            if not force and isinstance(reason, FileExistsError):
                if reason.errno == 'existing-file':
                    raise reason
                return

        if async:
            # Async mode, we delay the task

            from .tasks import generate_images_from_pdf_media

            # Will return an ``AsyncResult`` from celery
            return generate_images_from_pdf_media.apply_async(
                kwargs={'pdf_media_pk': self.pk},
                countdown=3
            )

        # Create the folder for the images
        os.makedirs(self.images_absolute_path, exist_ok=True)

        pdf_path = self.file.path

        try:

            # Generate the images from the pdf, using ghost script
            args = [
                'gs',
                '-dNOPAUSE',  # Disables the prompt and pause at the end of each page.
                '-sDEVICE=png16m',  # The encoding device to use
                '-dFirstPage=1',
                '-dLastPage=%d' % self.nb_pdf_pages,
                '-r%s' % config.IMAGES_FROM_PDF_RESOLUTION,  # Image resolution (ex: 70, 100, etc)
                '-sOutputFile=%s/%s-%%03d.png' % (
                    self.images_absolute_path,
                    self.file_hash
                ),
                '-dTextAlphaBits=4',  # Control the use of subsample anti-aliasing
                pdf_path,
                '-c',
                'quit'  # Causes Ghostscript to exit after processing all files
            ]
            subprocess.check_output(args)

            if not len(self.images_url_list) == self.nb_pdf_pages:
                # Something went wrong, raise

                raise ValueError(
                    'Pdf to images task on the document %s failed for the following reason: '
                    'The number of extracted images does not match the pdf page number' % self.pk
                )

        except (ValueError, subprocess.CalledProcessError):
            # We didn't get a correct number of page, or  something else went wrong
            # "rollback" and raise
            shutil.rmtree(self.images_absolute_path)

            raise

        # Finally return the path
        return self.images_absolute_path


class PresentationMedia(Dependency, Syncable):
    """Django model representing a link between a ``Presentation`` and a ``Media``

    Attributes
    ----------
    is_restricted: django ``BooleanField`` field
    media: django ``OneToOneField`` to ``Media``
    presentation: django ``OneToOneField`` to ``Presentation``

    """

    is_restricted = models.BooleanField(
        default=False,
        verbose_name=_('Is restricted'),
    )

    media = models.OneToOneField(
        Media,
        verbose_name=_('Medias'),
        related_name='presentation_media',
        on_delete=models.CASCADE,
    )

    presentation = models.ForeignKey(
        'Presentation',
        verbose_name=_('Presentation'),
        related_name='presentation_medias',
        on_delete=models.CASCADE,
    )

    objects = DependencyPresentationMediaManager.from_queryset(
        DependencyPresentationMediaQuerySet
    )()
    non_lockable_fields = Dependency.non_lockable_fields + Syncable.non_lockable_fields
    versionable_link = (Training, 'presentation__training')

    class Meta:
        unique_together = ('media', 'presentation', 'is_restricted',)


class Presentation(TrainingActivity):
    """Django model representing a presentation

    Attributes
    ----------
    medias: django ``ForeignKey`` to ``Media``
        The medias for this presentation
    is_accessible_by_trainees: django ``BooleanField``
        Determine if this presentation can be accessible by the trainees

    """

    medias = models.ManyToManyField(
        Media,
        related_name='presentation',
        verbose_name=_('Medias'),
        through=PresentationMedia
    )

    is_accessible_by_trainees = models.BooleanField(
        default=True,
        verbose_name=_('Is available on trainee interface')
    )

    verbose_activity_name = _('Presentation')

    @cached_property
    def activity_type(self):
        """Return the activity type depending of the ``is_accessible_by_trainees`` flag

        Returns
        -------
        str

        """

        if self.is_accessible_by_trainees:
            return 'presentation'
        else:
            return 'restricted_presentation'

    @property
    def pdf(self):
        """Shortcut method to get the pdf media associated to this presentation

        Returns
        -------
        ``PdfMedia``

        """

        return PresentationMedia.objects.get_media_for_presentation(self)

    @property
    def restricted_pdf(self):
        """Shortcut method to get the restricted pdf media associated to this presentation

        Returns
        -------
        ``PdfMedia`` or None

        """

        return PresentationMedia.objects.get_media_for_presentation(self, is_restricted=True)

    @classmethod
    def get_training_create_url(cls, training):
        """Get the url to create a new presentation for the provided training

        Parameters
        ----------
        training: Training
            The training for which we want to create a new presentation.

        Returns
        -------
        string

        """

        return reverse('trainings:presentation-create', kwargs={'training_uuid': training.pk})

    @classmethod
    def get_create_url(cls):
        """Get the url to create a new presentation.

        Returns
        -------
        string

        """

        return reverse('media:presentation-create')

    def get_absolute_url(self):
        """Get the url of the current presentation.

        Returns
        -------
        string

        """

        return self.get_detail_url()

    def get_detail_url(self):
        """Get the url to view the current presentation.

        Returns
        -------
        string

        """

        return reverse('media:presentation-detail', kwargs={'presentation_uuid': self.pk})

    def get_edit_url(self):
        """Get the url to edit the current presentation.

        Returns
        -------
        string

        """

        return reverse('media:presentation-update', kwargs={'presentation_uuid': self.pk})


class Note(Syncable):
    """Django model representing a note

    Attributes
    ----------
    content: django ``TextField``
        The content of this note
    user: django foreign key to ``MTPUser``
        The user who took this note
    training_session: django foreign key to ``TrainingSession``
        The training session during this note was took
    presentation: django foreign key to ``Presentation``, nullable
        The presentation this note was taken on
    presentation_page: django ``PositiveSmallIntegerField`` field, nullable
        The presentation page this note was token on

    Notes
    -----
    The given user can have:
    - One note not linked with a presentation / session
    - One note linked to a presentation / session / presentation page

    """

    content = models.TextField(
        verbose_name=_('Content')
    )

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name='notes',
        verbose_name=_('User')
    )

    training_session = models.ForeignKey(
        TrainingSession,
        related_name='notes',
        verbose_name=_('Training session')
    )

    presentation = models.ForeignKey(
        Presentation,
        related_name='notes',
        verbose_name=_('Presentation'),
        null=True,
        blank=True,
    )

    presentation_page = models.PositiveSmallIntegerField(
        verbose_name=_('Presentation page'),
        null=True,
        blank=True,
    )

    objects = models.Manager.from_queryset(NoteQueryset)()

    class Meta:
        unique_together = ('training_session', 'user', 'presentation', 'presentation_page', )


class Video(TrainingActivity):
    """Django model representing a presentation

    Attributes
    ----------
    media: django ``OneToOneField`` to ``Media``
        The video

    """

    media = models.OneToOneField(
        Media,
        blank=True,
        related_name='video',
        on_delete=models.CASCADE
    )

    verbose_activity_name = _('Video')

    @classmethod
    def get_training_create_url(cls, training):
        """Get the url to create a new video for the provided training

        Parameters
        ----------
        training: Training
            The training for which we want to create a new video.

        Returns
        -------
        string

        """

        return reverse('trainings:video-create', kwargs={'training_uuid': training.pk})

    @classmethod
    def get_create_url(cls):
        """Get the url to create a new video.

        Returns
        -------
        string

        """

        return reverse('media:video-create')

    def get_absolute_url(self):
        """Get the url of the current video.

        Returns
        -------
        string

        """

        return self.get_detail_url()

    def get_detail_url(self):
        """Get the url to view the current video.

        Returns
        -------
        string

        """

        return reverse('media:video-detail', kwargs={'video_uuid': self.pk})

    def get_edit_url(self):
        """Get the url to edit the current video.

        Returns
        -------
        string

        """

        return reverse('media:video-update', kwargs={'video_uuid': self.pk})
