from onegov.activity.models.occasion import Occasion
from onegov.core.orm import Base
from onegov.core.orm.mixins import TimestampMixin
from onegov.core.orm.types import UUID
from sqlalchemy import Column
from sqlalchemy import Enum
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy import Index
from sqlalchemy import Integer
from sqlalchemy import Numeric
from sqlalchemy import Text
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import object_session, relationship
from sqlalchemy_utils import aggregated
from uuid import uuid4


class Booking(Base, TimestampMixin):
    """ Bookings are created by users for occasions.

    Initially, bookings are open. In this state they represent a "wish" rather
    than booking.

    Because bookings are wishes initially, they get a priority as well as
    a group code which links multiple bookings by multiple users together.

    They system will try to figure out the best booking using the priority
    as well as the group code.

    """

    __tablename__ = 'bookings'

    def __hash__(self):
        return hash(self.id)

    def __eq__(self, other):
        return self.id == other.id

    #: the public id of the booking
    id = Column(UUID, primary_key=True, default=uuid4)

    #: the user owning the booking
    username = Column(Text, ForeignKey('users.username'), nullable=False)

    #: the priority of the booking, a higher number = a higher priority
    priority = Column(Integer, nullable=False, default=0)

    #: the group code of the booking, if missing the booking is not in a group
    group_code = Column(Text, nullable=True)

    #: the attendee behind this booking
    attendee_id = Column(UUID, ForeignKey("attendees.id"), nullable=False)

    #: the occasion this booking belongs to
    occasion_id = Column(UUID, ForeignKey("occasions.id"), nullable=False)

    #: the cost of the booking
    cost = Column(Numeric(precision=8, scale=2), nullable=True)

    #: the calculated score of the booking
    score = Column(Numeric(precision=14, scale=9), nullable=False, default=0)

    #: the period this booking belongs to
    @aggregated('occasion', Column(
        UUID, ForeignKey("periods.id"), nullable=False)
    )
    def period_id(self):
        return func.coalesce(Occasion.period_id, None)

    #: the state of the booking
    state = Column(
        Enum(
            'open',
            'blocked',
            'accepted',
            'denied',
            'cancelled',
            name='booking_state'
        ),
        nullable=False,
        default='open'
    )

    __mapper_args__ = {
        'order_by': priority
    }

    __table_args__ = (
        Index(
            'one_booking_per_attendee', 'occasion_id', 'attendee_id',
            unique=True
        ),
        Index('bookings_by_state', 'state', 'username')
    )

    #: access the user linked to this booking
    user = relationship('User')

    def group_code_count(self, states=('open', 'accepted')):
        """ Returns the number of bookings with the same group code. """
        query = object_session(self)\
            .query(Booking)\
            .with_entities(func.count(Booking.id))\
            .filter(Booking.group_code == self.group_code)

        if states != '*':
            query = query.filter(Booking.state.in_(states))\

        return query.scalar()

    def period_bound_booking_state(self, period):
        """ During pre-booking we don't show the actual state of the booking,
        unless the occasion was cancelled, otherwise the user might see
        accepted bookings at a point where those states are not confirmed yet.

        This methods interprets the period/state accordingly.

        """
        if period.confirmed == True:
            return self.state

        return self.state == 'cancelled' and 'cancelled' or 'open'

    def set_priority_bit(self, index, bit):
        """ Changes the priority, setting the the nth bit from the right to
        the value of ``bit`` (index/n begins at 0).

        The first bit (index=0) is reserved for starring/unstarring.
        The second bit (index=1) is reserved for nobble/unnobble.

        As a result, starring is less influental than locking.

        To give some context: Starring is used by the attendees to select
        which bookings they favor. Nobbling is used by administrators to force
        certain bookings to be preferred.

        """

        assert bit in (0, 1)
        assert index in (0, 1)

        mask = 1 << index

        self.priority &= ~mask

        if bit:
            self.priority |= mask

    def star(self, max_stars=3):
        """ Stars the current booking, up to a limit per period and attendee.

        Starring sets the star-bit to 1.

        :return: True if successful (or already set), False if over limit.

        """

        if self.starred:
            return True

        session = object_session(self)

        q = session.query(Booking.id)
        q = q.filter(Booking.attendee_id == self.attendee_id)
        q = q.filter(Booking.username == self.username)
        q = q.filter(Booking.period_id == self.period_id)
        q = q.filter(Booking.id != self.id)
        q = q.filter(Booking.starred == True)
        q = q.limit(max_stars + 1)

        if q.count() < max_stars:
            self.set_priority_bit(0, 1)
            return True

        return False

    def unstar(self):
        self.set_priority_bit(0, 0)

    def nobble(self):
        self.set_priority_bit(1, 1)

    def unnobble(self):
        self.set_priority_bit(1, 0)

    @hybrid_property
    def starred(self):
        return self.priority & 1 << 0 != 0

    @starred.expression
    def starred(self):
        return self.priority.op('&')(1 << 0) != 0

    @hybrid_property
    def nobbled(self):
        return self.priority & 1 << 1 != 0

    @nobbled.expression
    def nobbled(self):
        return self.priority.op('&')(1 << 1) != 0

    @property
    def dates(self):
        return self.occasion.dates

    @property
    def order(self):
        return self.occasion.order

    def overlaps(self, other, with_anti_affinity_check=False):
        # XXX circular import
        from onegov.activity.matching import utils

        return utils.overlaps(
            self, other,
            minutes_between=self.period.minutes_between,
            alignment=self.period.alignment,
            with_anti_affinity_check=with_anti_affinity_check,
        )

from onegov.activity.models.occasion import Occasion
from onegov.core.orm import Base
from onegov.core.orm.mixins import TimestampMixin
from onegov.core.orm.types import UUID
from sqlalchemy import Column
from sqlalchemy import Enum
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy import Index
from sqlalchemy import Integer
from sqlalchemy import Numeric
from sqlalchemy import Text
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import object_session, relationship
from sqlalchemy_utils import aggregated
from uuid import uuid4


class Booking(Base, TimestampMixin):
    """ Bookings are created by users for occasions.

    Initially, bookings are open. In this state they represent a "wish" rather
    than booking.

    Because bookings are wishes initially, they get a priority as well as
    a group code which links multiple bookings by multiple users together.

    They system will try to figure out the best booking using the priority
    as well as the group code.

    """

    __tablename__ = 'bookings'

    def __hash__(self):
        return hash(self.id)

    def __eq__(self, other):
        return self.id == other.id

    #: the public id of the booking
    id = Column(UUID, primary_key=True, default=uuid4)

    #: the user owning the booking
    username = Column(Text, ForeignKey('users.username'), nullable=False)

    #: the priority of the booking, a higher number = a higher priority
    priority = Column(Integer, nullable=False, default=0)

    #: the group code of the booking, if missing the booking is not in a group
    group_code = Column(Text, nullable=True)

    #: the attendee behind this booking
    attendee_id = Column(UUID, ForeignKey("attendees.id"), nullable=False)

    #: the occasion this booking belongs to
    occasion_id = Column(UUID, ForeignKey("occasions.id"), nullable=False)

    #: the cost of the booking
    cost = Column(Numeric(precision=8, scale=2), nullable=True)

    #: the calculated score of the booking
    score = Column(Numeric(precision=14, scale=9), nullable=False, default=0)

    #: the period this booking belongs to
    @aggregated('occasion', Column(
        UUID, ForeignKey("periods.id"), nullable=False)
    )
    def period_id(self):
        return func.coalesce(Occasion.period_id, None)

    #: the state of the booking
    state = Column(
        Enum(
            'open',
            'blocked',
            'accepted',
            'denied',
            'cancelled',
            name='booking_state'
        ),
        nullable=False,
        default='open'
    )

    __mapper_args__ = {
        'order_by': priority
    }

    __table_args__ = (
        Index(
            'one_booking_per_attendee', 'occasion_id', 'attendee_id',
            unique=True
        ),
        Index('bookings_by_state', 'state', 'username')
    )

    #: access the user linked to this booking
    user = relationship('User')

    def group_code_count(self, states=('open', 'accepted')):
        """ Returns the number of bookings with the same group code. """
        query = object_session(self)\
            .query(Booking)\
            .with_entities(func.count(Booking.id))\
            .filter(Booking.group_code == self.group_code)

        if states != '*':
            query = query.filter(Booking.state.in_(states))\

        return query.scalar()

    def period_bound_booking_state(self, period):
        """ During pre-booking we don't show the actual state of the booking,
        unless the occasion was cancelled, otherwise the user might see
        accepted bookings at a point where those states are not confirmed yet.

        This methods interprets the period/state accordingly.

        """
        if period.confirmed == True:
            return self.state

        return self.state == 'cancelled' and 'cancelled' or 'open'

    def set_priority_bit(self, index, bit):
        """ Changes the priority, setting the the nth bit from the right to
        the value of ``bit`` (index/n begins at 0).

        The first bit (index=0) is reserved for starring/unstarring.
        The second bit (index=1) is reserved for nobble/unnobble.

        As a result, starring is less influental than locking.

        To give some context: Starring is used by the attendees to select
        which bookings they favor. Nobbling is used by administrators to force
        certain bookings to be preferred.

        """

        assert bit in (0, 1)
        assert index in (0, 1)

        mask = 1 << index

        self.priority &= ~mask

        if bit:
            self.priority |= mask

    def star(self, max_stars=3):
        """ Stars the current booking, up to a limit per period and attendee.

        Starring sets the star-bit to 1.

        :return: True if successful (or already set), False if over limit.

        """

        if self.starred:
            return True

        session = object_session(self)

        q = session.query(Booking.id)
        q = q.filter(Booking.attendee_id == self.attendee_id)
        q = q.filter(Booking.username == self.username)
        q = q.filter(Booking.period_id == self.period_id)
        q = q.filter(Booking.id != self.id)
        q = q.filter(Booking.starred == True)
        q = q.limit(max_stars + 1)

        if q.count() < max_stars:
            self.set_priority_bit(0, 1)
            return True

        return False

    def unstar(self):
        self.set_priority_bit(0, 0)

    def nobble(self):
        self.set_priority_bit(1, 1)

    def unnobble(self):
        self.set_priority_bit(1, 0)

    @hybrid_property
    def starred(self):
        return self.priority & 1 << 0 != 0

    @starred.expression
    def starred(self):
        return self.priority.op('&')(1 << 0) != 0

    @hybrid_property
    def nobbled(self):
        return self.priority & 1 << 1 != 0

    @nobbled.expression
    def nobbled(self):
        return self.priority.op('&')(1 << 1) != 0

    @property
    def dates(self):
        return self.occasion.dates

    @property
    def order(self):
        return self.occasion.order

    def overlaps(self, other, with_anti_affinity_check=False):
        # XXX circular import
        from onegov.activity.matching import utils

        return utils.overlaps(
            self, other,
            minutes_between=self.period.minutes_between,
            alignment=self.period.alignment,
            with_anti_affinity_check=with_anti_affinity_check,
        )

Bookings are created by users for occasions.      Initially, bookings are open. In this state they represent a "wish" rather     than booking.      Because bookings are wishes initially, they get a priority as well as     a group code which links multiple bookings by multiple users together.      They system will try to figure out the best booking using the priority     as well as the group code. #: the public id of the booking #: the user owning the booking #: the priority of the booking, a higher number = a higher priority #: the group code of the booking, if missing the booking is not in a group #: the attendee behind this booking #: the occasion this booking belongs to #: the cost of the booking #: the calculated score of the booking #: the period this booking belongs to #: the state of the booking #: access the user linked to this booking Returns the number of bookings with the same group code. During pre-booking we don't show the actual state of the booking,         unless the occasion was cancelled, otherwise the user might see         accepted bookings at a point where those states are not confirmed yet.          This methods interprets the period/state accordingly. Changes the priority, setting the the nth bit from the right to         the value of ``bit`` (index/n begins at 0).          The first bit (index=0) is reserved for starring/unstarring.         The second bit (index=1) is reserved for nobble/unnobble.          As a result, starring is less influental than locking.          To give some context: Starring is used by the attendees to select         which bookings they favor. Nobbling is used by administrators to force         certain bookings to be preferred. Stars the current booking, up to a limit per period and attendee.          Starring sets the star-bit to 1.          :return: True if successful (or already set), False if over limit. # XXX circular import