# -*- coding: utf-8 -*-
# Stalker a Production Asset Management System
# Copyright (C) 2009-2014 Erkan Ozgur Yilmaz
#
# This file is part of Stalker.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

from sqlalchemy import Column, Integer, ForeignKey, Table
from sqlalchemy.orm import relationship, synonym, validates

from stalker import ImageFormat
from stalker.db.declarative import Base
from stalker.models.task import Task
from stalker.models.mixins import (StatusMixin, ReferenceMixin, CodeMixin)

from stalker.log import logging_level
import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging_level)


class Shot(Task, CodeMixin):
    """Manages Shot related data.

    .. warning::

       .. deprecated:: 0.1.2

       Because most of the shots in different projects may going to have
       the same name, which is a kind of a code like SH001, SH012A etc., and
       in Stalker you can not have two entities with the same name if their
       types are also matching, to guarantee all the shots are going to have
       different names the :attr:`.name` attribute of the Shot instances are
       automatically set to a randomly generated **uuid4** sequence.

    .. note::

       .. versionadded:: 0.1.2

       The name of the shot can be freely set without worrying about clashing
       names.

    .. note::

       .. versionadded:: 0.2.0

       Shot instances now can have their own image format. So you can set up
       different resolutions per shot.

    .. note::

       .. versionadded:: 0.2.0

       Shot instances can now be created with a Project instance only, without
       needing a Sequence instance. Sequences are now a kind of a grouping
       attribute for the Shots. And Shots can have more than one Sequence.

    .. note::

       .. versionadded:: 0.2.0

       Shots now have a new attribute called ``scenes``, holding
       :class:`.Scene` instances which is another grouping attribute like
       ``sequences``. Where Sequences are grouping the Shots according to their
       temporal position to each other, Scenes are grouping the Shots according
       to their view to the world, that is shots taking place in the same set
       configuration can be grouped together by using Scenes.

    Two shots with the same :attr:`.code` can not be assigned to the same
    :class:`.Sequence`.

    The :attr:`.cut_out` and :attr:`.cut_duration` attributes effects each
    other. Setting the :attr:`.cut_out` will change the :attr:`.cut_duration`
    and setting the :attr:`.cut_duration` will change the :attr:`.cut_out`
    value. The default value of the :attr:`.cut_duration` attribute is
    calculated from the :attr:`.cut_in` and :attr:`.cut_out` attributes. If
    both :attr:`.cut_out` and :attr:`.cut_duration` arguments are set to None,
    the :attr:`.cut_duration` defaults to 1 and :attr:`.cut_out` will be set to
    :attr:`.cut_in` + :attr:`.cut_duration`. So the priority of the attributes
    are as follows:

      :attr:`.cut_in` >
      :attr:`.cut_out` >
      :attr:`.cut_duration`

    .. note::

       .. versionadded:: 0.2.4

       :attr:`.handles_at_start` and :attr:`.handles_at_end` attributes.

    :param project: This is the :class:`.Project` instance that this shot
      belongs to. A Shot can not be created without a Project instance.

    :type project: :class:`.Project`

    :param sequences: This is a list of :class:`.Sequence`\ s that this shot is
      assigned to. A Shot can be created without having a Sequence instance.

    :type sequences: list of :class:`.Sequence`

    :param int cut_in: The in frame number that this shot starts. The default
      value is 1. When the ``cut_in`` is bigger then ``cut_out``, the
      :attr:`.cut_out` attribute is set to :attr:`.cut_in` + 1.

    :param int cut_duration: The duration of this shot in frames. It should
      be zero or a positive integer value (natural number?) or . The default
      value is None.

    :param int cut_out: The out frame number that this shot ends. If it is
      given as a value lower then the ``cut_in`` parameter, then the
      :attr:`.cut_out` will be recalculated from the existent :attr:`.cut_in`
      :attr:`.cut_duration` attributes. Can be skipped. The default value is
      None.

    :param image_format: The image format of this shot. This is an optional
      variable to differentiate the image format per shot. The default value is
      the same with the Project that this Shot belongs to.

    :type image_format: :class:`.ImageFormat`
    """
    __auto_name__ = True
    __tablename__ = 'Shots'
    __mapper_args__ = {'polymorphic_identity': 'Shot'}

    shot_id = Column('id', Integer, ForeignKey('Tasks.id'),
                     primary_key=True)

    sequences = relationship(
        'Sequence',
        secondary='Shot_Sequences',
        primaryjoin='Shots.c.id==Shot_Sequences.c.shot_id',
        secondaryjoin='Shot_Sequences.c.sequence_id==Sequences.c.id',
        back_populates='shots'
    )

    scenes = relationship(
        'Scene',
        secondary='Shot_Scenes',
        primaryjoin='Shots.c.id==Shot_Scenes.c.shot_id',
        secondaryjoin='Shot_Scenes.c.scene_id==Scenes.c.id',
        back_populates='shots'
    )

    image_format_id = Column(Integer, ForeignKey("ImageFormats.id"))
    image_format = relationship(
        "ImageFormat",
        primaryjoin="Shots.c.image_format_id==ImageFormats.c.id",
        doc="""The :class:`.ImageFormat` of this shot.

        This value defines the output image format of this shot, should be an
        instance of :class:`.ImageFormat`.
        """
    )

    # the cut_duration attribute is not going to be stored in the database,
    # only the cut_in and cut_out will be enough to calculate the cut_duration
    _cut_in = Column("cut_in", Integer)
    _cut_out = Column("cut_out", Integer)

    def __init__(self,
                 code=None,
                 project=None,
                 sequences=None,
                 scenes=None,
                 cut_in=1,
                 cut_out=None,
                 cut_duration=None,
                 image_format=None,
                 **kwargs):

        # initialize TaskableMixin
        kwargs['project'] = project
        kwargs['code'] = code

        # check for the code and project before ProjectMixin
        self._check_code_availability(code, project)

        super(Shot, self).__init__(**kwargs)
        ReferenceMixin.__init__(self, **kwargs)
        StatusMixin.__init__(self, **kwargs)
        CodeMixin.__init__(self, **kwargs)

        if sequences is None:
            sequences = []
        self.sequences = sequences

        if scenes is None:
            scenes = []
        self.scenes = scenes

        self.image_format = image_format

        self._cut_in, self._cut_duration, self._cut_out = \
            self._validate_cut_info(cut_in, cut_duration, cut_out)

    def __repr__(self):
        """the representation of the Shot
        """
        return "<%s (%s, %s)>" % (self.entity_type, self.name, self.code)

    def __eq__(self, other):
        """equality operator
        """
        return isinstance(other, Shot) and self.code == other.code and \
            self.project == other.project

    def __hash__(self):
        """the overridden __hash__ method
        """
        return hash(self.id) + 2 * hash(self.name) + 3 * hash(self.entity_type)

    def _check_code_availability(self, code, project):
        """checks if the given code is available in the given project

        :param code: the code string
        :param project: the stalker.models.project.Project instance that this
          shot is a part of
        :return: bool
        """
        # TODO: try to use SQL Queries instead of Pure Python
        if project and code:
            # the shots are task instances, use project.tasks
            for task in project.tasks:
                if isinstance(task, Shot):
                    shot = task
                    if shot.code == code:
                        raise ValueError(
                            "The given project already has a Shot with a code "
                            "of %s" % self.code
                        )
        return True

    @classmethod
    def _validate_cut_info(cls, cut_in, cut_duration, cut_out):
        """validates the cut values all together
        """
        logger.debug('cut_in      : %s' % cut_in)
        logger.debug('cut_duration: %s' % cut_duration)
        logger.debug('cut_out     : %s' % cut_out)

        if not isinstance(cut_in, int):
            logger.debug('cut_in is not an int, setting to None')
            cut_in = None

        if not isinstance(cut_duration, int):
            logger.debug('cut_duration is not an int, setting to None')
            cut_duration = None

        if not isinstance(cut_out, int):
            logger.debug('cut_out is not an int, setting to None')
            cut_out = None

        # check cut_in
        if cut_in is None:
            # try to calculate the cut_in from cut_out and cut_duration
            if cut_out is None:
                # set the defaults
                cut_in = 1

                if cut_duration is None:
                    # set the defaults
                    cut_duration = 1

                cut_out = cut_in + cut_duration - 1
            else:
                if cut_duration is None:
                    cut_duration = 1

                cut_in = cut_out - cut_duration + 1

        # check cut_out
        if cut_out is None:
            if cut_duration is None:
                cut_duration = 1

            cut_out = cut_in + cut_duration - 1

        if cut_out < cut_in:
            # check duration
            if cut_duration is None or cut_duration < 1:
                cut_duration = 1

            cut_out = cut_in + cut_duration - 1

        cut_duration = cut_out - cut_in + 1

        return cut_in, cut_duration, cut_out

    @validates('sequences')
    def _validate_sequence(self, key, sequence):
        """validates the given sequence value
        """
        from stalker.models.sequence import Sequence

        if not isinstance(sequence, Sequence):
            raise TypeError(
                "%s.sequences should all be stalker.models.sequence.Sequence "
                "instances, not %s" %
                (self.__class__.__name__, sequence.__class__.__name__)
            )
        return sequence

    @validates('scenes')
    def _validate_scenes(self, key, scene):
        """validates the given scene value
        """
        from stalker.models.scene import Scene

        if not isinstance(scene, Scene):
            raise TypeError(
                "%s.scenes should all be stalker.models.scene.Scene "
                "instances, not %s" %
                (self.__class__.__name__, scene.__class__.__name__)
            )
        return scene

    @validates('image_format')
    def _validate_image_format(self, key, imf):
        """validates the given imf value
        """
        if imf is None:
            # use the projects image format
            imf = self.project.image_format

        if imf is not None:
            if not isinstance(imf, ImageFormat):
                raise TypeError(
                    '%s.image_format should be an instance of '
                    'stalker.models.format.ImageFormat, not %s' %
                    (self.__class__.__name__, imf.__class__.__name__)
                )

        return imf

    @property
    def cut_duration(self):
        if not hasattr(self, '_cut_duration'):
            setattr(self, '_cut_duration', None)

        if self._cut_duration is None:
            self._cut_in, self._cut_duration, self._cut_out = \
                self._validate_cut_info(self._cut_in, None, self._cut_out)
        return self._cut_duration

    @cut_duration.setter
    def cut_duration(self, cut_duration):
        if cut_duration is not None:
            if isinstance(cut_duration, int):
                if cut_duration < 1:
                    # use None for cut_duration and let it be calculated from
                    # cut_in and cut_out
                    self._cut_in, self._cut_duration, self._cut_out = \
                        self._validate_cut_info(self.cut_in, None,
                                                self.cut_out)
                else:
                    # set the end to None
                    # to make it recalculated
                    self._cut_in, self._cut_duration, self._cut_out = \
                        self._validate_cut_info(self.cut_in, cut_duration,
                                                None)
        else:
            self._cut_in, self._cut_duration, self._cut_out = \
                self._validate_cut_info(self.cut_in, None, self.cut_out)

    def _cut_in_getter(self):
        return self._cut_in

    def _cut_in_setter(self, cut_in):
        self._cut_in, self._cut_duration, self._cut_out = \
            self._validate_cut_info(cut_in, self.cut_duration, self.cut_out)

    cut_in = synonym(
        "_cut_in",
        descriptor=property(_cut_in_getter, _cut_in_setter),
        doc="""The in frame number that this shot starts.

        The default value is 1. When the cut_in is bigger then
        :attr:`.cut_out`, the :attr:`.cut_out` value is update to
        :attr:`.cut_in` + 1."""
    )

    def _cut_out_getter(self):
        return self._cut_out

    def _cut_out_setter(self, cut_out):
        self._cut_in, self._cut_duration, self._cut_out = \
            self._validate_cut_info(self.cut_in, self.cut_duration, cut_out)

    cut_out = synonym(
        "_cut_out",
        descriptor=property(_cut_out_getter, _cut_out_setter),
        doc="""The out frame number that this shot ends.

        When the :attr:`.cut_out` is set to a value lower than :attr:`.cut_in`,
        :attr:`.cut_out` will be updated to :attr:`.cut_in` + 1. The default
        value is :attr:`.cut_in` + :attr:`.cut_duration`."""
    )


Shot_Sequences = Table(
    'Shot_Sequences', Base.metadata,
    Column('shot_id', Integer, ForeignKey('Shots.id'), primary_key=True),
    Column('sequence_id', Integer, ForeignKey('Sequences.id'),
           primary_key=True)
)

Shot_Scenes = Table(
    'Shot_Scenes', Base.metadata,
    Column('shot_id', Integer, ForeignKey('Shots.id'), primary_key=True),
    Column('scene_id', Integer, ForeignKey('Scenes.id'), primary_key=True)
)
