from os import path, SEEK_END, SEEK_SET
from io import BytesIO
from uuid import uuid4
from collections import namedtuple

from django import forms
from django.core.files.storage import default_storage
from django.core.files.uploadedfile import InMemoryUploadedFile

from .utils import scale_and_crop, xml_has_javascript
from .exceptions import MarkdownxImageUploadError

from .settings import (
    MARKDOWNX_IMAGE_MAX_SIZE,
    MARKDOWNX_MEDIA_PATH,
    MARKDOWNX_UPLOAD_CONTENT_TYPES,
    MARKDOWNX_UPLOAD_MAX_SIZE,
    MARKDOWNX_SVG_JAVASCRIPT_PROTECTION
)


class ImageForm(forms.Form):
    """
    Used for the handling of images uploaded using the editor through :guilabel:`AJAX`.
    """

    image = forms.FileField()

    # Separately defined as it needs to be
    # processed a text file rather than image.
    _SVG_TYPE = 'image/svg+xml'

    def save(self, commit=True):
        """
        Saves the uploaded image in the designated location.

        If image type is not SVG, a byteIO of image content_type is created and
        subsequently save; otherwise, the SVG is saved in its existing ``charset``
        as an ``image/svg+xml``.

        *Note*: The dimension of image files (excluding SVG) are set using ``PIL``.

        :param commit: If ``True``, the file is saved to the disk;
                       otherwise, it is held in the memory.
        :type commit: bool
        :return: An instance of saved image if ``commit is True``,
                 else ``namedtuple(path, image)``.
        :rtype: bool, namedtuple
        """
        image = self.files.get('image')
        content_type = image.content_type
        file_name = image.name
        image_extension = content_type.split('/')[-1].upper()
        image_size = image.size

        if content_type.lower() != self._SVG_TYPE:
            # Processing the raster graphic image.
            # Note that vector graphics in SVG format
            # do not require additional processing and
            # may be stored as uploaded.
            image = self._process_raster(image, image_extension)
            image_size = image.tell()
            image.seek(0, SEEK_SET)

        # Processed file (or the actual file in the case of SVG) is now
        # saved in the memory as a Django object.
        uploaded_image = InMemoryUploadedFile(
            file=image,
            field_name=None,
            name=file_name,
            content_type=content_type,
            size=image_size,
            charset=None
        )

        if (content_type.lower() == self._SVG_TYPE
                and MARKDOWNX_SVG_JAVASCRIPT_PROTECTION
                and xml_has_javascript(uploaded_image.read())):

            raise MarkdownxImageUploadError(
                'Failed security monitoring: SVG file contains JavaScript.'
            )

        return self._save(uploaded_image, file_name, commit)

    def _save(self, image, file_name, commit):
        """
        Final saving process, called internally after processing tasks are complete.

        :param image: Prepared image
        :type image: django.core.files.uploadedfile.InMemoryUploadedFile
        :param file_name: Name of the file using which the image is to be saved.
        :type file_name: str
        :param commit: If ``True``, the image is saved onto the disk.
        :type commit: bool
        :return: URL of the uploaded image ``commit=True``, otherwise a namedtuple
                 of ``(path, image)`` where ``path`` is the absolute path generated
                 for saving the file, and ``image`` is the prepared image.
        :rtype: str, namedtuple
        """
        # Defining a universally unique name for the file
        # to be saved on the disk.
        unique_file_name = self.get_unique_file_name(file_name)
        full_path = path.join(MARKDOWNX_MEDIA_PATH, unique_file_name)

        if commit:
            default_storage.save(full_path, image)
            return default_storage.url(full_path)

        # If `commit is False`, return the path and in-memory image.
        image_data = namedtuple('image_data', ['path', 'image'])
        return image_data(path=full_path, image=image)

    @staticmethod
    def _process_raster(image, extension):
        """
        Processing of raster graphic image using Python Imaging Library (PIL).

        This is where raster graphics are processed to the specifications
        as defined in ``settings.py``.

        *Note*: The file needs to be uploaded and saved temporarily in the
        memory to enable processing tasks using Python Imaging Library (PIL)
        to take place and subsequently retained until written onto the disk.

        :param image: Non-SVG image as processed by Django.
        :type image: django.forms.BaseForm.file
        :param extension: Image extension (e.g.: png, jpg, gif)
        :type extension: str
        :return: The image object ready to be written into a file.
        :rtype: BytesIO
        """
        thumb_io = BytesIO()
        preped_image = scale_and_crop(image, **MARKDOWNX_IMAGE_MAX_SIZE)
        preped_image.save(thumb_io, extension)
        thumb_io.seek(0, SEEK_END)
        return thumb_io

    @staticmethod
    def get_unique_file_name(file_name):
        """
        Generates a universally unique ID using Python ``UUID`` and attaches the
        extension of file name to it.

        :param file_name: Name of the uploaded file, including the extension.
        :type file_name: str
        :return: Universally unique ID, ending with the extension extracted
                 from ``file_name``.
        :rtype: str
        """
        extension = 1
        extension_dot_index = 1

        file_name = "{unique_name}.{extension}".format(
            unique_name=uuid4(),
            extension=path.splitext(file_name)[extension][extension_dot_index:]
        )
        return file_name

    def clean(self):
        """
        Checks the upload against allowed extensions and maximum size.

        :return: Upload
        """
        upload = self.cleaned_data.get('image')

        # -----------------------------------------------
        # See comments in `self._error_templates` for
        # additional information on each error.
        # -----------------------------------------------
        if not upload:
            raise MarkdownxImageUploadError.not_uploaded()

        content_type = upload.content_type
        file_size = upload.size

        if content_type not in MARKDOWNX_UPLOAD_CONTENT_TYPES:

            raise MarkdownxImageUploadError.unsupported_format()

        elif file_size > MARKDOWNX_UPLOAD_MAX_SIZE:

            raise MarkdownxImageUploadError.invalid_size(
                current=file_size,
                expected=MARKDOWNX_UPLOAD_MAX_SIZE
            )

        return upload
