""" Analaysis_data file """

from app.constants import (value_error_msg, duplicate_msg, space, barre,
                           data_saved, nb_of_category, letter_or_symb_msg,
                           sub_tab_erased)

from app.mysql_shortcut import (cnx, cursor,
                                del_sub_tab, fill_sub_table,
                                query_all_sub_id_from_sub,
                                query_prod_id_where_cat_id)

# FONCTION TO CHECK IF USER HAS SELECT A RIGHT VALUE
def checking_right_selection():

    user_choice = ""

    while user_choice not in [0, 1, 2, 3]:

        try:

            user_choice = int(input(space * 87))

            if user_choice not in [0, 1, 2, 3]:
                print(value_error_msg.format(user_choice))

        except ValueError:
            print(value_error_msg.format(letter_or_symb_msg))

    else:
        return user_choice

# FONCTION TO CHECK IF USER HAS SELECT A RIGHT CATEGORY
def checking_right_category():

    category_id = ""

    while category_id not in range(0, nb_of_category):

        try:

            category_id = int(input(space * 87))

            if category_id not in range(0, nb_of_category):
                print(value_error_msg.format(category_id))

        except ValueError:
            print(value_error_msg.format(letter_or_symb_msg))

    else:
        return category_id

# FONCTION TO CHECK IF USER HAS SELECT A "VALID" PRODUCT
def checking_right_value(category_id):

    cursor.execute(query_prod_id_where_cat_id.format(category_id))
    list_of_id_product = cursor.fetchall()

    cursor.execute(query_all_sub_id_from_sub)
    list_of_all_id_substitute = cursor.fetchall()

    checking = False
    while checking is not True:

        try:

            product_id = int(input(space * 87))
            if product_id != 0:
                sub_allready_exist = checking_duplicate_substitute(
                    product_id, list_of_all_id_substitute)

                if sub_allready_exist is False:

                    for prod_id in list_of_id_product:

                        prod_id = prod_id[0]

                        if product_id != prod_id:
                            pass
                        elif product_id == prod_id:
                            return product_id

                elif sub_allready_exist is True:
                    product_id = 0
                    print(barre)
                    print(duplicate_msg)
            if product_id == 0:
                return None

            print(value_error_msg.format(product_id))

        except ValueError:
            print(value_error_msg.format(letter_or_symb_msg))

# FONCTION TO CHECK IF USER'S PRODUCT IS NOT ALLREADY IN SUBSITUTE TABLE
def checking_duplicate_substitute(product_id, list_of_all_id_sub):

    for substituted_id in list_of_all_id_sub:

        sub_id = substituted_id[0]

        if int(product_id) == int(sub_id):
            return True

    else:
        return False

# FONCTION TO SAVE SUBSTITUTION
def checking_save_option(substitute_id, product_id):

    save_choice = ""
    while save_choice not in [0, 1]:

        try:

            save_choice = int(input(space * 87))

            if save_choice == 1:

                substitute_data = [substitute_id, product_id]

                cursor.execute(fill_sub_table, substitute_data)

                cnx.commit()
                print(barre)
                print(data_saved)

            elif save_choice == 0:
                return

            else:
                print(value_error_msg.format(save_choice))

        except ValueError:
            print(value_error_msg.format(letter_or_symb_msg))

# FUNCTION TO CHECK WHICH DELETION OPTION THE USER HAS SELECTED
def checking_delete_option():

    del_option = ""
    while del_option not in [0, 1]:

        try:

            del_option = int(input(space * 87))

            if del_option == 0:
                return

            elif del_option == 1:
                cursor.execute(del_sub_tab)
                cnx.commit()
                print(barre)
                print(sub_tab_erased)

        except ValueError:
            print(value_error_msg.format(letter_or_symb_msg))


""""
    Building a Text Classification System using TextBlob
    https://textblob.readthedocs.io/en/dev/classifiers.html
"""

from textblob.classifiers import NaiveBayesClassifier

class PosOrNeg:
    def __init__(self):
        self.train = [
            ('y', 'pos'),
            ('yes','pos'),
            ('yea','pos'),
            ('i would love to', 'pos'),
            ('i would like to','pos'),
            ('that is correct', 'pos'),
            ('i want that', 'pos'),
            ('positive','pos'),
            ('true', 'pos'),
            ('i do', 'pos'),
            ('i did', 'pos'),
            ('i did mean that', 'pos'),
            ('yes, that is correct', 'pos'),
            ('n', 'neg'),
            ('no','neg'),
            ('negative', 'neg'),
            ('that is incorrect', 'neg'),
            ('i dont want to do that', 'neg'),
            ('no, that is not correct', 'neg'),
            ('i don\' want to do that', 'neg'),
            ('don\'t use that', 'neg'),
            ('dont use that', 'neg'),
            ('that is wrong', 'neg'),
            ('that is not right', 'neg'),
            ('false', 'neg'),
            ('i would not like to use those words', 'neg'),
            ('i didn\'t mean that', 'neg'),
            ('i didn\'t', 'neg'),
            ('i don\'t like that one', 'neg'),
        ]
        self.cl = NaiveBayesClassifier(self.train)

    def classify(self, sentence):
        return self.cl.classify(sentence)

test = [
    ('i do', 'pos'),
    ('i did', 'pos'),
    ('i didn\'t', 'neg'),
    ('i don\'t like that one', 'neg'),
    ('yes, that is correct', 'pos'),
    ('i didn\'t mean that' , 'neg')
]


"""This module provides all classes that manipulate the database for received events

It provides methods to insert/delete/update columns on the database depending on
which event was received by the `update` method on `WebhookDataWriter` and passed to
`update` on `DataProcessor`.

"""
import sqlalchemy
from sqlalchemy.orm.attributes import flag_modified

from mconf_aggr.aggregator.aggregator import AggregatorCallback, CallbackError
from mconf_aggr.aggregator.utils import time_logger
from mconf_aggr.logger import get_logger
from mconf_aggr.webhook.database import DatabaseConnector
from mconf_aggr.webhook.database_model import (
    Institutions,
    Meetings,
    MeetingsEvents,
    Recordings,
    Servers,
    SharedSecrets,
    UsersEvents,
)
from mconf_aggr.webhook.exceptions import (
    DatabaseNotReadyError,
    InvalidWebhookEventError,
    WebhookDatabaseError,
)

session_scope = DatabaseConnector.get_session_scope()

class Status:
    PROCESSING = "processing"
    PROCESSED = "processed"
    PUBLISHED = "published"
    UNPUBLISHED = "unpublished"
    DELETED = "deleted"

class DatabaseEventHandler:
    """This is an abstract class that handles webhook events."""

    def __init__(self, session, logger=None):
        """Constructor of the DataEventHandler.

        Parameters
        ----------
        session : sqlalchemy.Session
            Session used by SQLAlchemy to interact with the database.
        """
        self.session = session
        self.logger = logger or get_logger()

    def handle(self, event):
        """This method is meant to be implemented downstream.

        Raises
        ------
        NotImplementedError
            If called from an object of class DatabaseEventHandler.
        """
        raise NotImplementedError()

class MeetingCreatedHandler(DatabaseEventHandler):
    """This class handles meeting-created events."""

    class MeetingCreatedMetadata:
        def __init__(self, metadata, default_value="", logger=None):
            self._metadata = metadata
            self._default_value = default_value
            self._logger = logger or get_logger()

        def __getattr__(self, name):
            field = name.replace("_", "-")
            value = None
            try:
                value = self._metadata.get(field, self._default_value)
            except AttributeError:
                value = self._default_value

            return value

    def handle(self, event):
        """Implementation of abstract handle method from DatabaseEventHandler.

        Parameters
        ----------
        event : event_mapper.WebhookEvent
            Event to be handled and written to database.
        """

        event = event.event

        self.logger.info(
            f"Processing meeting-created event for internal-meeting-id: \
                         '{event.internal_meeting_id}'."
        )

        # Create tables meetings_events and meetings.
        if (
            self.session.query(MeetingsEvents)
            .filter(MeetingsEvents.internal_meeting_id == event.internal_meeting_id)
            .first()
        ):
            self.logger.warning(
                f"Meeting with internal-meeting-id '{event.internal_meeting_id}' \
                                already exists."
            )
            return

        new_meetings_events = MeetingsEvents(**event._asdict())
        new_meetings_events.has_forcibly_ended = False
        new_meetings_events.unique_users = 0
        new_meetings_events.start_time = event.create_time

        metadata = self.MeetingCreatedMetadata(event.meta_data, None, self.logger)
        new_meetings_events.shared_secret_guid = metadata.mconf_shared_secret_guid
        new_meetings_events.shared_secret_name = metadata.mconf_secret_name
        new_meetings_events.server_guid = metadata.mconf_server_guid
        new_meetings_events.server_url = metadata.mconf_server_url
        new_meetings_events.institution_guid = metadata.mconf_institution_guid

        if new_meetings_events.parent_meeting_id == "bbb-none":
            new_meetings_events.parent_meeting_id = None

        if metadata.mconf_shared_secret_guid and not metadata.mconf_secret_name:
            new_meetings_events.shared_secret_name = (
                self.session.query(SharedSecrets)
                .filter(SharedSecrets.guid == metadata.mconf_shared_secret_guid)
                .first()
                .name
            )

        if not metadata.mconf_shared_secret_guid:
            self.logger.info(
                f"Empty shared secret guid, meeting '{event.internal_meeting_id}' \
                             insertion falling back to institution name: '{metadata.mconflb_institution_name}'"
            )
            # fallback to name of institution
            try:
                # Find the secret using mconflb-institution-name which corresponds
                # to the shared secret name. The use of term 'institution' is
                # misleading here
                found_secret = (
                    self.session.query(SharedSecrets)
                    .filter(SharedSecrets.name == metadata.mconflb_institution_name)
                    .first()
                )
                new_meetings_events.shared_secret_guid = found_secret.guid
                self.logger.info(
                    f"Found secret: '{found_secret.name}' for meeting \
                                 '{event.internal_meeting_id}'"
                )

                # We found the secret, try to find its institution to complete
                # the table with information
                try:
                    found_institution = (
                        self.session.query(Institutions)
                        .filter(Institutions.guid == found_secret.institution_guid)
                        .first()
                    )
                    new_meetings_events.institution_guid = found_institution.guid
                    new_meetings_events.shared_secret_name = found_secret.name
                except RuntimeError:
                    self.logger.warning(
                        f"Could not find institution for secret \
                                        '{found_secret.name}'"
                    )

                self.logger.info(
                    f"Found institution: '{found_institution.name}' \
                                 for meeting '{event.internal_meeting_id}'"
                )
            except AttributeError:
                self.logger.warning(
                    f"Could not match institution name \
                                    '{metadata.mconflb_institution_name}' to an institution"
                )

        if not metadata.mconf_server_guid and not metadata.mconf_server_url:
            servers_table = (
                self.session.query(Servers).filter(Servers.name == event.server_url).first()
            )

            new_meetings_events.server_url, new_meetings_events.server_guid = (
                servers_table.name,
                servers_table.guid,
            )

        # running external meeting id must be unique
        if (
            self.session.query(Meetings)
            .filter(Meetings.ext_meeting_id == event.external_meeting_id)
            .first()
        ):
            self.logger.warning(f"Meeting with external-meeting-id '{event.external_meeting_id}'")
            return

        new_meeting = Meetings(
            running=False,
            has_user_joined=False,
            participant_count=0,
            listener_count=0,
            voice_participant_count=0,
            video_count=0,
            moderator_count=0,
            attendees=[],
            m_shared_secret_guid=new_meetings_events.shared_secret_guid,
            m_institution_guid=new_meetings_events.institution_guid,
            ext_meeting_id=new_meetings_events.external_meeting_id,
            int_meeting_id=new_meetings_events.internal_meeting_id,
            transfer=False,
            transfer_count=0,
        )
        new_meeting.meeting_event = new_meetings_events

        self.session.add(new_meeting)

class MeetingEndedHandler(DatabaseEventHandler):
    """This class handles meeting-ended events."""

    def handle(self, event):
        """Implementation of abstract handle method from DatabaseEventHandler.

        Parameters
        ----------
        event : event_mapper.WebhookEvent
            Event to be handled and written to database.
        """

        event = event.event

        int_id = event.internal_meeting_id
        self.logger.info(f"Processing meeting-ended event for internal-meeting-id: '{int_id}'.")

        # Table meetings_events to be updated.
        meetings_event = (
            self.session.query(MeetingsEvents)
            .filter(MeetingsEvents.internal_meeting_id == int_id)
            .first()
        )

        if meetings_event:
            # Update all users that don't have a leave time to the meeting's end time
            (
                self.session.query(UsersEvents)
                .filter(
                    UsersEvents.meeting_event_id == meetings_event.id,
                    UsersEvents.leave_time.is_(None),
                )
                .update({"leave_time": event.end_time}, synchronize_session="fetch")
            )
            # make sure the users events are updated before deleting the meeting
            self.session.commit()

            meetings_event.end_time = event.end_time

            self.session.add(meetings_event)

            # Table meetings to be updated.
            meetings_table = (
                self.session.query(Meetings).filter(Meetings.int_meeting_id == int_id).first()
            )

            if meetings_table:
                self.session.delete(meetings_table)

class UserJoinedHandler(DatabaseEventHandler):
    """This class handles user-joined events."""

    def handle(self, event):
        """Implementation of abstract handle method from DatabaseEventHandler.

        Parameters
        ----------
        event : event_mapper.WebhookEvent
            Event to be handled and written to database.
        """

        event = event.event

        int_id = event.internal_meeting_id

        self.logger.info(
            f"Processing user-joined event for internal-user-id \
                         '{event.internal_user_id}'.'"
        )

        users_events_table = self._get_users_events(event)

        # Create attendee JSON for meetings table.
        attendee = {
            "external_user_id": event.external_user_id,
            "internal_user_id": event.internal_user_id,
            "full_name": str(event.name),
            "role": event.role,
            "is_presenter": event.is_presenter,
            "is_listening_only": False,
            "has_joined_voice": False,
            "has_video": False,
            "user_data": event.userdata,
        }

        # Query for meetings_events to link with users_events table.
        meetings_event = (
            self.session.query(MeetingsEvents)
            .filter(MeetingsEvents.internal_meeting_id == int_id)
            .first()
        )

        if meetings_event:
            users_events_table.meeting_event = meetings_event

            # Table meetings to be updated.
            meetings_table = (
                self.session.query(Meetings).filter(Meetings.int_meeting_id == int_id).first()
            )

            if meetings_table:
                meetings_table.attendees = self._attendee_json(meetings_table.attendees, attendee)
                self._update_meeting(meetings_table)

                # SQLAlchemy was not considering the attendees array as modified, so it
                # had to be forced.
                flag_modified(meetings_table, "attendees")

                self.session.add(users_events_table)
                self.session.add(meetings_table)
                self.session.flush()
            else:
                self.logger.warning(f"No meeting found for user '{event.internal_user_id}'.")
                raise WebhookDatabaseError(f"no meeting found for user '{event.internal_user_id}'")

            # Update unique_users in table meetings_events.
            users_joined = (
                self.session.query(UsersEvents)
                .join(UsersEvents.meeting_event)
                .filter(MeetingsEvents.internal_meeting_id == int_id)
                .count()
            )

            meetings_event.unique_users = users_joined

            # Meeting starts when first user joins it.
            if users_joined == 1:
                meetings_event.start_time = event.join_time

            self.session.add(meetings_event)

    def _get_users_events(self, raw_event):
        event_dict = raw_event._asdict()

        column_names = set(column.key for column in UsersEvents.__table__.columns)
        keys = event_dict.keys()

        for key in list(keys):
            if key not in column_names:
                del event_dict[key]

        users_events = UsersEvents(**event_dict)

        return users_events

    def _attendee_json(self, base, new):
        if not base:
            arr = []
            arr.append(new)

            return arr
        else:
            arr = base
            for elem in arr:
                if elem["internal_user_id"] == new["internal_user_id"]:
                    return arr
            arr.append(new)

            return arr

    def _update_meeting(self, meetings_table):
        meetings_table.running = True
        _update_meeting(meetings_table)
        meetings_table.has_user_joined = True

class UserLeftHandler(DatabaseEventHandler):
    """This class handles user-left events."""

    def handle(self, event):
        """Implementation of abstract handle method from DatabaseEventHandler.

        Parameters
        ----------
        event : event_mapper.WebhookEvent
            Event to be handled and written to database.
        """

        event = event.event

        user_id = event.internal_user_id
        int_id = event.internal_meeting_id
        self.logger.info(
            f"Processing user-left message for internal-user-id \
                         '{user_id}' in meeting '{int_id}'."
        )

        # Table meetings to be updated.
        meetings_table = (
            self.session.query(Meetings)
            .join(Meetings.meeting_event)
            .filter(MeetingsEvents.internal_meeting_id == int_id)
            .first()
        )

        if meetings_table:
            self._remove_attendee(meetings_table, user_id)
            self._update_meeting(meetings_table)

            # Mark meetings.attendees as modified for SQLAlchemy.
            flag_modified(meetings_table, "attendees")

            self.session.add(meetings_table)
        else:
            self.logger.warning(f"No meeting found with internal-meeting-id '{int_id}'.")

        # Table users_events to be updated.
        users_table = (
            self.session.query(UsersEvents).filter(UsersEvents.internal_user_id == user_id).first()
        )

        if users_table:
            # Update table users_events.
            users_table.leave_time = event.leave_time
        else:
            self.logger.warning(f"No user found with internal-user-id '{user_id}'.")

    def _remove_attendee(self, meetings_table, user_id):
        for idx, attendee in enumerate(meetings_table.attendees):
            if attendee["internal_user_id"] == user_id:
                del meetings_table.attendees[idx]

    def _update_meeting(self, meetings_table):
        _update_meeting(meetings_table)

class UserEventHandler(DatabaseEventHandler):
    """This class handles general user events."""

    def handle(self, event):
        """Implementation of abstract handle method from DatabaseEventHandler.

        Parameters
        ----------
        event : event_mapper.WebhookEvent
            Event to be handled and written to database.
        """
        event_type = event.event_type
        event = event.event

        self.event_type = event_type

        user_id = event.internal_user_id
        int_id = event.internal_meeting_id
        self.logger.info(
            f"Processing {event.event_name} event for internal-user-id \
                         '{user_id}' on meeting '{int_id}'."
        )

        # Table meetings to be updated.
        meetings_table = (
            self.session.query(Meetings)
            .join(Meetings.meeting_event)
            .filter(MeetingsEvents.internal_meeting_id == int_id)
            .first()
        )

        if meetings_table:
            self._update_attendees(meetings_table.attendees, event, user_id)
            self._update_meeting(meetings_table)

            flag_modified(meetings_table, "attendees")

            self.session.add(meetings_table)
        else:
            self.logger.warning(f"No meeting found with internal-meeting-id '{int_id}'.")

    def _update_meeting(self, meetings_table):
        _update_meeting(meetings_table)

    def _update_attendees(self, base, update, user_id):
        for attendee in base:
            if attendee["internal_user_id"] == user_id:
                if update.event_name == self.event_type:
                    self._update_attendee(attendee, update)

    def _update_attendee(self, attendee, update):
        raise NotImplementedError("Give specific behavior for the user event")

class UserVoiceEnabledHandler(UserEventHandler):
    """This class adds specific behavior for user-audio-voice-enabled events."""

    def _update_attendee(self, attendee, update):
        attendee["has_joined_voice"] = update.has_joined_voice
        attendee["is_listening_only"] = update.is_listening_only

class UserVoiceDisabledHandler(UserEventHandler):
    """This class adds specific behavior for user-audio-voice-disabled events."""

    def _update_attendee(self, attendee, update):
        attendee["has_joined_voice"] = False
        attendee["is_listening_only"] = False

class UserListenOnlyEnabledHandler(UserEventHandler):
    """This class adds specific behavior for user-audio-listen-only-enabled events."""

    def _update_attendee(self, attendee, update):
        attendee["is_listening_only"] = True

class UserListenOnlyDisabledHandler(UserEventHandler):
    """This class adds specific behavior for user-audio-listen-only-disabled events."""

    def _update_attendee(self, attendee, update):
        attendee["is_listening_only"] = False

class UserCamBroadcastStartHandler(UserEventHandler):
    """This class adds specific behavior for user-cam-broadcast-start events."""

    def _update_attendee(self, attendee, update):
        attendee["has_video"] = True

class UserCamBroadcastEndHandler(UserEventHandler):
    """This class adds specific behavior for user-cam-broadcast-end events."""

    def _update_attendee(self, attendee, update):
        attendee["has_video"] = False

class UserPresenterAssignedHandler(UserEventHandler):
    """This class adds specific behavior for user-presenter-assigned events."""

    def _update_attendee(self, attendee, update):
        attendee["is_presenter"] = True

class UserPresenterUnassignedHandler(UserEventHandler):
    """This class adds specific behavior for user-presenter-unassigned events."""

    def _update_attendee(self, attendee, update):
        attendee["is_presenter"] = False

class RapArchiveHandler(DatabaseEventHandler):
    """This class handles rap-archive-ended recording events."""

    def handle(self, event):
        """Implementation of abstract handle method from DatabaseEventHandler.

        Parameters
        ----------
        event : event_mapper.WebhookEvent
            Event to be handled and written to database.
        """
        event_type = event.event_type
        event = event.event

        int_id = event.internal_meeting_id
        record_id = event.record_id
        self.logger.info(
            f"Processing {event_type} event for "
            + f"internal-meeting-id '{int_id}' and record_id "
            + f"'{record_id}'."
        )

        recorded = event.recorded

        # Meeting was set to be recorded.
        if recorded:
            recording = _fetch_or_create_recording(self.session, event, Status.PROCESSING)

            meetings_event = _fetch_meetings_event(self.session, int_id)

            if meetings_event:
                _update_recording_with_meetings_event_data(recording, meetings_event)
            else:
                self.logger.warning(f"No meeting found for recording '{event.record_id}'.")

            recording.current_step = event.current_step

            self.session.add(recording)
            self.logger.info(f"Records_row: {recording}")

class RapHandler(DatabaseEventHandler):
    """This class handles general recording events."""

    def handle(self, event):
        """Implementation of abstract handle method from DatabaseEventHandler.

        Parameters
        ----------
        event : event_mapper.WebhookEvent
            Event to be handled and written to database.
        """
        event_type = event.event_type
        server_url = event.server_url
        event = event.event

        int_id = event.internal_meeting_id
        record_id = event.record_id

        self.logger.info(
            f"Processing {event_type} event for "
            + f"internal-meeting-id '{int_id}' and record_id "
            + f"'{record_id}'."
        )

        recording = _fetch_or_create_recording(self.session, event, Status.DELETED)

        # Assume the requester server to be the new host of the recording.
        if event_type == "rap-sanity-started":
            recording = self._handle_rap_sanity_started(server_url, recording)
            # if int_id != record_id, then it's not the first recording in the
            # group. In this case, se status to PROCESSING to match the first
            # recording.
            if int_id != record_id:
                recording.status = Status.PROCESSING

        meetings_event = _fetch_meetings_event(self.session, int_id)

        if meetings_event:
            _update_recording_with_meetings_event_data(recording, meetings_event)
        else:
            self.logger.warning(f"No meeting found for recording '{event.record_id}'.")

        recording.current_step = event.current_step

        self.session.add(recording)
        self.logger.info(f"Records_row: {recording}")

    def _handle_rap_sanity_started(self, server_url, recording):
        server_id_result = self.session.query(Servers.id).filter(Servers.name == server_url).first()
        if server_id_result:
            if server_id_result.id != recording.server_id:
                self.logger.info(f"Recording host was updated to '{server_url}'.")
            recording.server_id = server_id_result.id
        else:
            self.logger.warning(f"No server found for recording '{recording.record_id}'.")

        return recording

class RapProcessHandler(DatabaseEventHandler):
    """This class handles processing recording events."""

    def handle(self, event):
        """Implementation of abstract handle method from DatabaseEventHandler.

        Parameters
        ----------
        event : event_mapper.WebhookEvent
            Event to be handled and written to database.
        """
        event_type = event.event_type
        event = event.event

        int_id = event.internal_meeting_id
        record_id = event.record_id
        self.logger.info(
            f"Processing {event_type} event for "
            + f"internal-meeting-id '{int_id}' and record_id "
            + f"'{record_id}'."
        )

        recording = _fetch_recording(self.session, record_id)

        # Table recordings already exists.
        if recording:
            # Update status based on event.
            current_status = recording.status
            workflow_status = recording.workflow.get(event.workflow, None)
            if event.current_step == "rap-process-started":
                recording = self.handle_rap_process_started(
                    recording, event, workflow_status, current_status
                )
            elif event.current_step == "rap-process-ended":
                recording = self.handle_rap_process_ended(
                    recording, event, workflow_status, current_status
                )
            else:
                self.logger.warning(
                    f"Invalid event '{event.current_step}' from current status '{current_status}' for recording '{event.record_id}'."
                )
        else:
            self.logger.warning(f"No recording found with id '{event.record_id}'.")

    def handle_rap_process_started(self, recording, event, workflow_status, current_status):
        if not workflow_status:
            recording.workflow = _update_workflow(recording, event.workflow, Status.PROCESSING)
        if current_status == Status.PROCESSING:
            recording.current_step = event.current_step

        self.session.add(recording)

        return recording

    def handle_rap_process_ended(self, recording, event, workflow_status, current_status):
        if workflow_status == Status.PROCESSING:
            recording.workflow = _update_workflow(recording, event.workflow, Status.PROCESSED)
        if current_status == Status.PROCESSING:
            recording.status = Status.PROCESSED
            recording.current_step = event.current_step

        self.session.add(recording)

        return recording

class RapPublishUnpublishHandler(DatabaseEventHandler):
    """This class handles publishing and unpublishing recording events."""

    def handle(self, event):
        """Implementation of abstract handle method from DatabaseEventHandler.

        Parameters
        ----------
        event : event_mapper.WebhookEvent
            Event to be handled and written to database.
        """
        event_type = event.event_type
        event = event.event

        int_id = event.internal_meeting_id
        record_id = event.record_id
        self.logger.info(
            f"Processing {event_type} event for "
            + f"internal-meeting-id '{int_id}' and record_id "
            + f"'{record_id}'."
        )

        recording = _fetch_recording(self.session, record_id)

        if recording:
            if event_type == "rap-unpublished":
                if recording.status == Status.PUBLISHED:
                    recording.status = Status.UNPUBLISHED
                    recording.published = False
                    self.session.add(recording)
                else:
                    self.logger.warning(
                        f"Tried to unpublish a recording with record-id '{record_id}' that is not yet published."
                    )
            elif event_type == "rap-published":
                if recording.status == Status.UNPUBLISHED:
                    recording.status = Status.PUBLISHED
                    recording.published = True
                    self.session.add(recording)
                else:
                    self.logger.warning(
                        f"Tried to publish a recording with meeting id '{record_id}' that is already published.",
                    )
        else:
            self.logger.warning(f"No recording found with record_id '{record_id}'.")

class RapDeleteHandler(DatabaseEventHandler):
    """This class handles deleting recording events."""

    def handle(self, event):
        """Implementation of abstract handle method from DatabaseEventHandler.

        Parameters
        ----------
        event : event_mapper.WebhookEvent
            Event to be handled and written to database.
        """

        event_type = event.event_type
        event = event.event

        int_id = event.internal_meeting_id
        record_id = event.record_id
        self.logger.info(
            f"Processing {event_type} event for "
            + f"internal-meeting-id '{int_id}' and record_id "
            + f"'{record_id}'."
        )

        recording = _fetch_recording(self.session, record_id)

        if recording:
            recording.status = Status.DELETED
            recording.published = False
            self.session.add(recording)
        else:
            self.logger.warning(f"No recording found with meeting id '{int_id}'.")

class RapPublishHandler(DatabaseEventHandler):
    """This class handles publishing recording events."""

    def handle(self, event):
        """Implementation of abstract handle method from DatabaseEventHandler.

        Parameters
        ----------
        event : event_mapper.WebhookEvent
            Event to be handled and written to database.
        """

        event_type = event.event_type
        event = event.event

        int_id = event.internal_meeting_id
        record_id = event.record_id
        self.logger.info(
            f"Processing {event_type} event for "
            + f"internal-meeting-id '{int_id}' and record_id "
            + f"'{record_id}'."
        )

        recording = _fetch_recording(self.session, record_id)

        # Table recordings already exists.
        if recording:
            if event.current_step == "rap-publish-started":
                recording = self._handle_publish_started(recording, event)
            elif event.current_step == "rap-publish-ended":
                recording = self._handle_publish_ended(recording, event)
            else:
                self.logger.warning(
                    f"Invalid event '{event.current_step}' from current status '{recording.status}' for meeting '{int_id}'."
                )
        else:
            self.logger.warning(f"No recording found with meeting id '{int_id}'.")

    def _handle_publish_started(self, recording, event):
        if recording.status == Status.PROCESSED:
            recording.current_step = event.current_step

        self.session.add(recording)

        return recording

    def _handle_publish_ended(self, recording, event):
        times = (
            self.session.query(MeetingsEvents.start_time, MeetingsEvents.end_time)
            .filter(MeetingsEvents.internal_meeting_id == event.internal_meeting_id)
            .first()
        )

        start_time, end_time = times

        recording.status = Status.PUBLISHED
        recording.published = True
        recording.name = str(event.name)
        recording.is_breakout = event.is_breakout
        recording.start_time = event.start_time or start_time
        recording.end_time = event.end_time or end_time
        recording.size = event.size or 0
        recording.raw_size = event.raw_size
        # If meta_data field exists, simply merge the new meta_data, without
        # overwriting currently saved meta_data.
        if recording.meta_data:
            event.meta_data.update(recording.meta_data)
        if (event.workflow == "presentation") or (event.workflow == "video"):
            event.meta_data.update({"mconf-decrypter-pending": "false"})
        recording.meta_data = event.meta_data
        recording.download = event.download
        recording.current_step = event.current_step
        updated_playback = _upsert_playback(recording, event.playback)
        recording.playback = updated_playback

        flag_modified(recording, "playback")

        recording.workflow = _update_workflow(recording, event.workflow, Status.PUBLISHED)
        recording.current_step = event.current_step

        self.session.add(recording)

        return recording

class MeetingTransferHandler(DatabaseEventHandler):
    """This class handles meeting transfer events."""

    def handle(self, event):
        """Implementation of abstract handle method from DatabaseEventHandler.

        Parameters
        ----------
        event : event_mapper.WebhookEvent
            Event to be handled and written to database.
        """

        event_type = event.event_type
        event = event.event
        transfer_status = False
        if event_type == "meeting-transfer-enabled":
            transfer_status = True

        int_id = event.internal_meeting_id
        self.logger.info(f"Processing {event_type} event for internal-meeting-id '{int_id}'.")

        transfer_table = (
            self.session.query(Meetings).filter(Meetings.int_meeting_id == int_id).first()
        )

        if transfer_table:
            transfer_table.transfer = transfer_status
            self.session.add(transfer_table)
        else:
            self.logger.warning(f"No meeting found with meeting id '{int_id}'.")

def _update_meeting(meetings_table):
    """Common updates on table meetings."""

    meetings_table.transfer_count = sum(
        1 for attendee in meetings_table.attendees if attendee["role"] == "TRANSFER"
    )
    meetings_table.participant_count = len(meetings_table.attendees) - meetings_table.transfer_count
    meetings_table.has_user_joined = meetings_table.participant_count != 0
    meetings_table.moderator_count = sum(
        1 for attendee in meetings_table.attendees if attendee["role"] == "MODERATOR"
    )
    meetings_table.transfer_count = sum(
        1 for attendee in meetings_table.attendees if attendee["role"] == "TRANSFER"
    )
    meetings_table.listener_count = sum(
        1 for attendee in meetings_table.attendees if attendee["is_listening_only"]
    )
    meetings_table.voice_participant_count = sum(
        1 for attendee in meetings_table.attendees if attendee["has_joined_voice"]
    )
    meetings_table.video_count = sum(1 for a in meetings_table.attendees if a["has_video"])

def _upsert_playback(records_table, event_playback):
    if len(event_playback) == 0:
        return records_table.playback

    playbacks = records_table.playback[:]

    for i, playback in enumerate(playbacks):
        if _has_same_playback_format(playback, event_playback):
            old_playback = playbacks[i]
            old_playback.update(event_playback)
            playbacks[i] = old_playback

            break
    else:  # In the case the for loop completes fully.
        playbacks.append(event_playback)

    return playbacks

def _update_workflow(records_table, event_workflow, new_status):
    workflows = records_table.workflow.copy()

    if event_workflow:
        workflows.update({event_workflow: new_status})

    return workflows

def _has_same_playback_format(playback1, playback2):
    if "format" in playback1 and "format" in playback2:
        if playback1["format"] == playback2["format"]:
            return True

    return False

def _fetch_recording(session, record_id):
    recording = session.query(Recordings).filter(Recordings.record_id == record_id).first()

    return recording

def _fetch_or_create_recording(session, event, first_status):
    int_id = event.internal_meeting_id
    record_id = event.record_id
    recording = session.query(Recordings).filter(Recordings.record_id == record_id).first()

    # Recording does not exist yet. Create it.
    if not recording:
        recording = Recordings(**event._asdict())

        # Start as deleted since we don't yet know if
        # this meeting was recorded or not
        recording.status = first_status
        recording.playback = []
        recording.workflow = {}
        recording.participants = int(
            session.query(UsersEvents.id)
            .join(MeetingsEvents)
            .filter(MeetingsEvents.internal_meeting_id == int_id)
            .count()
        )
    elif recording.status == Status.DELETED:
        recording.status = Status.PROCESSING

    return recording

def _fetch_meetings_event(session, internal_meeting_id):
    meetings_event = (
        session.query(MeetingsEvents)
        .filter(MeetingsEvents.internal_meeting_id == internal_meeting_id)
        .first()
    )
    return meetings_event

def _update_recording_with_meetings_event_data(recording, meetings_event):
    recording.end_time = meetings_event.end_time
    recording.external_meeting_id = meetings_event.external_meeting_id
    recording.internal_meeting_id = meetings_event.internal_meeting_id
    recording.is_breakout = meetings_event.is_breakout
    recording.meeting_event_id = meetings_event.id
    recording.parent_meeting_id = meetings_event.parent_meeting_id
    recording.r_institution_guid = meetings_event.institution_guid
    recording.r_shared_secret_guid = meetings_event.shared_secret_guid
    recording.start_time = meetings_event.start_time

class DataProcessor:
    """Data processor (dispatcher) of the received event."""

    def __init__(self, session, logger=None):
        """Constructor of the DataProcessor.

        Parameters
        ----------
        session : sqlalchemy.Session
            Session used by SQLAlchemy to interact with the database.
        """
        self.session = session
        self.logger = logger or get_logger()

    def update(self, event):
        event_handler = self._select_handler(event.event_type)

        event_handler.handle(event)

    def _select_handler(self, event_type):
        """Event dispatcher.

        Choose which handler will process the event based on the event type.

        Parameters
        ----------
        event : event_mapper.WebhookEvent
            An event to be handled and persisted into database.
        """
        self.logger.debug("Selecting event processor.")

        if event_type == "meeting-created":
            event_handler = MeetingCreatedHandler(self.session)

        elif event_type == "user-joined":
            event_handler = UserJoinedHandler(self.session)

        elif event_type == "user-left":
            event_handler = UserLeftHandler(self.session)

        elif event_type == "meeting-ended":
            event_handler = MeetingEndedHandler(self.session)

        elif event_type == "user-audio-voice-enabled":
            event_handler = UserVoiceEnabledHandler(self.session)

        elif event_type == "user-audio-voice-disabled":
            event_handler = UserVoiceDisabledHandler(self.session)

        elif event_type == "user-audio-listen-only-enabled":
            event_handler = UserListenOnlyEnabledHandler(self.session)

        elif event_type == "user-audio-listen-only-disabled":
            event_handler = UserListenOnlyDisabledHandler(self.session)

        elif event_type == "user-cam-broadcast-start":
            event_handler = UserCamBroadcastStartHandler(self.session)

        elif event_type == "user-cam-broadcast-end":
            event_handler = UserCamBroadcastEndHandler(self.session)

        elif event_type == "user-presenter-assigned":
            event_handler = UserPresenterAssignedHandler(self.session)

        elif event_type == "user-presenter-unassigned":
            event_handler = UserPresenterUnassignedHandler(self.session)

        elif event_type in [
            "rap-archive-started",
            "rap-sanity-started",
            "rap-sanity-ended",
            "rap-post-archive-started",
            "rap-post-archive-ended",
            "rap-post-process-started",
            "rap-post-process-ended",
            "rap-post-publish-started",
            "rap-post-publish-ended",
        ]:
            event_handler = RapHandler(self.session)

        elif event_type in ["rap-archive-ended"]:
            event_handler = RapArchiveHandler(self.session)

        elif event_type in ["rap-process-started", "rap-process-ended"]:
            event_handler = RapProcessHandler(self.session)

        elif event_type in ["rap-publish-started", "rap-publish-ended"]:
            event_handler = RapPublishHandler(self.session)

        elif event_type in ["rap-unpublished", "rap-published"]:
            event_handler = RapPublishUnpublishHandler(self.session)

        elif event_type == "rap-deleted":
            event_handler = RapDeleteHandler(self.session)

        elif event_type in ["meeting-transfer-enabled", "meeting-transfer-disabled"]:
            event_handler = MeetingTransferHandler(self.session)

        else:
            self.logger.warning(f"Unknown event type '{event_type}'.")
            raise InvalidWebhookEventError(f"unknown event type '{event_type}'")

        return event_handler

class WebhookDataWriter(AggregatorCallback):
    """Writer of data retrieved from webhooks.

    This class implements the AggregatorCallback which means its `run()` method
    is intended to run in a separate thread, writing incoming data to the
    database.

    Before using it, one should call its `setup()` method to configure
    any resource used to write data such as database connections etc.
    After that, its `run()` method can be run in a separate thread continuously.
    When finished, its `teardown` can be called to close any opened resource.
    """

    def __init__(self, connector=None, logger=None):
        """Constructor of the WebhookDataWriter.

        Parameters
        ----------
        connector : Database connector (driver).
            If not supplied, it will instantiate a new `PostgresConnector`.
        """
        self.logger = logger or get_logger()

    def setup(self):
        """Setup any resources needed to iteract with the database."""
        self.logger.info("Setting up WebhookDataWriter")

    def teardown(self):
        """Release any resources used to iteract with the database."""
        self.logger.info("Tearing down WebhookDataWriter")

    def run(self, data):
        """Run main logic of the writer.

        This method is intended to run in a separate thread by the aggregator
        whenever new data must be persisted.

        data : event_mapper.WebhookEvent
            This is a single event to be handled and persisted into database.

        Raises
        ------
        aggregator.aggregator.CallbackError
            If any error occur while persisting event into database.
        """
        try:
            with time_logger(
                self.logger.info, "Processing information to database took {elapsed}s."
            ):
                with session_scope() as session:
                    DataProcessor(session).update(data)
        except sqlalchemy.exc.OperationalError as err:
            self.logger.error(f"Operational error on database. Not persisting data: {err}")

            raise CallbackError() from err
        except WebhookDatabaseError as err:
            self.logger.error(
                f"An error occurred while persisting data. \
                              Not persisting data: {err}"
            )

            raise CallbackError() from err
        except Exception as err:
            self.logger.error(f"Unknown error on database handler. Not persisting data: {err}")

            raise CallbackError() from err

class AuthenticationHandler:
    """Provide a way to get server data from database."""

    def __init__(self, logger=None):
        """Constructor of the `AuthenticationHandler`.

        Parameters
        ----------
        logger : loguru.Logger
            If not supplied, it will instantiate a new logger.
        """
        self.logger = logger or get_logger()

    def secret(self, server):
        """Get a shared secret for a given server in the database.

        Parameters
        ----------
        server : str
            Server for which it should get a token from database.

        Returns
        -------
        token : str
            Token of the server as retrieved from database.
        """
        found_secret = None
        with session_scope() as session:
            try:
                server = session.query(Servers.secret).filter(Servers.name == server).first()
            except sqlalchemy.exc.OperationalError as err:
                self.logger.error(f"Operational error on database while validating token: {err}")
                server = None
            except Exception as err:
                self.logger.warning(f"Unknown error while validating token: {err}")
                server = None

            if server:
                # If it found a row for the given server, extract its secret column.
                found_secret = server.secret

        return found_secret

class WebhookServerHandler:
    """Provide a way to get all available servers from database."""

    def __init__(self, logger=None):
        """Constructor of the `WebhookServerHandler`.

        Parameters
        ----------
        logger : loguru.Logger
            If not supplied, it will instantiate a new logger.
        """
        self.logger = logger or get_logger()

    def servers(self):
        """Get all available servers from database.

        Returns
        -------
        servers : Servers.
            The list of all available servers from database.
        """
        servers = None
        with session_scope() as session:
            try:
                servers = session.query(Servers).all()
            except sqlalchemy.exc.OperationalError as err:
                self.logger.error(f"Operational error on database while gathering servers: {err}")

                raise DatabaseNotReadyError()
            except Exception as err:
                self.logger.warning(f"Unknown error while gathering servers: {err}")

                raise DatabaseNotReadyError()
            else:
                # Since it returns Servers objects used by the database, we need
                # to detach the objects returned from the database's session.
                session.expunge_all()

                return servers


import numpy as np
import random
import pandas as pd
import torch
import nengo
from scipy.stats import entropy, skew, kurtosis, normaltest

def get_state(player, game, agent, dim=0, turn_basis=None, coin_basis=None, turn_exp=None, coin_exp=None, representation="one-hot"):
	t = len(game.investor_give) if player=='investor' else len(game.trustee_give)
	if agent=='TQ':
		index = t if player=='investor' else t * (game.coins*game.match+1) + game.investor_give[-1]*game.match
		return index
	if agent=='DQN':
		if representation=="one-hot":
			index = t if player=='investor' else t * (game.coins*game.match+1) + game.investor_give[-1]*game.match
			vector = np.zeros((dim))
			vector[index] = 1
			return torch.FloatTensor(vector)
		elif representation=="ssp":
			c = game.coins if player=='investor' else game.investor_give[-1]*game.match
			vector = encode_state(t, c, turn_basis, coin_basis)
			return torch.FloatTensor(vector)
	if agent=="IBL":
		if player=="investor":
			return t, game.coins
		else:
			return t, game.investor_give[-1]*game.match
	if agent=="SPA":
		if representation=="ssp":
			c = game.coins if player=='investor' else game.investor_give[-1]*game.match
			vector = encode_state(t, c, turn_basis, coin_basis, turn_exp, coin_exp) if t<5 else np.zeros((dim))
			return vector

def action_to_coins(player, state, n_actions, game):
	available = game.coins if player=='investor' else game.investor_give[-1]*game.match  # coins available
	precise_give = state * available
	possible_actions = np.linspace(0, available, n_actions).astype(int)
	action_idx = (np.abs(possible_actions - precise_give)).argmin()
	action = possible_actions[action_idx]
	give = action
	keep = available - action
	return give, keep, action_idx

def generosity(player, give, keep):
	return np.NaN if give+keep==0 and player=='trustee' else give/(give+keep)

def encode_state(t, c, turn_basis, coin_basis, turn_exp=1.0, coin_exp=1.0):
	return np.fft.ifft(turn_basis**(t*turn_exp) * coin_basis**(c*coin_exp)).real.squeeze()

def make_unitary(v):
	return v/np.absolute(v)

def measure_sparsity(spikes1, spikes2):
	n_neurons = spikes1.shape[0]
	diff = []
	quiet = 0
	for n in range(n_neurons):
		if spikes1[n]+spikes2[n]>0:
			diff.append((spikes1[n]-spikes2[n]) / (spikes1[n]+spikes2[n]))
		else:
			quiet += 1
	diff = np.array(diff)
	quiet = quiet / n_neurons
	pdiff = (np.histogram(diff)[0][0] + np.histogram(diff)[0][-1]) / diff.shape[0]
	return 100*pdiff, 100*quiet

def measure_similarity(ssp1, ssp2, mode="cosine"):
    if mode=="dot":
        return np.sum(ssp1 * ssp2)
    elif mode=="cosine":
        return np.sum(ssp1 * ssp2) / (np.linalg.norm(ssp1, ord=2) * np.linalg.norm(ssp2, ord=2))

def get_rewards(player, game, w_s, w_o, w_i, normalize, gamma):
	rewards_self = np.array(game.investor_reward) if player=='investor' else np.array(game.trustee_reward)
	rewards_other = np.array(game.trustee_reward) if player=='investor' else np.array(game.investor_reward)
	rewards = w_s*rewards_self + w_o*rewards_other - w_i*np.abs(rewards_self-rewards_other)
	if normalize:
		rewards = rewards / (game.coins * game.match)
		rewards[:-1] = (1-gamma)*rewards[:-1]
	return rewards


# coding=utf-8
from json import dumps

from django.core.serializers.json import DjangoJSONEncoder

# Constantes Errores

ERROR_LOGIN_REQUERIDO = 'ERROR_LOGIN_REQUERIDO'
ERROR_LOGOUT_FALLIDO = 'ERROR_LOGOUT_FALLIDO'
ERROR_NO_TIENE_PERMISOS = 'ERROR_NO_TIENE_PERMISOS'
ERROR_DE_SISTEMA = 'ERROR_DE_SISTEMA'
ERROR_METODO_INCORRECTO = 'ERROR_METODO_INCORRECTO'
ERROR_DATOS_INCORRECTOS = 'ERROR_DATOS_INCORRECTOS'
ERROR_VALIDACION_DATOS = 'ERROR_VALIDACION_DATOS'
ERROR_DATOS_FALTANTES = 'ERROR_DATOS_FALTANTES'
ERROR_CREDENCIALES_INCORRECTAS = 'ERROR_CREDENCIALES_INCORRECTAS'

ERROR_USUARIO_YA_TIENE_ESE_ROL = 'ERROR_USUARIO_YA_TIENE_ESE_ROL'
ERROR_USUARIO_NO_ADMINISTRADOR = 'ERROR_USUARIO_NO_ADMINISTRADOR'
ERROR_USUARIO_NO_ES_ENCARGADO = 'ERROR_USUARIO_NO_ES_ENCARGADO'
ERROR_USUARIO_NO_ENCONTRADO = 'ERROR_USUARIO_NO_ENCONTRADO'
ERROR_USUARIO_NO_ENCARGADO = 'ERROR_USUARIO_NO_ENCARGADO'
ERROR_USUARIO_EXISTENTE = 'ERROR_USUARIO_EXISTENTE'
ERROR_EMAIL_EXISTENTE = 'ERROR_EMAIL_EXISTENTE'
ERROR_USUARIO_FINCA_NO_ENCONTRADO = 'ERROR_USUARIO_FINCA_NO_ENCONTRADO'
ERROR_USUARIO_NO_HABILITADO_EN_FINCA = 'ERROR_USUARIO_NO_HABILITADO_EN_FINCA'
ERROR_USUARIO_YA_TIENE_ROL_EN_FINCA = 'ERROR_USUARIO_YA_TIENE_ROL_EN_FINCA'
ERROR_FINCA_YA_EXISTENTE = 'ERROR_FINCA_YA_EXISTENTE'
ERROR_PROVEEDOR_NO_ENCONTRADO = 'ERROR_PROVEEDOR_NO_ENCONTRADO'
ERROR_FINCA_NO_ENCONTRADA = 'ERROR_FINCA_NO_ENCONTRADA'
ERROR_FINCA_YA_APROBADA = 'ERROR_FINCA_YA_APROBADA'
ERROR_FINCA_YA_HABILITADA = 'ERROR_FINCA_YA_HABILITADA'
ERROR_FINCA_YA_DESAPROBADA = 'ERROR_FINCA_YA_DESAPROBADA'
ERROR_FINCA_YA_DESHABILITADA = 'ERROR_FINCA_YA_DESHABILITADA'
ERROR_SECTOR_SUPERA_TAMANIO_FINCA = 'ERROR_SECTOR_SUPERA_TAMANIO_FINCA'
ERROR_SECTORES_SUPERAN_TAMANIO_FINCA = 'ERROR_SECTORES_SUPERAN_TAMANIO_FINCA'
ERROR_SECTOR_YA_EXISTENTE = 'ERROR_SECTOR_YA_EXISTENTE'
ERROR_SECTOR_NO_ENCONTRADO = 'ERROR_SECTOR_NO_ENCONTRADO'
ERROR_SECTOR_NO_HABILITADO = 'ERROR_SECTOR_NO_HABILITADO'
ERROR_MECANISMO_RIEGO_FINCA_NO_EXISTE = 'ERROR_MECANISMO_RIEGO_FINCA_NO_EXISTE'
ERROR_MECANISMO_RIEGO_FINCA_NO_HABILITADO = 'ERROR_MECANISMO_RIEGO_FINCA_NO_HABILITADO'
ERROR_IP_YA_USADA = 'ERROR_IP_YA_USADA'
ERROR_MECANISMO_RIEGO_FINCA_YA_EXISTE = 'ERROR_MECANISMO_RIEGO_FINCA_YA_EXISTE'
ERROR_MECANISMO_RIEGO_FINCA_SECTOR_NO_HABILITADO = 'ERROR_MECANISMO_RIEGO_FINCA_SECTOR_NO_HABILITADO'
ERROR_SECTOR_YA_TIENE_MECANISMO_HABILITADO = 'ERROR_SECTOR_YA_TIENE_MECANISMO_HABILITADO'
ERROR_MECANISMO_RIEGO_FINCA_SECTOR_NO_EXISTE = 'ERROR_MECANISMO_RIEGO_FINCA_SECTOR_NO_EXISTE'

ERROR_CULTIVO_NO_EXISTENTE = 'ERROR_CULTIVO_NO_EXISTENTE'
ERROR_CULTIVO_NO_HABILITADO = 'ERROR_CULTIVO_NO_HABILITADO'
ERROR_SUBTIPO_CULTIVO_NO_EXISTENTE = "ERROR_SUBTIPO_CULTIVO_NO_EXISTENTE"
ERROR_SUBTIPO_CULTIVO_NO_HABILITADO = "ERROR_SUBTIPO_CULTIVO_NO_HABILITADO"
ERROR_TIPO_CULTIVO_NO_HABILITADO = "ERROR_TIPO_CULTIVO_NO_HABILITADO"
ERROR_SECTOR_YA_TIENE_CULTIVO_ASIGNADO = 'ERROR_SECTOR_YA_TIENE_CULTIVO_ASIGNADO'

ERROR_TIPO_MEDICION_NO_EXISTENTE = 'ERROR_TIPO_MEDICION_NO_EXISTENTE'
ERROR_TIPO_MEDICION_NO_HABILITADA = 'ERROR_TIPO_MEDICION_NO_HABILITADA'
ERROR_TIPO_MEDICION_CLIMATICA_NO_EXISTENTE = 'ERROR_TIPO_MEDICION_CLIMATICA_NO_EXISTENTE'
ERROR_SENSOR_NO_EXISTENTE = 'ERROR_SENSOR_NO_EXISTENTE'

ERROR_SENSOR_NO_PERTENECE_A_FINCA = 'ERROR_SENSOR_NO_PERTENECE_A_FINCA'
ERROR_SENSOR_NO_HABILITADO = 'ERROR_SENSOR_NO_HABILITADO'
ERROR_SENSOR_YA_ASIGNADO = 'ERROR_SENSOR_YA_ASIGNADO'
ERROR_SENSOR_NO_ASIGNADO_A_COMPONENTE = 'ERROR_SENSOR_NO_ASIGNADO_A_COMPONENTE'
ERROR_COMPONENTE_NO_ACEPTA_MAS_SENSORES = 'ERROR_COMPONENTE_NO_ACEPTA_MAS_SENSORES'
ERROR_COMPONENTE_SENSOR_NO_EXISTENTE = 'ERROR_COMPONENTE_SENSOR_NO_EXISTENTE'
ERROR_COMPONENTE_SENSOR_NO_PERTENECE_A_FINCA = 'ERROR_COMPONENTE_SENSOR_NO_PERTENECE_A_FINCA'
ERROR_COMPONENTE_SENSOR_NO_HABILITADO = 'ERROR_COMPONENTE_SENSOR_NO_HABILITADO'
ERROR_COMPONENTE_SENSOR_YA_ASIGNADO = 'ERROR_COMPONENTE_SENSOR_YA_ASIGNADO'
ERROR_COMPONENTE_SENSOR_YA_HABILITADO = 'ERROR_COMPONENTE_SENSOR_YA_HABILITADO'
ERROR_COMPONENTE_SENSOR_NO_ASIGNADO_A_SECTOR = 'ERROR_COMPONENTE_SENSOR_NO_ASIGNADO_A_SECTOR'
ERROR_SECTOR_YA_TIENE_COMPONENTE_HABILITADO = 'ERROR_SECTOR_YA_TIENE_COMPONENTE_HABILITADO'

ERROR_FINCA_NO_HABILITADA = 'ERROR_FINCA_NO_HABILITADA'
ERROR_FINCA_NO_TIENE_PROVEEDOR_HABILITADO = 'ERROR_FINCA_NO_TIENE_PROVEEDOR_HABILITADO'
ERROR_FRECUENCIA_MAXIMA_SUPERADA = 'ERROR_FRECUENCIA_MAXIMA_SUPERADA'
ERROR_NO_EXISTE_PROVEEDOR_FINCA = 'ERROR_NO_EXISTE_PROVEEDOR_FINCA'
ERROR_FINCA_TIENE_UN_PROVEEDOR_HABILITADO = 'ERROR_FINCA_TIENE_UN_PROVEEDOR_HABILITADO'

# Modulo configuracion riego
ERROR_CRITERIO_NO_EXISTE = 'ERROR_CRITERIO_NO_EXISTE'
ERROR_CONFIGURACION_RIEGO_NO_EXISTE = 'ERROR_CONFIGURACION_RIEGO_NO_EXISTE'
ERROR_EJECUCION_RIEGO = "ERROR_EJECUCION_RIEGO"
ERROR_RIEGO_EN_EJECUCION = 'ERROR_RIEGO_EN_EJECUCION'
ERROR_MODIFICACION_CONFIGURACION_RIEGO = "ERROR_MODIFICACION_CONFIGURACION_RIEGO"

# Modulo reportes
ERROR_CONFIGURACION_EVENTO_NO_ENCONTRADA = 'ERROR_CONFIGURACION_EVENTO_NO_ENCONTRADA'
ERROR_CONFIGURACION_EVENTO_YA_DESACTIVADA = 'ERROR_CONFIGURACION_EVENTO_YA_DESACTIVADA'
ERROR_CONFIGURACION_EVENTO_YA_ACTIVADA = 'ERROR_CONFIGURACION_EVENTO_YA_ACTIVADA'
ERROR_HELADA_NO_CARGADA = 'ERROR_HELADA_NO_CARGADA'

# Detalle errores

DETALLE_ERROR_DESCONOCIDO = "Error desconocido"
DETALLE_ERROR_DATOS_INCOMPLETOS = "Faltan ingresar datos"
DETALLE_ERROR_DATOS_INCORRECTOS = "Datos incorrectos"
DETALLE_ERROR_SISTEMA = "Error procesando llamada"
DETALLE_ERROR_ENVIO_EMAIL = "Error enviando email"

# Modulo seguridad
DETALLE_ERROR_LOG_IN_FALLIDO = 'Usuario o contraseña incorrectos'
DETALLE_ERROR_LOG_OUT_FALLIDO = 'Log out fallo'
DETALLE_ERROR_TIPO_SESION_DATOS_FALTANTES = 'Faltan datos de tipo de sesion'

DETALLE_ERROR_NO_TIENE_PERMISOS = 'Usuario no tiene permisos para ejecutar esta operacion'

DETALLE_ERROR_REGISTRACION_USUARIO_EXISTENTE = 'Usuario ya existe en el sistema'
DETALLE_ERROR_REGISTRACION_USUARIO_FALTANTE = 'Falta ingresar usuario'
DETALLE_ERROR_REGISTRACION_USUARIO_INCORRECTO = 'Usuario incorrecto'
DETALLE_ERROR_REGISTRACION_CONTRASENIA_FALTANTE = 'Falta ingresar contrasena'
DETALLE_ERROR_REGISTRACION_NOMBRE_FALTANTE = 'Falta ingresar nombre'
DETALLE_ERROR_REGISTRACION_NOMBRE_INCORRECTO = 'Nombre incorrecto'
DETALLE_ERROR_REGISTRACION_APELLIDO_FALTANTE = 'Falta ingresar apellido'
DETALLE_ERROR_REGISTRACION_APELLIDO_INCORRECTO = 'Apellido incorrecto'
DETALLE_ERROR_REGISTRACION_DNI_FALTANTE = 'Falta ingresar dni'
DETALLE_ERROR_REGISTRACION_DNI_INCORRECTO = 'Formato dni ingresado es incorrecto'
DETALLE_ERROR_REGISTRACION_CUIT_FALTANTE = 'Falta ingresar CUIT'
DETALLE_ERROR_REGISTRACION_CUIT_INCORRECTO = 'Formato CUIT ingresado es incorrecto'
DETALLE_ERROR_REGISTRACION_DOMICILIO_FALTANTE = 'Falta ingresar domicilio'
DETALLE_ERROR_REGISTRACION_DOMICILIO_INCORRECTO = 'Formato domicilio ingresado es incorrecto'
DETALLE_ERROR_REGISTRACION_FECHA_NACIMIENTO_FALTANTE = 'Falta ingresar fecha de nacimiento'
DETALLE_ERROR_REGISTRACION_FECHA_NACIMIENTO_INCORRECTO = 'Formato de fecha de nacimiento ingresada es incorrecto'
DETALLE_ERROR_REGISTRACION_IMAGEN_FALTANTE = 'Falta ingresar imagen'
DETALLE_ERROR_REGISTRACION_IMAGEN_INCORRECTA = 'Formato imagen de ingresada es incorrecto'

DETALLE_ERROR_EMAIL_EXISTENTE = 'Ya existe un usuario con ese email'
DETALLE_ERROR_EMAIL_FALTANTE = 'Falta ingresar email'
DETALLE_ERROR_EMAIL_INCORRECTO = 'Formato email incorrecto'

DETALLE_ERROR_CONTRASENIA_INCORRECTA = 'Contraseña incorrecta'
DETALLE_ERROR_CONTRASENIA_FORMATO_INCORRECTO = 'Formato contraseña incorrecto: debe tener mas de 8 caracteres y' \
                                                    ' no puede ser solo numérica'

DETALLE_ERROR_CONTRASENIA_VIEJA_INCORRECTA = 'La contraseña actual ingresada es incorrecta'
DETALLE_ERROR_CAMBIAR_CONTRASENIA_VIEJA_FALTANTE = 'Falta ingresar contraseña actual'
DETALLE_ERROR_CAMBIAR_CONTRASENIA_NUEVA_FALTANTE = 'Falta ingresar contraseña nueva'

DETALLE_ERROR_CODIGO_VERIFICACION_FALTANTE = 'Falta ingresar codigo de verificacion'
DETALLE_ERROR_CODIGO_VERIFICACION_INCORRECTO = 'Codigo de verificacion incorrecto'

# Finca
DETALLE_ERROR_FINCA_DATOS_FALTANTES = "Falta ingresar datos de finca"

# Modulo configuracion riego
DETALLE_ERROR_EJECUION_RIEGO = "Ejecucion de riego fallo"
DETALLE_ERROR_DETENCION_RIEGO = "Detencion de riego fallo"

DETALLE_ERROR_MODIFICACION_CONFIGURACION_RIEGO = 'La configuracion de riego ya fue eliminada'
DETALLE_ERROR_CONFIGURACION_RIEGO_INCORRECTA = 'La configuracion de riego es incorrecta'
DETALLE_ERROR_RELACION_CONFIGURACION_RIEGO_CRITERIO = 'El criterio de riego no se corresponde con la configuracion'
DETALLE_ERROR_TIPO_CRITERIO_RIEGO_INCORRECTO = 'El tipo de criterio riego es incorrecto'
DETALLE_ERROR_TIPO_CRITERIO_RIEGO_INCORRECTO_MEDICION = "El criterio elegido no es por medicion"
DETALLE_ERROR_TIPO_CRITERIO_RIEGO_INCORRECTO_HORA = "El criterio elegido no es por hora"
DETALLE_ERROR_TIPO_CRITERIO_RIEGO_INCORRECTO_VOLUMEN = "El criterio elegido no es por volumen de agua"
DETALLE_ERROR_TIPO_CRITERIO_RIEGO_INICIAL_INCORRECTO = 'El tipo de criterio riego no es valido como criterio inicial'
DETALLE_ERROR_TIPO_CRITERIO_RIEGO_FINAL_INCORRECTO = 'El tipo de criterio riego no es valido como criterio final'
DETALLE_ERROR_CRITERIO_RIEGO_INEXISTENTE = 'El criterio de riego seleccionado no existe'
DETALLE_ERROR_CRITERIO_RIEGO_VALOR_MEDICION_INEXISTENTE = 'Falta ingresar valor de medicion'
DETALLE_ERROR_CRITERIO_RIEGO_OPERADOR_MEDICION_INEXISTENTE = 'Falta ingresar operador criterio por medicion'
DETALLE_ERROR_CRITERIO_RIEGO_TIPO_MEDICION_INEXISTENTE = 'Falta ingresar tipo de medicion'
DETALLE_ERROR_CRITERIO_RIEGO_HORA_INICIO_INEXISTENTE = 'Falta ingresar hora de inicio'
DETALLE_ERROR_CRITERIO_RIEGO_DIA_INICIO_INEXISTENTE = 'Falta ingresar dia de inicio'
DETALLE_ERROR_CRITERIO_RIEGO_VOLUMEN_INEXISTENTE = 'Falta ingresar volumen de agua'

DETALLE_ERROR_CONFIGURACION_RIEGO_AUTOMATICA_CRITERIO = 'A una configuracion de riego automatica no se le asignan' \
                                                        ' criterios'
DETALLE_ERROR_CONFIGURACION_RIEGO_CANTIDAD_MAX_CRITERIO_INICIAL = 'La cantidad maxima de criterios iniciales fue ' \
                                                                  'alcanzada'
DETALLE_ERROR_CONFIGURACION_RIEGO_CANTIDAD_MAX_CRITERIO_FINAL = 'La cantidad maxima de criterios finales fue alcanzada'

DETALLE_ERROR_CONFIGURACION_RIEGO_EN_EJECUCION = 'Configuracion de riego en ejecucion, no se puede modificar'

# Constantes Keys

KEY_ERROR_CODE = 'error_code'
KEY_ERROR_DESCRIPTION = 'error_description'

# Metodos

def build_error(response, error_code, error_descripcion):
    content = {KEY_ERROR_DESCRIPTION: error_descripcion, KEY_ERROR_CODE: error_code}
    response.content = dumps(content, cls=DjangoJSONEncoder)

    return response

def build_bad_request_error(response, error_code, error_descripcion):
    response.status_code = 400
    return build_error(response, error_code, error_descripcion)

def build_unauthorized_error(response, error_code, error_descripcion):
    response.status_code = 401

    return build_error(response, error_code, error_descripcion)

def build_method_not_allowed_error(response, error_code, error_descripcion):
    response.status_code = 405

    return build_error(response, error_code, error_descripcion)

def build_internal_server_error(response, error_code, error_descripcion):
    response.status_code = 500

    return build_error(response, error_code, error_descripcion)


import numpy as np

# self.tree[node_id] = {'type': self.NON_LEAF_TYPE,
#                       'node_id': node_id,
#                       'feature_id': best_feature_id,
#                       'threshold': best_treshold,
#                       'impurity': best_impurity,
#                       'obj_num': X.shape[0],
#                       'value': node_value}

class TreeNode:
    NON_LEAF_TYPE = 'NON_LEAF'
    LEAF_TYPE = 'LEAF'

    node_id = None
    value = None
    gain = float('-inf')
    objects_in_node = None
    type = NON_LEAF_TYPE
    feature_id = None
    threshold = None

    def __init__(self):
        pass

class XGBoostTree:

    NONE_VALUE = float('-inf')

    def __init__(self,
                 max_depth=5,
                 max_features = None,
                 random_state = None,
                 split_on_diff = True,
                 lamb = 1.0,
                 gamma = 1.0):

        #self.tree = dict()

        max_count_nodes = (2**np.arange(max_depth+1)).sum()

        self.tree = [None]*max_count_nodes

        self.lamb = lamb
        self.gamma = gamma

        self.max_depth = max_depth
        self.random_state = random_state
        self.split_on_diff = split_on_diff

        if self.random_state is not None:
            np.random.seed(self.random_state)

        if max_features == 'sqrt':
            self.get_feature_ids = self.__get_feature_ids_sqrt
        elif max_features == 'log2':
            self.get_feature_ids = self.__get_feature_ids_log2
        elif max_features is None:
            self.get_feature_ids = self.__get_feature_ids_N
        else:
            raise -1

    def __get_feature_ids_sqrt(self, n_feature):

        feature_ids = np.arange(n_feature)
        np.random.shuffle(feature_ids)

        return feature_ids[:np.sqrt(n_feature)]  # Ваш код

    def __get_feature_ids_log2(self, n_feature):
        feature_ids = np.arange(n_feature)

        np.random.shuffle(feature_ids)

        return feature_ids[:np.log2(n_feature)]  # Ваш код

    def __get_feature_ids_N(self, n_feature):
        feature_ids = np.arange(n_feature)
        #np.random.shuffle(feature_ids)

        return feature_ids  # Ваш код

    def __sort_samples(self, x, y, feature_idx = None):
        sorted_idx = x.argsort(axis=0)
        return x[sorted_idx], y[sorted_idx]

    def __calc_xgboost_impurities_array(self, grad, gessian):
        N = grad.shape[1]

        G_full =  (grad).sum(1)
        S_full = (gessian).sum(1)

        full_Gain = (G_full)**2 / (S_full + self.lamb)
        sum_G_L = (grad)[:, :N-1].cumsum(1)
        sum_G_R = G_full[:, None] - sum_G_L

        sum_G_L_squared = sum_G_L**2
        sum_G_R_squared = sum_G_R**2

        sum_S_L = (gessian)[:, :N-1].cumsum(1)
        sum_S_R = S_full[:, None] - sum_S_L

        impurities = sum_G_L_squared/ (sum_S_L + self.lamb) \
                     + sum_G_R_squared/ (sum_S_R + self.lamb) \
                     - full_Gain[:, None] - self.gamma

        return impurities

    def __calc_possible_border_ids(self, X, eps = 1e-15):
        diff = np.diff(X.T, axis = 1)
        idxs = np.argwhere(np.abs(diff) > eps)
        list_result_border_ids = []
        for i in range(X.shape[1]):
            pidxs = idxs[idxs[:, 0] == i][:,1]
            if pidxs.shape[0] == 0:
                pidxs = np.array([self.NONE_VALUE])
            list_result_border_ids.append(pidxs)

        return list_result_border_ids

    def __div_samples(self, x, grad, grad2, feature_id, threshold):
        left_mask = x[:, feature_id] < threshold
        right_mask = ~left_mask
        return x[left_mask], x[right_mask], grad[left_mask], grad[right_mask], grad2[left_mask], grad2[right_mask]

    def __find_threshold_array(self, X, grad, gessian, feature_ids):
        sorted_ids = np.argsort(X[:, feature_ids], axis=0)
        sorted_X = X[sorted_ids, feature_ids]
        sorted_grad  = grad[sorted_ids].T
        sorted_gessian = gessian[sorted_ids].T

        possible_border_ids = self.__calc_possible_border_ids(sorted_X)

        # possible_border_ids = np.arange(self.min_samples_split, sorted_X.shape[0] - self.min_samples_split)
        if len(sorted_grad.shape) ==1:
            sorted_Y = sorted_grad[None, :]

        if len(sorted_X.shape) == 1:
            sorted_X = sorted_X[None, :]

        gains = self.__calc_xgboost_impurities_array(sorted_grad, sorted_gessian)

        if not self.split_on_diff:
            max_gain_index = np.argmax(gains, axis =1)
            max_gain = gains[np.arange(gains.shape[0]), max_gain_index.ravel()]
        else:
            max_gain_index = []
            max_gain_index_none_ids = []

            for feature_index, feature_pbids in enumerate(possible_border_ids):
                if feature_pbids[0] == self.NONE_VALUE:
                    max_gain_index.append(0)
                    max_gain_index_none_ids.append(len(max_gain_index)-1)
                    continue

                idx = np.argmax(gains[feature_index, feature_pbids])
                max_gain_index.append(feature_pbids[idx])

            max_gain_index = np.array(max_gain_index)
            max_gain_index_none_ids = np.array(max_gain_index_none_ids)
            try:
                max_gain = gains[np.arange(gains.shape[0]), max_gain_index.ravel()]
            except:
                print('aa')
            if max_gain_index_none_ids.shape[0] > 0:
                max_gain[max_gain_index_none_ids] = self.NONE_VALUE

        best_split_right_index = max_gain_index + 1

        # max_gain[best_split_right_index <= self.min_samples_split] = self.NONE_VALUE
        # max_gain[best_split_right_index >= sorted_grad.shape[1] - self.min_samples_split] = self.NONE_VALUE

        best_threshold = (sorted_X[best_split_right_index-1, np.arange(sorted_X.shape[1])]
                          + sorted_X[best_split_right_index, np.arange(sorted_X.shape[1])] ) / 2.0
        #best_threshold = sorted_X[best_split_right_index, np.arange(sorted_X.shape[1])]

        best_gain_index = np.argmax(max_gain)
        best_gain = max_gain[best_gain_index]

        best_treshold = best_threshold[best_gain_index]
        best_feature_id = feature_ids[best_gain_index]

        return best_gain, best_feature_id, best_treshold

    def __get_node_value(self, x, grad, gessian):
        G =  (grad).sum()
        S = (gessian).sum()

        node_value =  -1.0 * G / (S + self.lamb)
        return node_value

    def __set_leaf(self, x, grad, gessian,  node_id):

        leaf_value = self.__get_node_value(x, grad, gessian)

        node = TreeNode()
        node.node_id = node_id
        node.value = leaf_value
        node.type = TreeNode.LEAF_TYPE
        node.objects_in_node = x.shape[0]
        node.gain = 0

        self.tree[node_id] = node

    def __check_is_leaf(self, X, grad, gessian, depth):

        if X.shape[0] < 2:
            return True

        # проверка, является ли вершина листовой
        if depth >= self.max_depth:
            return True

        return  False

    def __fit_node(self, X, grad, gessian, node_id, depth):

        #print('fitting depth: {0} node_id: {1}'.format(depth, node_id))

        if self.__check_is_leaf(X, grad, gessian, depth):
            self.__set_leaf(X, grad, gessian, node_id)
            return

        feature_ids = self.get_feature_ids(X.shape[1])

        #th_results = self.split_xgboost(X, grad, gessian)
        th_results = self.__find_threshold_array(X, grad, gessian, feature_ids)
        # gains = th_results[0]
        # thresholds = th_results[1]
        #
        # best_gain_index = np.argmax(gains)
        # best_gain = gains[best_gain_index]
        #
        # best_treshold = thresholds[best_gain_index]
        # best_feature_id = feature_ids[best_gain_index]

        best_gain,best_feature_id, best_treshold = th_results

        # # если не удалось разделить выборку, то узел - выходной
        # if best_impurity == self.NONE_VALUE:
        #     self.__set_leaf(X, grad, gessian, node_id)
        #     return

        left_mask = (X[:, best_feature_id] > best_treshold)
        right_mask = ~left_mask

        left_x, left_grad = X[left_mask], grad[left_mask]
        right_x, right_grad = X[right_mask], grad[right_mask]
        left_gessian = gessian[left_mask]
        right_gessian = gessian[right_mask]

        if left_x.shape[0] == 0 or right_x.shape[0] == 0:
            self.__set_leaf(X, grad, gessian, node_id)
            return

        node = TreeNode()

        node.type = TreeNode.NON_LEAF_TYPE
        node.node_id = node_id
        node.objects_in_node = X.shape[0]

        node.value = self.__get_node_value(X, grad, gessian)
        node.gain = best_gain
        node.threshold = best_treshold
        node.feature_id = best_feature_id

        self.tree[node_id] = node

        # self.tree[node_id] = {'type': self.NON_LEAF_TYPE,
        #                       'node_id': node_id,
        #                       'feature_id': best_feature_id,
        #                       'threshold': best_treshold,
        #                       'impurity':  best_impurity,
        #                       'obj_num': X.shape[0],
        #                       'value': node_value}

        self.__fit_node(left_x, left_grad, left_gessian, 2 * node_id + 1, depth + 1)
        self.__fit_node(right_x, right_grad,  right_gessian, 2 * node_id + 2, depth + 1)

        return

    def fit(self, X, grad, gessian):
        self.__fit_node(X, grad, gessian, 0, 0)
        self.pruning(0)
        # for node_id, node in enumerate(self.tree):
        #     if node is not None and node['type'] == self.LEAF_TYPE:
        #             self.__pruning(node_id)

    def pruning(self, node_id):
        node = self.tree[node_id]

        if node.type == TreeNode.NON_LEAF_TYPE:
            result = (node.gain < 0) & self.pruning(node_id * 2 + 1) & self.pruning(node_id * 2 + 2)
            if result == 1:
                # print('pruning '+ str(node.gain))
                self.tree[node_id].type = TreeNode.LEAF_TYPE
            return result
        elif node.gain > 0:
            return 0
        else:
            return 1

    def __predict_value(self, x, node_id):
        node = self.tree[node_id]

        if node.type == TreeNode.NON_LEAF_TYPE:
            feature_id, threshold = node.feature_id, node.threshold
            if x[feature_id] > threshold:
                return self.__predict_value(x, 2 * node_id + 1)
            else:
                return self.__predict_value(x, 2 * node_id + 2)
        else:
            return node.value

    def predict(self, X):
        return np.array([self.__predict_value(x, 0) for x in X])

    def get_leaf_values(self):
        res = []
        for node in self.tree:
            if node.type == TreeNode.LEAF_TYPE:
                res.append(node.value)
        return res


import pygame
import sys

# Initialize Pygame and create a window
pygame.init()
screen_width, screen_height = 600, 500
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Mario")

# Load the images of Mario running
plyer_image = [pygame.image.load('Bilder/oben.png').convert_alpha(),
               pygame.image.load('Bilder/unten.png').convert_alpha(),
               pygame.image.load("Bilder/rechts.png").convert_alpha(),
               pygame.image.load("Bilder/links.png").convert_alpha(),
               pygame.image.load("Bilder/left1.png").convert_alpha(),
               pygame.image.load("Bilder/right1.png").convert_alpha()]

def move_stone(x, y, plyer_rect1):
    key_s = pygame.key.get_pressed()
    if key_s[pygame.K_UP]:
        y -= speed
    if key_s[pygame.K_DOWN]:
        y += speed
    if key_s[pygame.K_LEFT]:
        x -= speed
    if key_s[pygame.K_RIGHT]:
        x += speed

        # Calculate the distance between the player and the stone
    distance = plyer_rect1.right - x

    # If the player is on the left of the stone, the distance is 5 or lower, and the player is moving to the right,
    # move the stone to the right as well
    if plyer_rect1.x < x and distance <= 5 and keys[pygame.K_d]:
        x += speed

def line(x_start, y_start, x_end, y_end):
    pygame.draw.line(screen, "black", (x_start, y_start), (x_end, y_end), width=5)

Stein_img = pygame.image.load("Bilder/Steine/1.png").convert_alpha()
stein = pygame.transform.smoothscale(Stein_img, (30, 30))
stein_rect = stein.get_rect(x=200, y=200)

# Load the background image and scale it to fit the screen
o_background = pygame.image.load("Bilder/background.jpg").convert_alpha()
background = pygame.transform.scale(o_background, (screen_width, screen_height))

# Set the player's initial position and speed
plyer = plyer_image[1].convert_alpha()
plyer = pygame.transform.scale(plyer, (30, 50))
plyer_rect = plyer_image[1].get_rect(x=100, y=200)

speed = 5
line_x = 0
# Create a clock to control the frame rate
clock = pygame.time.Clock()

# Main game loop
while True:
    # Handle events
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    # Update the player's position based on key input
    keys = pygame.key.get_pressed()
    if keys[pygame.K_RIGHT] and keys[pygame.K_LEFT]:
        plyer = plyer_image[1]

    if keys[pygame.K_UP]:
        plyer = plyer_image[0]
        plyer = pygame.transform.scale(plyer, (30, 50))
        plyer_rect.move_ip(0, -speed)
    if keys[pygame.K_DOWN]:
        plyer = plyer_image[1]
        plyer = pygame.transform.scale(plyer, (30, 50))
        plyer_rect.move_ip(0, speed)
    if keys[pygame.K_RIGHT] and not keys[pygame.K_LEFT]:
        if pygame.time.get_ticks() % 100 < 50:
            plyer = plyer_image[2]
            plyer = pygame.transform.scale(plyer, (30, 50))
        else:
            plyer = plyer_image[5]
            plyer = pygame.transform.scale(plyer, (30, 50))
        plyer_rect.move_ip(speed, 0)
    if keys[pygame.K_LEFT] and not keys[pygame.K_RIGHT]:
        if pygame.time.get_ticks() % 100 < 50:
            plyer = plyer_image[3]
            plyer = pygame.transform.scale(plyer, (30, 50))
        else:
            plyer = plyer_image[4]
            plyer = pygame.transform.scale(plyer, (30, 50))
        plyer_rect.move_ip(-speed, 0)

    # Here is the movement of the stone
    dist_bottom = plyer_rect.bottom - stein_rect.top
    dist_top = plyer_rect.top - stein_rect.bottom
    dist_right = plyer_rect.right - stein_rect.left
    dist_left = plyer_rect.left - stein_rect.right

    if 0 <= dist_bottom <= 80 and 10 <= dist_right <= 30 and keys[pygame.K_RIGHT]: stein_rect.x += speed
    if 0 <= dist_bottom <= 80 and -30 <= dist_left <= 0 and keys[pygame.K_LEFT]:   stein_rect.x -= speed
    if 0 <= dist_bottom <= 20 and -60 <= dist_left <= -5 and keys[pygame.K_DOWN]:  stein_rect.y += speed
    if -20 <= dist_top <= 0 and -60 <= dist_left <= -5 and keys[pygame.K_UP]:      stein_rect.y -= speed

    # print to help
    if pygame.time.get_ticks() % 1000 < 50 / 3:
        print("line: 118", "bottom:", dist_bottom)

    # The line movement
    if keys[pygame.K_e]:
        if line_x < 50:
            line_x += 1
    if keys[pygame.K_q]:
        if line_x > 0:
            line_x -= 1

    if line_x+plyer_rect.left+30 >= stein_rect.x and 31 < dist_bottom < 56:
        if keys[pygame.K_f]:
            stein_rect.x = plyer_rect.x+line_x+20
            stein_rect.y = plyer_rect.centery-10

    # Keep the player within the screen boundaries
    if plyer_rect.bottom > screen_height:
        plyer_rect.bottom = screen_height
    if plyer_rect.right > screen_width:
        plyer_rect.right = screen_width
    if plyer_rect.top < 0:
        plyer_rect.top = 0
    if plyer_rect.left < 0:
        plyer_rect.left = 0

    # Clear the screen and draw the background and player
    screen.blit(background, (0, 0))
    screen.blit(plyer, plyer_rect)
    screen.blit(stein, stein_rect)
    line(plyer_rect.centerx + 15, plyer_rect.centery, plyer_rect.centerx + line_x + 15, plyer_rect.centery)

    # Update the display
    pygame.display.update()

    # Limit the frame rate to 60 FPS
    clock.tick(60)


# -*- coding: utf-8 -*-
from __future__ import division

from annotation.models import Document, Label, Score
from annotation.models import NBC_class_count, NBC_word_count_given_class, NBC_vocabulary
from django.db.models import F, FloatField, Sum, IntegerField

from nltk import word_tokenize
from math import log, exp
from operator import itemgetter, attrgetter
from itertools import groupby
from pprint import pprint
import numpy as np
import re
import sys

import pdb

def update_class_count(labels):
    # This function updates the counts for the class prior. If there
    # are existing class count objects, their counts are being
    # updated. Otherwise the objects are initialized with 0.
    for label in map(lambda l: l.label, labels):
        cc, created = NBC_class_count.objects.get_or_create(label=label)
        if not created:
            cc.count = cc.count +1
            cc.save()

def update_word_count(tokens, labelObjs):
    # To compute the conditional probability of a term given the class
    # this function counts how often each term occurs in each class of
    # the training document.
    labels = map(lambda l: l.label, labelObjs)
    for token in tokens:
        for label in labels:
            try:
                vocab, created_vocab = NBC_vocabulary.objects.get_or_create(word=token)
            except:
                duplicates = NBC_vocabulary.objects.filter(word=token)
                duplicates.delete()
                wcgc = NBC_vocabulary(word=token)
                wcgc.save()
            try:
                wcgc, created_wcgc = NBC_word_count_given_class.objects.get_or_create(
                    word=token, label=label)
            except:
                duplicates = NBC_word_count_given_class.objects.filter(word=token, label=label)
                count = sum(map(attrgetter('count'), duplicates))
                duplicates.delete()
                wcgc = NBC_word_count_given_class(word=token, label=label)
                wcgc.count = count
                wcgc.save()
                #
            if not created_wcgc:
                wcgc.count = wcgc.count +1
                wcgc.save()
                #
    ccs = list(NBC_class_count.objects.filter(label__in=labels))
    for cc in ccs:
        cc.total_word_count = cc.total_word_count + len(tokens)
        cc.save()

# def relative_term_freq(token, label):
#     # Conditional probability P(term|class)
#     vocab = NBC_vocabulary.objects.filter(word=token).first()
#     if vocab:
#         wcgc = NBC_word_count_given_class.objects.filter(
#             word=vocab, label=label).first()
#         if wcgc:
#             Tct = wcgc.count
#         else:
#             Tct = 0
#     else:
#         Tct = 0
#         #
#     Tct_sum = NBC_class_count.objects.filter(
#         label=label).first().total_word_count + NBC_vocabulary.objects.all().count()
#     return (Tct + 1) / Tct_sum

def preprocessing(raw_document):
    urls = r'(http.+?(\s|$))'
    specialchar =  r'|[^A-Za-z\s]'
    doc = raw_document.lower()
    tokens = word_tokenize(re.sub(urls+specialchar, ' ', doc))
    return tokens

def online_train(document, labels):
    update_class_count(labels)
    update_word_count(document.preprocessed.split(' '), labels)

def predict(document, saveScores=True):
    # To predict the label of a document first get the preprocessed
    # version.
    tokens = document.preprocessed.split(' ')
    scores = {} # holds the end result
    vocabulary_count = NBC_vocabulary.objects.all().count()
    # The calculation for each label can be done independently
    ccs = list(NBC_class_count.objects.all())
    labels = map(attrgetter('label'), ccs)
    #
    wcgcObjs = list(NBC_word_count_given_class.objects.filter(word__in=tokens,
                                                              label__in=labels))
    # This is a dict where each key is a label and each value is a
    # dict itself, where the keys are words and the values are the
    # counts of how often the word occured given the label.
    wcgcs = dict((label, dict((word, map(attrgetter('count'), w)[0])
                              for word, w in groupby(sorted(v, key=attrgetter('word')),
                                                     key=attrgetter('word'))))
                 for label, v in groupby(sorted(wcgcObjs, key=attrgetter('label')),
                                         key=attrgetter('label')))
    # Sum over the field 'count' in the table NBC_class_count to know
    # how many documents there are in total.
    N = sum(map(attrgetter('count'), ccs))
    for c in ccs:
        label = c.label
        scores[label] = {}
        if c.count:
            prior = log(c.count/N)
        else:
            prior = -sys.maxint -1
            #
        scores[label]['prior'] = prior
        scores[label]['term_given_label'] = 0
        Tct_tmp = wcgcs.get(label, 0)
        Tct_sum = c.total_word_count + vocabulary_count
        for token in tokens:
            if Tct_tmp:
                Tct = Tct_tmp.get(token, 0)
                    #
            relative_term_freq = (Tct + 1) / Tct_sum
            scores[label]['term_given_label'] += log(relative_term_freq)
            #
        scores[label]['total'] = scores[label]['prior']+scores[label]['term_given_label']
        #scores[label]['total'] = scores[label]['term_given_label']
        scores[label]['prior'] = exp(scores[label]['prior'])
        #scores[label]['term_given_label'] = exp(scores[label]['term_given_label'])
        scores[label]['total'] = exp(scores[label]['total'])
        #
    # ask eugen again
    for c in ccs:
        label = c.label
        scores[label]['normalized'] = np.power(scores[label]['total'], 1/(len(tokens)+1))
    # normalize
    total_sum = 0
    for c in ccs:
        label = c.label
        total_sum += scores[label]['normalized']
    for c in ccs:
        label = c.label
        if scores[label]['normalized'] != 0:
            scores[label]['normalized'] = scores[label]['normalized'] / total_sum
        else:
            scores[label]['normalized'] = 0.0
        # save scores to db
        # if saveScores:
        #     dbScore, created = Score.objects.get_or_create(document=document,
        #                                                    label=Label.objects.filter(label=c.label).first())
        #     dbScore.nbc_normalized = scores[label]['normalized']
        #     dbScore.nbc_prior = scores[label]['prior']
        #     dbScore.nbc_term_given_label = scores[label]['term_given_label']
        #     dbScore.nbc_total = scores[label]['total']
        #     dbScore.save()
    return scores

def predict_label(document, scores={}):
    # This function adds a little abstraction for convenience but more
    # importantly it decides which label is picked based on the
    # probabilities for each label.
    if scores:
        scores = map(lambda l: (l[0], l[1]['total']), scores.items())
    else:
        scores = map(lambda l: (l[0], l[1]['total']),
                     predict(document).items())
    if scores:
        predicted_label = Label.objects.filter(
            label=max(scores,key=itemgetter(1))[0]).first()
    else:
        predicted_label = None
    return predicted_label

    # predict the labels for all scores
    # pred_label = map(lambda score: predict_label(None, score), pred_scores)
    # go over all pred_scores and select the normalized score for the
    # predicted label.
    # pred_norm = map(lambda idx:
    #                 pred_scores[idx][pred_label[idx].label]['normalized'],
    #                 range(len(documents)))


from common.model_manager.payment_manager import PaymentManager
from common.model_manager.utils import get_amount
from common.msg_service.wechat_msg_send import MerchantMessageSender
from common.voice_synthesis.baidu import get_baidu_token
from ws_service.utils import PublishToRedisChannel

def send_pay_success_wechat_message(payment):
    """付款成功微信消息推送"""

    payment = PaymentManager(payment)

    message_sender = MerchantMessageSender(payment.merchant)
    message_sender.on_pay_success(
        merchant_receive='%.2f' % get_amount(payment.paid_price),
        payment_type='普通订单' if not payment.coupon else '优惠券订单',
        datetime=payment.datetime,
        pay_serial_number=payment.serial_number,
    )

def send_voice_message_to_merchant_app(payment):
    """send payment pk to app through websocket, and then app request server a voice by the pk."""

    payment = PaymentManager(payment)

    token, error_msg = get_baidu_token()
    data = dict(
        channel_key=f'merchant_{payment.merchant_id}_new_payment',
        voice_text=payment.voice_text,
        baidu_token=token,
        error_msg=error_msg
    )

    PublishToRedisChannel.publish_to_channel(data=data)


import asyncio
import datetime
import discord
from discord.ext import commands, tasks
from ee250_final.user import User
import io
import json
import logging
import os
import paho.mqtt.client as mqtt
from PIL import Image
import schedule
import threading
import time

logging.basicConfig(level=logging.INFO)

class Commands:
    LED_OFF = "LED off"
    LED_ON = "LED on"
    FEED = "feed"

class Topics:
    FEEDER = "{username}/feeder"
    IMAGE = "rpi/webcam"
    LED = "{username}/LED"

class DiscordPub(commands.Cog):

    def __init__(self, bot: commands.Bot, channel_id, host, port, keepalive):
        self._feeder_scheduler = schedule.Scheduler()
        self._sched_stop = threading.Event()
        self._scheduler_thread = threading.Thread(target=self._feeder_timer)
        self._scheduler_thread.start()

        self._bot = bot
        self._channel_id = channel_id
        self._mqtt_client = mqtt.Client()
        self._mqtt_client.on_connect = self._on_connect
        self._mqtt_client.connect(host=host, port=port, keepalive=keepalive)
        self._mqtt_client.loop_start()
        self._users = []
        self._is_publish_image = False

        self.check_for_images_to_send.start()

    def _feeder_timer(self):
        while not self._sched_stop.is_set():
            self._feeder_scheduler.run_pending()
            time.sleep(10)

    def _on_connect(self, client, userdata, flags, rc):
        logging.info("MQTT publisher client connected to server with code {}".format(str(rc)))
        self._mqtt_client.subscribe(Topics.IMAGE)
        self._mqtt_client.message_callback_add(Topics.IMAGE, self._save_image_data)

    def _save_image_data(self, client, userdata, msg):
        with open("scripts/temp.jpg", "wb") as f:
            f.write(msg.payload)
        self._is_publish_image = True

    @tasks.loop(seconds=10.0)
    async def check_for_images_to_send(self):
        if self._is_publish_image:
            channel = await self._bot.fetch_channel(self._channel_id)
            await channel.send(file=discord.File("scripts/temp.jpg"))
            logging.info("Image sent from bot")
            os.remove("scripts/temp.jpg")
            self._is_publish_image = False

    @commands.Cog.listener()
    async def on_member_join(self, member):
        channel = member.guild.system_channel
        if channel is not None:
            await channel.send('hello, {0.mention}!'.format(member))

    @commands.command(
        name="set", help="set times to trigger the automatic feeder"
    )
    async def ask_to_set_times(self, message: discord.Message):
        name = message.author.name
        user = self._find_user(name)

        await message.channel.send("enter times (hh:mm:ss) separated by a space.")

        def is_valid_time(message):
            times_array = message.content.split()
            for value in times_array:
                try:
                    datetime.datetime.strptime(value, "%H:%M:%S")
                except ValueError:
                    return False
            return True

        try:
            times: discord.Message = await self._bot.wait_for(
                "message", check=is_valid_time, timeout=40.0
            )
        except asyncio.TimeoutError:
            return await message.channel.send(
                "sorry, you took too long. please enter the command again."
            )

        self._save_times(times, user)
        await message.channel.send("cool, thanks!")

    @commands.command(
        name="feed", help="send 'feed' command to rpi feeder"
    )
    async def send_feed_command(self, message: discord.Message):
        name = message.author.name
        user: User = self._find_user(name)
        self._mqtt_client.publish(user.feeder_topic, Commands.FEED)

    @commands.command(
        name="LED_off", help="Send message to RPI to turn off LED"
    )
    async def turn_led_off(self, message: discord.Message):
        name = message.author.name
        user = self._find_user(name)
        logging.info(user)
        logging.info(user.led_topic)
        if user is not None:
            self._mqtt_client.publish(user.led_topic, Commands.LED_OFF)

    @commands.command(
        name="LED_on", help="Send message to RPI to turn on LED"
    )
    async def turn_led_on(self, message: discord.Message):
        name = message.author.name
        user = self._find_user(name)
        self._mqtt_client.publish(user.led_topic, Commands.LED_ON)

    def add_user(self, name: str):
        user = User(name)
        self._users.append(user)
        logging.info("New user added: {}".format(name))
        return user

    def _find_user(self, name: str, create_user_if_not_exists=True):
        user_to_return = None
        for user in self._users:
            if user.name == name:
                return user
        if create_user_if_not_exists:
            user_to_return = self.add_user(name)
        return user_to_return

    def _save_times(self, message: discord.Message, user: User):
        times_array = message.content.split()
        user.add_feeding_times(times_array)
        for time in times_array:
            self._feeder_scheduler.every().day.at(time).do(
                self._mqtt_client.publish,
                user.feeder_topic,
                Commands.FEED
            )


import numpy as np
from bayopt import BayOptRBF, RandomQuadratic, RandomGaussian, plot_3d, bounded_min

if __name__ == "__main__":
    # np.random.seed(42)

    # Inicializo los datos.
    n_dim = 2
    bounds = np.array([(-1, 1), (-1, 1)])
    RS = RandomQuadratic(n_dim=n_dim, bounds=bounds, offset=True, noise=0.01)

    x_data = np.array([[0, 0], [1, 1], [1, -1], [-1, 1], [-1, -1]])
    # x_data = np.random.uniform(low=-2, high=2, size=(500, n_dim))
    # x_data = np.array([[0,0],[1,1],[1,-1],[-1,1],[-1,-1],[2,2],[2,-2],[-2,2],[-2,-2]])
    y_data = np.array([RS(x) for x in x_data])

    model = BayOptRBF(x_data, y_data)

    plot_3d(RS, model.regressor)


import random

def secret_santa():
    list_1 = ["Amira","1", "Chad","2", "Mariah","3","Yasmin","4"]

    giftee_options = dict()
    for i in range(0,len(list_1),2):
      giftee_options.update({list_1[i]: list_1[i+1]})

    same = {"Same":0}
    different = {"Different":0}

    choose_again = 'yes'

    #rules
    while choose_again == 'yes':
        print('Welcome to Secret Santa !')
        print('You are Santa. You have guess who the elves want you to gift')
        print('Insert a number representing the person you believe should get a present this year')
        print('Your elves will tell you if they believe if its a good idea')
        print('If the elves have a different choice than you then you have to gift the elves choice instead')
        print('Please input the correct number according')
        print('Who Are You Picking?')

        elf_choice = get_elf_choice()
        santa_choice = get_santa_choice()

        if elf_choice == santa_choice:
          same["Same"] += 1
        else:
          different["Different"] += 1

        print('You chose', santa_choice, '.')
        print('Elves chose', elf_choice, '.')

        print(giftee_result(elf_choice, santa_choice))

        choose_again = input("Would You Like to Guess Again?Enter yes or no ")

    print('Your total correct choices', same, '.')
    print('Your total wrong choices', different, '.')

def get_elf_choice():

    choice = random.randint(1,3)

    if choice == 1:
        choice = 'Amira'
    elif choice == 2:
        choice = 'Chad'
    elif choice == 3:
        choice = 'Mariah'
    else:
        choice = 'Yasmin'

    return choice

def get_santa_choice():

    choice = int(input("Select Picker Amira(1), Chad(2), Mariah(3), Yasmin(4): "))

    while choice != 1 and choice != 2 and choice != 3 and choice != 4:
        print('These people are valid Amira(type in 1), Chad(type in 2),Mariah(type in 3')
        print('or Yasmin(type in 4).')
        choice = int(input('Enter a valid number please: '))

    if choice == 1:
        choice = 'Amira'
    elif choice == 2:
        choice = 'Chad'
    elif choice == 3:
        choice = 'Mariah'
    else:
        choice = 'Yasmin'

    return choice

def giftee_result(elf_choice, santa_choice):

    if elf_choice == santa_choice:
        result = 'same'
        return("You Get To Keep Your Giftee!")
    else:
        result = 'different'
        return ("The eleves went a different direction :/ ")

secret_santa()