""" This module holds the models of the ``documents`` app. """

import json
import logging
import os
import re
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 extended_choices import Choices

from mtp.apps.accounts.models import MTPUser
from mtp.apps.common.utils import upload_to_generator
from mtp.apps.trainings.models import TrainingActivity, TrainingSession
from mtp.apps.sync.models import Syncable

from mtp.discrete_apps.hashable.hashers import with_default
from mtp.discrete_apps.hashable.models import Hashable

logger = logging.getLogger('mtp.apps.documents')  # pylint: disable=invalid-name


class Document(Hashable, TrainingActivity):
    """Django model representing a document.

    Attributes
    ----------
    FILE_TYPES : ``Choices`` instance
        The file type will be determined with the ``get_file_type`` method.

    trainer_document_file : django ``FileField`` field
        The trainer's document file.
    trainer_document_file_hash : django ``CharField`` field
        The calculated hash of the ``trainer_document_file`` file.
    trainee_document_file : django ``FileField`` field
        The trainees document file
    trainee_document_file_hash : django ``CharField`` field
        The calculated hash of the ``trainee_document_file`` file.
    trainer_only : django ``BooleanField`` field
        If True, only the trainer can access the document on the client applications.

    Notes
    -----
    This model inherits from ``TrainingActivity`` and therefore is a ``Dependency`` of
    the ``Training`` model.

    """

    PDF_IMAGES_FOLDER_PREFIX = 'pdf_images_'

    FILE_TYPES = Choices(
        ('TEXT', 'txt', 'Text'),
        ('PDF', 'pdf', 'PDF'),
        ('VIDEO', 'vid', 'Video'),
        ('PPT', 'ppt', 'PPT'),
        ('OTHER', 'oth', 'Other')
    )
    FILE_TYPES.add_subset('DOWNLOADABLE', ('PDF', 'VIDEO'))

    DOCUMENT_TABLE = {
        'txt': FILE_TYPES.TEXT,
        'pdf': FILE_TYPES.PDF,
        'mov': FILE_TYPES.VIDEO,
        'm4v': FILE_TYPES.VIDEO,
        'mp4': FILE_TYPES.VIDEO,
        'ppt': FILE_TYPES.PPT,
        'pptx': FILE_TYPES.PPT,
    }

    trainer_document_file = models.FileField(
        blank=True,
        help_text=_('Only PDF and MP4 files are allowed.'),
        upload_to=upload_to_generator,
        verbose_name=_('Trainer file'),
    )
    trainer_document_file_hash = models.CharField(max_length=36, blank=True)
    trainee_document_file = models.FileField(
        help_text=_('Only PDF and MP4 files are allowed.'),
        upload_to=upload_to_generator,
        verbose_name=_('Trainees file'),
    )
    trainee_document_file_hash = models.CharField(max_length=36, blank=True)

    trainer_only = models.BooleanField(default=False,
                                       verbose_name=_("Visible by trainers only"))

    verbose_activity_name = _('Document')

    hashable_fields = [
        ('trainer_document_file', 'trainer_document_file_hash',
         with_default(settings.EMPTY_FILE_HASH)),
        ('trainee_document_file', 'trainee_document_file_hash',
         with_default(settings.EMPTY_FILE_HASH))
    ]

    def save(self, *args, **kwargs):
        """Save the instance and check if we should start a task to extract images."""

        export_pdf_to_images = kwargs.pop('export_pdf_to_images', True)

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

        if self.trainee_document_file_type == self.FILE_TYPES.PDF and export_pdf_to_images:
            self.generate_images_from_pdf(async=True)

    def is_generate_images_from_pdf_needed(self):
        """Check if we should extract images from the document

        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.

        """

        # We simply don't want to generate the images, according to the current settings
        if not settings.IMAGES_FROM_PDF_ACTIVATE:
            return False, ValueError('IMAGES_FROM_PDF_ACTIVATE settings is False')

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

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

        return True, None

    @cached_property
    def nb_pdf_pages(self):
        """Get the number of pages of the document's 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.trainee_document_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 document %s' % (
                    self.pk
                )
            )

    def generate_images_from_pdf(self, async=False, force=False):
        """Generate images from a pdf document

        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_from_pdf_needed()

        if not needed:
            # We simply don't want to, according to the settings
            if isinstance(reason, ValueError):
                return

            # 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

            # Will return an ``AsyncResult`` from celery
            return generate_images_from_pdf.delay(
                document_pk=self.pk
            )

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

        pdf_path = self.trainee_document_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' % settings.IMAGES_FROM_PDF_RESOLUTION,  # Image resolution (ex: 70, 100, etc)
                '-sOutputFile=%s/%s-%%03d.png' % (
                    self.pdf_images_absolute_path,
                    self.trainee_document_file_hash
                ),
                '-dTextAlphaBits=4',  # Control the use of subsample antialiasing
                pdf_path,
                '-c',
                'quit'  # Causes Ghostscript to exit after processing all files
            ]
            subprocess.check_output(args)

            if not len(self.pdf_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.pdf_images_absolute_path)

            raise

        # Finally return the path
        return self.pdf_images_absolute_path

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

        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],
                self.PDF_IMAGES_FOLDER_PREFIX + self.trainee_document_file_hash,
            )
        )

    def get_file_type(self, file_name):
        """Shortcut method to get the file type from the file name."""

        file_extension = file_name.split('.')[-1].lower()
        try:
            return self.DOCUMENT_TABLE[file_extension]
        except KeyError:
            return self.FILE_TYPES.OTHER

    @cached_property
    def pdf_images_url_list(self):
        """Get the image url list

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

        Examples
        --------
        >>> [
        ...     '/media/documents/pdf_images_file_hash/file_hash-001.jpg',
        ...     '/media/documents/pdf_images_file_hash/file_hash-002.jpg'
        >>> ]

        """

        image_urls = []

        if not os.path.isdir(self.pdf_images_absolute_path):
            return None

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

        # Build the images folder name
        images_folder_name = self.PDF_IMAGES_FOLDER_PREFIX + self.trainee_document_file_hash

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

        return sorted(image_urls)

    @cached_property
    def trainer_document_file_type(self):
        """Get the file type of the ``trainer_document_file`` field."""

        return self.get_file_type(self.trainer_document_file.name)

    @cached_property
    def trainee_document_file_type(self):
        """Get the file type of the ``trainee_document_file`` field."""

        return self.get_file_type(self.trainee_document_file.name)

    @cached_property
    def is_video(self):
        """Test if the ``trainee_document_file`` is a "video" type one

        Returns
        -------
        bool
            ``True`` if the document is a video type, else ``False``

        """

        return self.trainee_document_file_type == self.FILE_TYPES.VIDEO

    @cached_property
    def media_files(self):
        """Get a list of media file paths for this document

        Returns
        -------
        list(str)
            A list of media relative paths for this document

        """

        return [
            self.trainee_document_file.name,
            self.trainer_document_file.name
        ]

    def compatibility_serialize(self):
        """Serialize the document to a python representation.

        Notes
        -----
        This method will be deprecated in the version 2 of the API.

        """

        document_objects = []
        trainee_document = {
            'id': self.id,
            'original_version': self.original_version_id,
            'url': self.trainee_document_file.url,
            'hash': self.trainee_document_file_hash,
            'type': 'video' if self.is_video else self.trainee_document_file_type,
            'title': self.name,
            'preview': 'video-preview.png' if self.is_video else 'document-preview.png',
            'file_name': '%s.%s' % (self.trainee_document_file_hash,
                                    self.trainee_document_file.name.split('.')[-1]),
            'module': 'document',
            'groups': ['trainer'] if self.trainer_only else ['trainer', 'trainee'],
            'cached': False if self.is_video else True,
            'order': self.sort_order,
            'needs_approval': self.needs_approval,
            'image_urls': self.pdf_images_url_list,
        }

        document_objects.append(trainee_document)

        if self.trainer_document_file:
            trainer_document = {
                'id': self.id + '_trainer',
                'original_version': self.original_version_id,
                'url': self.trainer_document_file.url,
                'hash': self.trainer_document_file_hash,
                'type': 'download',
                'title': self.name,
                'file_name': '%s-annoted.%s' % (self.trainee_document_file_hash,
                                                self.trainee_document_file.name.split('.')[-1]),
                'module': 'document',
                'groups': ['trainer'] if self.trainer_only else ['trainer', 'trainee'],
                'cached': True,
                'order': self.sort_order,
                'needs_approval': self.needs_approval
            }

            document_objects.append(trainer_document)

        return document_objects

    def get_absolute_url(self):
        """ Get the url of to view the document.

        Returns
        -------
        string
            The wanted url.

        """
        return self.get_detail_url()

    def get_detail_url(self):
        """ Get the url of to view the document.

        Returns
        -------
        string
            The wanted url.

        """
        return reverse('documents:document-detail', kwargs={'document_uuid': self.pk})

    def get_edit_url(self):
        """ Get the url of to edit the document.

        Returns
        -------
        string
            The wanted url.

        """
        return reverse('documents:document-update', kwargs={'document_uuid': self.pk})

    @classmethod
    def get_create_url(self, training):
        """ Get the url of to create a new document.

        Returns
        -------
        string
            The wanted url.

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


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

    Attributes
    ----------
    user : foreign key to ``mtp.apps.accounts.models.MTPUser``
        The user the note belongs to.
    session : foreign key to ``mtp.apps.trainings.models.TrainingSession``
        The session the note where taken into.
    document : foreign key to ``mtp.apps.documents.models.Document``
        The document the note belongs to.
    type : django ``CharField`` field.
        The type of the note.
    text : django ``TextField`` field.
        The content of the note.
    page : django ``PositiveIntegerField`` field.
        The page of the document the note was taken on.
    coord_x : django ``FloatField`` field.
        The X axis coordinates of the note.
    coord_y : django ``FloatField`` field.
        The Y axis coordinates of the note.
    width : django ``FloatField`` field.
        The width of the note.
    height : django ``FloatField`` field.
        The height of the note.

    Notes
    -----
    This model is ``Syncable``.

    """
    user = models.ForeignKey(MTPUser, db_index=True)
    session = models.ForeignKey(TrainingSession)
    document = models.ForeignKey(Document, null=True, blank=True)

    note_type = models.CharField(max_length=100)
    text = models.TextField()

    page = models.PositiveIntegerField()
    coord_x = models.FloatField()
    coord_y = models.FloatField()
    width = models.FloatField()
    height = models.FloatField()

    def __str__(self):
        return '%s - %s - %d - %s' % (self.user, self.document,
                                      self.page, self.text[:10])

    def x_end(self):
        return self.coord_x + settings.ANNOT_SIZE

    def y_end(self):
        return self.coord_y + settings.ANNOT_SIZE

    def sanitize_text(self):
        """Sanitize the text of the note."""

        return re.sub(
            '[\\\/\[\]()#{}\%<>]',
            lambda x: '\%s' % x.groups(0),
            self.text
        )


ACTION_CHOICES = Choices(
    ('OPEN', 'op', 'open'),
    ('POPIN', 'po', 'popin'),
    ('POPIN_OPEN', 'pp', 'popin open'),
    ('LOCKED', 'lo', 'locked'),
    ('AUTHORIZATION', 'au', 'authorization')
)


class BackgroundExecution(Syncable):

    action = models.CharField(max_length=20, choices=ACTION_CHOICES)

    document = models.ForeignKey(Document, null=True, blank=True)
    title = models.CharField(max_length=50, blank=True)
    description = models.CharField(max_length=250, blank=True)

    begin_date = models.TimeField()
    end_date = models.TimeField()

    locked = models.BooleanField(default=False)

    mgroups = models.ManyToManyField('accounts.Group', related_name='bg_exec_mgroups')
    auto_mgroups = models.ManyToManyField('accounts.Group', related_name='bg_exec_auto_mgroups')


    def get_param(self):

        if self.action == ACTION_CHOICES.OPEN:
            params = {
                'module': 'document', 'id': self.document.id
            }

        elif self.action == ACTION_CHOICES.POPIN_OPEN:
            params = {
                'title': self.title,
                'description': self.description,
                'btns': [{
                    'label': "Fermer", 'image': 'close', 'action': True
                }]
            }

        elif self.action == ACTION_CHOICES.POPIN:
            params = {
                'title': self.title,
                'description': self.description,
                'btns': [{
                    'type': 'valid', 'label': "Ouvrir"
                }, {
                    'label': "Fermer", 'image': 'cancel', 'action': True
                }],
                'actions': [{
                    'action': 'open', 'module': 'document', 'id': self.document.id
                }]
            }

        elif self.action == ACTION_CHOICES.LOCKED:
            params = {
                'module': 'document', 'id': self.document.id, 'value': self.locked
            }

        elif self.action == ACTION_CHOICES.AUTHORIZATION:
            params = {
                'value': self.auto_mgroups.filter(is_deleted=False)
            }

        return json.dumps(params)

    def get_begin_date(self):
        date_ = self.begin_date
        return date_.hour * 3600 + date_.minute * 60 + date_.second

    def get_end_date(self):
        date_ = self.end_date
        return date_.hour * 3600 + date_.minute * 60 + date_.second
