"""
TimeSlot node.
"""
from sqlalchemy import or_

from opendf.applications.smcalflow.nodes.time_nodes import *
from opendf.defs import VIEW, posname
from opendf.applications.smcalflow.exceptions.python_exception import TimeSlotException
from opendf.utils.utils import flatten_list, to_list, Message

SI_START = 10  # partial time interval as start
SI_END = 11
SI_BOUND = 12
SI_INTER = 13
SI_DUR = 14

class slot_interval:
    def __init__(self, iv, obj=None, typ=None, dur=None, durtyp=None, comp=None):
        self.interval = iv  # partial interval (None for dur)
        self.typ = typ  # PT_START/...
        self.dur = dur  # python datedelta for dur, else None
        self.durtyp = durtyp  # 'eq' / 'lt' ...
        self.obj = obj  # the original TimeSlot?()
        self.comp = comp  # compound slot

    si_names = {SI_START: 'start', SI_END: 'end', SI_DUR: 'dur', SI_BOUND: 'bound', SI_INTER: 'inter', }

    def __repr__(self):
        s = '%s %s' % (self.si_names[self.typ], self.interval if self.interval else 'obj=%s' % self.obj)
        return s

    def comparable_slot(self, other, mode=None):
        if self.typ == SI_DUR or other.typ == SI_DUR:
            return True
        return self.interval.comparable(other.interval, mode)

    def compatible_dur(self, other):
        if self.typ != SI_DUR or other.typ != SI_DUR:
            return True
        if self.durtyp == 'eq' or other.durtyp == 'eq':
            return False
        if self.durtyp in ['lt', 'le'] and other.durtyp in ['lt', 'le']:
            return False
        if self.durtyp in ['gt', 'ge'] and other.durtyp in ['gt', 'ge']:
            return False
        if self.durtyp in ['lt', 'le'] and other.durtyp in ['gt', 'ge'] and self.dur >= other.dur:
            return False
        if self.durtyp in ['gt', 'ge'] and other.durtyp in ['lt', 'le'] and self.dur <= other.dur:
            return False
        return True

    def compatible_bound(self, other):
        if self.typ != SI_BOUND or other.typ != SI_BOUND:
            return True
        if not self.interval.comparable(other.interval):
            return True
        if self.interval.intersect(other.interval):  # no negative bounds...
            return True
        return False

    def compatible_inter(self, other):
        if self.typ != SI_INTER or other.typ != SI_INTER:
            return True
        if not self.interval.comparable(other.interval):
            return True
        # always true for positive. For negative may not be true
        siv, oiv = self.interval, other.interval
        if siv.typ == 'nitv':
            if oiv.typ in ['nitv', 'neq']:
                return siv.intersect(oiv)
            else:
                return siv.bounds(oiv)
        if oiv.typ == 'nitv':
            return siv.intersect(oiv)
        if siv.typ in ['eq', 'neq'] and oiv.typ in ['eq', 'neq'] and siv.intersect(oiv):
            return False
        return True

    # update a list for a specific field - add new interval, and possibly remove older, incompatible, ones
    def update_field_list(self, curr, prune):
        upd = []
        typ = self.typ
        for o in curr:
            compat = self.compatible_dur(o) if typ == SI_DUR else \
                self.compatible_bound(o) if typ == SI_BOUND else \
                    self.compatible_inter(o) if typ == SI_INTER else \
                        self.interval.compatible(o.interval)
            if compat:
                upd.append(o)
            elif o not in prune:
                prune.append(o)
        upd.append(self)
        return upd, prune

    # TODO: verify logic for all the prune_xx_list cases

    # curr is a list of bound constraints.
    # self is an interval (of a type not bound - that was already covered in update_field_list).
    # prune bounds which are incompatible with self.
    def prune_bound_list(self, curr, prune):
        upd = []
        typ = self.typ
        for o in curr:
            compat = True
            if typ in [SI_START, SI_END]:
                compat = True if not self.interval.comparable(o.interval) else o.interval.bounds(self.interval)
            elif typ == SI_DUR:
                compat = self.dur > o.interval.length()
            elif typ == SI_INTER:
                compat = False  # TODO:
            if compat:
                upd.append(o)
            elif o not in prune:
                prune.append(o)
        return upd, prune

    def prune_inter_list(self, curr, prune):
        upd = []
        typ = self.typ
        for o in curr:
            compat = True
            if typ == SI_START:
                compat = not self.interval.strictly_after(o.interval)
            elif typ == SI_END:
                compat = not self.interval.strictly_before(o.interval)
            elif typ == SI_DUR:
                compat = True
            elif typ == SI_BOUND:
                compat = False  # TODO:
            if compat:
                upd.append(o)
            elif o not in prune:
                prune.append(o)
        return upd, prune

    # handles only bound, inter, and direct interaction with start - no INDIRECT interaction between start/end/dur
    def prune_start_list_simp(self, curr, prune):
        upd = []
        typ = self.typ
        for o in curr:
            compat = True
            if typ == SI_BOUND:
                compat = not (self.interval.strictly_before(o.interval) or self.interval.strictly_after(o.interval))
            elif typ == SI_INTER:
                compat = not self.interval.strictly_before(o.interval)
            elif typ == SI_END:
                compat = not self.interval.strictly_before(o.interval)  # direct interaction (end before start)
            if compat:
                upd.append(o)
            elif o not in prune:
                prune.append(o)
        return upd, prune

    # handles only bound, inter, and direct interaction with end - no INDIRECT interaction between start/end/dur
    def prune_end_list_simp(self, curr, prune):
        upd = []
        typ = self.typ
        for o in curr:
            compat = True
            if typ == SI_BOUND:
                compat = not (self.interval.strictly_before(o.interval) or self.interval.strictly_after(o.interval))
            elif typ == SI_INTER:
                compat = not self.interval.strictly_after(o.interval)
            elif typ == SI_START:
                compat = not self.interval.strictly_after(o.interval)
            if compat:
                upd.append(o)
            elif o not in prune:
                prune.append(o)
        return upd, prune

    # handles only bound, inter, and direct interaction with end - no INDIRECT interaction between start/end/dur
    def prune_dur_list_simp(self, curr, prune):
        upd = []
        typ = self.typ
        for o in curr:
            compat = True
            if typ == SI_BOUND:
                compat = self.interval.length() < o.dur
            elif typ == SI_INTER:
                compat = True
            if compat:
                upd.append(o)
            elif o not in prune:
                prune.append(o)
        return upd, prune

    # This is called when the latest interval is a bound
    # - the interval is already added to the curr_bound, as the last element.
    # - the curr_bound list does NOT have to be updated - it was updated in the previous turn, and adding a new
    #   bound (which might have pruned a previous bound object) will not cause any more bounds to be pruned
    def prune_lists_by_bound(self, curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune):
        curr_inter, prune = self.prune_inter_list(curr_inter, prune)
        curr_start, prune = self.prune_start_list_simp(curr_start, prune)
        curr_end, prune = self.prune_end_list_simp(curr_end, prune)
        curr_dur, prune = self.prune_dur_list_simp(curr_dur, prune)
        return curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune

    def prune_lists_by_inter(self, curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune):
        curr_bound, prune = self.prune_bound_list(curr_bound, prune)
        curr_start, prune = self.prune_start_list_simp(curr_start, prune)
        curr_end, prune = self.prune_end_list_simp(curr_end, prune)
        curr_dur, prune = self.prune_dur_list_simp(curr_dur, prune)
        return curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune

    def prune_lists_by_start(self, curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune):
        curr_bound, prune = self.prune_bound_list(curr_bound, prune)
        curr_inter, prune = self.prune_inter_list(curr_inter, prune)
        curr_end, prune = self.prune_end_list_simp(curr_end, prune)
        curr_dur, prune = self.prune_dur_list_simp(curr_dur, prune)
        # curr interval (self) is start.
        # find which is the last constraint (from end or dur) which is comparable (compatible?) with it, and prune
        # the "other". for now - aggressively prune the other if comparable
        # TODO: we should prune only if the "other" is incompatible with the combination of self+"last"
        c_end = [o for o in curr_end if self.interval.comparable(o.interval)]
        c_dur = [o for o in curr_dur if self.interval.comparable(o.interval)]
        if c_end and c_dur:
            if c_end[-1].obj.created_turn > c_dur[-1].obj.created_turn:
                curr_dur = [o for o in curr_dur if o not in c_dur]
                prune += c_dur
            else:
                curr_end = [o for o in curr_end if o not in c_end]
                prune += c_end
        return curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune

    def prune_lists_by_end(self, curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune):
        curr_bound, prune = self.prune_bound_list(curr_bound, prune)
        curr_inter, prune = self.prune_inter_list(curr_inter, prune)
        curr_start, prune = self.prune_start_list_simp(curr_start, prune)
        curr_dur, prune = self.prune_dur_list_simp(curr_dur, prune)
        c_start = [o for o in curr_start if self.interval.comparable(o.interval)]
        c_dur = [o for o in curr_dur if self.interval.comparable(o.interval)]
        if c_start and c_dur:
            if c_start[-1].obj.created_turn > c_dur[-1].obj.created_turn:
                curr_dur = [o for o in curr_dur if o not in c_dur]
                prune += c_dur
            else:
                curr_start = [o for o in curr_start if o not in c_start]
                prune += c_start
        return curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune

    def prune_lists_by_dur(self, curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune):
        curr_bound, prune = self.prune_bound_list(curr_bound, prune)
        curr_inter, prune = self.prune_inter_list(curr_inter, prune)
        curr_start, prune = self.prune_start_list_simp(curr_start, prune)
        curr_end, prune = self.prune_end_list_simp(curr_end, prune)
        c_start = [o for o in curr_start if o.interval.comparable(self.interval)]
        c_end = [o for o in curr_end if o.interval.comparable(self.interval)]
        if c_start and c_end:
            if c_start[-1].obj.created_turn >= c_end[-1].obj.created_turn:
                curr_end = [o for o in curr_end if o not in c_end]
                prune += c_end
            else:
                curr_start = [o for o in curr_start if o not in c_start]
                prune += c_start
        return curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune

    def prune_curr(self, curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune):
        typ = self.typ

        # 1. separate update per field
        if typ == SI_START:
            curr_start, prune = self.update_field_list(curr_start, prune)
        elif typ == SI_END:
            curr_end, prune = self.update_field_list(curr_end, prune)
        elif typ == SI_DUR:
            curr_dur, prune = self.update_field_list(curr_dur, prune)
        elif typ == SI_BOUND:
            curr_bound, prune = self.update_field_list(curr_bound, prune)
        elif typ == SI_INTER:
            curr_inter, prune = self.update_field_list(curr_inter, prune)

        # 2. prune with interactions
        # don't try to prune again same type - the interval is already in the curr_... - will prune itself
        if typ == SI_BOUND:
            curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune = \
                self.prune_lists_by_bound(curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune)

        elif typ == SI_INTER:
            curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune = \
                self.prune_lists_by_inter(curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune)

        elif typ == SI_START:
            curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune = \
                self.prune_lists_by_start(curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune)

        elif typ == SI_END:
            curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune = \
                self.prune_lists_by_end(curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune)

        elif typ == SI_DUR:
            curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune = \
                self.prune_lists_by_dur(curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune)

        return curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune

def bound_surround_event(bound, start_column, end_column, selection, kwargs):
    """
    Add conditions to `selection` in order to check if `bound` surrounds the interval defined by `start_column` and
    `end_column`.

    :param bound: the bound
    :type bound: Range
    :param start_column: the start column
    :type start_column: Any
    :param end_column: the end column
    :type end_column: Any
    :param selection: the selection
    :type selection: Any
    :param kwargs: additional parameters
    :type kwargs: Dict[str, Any]
    :return: the new selection, with the surrounding conditions
    :rtype: Any
    """
    qualifier = kwargs.get("qualifier")
    kwargs["qualifier"] = GE()
    selection = bound.input_view("start").generate_sql_where(selection, start_column, **kwargs)
    kwargs["qualifier"] = LE()
    selection = bound.input_view("end").generate_sql_where(selection, end_column, **kwargs)
    kwargs["qualifier"] = qualifier
    return selection

def event_surround_point(start_column, end_column, point, selection, kwargs):
    """
    Add conditions to `selection` in order to check if the event, represented by `start_column` and `end_column`
    surrounds the `point` in time.

    :param start_column: the start column
    :type start_column: Any
    :param end_column: the end column
    :type end_column: Any
    :param point: the point in time
    :type point: Date or Time or DateTime
    :param selection: the selection
    :type selection: Any
    :param kwargs: additional parameters
    :type kwargs: Dict[str, Any]
    :return: the new selection, with the surrounding conditions
    :rtype: Any
    """
    qualifier = kwargs.get("qualifier")
    kwargs["qualifier"] = LE()
    selection = point.generate_sql_where(selection, start_column, **kwargs)
    kwargs["qualifier"] = GE()
    selection = point.generate_sql_where(selection, end_column, **kwargs)
    kwargs["qualifier"] = qualifier
    return selection

class TimeSlot(Node):
    """
    Describes a time slot.
    """

    # TimeSlot unifies values and constraints which describe a time slot object / constraint.
    # The basic object has start, end, and duration; additionally, constraints can specify a (partial) time span
    # which the slot should be bound by or intersect with.
    # Since all these arguments are inter-connected, processing can not be done independently.
    # TimeSlot implements the logic for the interaction between them.
    # In the simplified annotation, we use modifiers to create constraints, so we can expect the following timeslots:
    #  - for an object from the DB - start, end, duration (as one TimeSlot object)
    #    - when converted to a constraint, it will be separated to 3 TimeSlot constraints - for start, end, duration
    #  - from a modifier:
    #    - this translates to a constraint with only one field:
    #       - has_start / has_end / has_duration - start / end / duration only (similarly for the negative ones)
    #          - at least for now, we assume the argument is one of:
    #                  - simple time value:                 DateTime, Date, Time - 9AM
    #                  - qualifier over simple time value:  DateTime, Date, Time - GT(9AM)
    #                  - range:              DateTimeRange, DateRange, TimeRange - Morning(), TimeRange(9AM, 11AM)
    #                       - which is internally converted to AND(GE(start), LE(end))
    #                         so that the default match() will work
    #                  i.e. - the argument can be represented by one interval
    #             - if there are aggregators, they will be outside the modifiers, not inside
    #               - >> any contradictory examples?
    #       - at_time - one argument (time point or interval), simple values, without any operators (quals / aggs)
    #             - e.g. at time(morning()) / at_time(9AM)
    #             - argument can be represented by one interval
    #          the plan is to translate this, depending on context, to either timeslot.bound or timeslot.inter
    #          - find event at_time(time point/range) means intersection
    #               - the point is during the event, or there is an intersection between the event and the range
    #          - create event at_time(range) means bounding interval:
    #               - create event EXACTLY at this range, or sub part of this range

    def __init__(self):
        super().__init__(type(self))
        self.signature.add_sig('start', [Date, Time, DateTime, TimeRange, DateRange, DateTimeRange])
        self.signature.add_sig('end', [Date, Time, DateTime, TimeRange, DateRange, DateTimeRange])
        self.signature.add_sig('duration', Period)
        # 'bound' - means the time slot is bounded to be within a time range
        #           (i.e. both start and end are within that range)
        #           e.g. "create a meeting in the afternoon / tomorrow"
        #         we could interpret point (as opposed to range) input as whole day / month / hour / ... ??
        #         note - "not in the morning" translates to negative 'inter', not 'bound' - no negative bounds
        self.signature.add_sig('bound', [Date, Time, DateTime, TimeRange, DateRange, DateTimeRange], custom=True)
        # 'inter' - means the time slot intersects a time point/range
        #      e.g. "find an event at 10AM / in the afternoon"
        self.signature.add_sig('inter', [Date, Time, DateTime, TimeRange, DateRange, DateTimeRange], custom=True)

    def generate_sql_where(self, selection, parent_id, **kwargs):
        """
        Generates the SQL where conditions to select objects conforming to TimeSlot.

        :param selection: the previous selection
        :type selection: Any
        :param parent_id: the parent id
        :type parent_id: Any
        :param kwargs: the mapping from the database columns to the time slot fields, must contain a mapping for
        start and end
        :type kwargs: Dict[str, Any]
        :return: the SQL query with the conditions
        :rtype: Any
        """
        start_column = kwargs["start"]
        end_column = kwargs["end"]

        if "start" in self.inputs:
            selection = self.input_view("start").generate_sql_where(selection, start_column, **kwargs)

        if "end" in self.inputs:
            selection = self.input_view("end").generate_sql_where(selection, end_column, **kwargs)

        if "duration" in self.inputs:
            selection = self.input_view("duration").generate_sql_where(selection, parent_id, **kwargs)

        # In order for it to work properly, the DateTime, Date or Time must be fully specified
        if "bound" in self.inputs:
            bound = self.input_view("bound")
            if isinstance(bound, Range):
                selection = bound_surround_event(bound, start_column, end_column, selection, kwargs)
            else:
                selection = bound.generate_sql_where(selection, start_column, **kwargs)
                selection = bound.generate_sql_where(selection, end_column, **kwargs)

        # In order for it to work properly, the DateTime, Date or Time must be fully specified
        if "inter" in self.inputs:
            bound = self.input_view("inter")
            if isinstance(bound, Range):
                conditions = []
                # we have start_column twice in the call below, because we want to check if the bound surrounds the
                # start of the event. Same for the end_column
                start = bound.input_view('start')
                end = bound.input_view('end')
                conditions.append(
                    bound_surround_event(bound, start_column, start_column, select(), kwargs).whereclause.self_group())
                conditions.append(
                    bound_surround_event(bound, end_column, end_column, select(), kwargs).whereclause.self_group())
                conditions.append(
                    event_surround_point(start_column, end_column, start, select(), kwargs).whereclause.self_group())
                conditions.append(
                    event_surround_point(start_column, end_column, end, select(), kwargs).whereclause.self_group())
                selection = selection.where(or_(*conditions).self_group())
            else:
                # intersect is a point
                selection = event_surround_point(start_column, end_column, bound, selection, kwargs)

        return selection

    def custom_match(self, nm, obj, iview=VIEW.INT, oview=None, check_level=False, match_miss=False):
        # self is a TimeSlot constraint, obj is a "real" TimeSlot object (not a constraint)
        # since obj is real object, guaranteed that intervals are always comparable
        if nm == 'bound':
            c = self.input_view('bound')  # constraint should be a simple object! (check?)
            pt = c.to_partialInterval()
            si = obj.to_slot_interval().interval
            return pt.bounds(si)
        elif nm == 'inter':
            c = self.input_view('inter')
            pt = c.to_partialInterval()
            si = obj.to_slot_interval().interval
            return pt.intersect(si)
        return True

    def describe(self, params=None):
        s = ''
        obj = []
        pp = [] if params is None else params
        if isinstance(params, dict):
            pp['add_prep'] = 1
        else:
            pp += ['add_prep']
        if 'start' in self.inputs:
            msg = self.input_view('start').describe(pp)
            t, o = msg.text, msg.objects
            if t:
                s += ' starting ' + t
            obj += o
        if 'end' in self.inputs and ('start' not in self.inputs or 'duration' not in self.inputs):
            msg = self.input_view('end').describe(pp)
            t, o = msg.text, msg.objects
            if t:
                s += ' ending ' + t
            obj += o
        if 'duration' in self.inputs:
            msg = self.input_view('duration').describe(params)
            t, o = msg.text, msg.objects
            if t:
                s += ' lasting ' + t
            obj += o
        if 'bound' in self.inputs:
            msg = self.input_view('bound').describe(params)
            t, o = msg.text, msg.objects
            if t:
                s += ' during ' + t
            obj += o
        if 'inter' in self.inputs:
            msg = self.input_view('inter').describe(params)
            t, o = msg.text, msg.objects
            if t:
                s += ' intersecting ' + t
            obj += o
        return Message(s, objects=obj)

    def slot_ctree_str(self, sep_date=True, open_slot=True):
        """
        Converts the timeslot field of an ACTUAL event into ONE timeslot constraint.

        :type sep_date: bool
        :param open_slot: if `True`, create strings for printing (not for construction)
        :type open_slot: bool
        :rtype: List[str]
        """
        prms = []
        if 'start' in self.inputs:
            # TODO: sep_date - separate year/month/day to individual constraints
            #   e.g. if the user specifies month, then we should not prune the whole date (year, day) as well
            st = self.input_view('start')
            s = []
            if 'date' in st.inputs:
                s.append('date=%s' % id_sexp(st.input_view('date')))
            if 'time' in st.inputs:
                s.append('time=%s' % id_sexp(st.input_view('time')))
            if s:
                if sep_date:
                    prms.extend(['start=DateTime(%s)' % i for i in s])
                else:
                    prms.append('start=DateTime(' + ','.join(s) + ')')
        if 'end' in self.inputs:
            # TODO: sep_date - separate year/month/day to individual constraints?
            en = self.input_view('end')
            s = []
            if 'date' in en.inputs:
                s.append('date=%s' % id_sexp(en.input_view('date')))
            if 'time' in en.inputs:
                s.append('time=%s' % id_sexp(en.input_view('time')))
            if s:
                if sep_date:
                    prms.extend(['end=DateTime(%s)' % i for i in s])
                else:
                    prms.append('end=DateTime(' + ','.join(s) + ')')
        if 'duration' in self.inputs:
            prms.append('duration=%s' % id_sexp(self.input_view('duration')))
        # actual object - does not have 'bound', 'inter'
        if not prms:
            return None
        if open_slot:
            return prms
        return ['slot=TimeSlot?(%s)' % p for p in prms]

    @staticmethod
    def get_ptime_and_qual(nd):
        t = nd.typename()
        if nd.not_operator():
            v = nd.to_partialDateTime()
            return v, 'eq'
        if nd.is_qualifier():
            tp = t.lower() if t in ['LT', 'LE', 'GT', 'GE', 'EQ'] else None
            n = nd.input_view(posname(1))
            if tp and n:
                v = n.to_partialDateTime()
                return v, tp
        raise TimeSlotException('Error - get_ptime_val %s\n%s' % (nd, nd.show()))

    @staticmethod
    def input_to_interval(nd, neg=False):
        """
        Converts input to an interval.
        Assumes input is either:
           1. simple time value (Date, Time, DateTime);
           2. simple time value with a qualifier;
           3. AND(x, y) where x,y are of type 2.
        """
        if isinstance(nd, Range):
            nd, _ = Node.call_construct_eval(nd.to_constr_sexp(), nd.context)  # temp context. new nodes will be discarded
        if nd.typename() == 'AND':
            s, e = nd.get_input_views([posname(1), posname(2)])  # verify pos input names...
            if s and e:
                v1, t1 = TimeSlot.get_ptime_and_qual(s)
                v2, t2 = TimeSlot.get_ptime_and_qual(e)
                tt = t1[0] + t2[0]
                if tt in ['lg', 'gl']:  # make sure this corresponds to a valid time range
                    if tt == 'lg':
                        v1, t1, v2, t2 = v2, t2, v1, t1
                    incl = ('(' if t1 == 'gt' else '[') + (')' if t2 == 'lt' else ']')
                    return PartialInterval(v1, v2, incl=incl, neg=neg)
        if nd.not_operator():
            v = nd.to_partialDateTime()
            return PartialInterval(v, None, neg=neg)
        if nd.is_qualifier():
            v, tp = TimeSlot.get_ptime_and_qual(nd)
            return PartialInterval(v, None, tp, neg=neg)
        raise TimeSlotException('Error - input_to_interval %s\n %s' % (nd, nd.show()))

    def to_slot_interval(self, neg=False):
        """
        Converts a TimeSlot to a slot_interval. The input to start, end, bound, inter may be a simple time value,
        or a simple tree (i.e. a tree which can be represented as one interval).
        """
        start, end, dur, bound, inter = self.get_input_views(['start', 'end', 'duration', 'bound', 'inter'])
        if start:
            return slot_interval(TimeSlot.input_to_interval(start, neg), typ=SI_START, obj=self)  # obj=start)
        if end:
            return slot_interval(TimeSlot.input_to_interval(end, neg), typ=SI_END, obj=self)  # , obj=end)
        if bound:
            return slot_interval(TimeSlot.input_to_interval(bound, neg), typ=SI_BOUND, obj=self)  # , obj=bound)
        if inter:
            return slot_interval(TimeSlot.input_to_interval(inter, neg), typ=SI_INTER, obj=self)  # , obj=inter)
        if dur:
            typ, t = dur.typename(), 'eq'
            if typ in ['LT', 'LE', 'GT', 'GE', 'EQ'] and posname(1) in dur:
                dur = dur.input_view(posname(1))
                t = typ.lower()
                t = neg_typ[t] if neg else t
                typ = dur.typename()
            if typ == 'Period':
                d = dur.to_Ptimedelta()
                pt = dur.to_partialDateTime()
                return slot_interval(pt, typ=SI_DUR, dur=d, durtyp=t, obj=self)  # , obj=dur)
        raise TimeSlotException('Error - to_slot_interval %s\n%s' % (self, self.show()))

    @staticmethod
    def to_interval_clusters(t, neg=False):
        """
        For each OR - return all the objects under it as a set of objects.
        """
        typ = t.typename()
        if typ == 'NOT':
            return TimeSlot.to_interval_clusters(t.input_view(posname(1)), not neg)
        elif typ == 'AND':
            iv = []
            for i in t.inputs:
                iv.append(TimeSlot.to_interval_clusters(t.input_view(i), neg)[0])
            return iv
        elif typ == 'OR':
            return [[TimeSlot.to_interval_clusters(t.input_view(i), neg)[0] for i in t.inputs]]
        elif typ == 'TimeSlot':
            return [t.to_slot_interval(neg=neg)]
        else:
            raise TimeSlotException('Error - to comp interval: %s\n%s' % (t, t.show()))

    # do the two clusters have comparable elements?
    # c1, c2 are lists of slot_interval
    @staticmethod
    def comparable_clusters(c1, c2):
        for i1 in to_list(c1):
            for i2 in to_list(c2):
                if i1.comparable_slot(i2, mode='L4'):
                    return True
        return False

    # TODO: verify
    @staticmethod
    def get_prune(turns):
        """
        Finds TimeSlots which need to be pruned.

        This is run on a TimeSlot constraint tree it is possible that the TimeSlot constraint tree is a truncation of a
        different tree (e.g. and Event tree) - in that case, we want to prune the original (Event) tree, not the
        TimeSlot tree, so we return the objects to be pruned, but don't prune the tree.

        Pruning time slots is tricky since the different fields (start/end/duration) are interdependent. The turns in
        the turn tree are assumed to be combined with an AND. If a turn has multiple constraints, unless there is an
        OR in the turn, we extract the separate constraints from the tree and combine them with an AND.

        In case of OR: (note that here we are only concerned with pruning. OR is not a problem for matching...). We
        don't forbid the use of OR, but it can be tricky to prune constraints with OR. The current strategy is to allow
        OR turns but prune previous OR turns if there is a chance they could contradict later turns. If there is an
        OR (or several ORs) (possibly combined with ANDs), we group all the constraints under an OR to a cluster.
        For now, we simply check if there is any later turn which is comparable (i.e. uses intersecting time
        components), and if so, we prune the whole OR cluster. This is overly aggressive, but because of the interaction
        between the different fields, we can't be sure that constraints on different fields don't contradict. If an
        OR cluster survives this, we treat it as if it was an AND - it should not affect the pruning results.

        :return: a list of TimeSlot nodes to be pruned (they are NOT pruned yet - this is done somewhere else)
        """
        turn_ivs = []  # list of lists - intervals for each turn
        all_ivs, turn_bnds, niv = [], [], 0  # intervals for all turns, with indices showing start/end of each turn
        has_ors = []

        prune = []  # list of slot_intervals

        # 1. get intervals / interval-clusters per turn
        for t in turns:
            iv = []
            h = False
            if t:
                if t.get_subnodes_of_type('OR'):
                    iv = TimeSlot.to_interval_clusters(t)
                    h = True
                else:
                    pos, neg = t.get_pos_neg_objs()
                    ipos, prune_pos = TimeSlot.slot_to_interval(pos)
                    ineg, prune_neg = TimeSlot.slot_to_interval(neg, neg=True)
                    # ipos, ineg = [o.to_slot_interval() for o in pos], [o.to_slot_interval(neg=True)
                    #                                                    for o in neg]
                    prune.extend(prune_pos)  # TODO - fix - this is not a slot_interval
                    prune.extend(prune_neg)
                    iv = ipos + ineg
            has_ors.append(h)
            turn_ivs.append(iv)
            turn_bnds.append((niv, niv + len(iv)))
            all_ivs.extend(iv)
            niv += len(iv)

        prune_slots = set()  # slot intervals to prune

        # 2. prune OR clusters - if comparable
        #    after this (aggressive) pruning, there are no comparable OR's left, so it should not
        #    matter if it's interpreted as AND or OR
        for i, ti in enumerate(turn_ivs):
            for j, tj in enumerate(turn_ivs):
                if j > i and ti and tj and (has_ors[i] or has_ors[j]):
                    for ki in ti:
                        for kj in tj:
                            if isinstance(ki, list) or isinstance(kj, list):
                                if TimeSlot.comparable_clusters(ki, kj):
                                    prune_slots.update(set(to_list(ki)))

        # 3. collect intervals which were not pruned
        all_ivs = [i for i in flatten_list(turn_ivs) if i not in prune_slots]

        # 4. collect intervals to prune

        # go over all intervals, from old to new. At each interval:
        #   1. update a (pruned) list of intervals, separately for each one of (start, end, dur);
        #   2. prune previous intervals taking into account interactions between different types.

        curr_start, curr_end, curr_dur, curr_bound, curr_inter = [], [], [], [], []
        for siv in all_ivs:
            curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune = \
                siv.prune_curr(curr_start, curr_end, curr_dur, curr_bound, curr_inter, prune)

        return prune

    @staticmethod
    def slot_to_interval(time_slots, neg=False):
        intervals = []
        prune = []
        for time_slot in time_slots:
            try:
                intervals.append(time_slot.to_slot_interval(neg=neg))
            except TimeSlotException:
                prune.append(time_slot)

        return intervals, prune


import os
import sys
import math
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib as mpl
import matplotlib.pyplot as plt
from scipy import stats

sys.path.append('./src/')
from h01_data.dataset.vox import VoxClamantis
from h02_learn.dataset import load_data, get_alphabet
from util import argparser
from util import util

aspect = {
    'font_scale': 1,
    'ratio': 1.625 * 1.9,
    'width': 6.5 * 1.5,
}
aspect['height'] = aspect['width'] / aspect['ratio']

cmap = sns.color_palette("Set2", 14)
sns.set(style="white", palette="Set2", color_codes=True)
sns.set_context("notebook", font_scale=aspect['font_scale'])
mpl.rc('font', family='serif', serif='Times New Roman')
mpl.rc('figure', figsize=(aspect['width'], aspect['height']))
mpl.rc('text', usetex=True)
sns.set_style({'font.family': 'serif', 'font.serif': 'Times New Roman'})

def get_args():
    argparser.add_argument('--mixed-effects-file', type=str, required=True)
    argparser.add_argument('--drop-position', action='store_true', default=False)
    argparser.add_argument('--drop-phone', action='store_true', default=False)
    argparser.add_argument('--analysis-type', type=str, default='lognormal')
    return argparser.parse_args()

def plot(df, uid_slope, args, hue=None):
    fig, _ = plt.subplots(figsize=(aspect['width'] / 2.2, aspect['height']))

    ax1 = sns.scatterplot(x='lang', y='slope', data=df, hue='family', style='dataset', markers=['o', 'P'], size='size', linewidth=0, alpha=.9, cmap="mako")
    ax1.set(xticklabels=[])

    if args.analysis_type != 'linear':
        ax1.axhline(1, ls='--', c='C7')
        ax1.axhline(uid_slope, ls='--', c='C1')

        ax1.text(.5, uid_slope, "Average effect", c='C1', ha='left', va='bottom')
        ax1.text(df.shape[0] - 10, 1, "No effect (zero line)", c='C7', ha='right', va='bottom')
        if not args.drop_position:
            plt.ylim([0.995, 1.046])
    else:
        ax1.axhline(0, ls='--', c='C7')
        ax1.axhline(uid_slope, ls='--', c='C1')

        ax1.text(.5, uid_slope, "Average effect", c='C1', ha='left', va='bottom')
        ax1.text(df.shape[0] - 2, 0, "No effect (zero line)", c='C7', ha='right', va='bottom')
        plt.ylim([-2.45, 5.1])

    plt.legend('', frameon=False)
    plt.ylabel('Trade-off Slope (millisecs / bit)')
    plt.xlabel('')

    extra_str = ''
    if args.drop_position and args.drop_phone:
        extra_str = '--drop_both'
    elif args.drop_position:
        extra_str = '--drop_position'
    elif args.drop_phone:
        extra_str = '--drop_phone'

    fname = 'results/plots/monoling-effects_full_%s%s.pdf' % (args.analysis_type, extra_str)
    fig.savefig(fname, bbox_inches='tight')

def clean_data(mdf, args):
    df = mdf.transpose().reset_index()
    df['lang'] = df['index'].apply(lambda x: x.split('--')[0])
    df['is_pvalue'] = df['index'].apply(lambda x: 'pvalue' if 'p_value' in x else 'slope')
    df = df.pivot(index='lang', columns='is_pvalue', values='loss').reset_index()
    df.sort_values('slope', inplace=True, ascending=True)

    if args.analysis_type != 'linear':
        df['slope'] = df['slope'].apply(lambda x: 2**x)

    return df

def plot_surprisal(mdf1, mdf2, args):
    df1 = clean_data(mdf1, args)
    df1['dataset'] = 'unitran'
    df1['size'] = .01
    langs = set(df1.lang.unique())
    df2 = clean_data(mdf2, args)
    df2['dataset'] = 'epitran'
    df2['size'] = 2
    df2 = df2[df2.lang.isin(langs)]
    df = pd.concat([df1, df2], axis=0)

    # uid_slope1 = df1.slope.mean()

    df_info = VoxClamantis.get_languages_info(args.data_path)
    df_info.set_index('bible.is / wilderness code', inplace=True)
    df = pd.merge(df, df_info, left_on='lang', right_on='bible.is / wilderness code')
    # import ipdb; ipdb.set_trace()
    print(df1.slope.mean(), df2.slope.mean())

    plot(df, df.slope.mean(), args)

def main():
    args = get_args()
    mdf1 = pd.read_csv('./results/unitran/mixed_effects_monoling.tsv', sep='\t')
    mdf2 = pd.read_csv('./results/epitran/mixed_effects_monoling.tsv', sep='\t')
    plot_surprisal(mdf1, mdf2, args)

if __name__ == '__main__':
    main()


import cv2
import numpy as np

class Effects(object):

    def render(self, image):

        # load calibration data
        #with np.load('webcam_calibration_ouput.npz') as X:
            #mtx, dist, _, _ = [X[i] for i in ('mtx','dist','rvecs','tvecs')]

        # set up criteria, object points and axis
        criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

        objp = np.zeros((7*10,3), np.float32)
        objp[:,:2] = np.mgrid[0:10,0:7].T.reshape(-1,2)

        # Arrays to store object points and image points from all the images.
        objpoints = [] # 3d point in real world space
        imgpoints = [] # 2d points in image plane.
        h = 0
        w =0
        images = glob.glob('images3/*.jpg')
        #print "hello"
        for fname in images:
            img = cv2.imread(fname)
            #print "hello2"
            gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
            h,w = img.shape[:2]
            # Find the chess board corners
            ret, corners = cv2.findChessboardCorners(gray, (10,7),None)
            #print ret
            # If found, add object points, image points (after refining them)
            if ret == True:
                objpoints.append(objp)

                corners2 = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
                imgpoints.append(corners2)

                # Draw and display the corners
                #img = cv2.drawChessboardCorners(img, (10,7), corners2,ret)
                #cv2.imshow('img',img)
                #cv2.waitKey(1000)

        #print objpoints
        #print imgpoints

        ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1],None,None)

        objp = np.zeros((6*7,3), np.float32)
        objp[:,:2] = np.mgrid[0:7,0:6].T.reshape(-1,2)

        axis = np.float32([[0,0,0], [0,3,0], [3,3,0], [3,0,0],
                           [0,0,-3],[0,3,-3],[3,3,-3],[3,0,-3] ])

        # find grid corners in image
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        ret, corners = cv2.findChessboardCorners(gray, (7,6), None)

        if ret == True:

            # project 3D points to image plane
            cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
            rvecs, tvecs, _ = cv2.solvePnPRansac(objp, corners, mtx, dist)
            imgpts, _ = cv2.projectPoints(axis, rvecs, tvecs, mtx, dist)

            # draw cube
            self._draw_cube(image, imgpts)

    def _draw_cube(self, img, imgpts):
        imgpts = np.int32(imgpts).reshape(-1,2)

        # draw floor
        cv2.drawContours(img, [imgpts[:4]],-1,(200,150,10),-3)

        # draw pillars
        for i,j in zip(range(4),range(4,8)):
            cv2.line(img, tuple(imgpts[i]), tuple(imgpts[j]),(255),3)

        # draw roof
        cv2.drawContours(img, [imgpts[4:]],-1,(200,150,10),3)


# imports rotation sweep data file and computes offset

# to do: propagate and calculate new errors on field values
# create general script to take in a file and perform coord & field transformation

import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit

file = "/Users/alexandraklipfel/Desktop/senior_thesis/prelim_mapper_arm_data/sweep_rotation_out_2-8.txt"
dat = np.loadtxt(file)
print(type(dat))
print(dat.shape)
length = dat.shape[0]
print(length)

# set reference values
T0 = 200
R0 = 0 #don't know
V0 = 0 #don't know

# grab columns
#position
T = dat[:, 0] #1st column
R = dat[:, 1] #2nd column
V = dat[:, 2] #3rd column
#field
BxProbe = dat[:, 3] #4th column
BxProbeErr = dat[:, 4] #5th column
ByProbe = dat[:, 5] #6th column
ByProbeErr = dat[:, 6] #7th column
BzProbe = dat[:, 7] #4th column
BzProbeErr = dat[:, 8] #5th column

rvals = (22.7 / 30000) * (T - T0)
phivals = (np.pi / 8000) * (R - R0)
zvals = (38.61 / 50000) * (V - V0)

# new data arrays in lab frame
#position
x = np.zeros(length)
y =np.zeros(length)
z = np.zeros(length)
#field
Bx = np.zeros(length)
BxErr = np.zeros(length)
By = np.zeros(length)
ByErr = np.zeros(length)
Bz = np.zeros(length)
BzErr = np.zeros(length)

for i in range(length):
    # transform to probe position to cartesian coords
    x[i] = rvals[i] * np.cos(phivals[i])
    y[i] = rvals[i] * np.sin(phivals[i])
    z[i] = zvals[i]
    # transform B field with a rotation in probe xz plane
    Bx[i] = BzProbe[i] * np.cos(phivals[i]) + BxProbe[i] * np.sin(phivals[i])
    By[i] = BzProbe[i] * np.sin(phivals[i]) - BxProbe[i] * np.cos(phivals[i])
    Bz[i] = -ByProbe[i]

# plot the x, y, z components vs. phi
plt.scatter(phivals, Bx)
plt.savefig("Bxscatter.png")
plt.show()

plt.scatter(phivals, By)
plt.savefig("Byscatter.png")
plt.show()

plt.scatter(phivals, Bz)
plt.savefig("Bzscatter.png")
plt.show()

plt.quiver(x, y, BzProbe, BxProbe)
plt.savefig("vf_probe")
plt.show()

plt.quiver(x, y, Bx, By)
plt.savefig("vf_lab")
plt.show()

# to compute offsets: fit sinusiod to phi vs. BxProbe and phi vs. PzProbe data. The vertical shift means a constant offset.

# function to fit
def curve(x, A, B, C, D):
    f = A * np.sin(B * x + C) + D
    return f

popt, pcov = curve_fit(curve, phivals, BxProbe)
print(popt)

plt.plot(phivals, curve(phivals, *popt))
plt.scatter(phivals, BxProbe, c="black")
plt.savefig("fitted_BxProbe.png")
plt.show()

popt2, pcov2 = curve_fit(curve, phivals, BzProbe)
print(popt2)

plt.plot(phivals, curve(phivals, *popt2))
plt.scatter(phivals, BzProbe, c="black")
plt.savefig("fitted_BzProbe.png")
plt.show()

# resulting offsets:
#x: -0.0255
#z: 0.252

print(length)


from selenium import webdriver
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.common.by import By

class SeleniumBrowser:
    def __init__(self, url, headless=False):
        firefox_options = Options()
        firefox_options.add_argument('--ignore-certificate-errors')
        firefox_options.add_argument('--incognito')
        if headless:
            firefox_options.add_argument('--headless')
        self.driver = webdriver.Firefox(options=firefox_options)
        self.driver.get(url)

    def get_element_by_id(self, id, timeout=10):
        wait = WebDriverWait(self.driver, 10)
        try:
            element = wait.until(ec.visibility_of_element_located((By.ID, id)))
            return element
        except:
            print(f"ERROR: Cannot find element with id {id}")
            return None

    def get_element_by_xpath(self, xpath, timeout=10):
        wait = WebDriverWait(self.driver, 10)
        try:
            element = wait.until(ec.presence_of_element_located((By.XPATH, xpath)))
            return element
        except:
            print(f"ERROR: Cannot find element with id {id}")
            return None

    def fill_input(self, element, input_string):
        element.send_keys(input_string)

    def click_button(self, button):
        button.click()

    def close(self):
        self.driver.quit()


from typing import Any, Dict, Iterable

from ...model import TimeToLiveConfiguration
from ...schema.schema import GraphObjectType
from .extractor import Extractor

class TimeToLiveConfigurationExtractor(Extractor):
    def __init__(
        self, graph_object_type: str, configurations: Iterable[Dict[str, Any]]
    ) -> None:
        self.graph_object_type = GraphObjectType(graph_object_type)
        self.configurations = configurations

    async def extract_records(self):
        for config in self.configurations:
            yield TimeToLiveConfiguration(
                **config, graph_object_type=self.graph_object_type
            )


import sys, os, traceback, subprocess, webbrowser

class GIT(object):

    @classmethod
    def works(cls):
        try:
            return bool(subprocess.check_output('git --version', shell=True))
        except:
            return False

class PIP(object):

    @classmethod
    def run(cls, command, check_output=False):
        if not cls.works():
            raise RuntimeError("Could not import pip.")

        try:
            return PIP.run_python_m(*command.split(), check_output=check_output)
        except subprocess.CalledProcessError as e:
            return e.returncode
        except:
            traceback.print_exc()
            print("Error using -m method")

    @classmethod
    def run_python_m(cls, *args, **kwargs):
        check_output = kwargs.pop('check_output', False)
        check = subprocess.check_output if check_output else subprocess.check_call
        return check([sys.executable, '-m', 'pip'] + list(args))

    @classmethod
    def run_pip_main(cls, *args, **kwargs):
        import pip

        args = list(args)
        check_output = kwargs.pop('check_output', False)

        if check_output:
            from io import StringIO

            out = StringIO()
            sys.stdout = out

            try:
                pip.main(args)
            except:
                traceback.print_exc()
            finally:
                sys.stdout = sys.__stdout__

                out.seek(0)
                pipdata = out.read()
                out.close()

                print(pipdata)
                return pipdata
        else:
            return pip.main(args)

    @classmethod
    def run_install(cls, cmd, quiet=False, check_output=False):
        return cls.run("install %s%s" % ('-q ' if quiet else '', cmd), check_output)

    @classmethod
    def run_show(cls, cmd, check_output=False):
        return cls.run("show %s" % cmd, check_output)

    @classmethod
    def works(cls):
        try:
            import pip
            return True
        except ImportError:
            return False

    @classmethod
    def get_module_version(cls, mod):
        try:
            out = cls.run_show(mod, check_output=True)

            if isinstance(out, bytes):
                out = out.decode()

            datas = out.replace('\r\n', '\n').split('\n')
            expectedversion = datas[3]

            if expectedversion.startswith('Version: '):
                return expectedversion.split()[1]
            else:
                return [x.split()[1] for x in datas if x.startswith("Version: ")][0]
        except:
            pass

def open_in_wb(text, printanyways=True, indents=4):
    import webbrowser
    # TODO: Figure out console browser stuff
    # also check GenericBrowser
    if isinstance(webbrowser.get(), webbrowser.BackgroundBrowser):
        print('%s%s' % (' ' * indents , text))
    else:
        webbrowser.open_new_tab(text)
        if printanyways:
            print('%s%s' % (' ' * indents , text))

def main():
    if not sys.version_info >= (3, 4):
        print("Python 3.4+ is required. This version is %s" % sys.version.split()[0])
        print("Attempting to locate python 3.4...")

        pycom = None

        # Maybe I should check for if the current dir is the musicbot folder, just in case

        if sys.platform.startswith('win'):
            try:
                subprocess.check_output('py -3.4 -c "exit()"', shell=True)
                pycom = 'py -3.4'
            except:

                try:
                    subprocess.check_output('python3 -c "exit()"', shell=True)
                    pycom = '/opt/python3/bin/python3'
                except:
                    pass

            if pycom:
                print("Python 3 found.  Launching bot...")
                os.system('start cmd /k %s run.py' % pycom)
                sys.exit(0)

        else:
            try:
                pycom = subprocess.check_output(['which', 'python3.4']).strip().decode()
            except:
                pass

            if pycom:
                print("\nPython 3 found.  Re-launching bot using: ")
                print("  %s run.py\n" % pycom)

                os.execlp(pycom, pycom, 'run.py')

        print("Please run the bot using python 3.4")
        input("Press enter to continue . . .")

        return

    if '--update' in sys.argv:
        if PIP.works():
            try:
                err = PIP.run_install('--upgrade -r requirements.txt')
            except subprocess.CalledProcessError as err:
                print("\nUpgrade failed, you may need to run it as admin/root")
                input("Press enter to continue . . .")
                return

        else:
            # TODO: Make this text less questionable
            print("\n"
                "Could not locate PIP. If you're sure you have it, run this:\n"
                "  your_pip_command install --upgrade -r requirements.txt"
                "\n\n")

            input("Press enter to continue . . .")
            return

    tried_requirementstxt = False
    tryagain = True

    while tryagain:
        try:
            from autologin import AutoLoginBot
            AutoLoginBot().run()
            break # check if restart? replace process?

        except (SyntaxError, ImportError) as e:
            if not tried_requirementstxt:
                tried_requirementstxt = True

                # TODO: Better output
                print(e)
                print("Attempting to install dependencies...")

                err = PIP.run_install('-r requirements.txt')

                if err:
                    print("\nYou may need to %s to install dependencies." %
                        ['use sudo', 'run as admin'][sys.platform.startswith('win')])
                    break
                else:
                    print("\nOk lets hope it worked\n")
            else:
                traceback.print_exc()
                print("Unknown ImportError, exiting.")
                break

if __name__ == '__main__':
    main()

{'pip': 'pip', 'StringIO': 'io.StringIO'}.works

{'pip': 'pip', 'StringIO': 'io.StringIO'}.run_install


from collections import OrderedDict
from math import sqrt
from nltk.corpus import brown
from nltk.tokenize import word_tokenize

training_set_percentage = 10/100
training_set_size = int(len(brown.fileids()) * training_set_percentage)
most_common = ['.', 0]

#-------------------------------------------------------#
#                 GENERAL FUNCTIONS                     #
#-------------------------------------------------------#

# get the index of element in the list to check
# usage: print(index_of([['a', 'b', 'c'], ['b', 'a', 'c'], ['c', 'a', 'b']], ['a', 'b']))
def index_of(check, element):
	length = len(element) # get the length of the element
	counter = -1 # initialize the counter to -1
	for iterator in check: # go through the list
		counter += 1 # increment the counter
		if (iterator[0:length] == element): # if the first part of the iterator, with equal length to the element's length, is the same as the element
			return counter # return the index
	return -1 # if the element is not found, return -1

#-------------------------------------------------------#
#                LEARNING FUNCTIONS                     #
#-------------------------------------------------------#

# get the tagged sentences starting from document i, up to document i + length (exclusive)
def get_tagged_sentences(i, length, skip):
	tagged_sentences = []
	for document in range(i, i + length): # go through all documents
		if (not (document in skip)):
			document_sentences = brown.tagged_sents(brown.fileids()[document], tagset="universal") # get all the sentences in the document
			tagged_sentences += [document_sentences[i] for i in range(0, len(document_sentences))] # append the sentences to the tagged sentences
	return tagged_sentences

# get the tagged words starting from document i, up to document i + length (exclusive)
def get_tagged_words(i, length, skip):
	tagged_words = []
	for document in range(i, i + length): # go through all documents
		if (not (document in skip)):
			document_words = brown.tagged_words(brown.fileids()[document], tagset="universal") # get all the sentences in the document
			tagged_words += [document_words[i] for i in range(0, len(document_words))] # append the sentences to the tagged sentences
	return tagged_words

# get the most common tag starting from document i, up to document i + length (exclusive)
def get_most_common_tag(i, length, skip):
	tagged_words = get_tagged_words(i, length, skip) # get the tagged words
	tags = [tagged_words[i][1] for i in range(0, len(tagged_words))] # extract the tags from the words
	total = len(tags) # count the total number of tags
	total_tags = {} # the dictionary of total tags

	for tag in tags: # go through each tag
		if (total_tags.get(tag, -1) == -1): # if the tag has not been encountered yet
			total_tags[tag] = 1 / total # add the tag to the dictionary
		else: # if the tag has been defined in the dictionary
			total_tags[tag] += 1 / total # add to its probability

	most_common = ['.', 0] # initialize the most common tag with a default tag and probability
	tag_keys = list(total_tags.keys()) # get the different tags
	for i in range(0, len(total_tags)): # go through all the tags
		if (total_tags.get(tag_keys[i]) > most_common[1]): # if the tag appears more than the current most common tag
			most_common[0] = tag_keys[i] # change the most common tag's name to the new one
			most_common[1] = total_tags.get(tag_keys[i]) # change the most common tag's probability to the new one

	return most_common

# calculate the start state probabilities of the training set
def initialize_start_probabilities(i, length, skip):
	tagged_sentences = get_tagged_sentences(i, length, skip) # get the tagged sentences
	tags = [tagged_sentences[i][0][1] for i in range (0, len(tagged_sentences))] # get all the tags of the first words in each sentence

	probabilities = {} # the probabilities that a tag is used to start a sentence
	total_sentences = len(tags) # calculate the total number of sentences
	for tag in tags: # visit each tag
		if (probabilities.get(tag, -1) == -1): # if the tag has not been used in the start of a sentence yet
			probabilities[tag] = 1/total_sentences # initialize it in the dictionary
		else: # if the tag has been used in the start of a sentence
			probabilities[tag] += 1/total_sentences # increment the number of times it has appeared in the start of a sentence

	return probabilities

# calculate the tag probabilities for words
def initialize_word_probabilities(i, length, skip):
	word_totals = {} # the total number of times a word appears
	words = get_tagged_words(i, length, skip) # get the tagged words
	for tagged_word in words: # visit each word
		word = tagged_word[0].lower()
		if (word_totals.get(word, -1) == -1): # if the word has not been declared in the dictionary
			word_totals[word] = 1 # initialize it in the dictionary
		else: # if the word has been declared in the dictionary
			word_totals[word] += 1 # increment the number of times it has appeared

	tags = [words[i][1] for i in range(0, len(words))] # extract the tags from the words
	total_tags = {} # the dictionary of total tags

	for tag in tags: # go through each tag
		if (total_tags.get(tag, -1) == -1): # if the tag has not been encountered yet
			total_tags[tag] = 1 # add the tag to the dictionary
		else: # if the tag has been defined in the dictionary
			total_tags[tag] += 1 # add to its probability

	word_tag_probabilities = [] # the probability that a word appears tagged with a particular tag
	for tagged_word in words: # visit each word
		word = tagged_word[0].lower() # get the actual word
		tag = tagged_word[1] # get the word's tag
		weight = 1 / total_tags[tag] # the weight is inversely proportional to the number of times that the tag appears
		#weight = 1 / word_totals.get(word) # the weight is inversely proportional to the number of times that the word appears
		tagged = [word, tag, weight] # create the tagged word

		index = index_of(word_tag_probabilities, tagged[0:2]) # look for the tagged word
		if (index == -1): # if the tagged word combination has not been declared in the dictionary
			word_tag_probabilities.append(tagged) # initialize it in the dictionary
		else: # if the tagged word combination has been declared in the dictionary
			word_tag_probabilities[index][2] += weight # increment the number of times the word has appeared with that particular tag

	return word_tag_probabilities

# calculate the tag succession probabilities - how likely it is that tag 'i' is followed by tag 'j'
def initialize_tag_probabilities(i, length, skip):
	tag_totals = OrderedDict() # the total number of times a tag combination appears
	words = get_tagged_words(i, length, skip) # get the tagged words
	for tagged_word in words: # visit each word
		tag = tagged_word[1]
		if (tag_totals.get(tag, -1) == -1): # if the word has not been declared in the dictionary
			tag_totals[tag] = 1 # initialize it in the dictionary
		else: # if the word has been declared in the dictionary
			tag_totals[tag] += 1 # increment the number of times it has appeared

	tag_couple_probabilities = OrderedDict() # the probability that tag 'i' is followed by tag 'j'
	tags = tag_totals.keys() # get the different tags
	for tag in tags: # go through each tag
		tag_couple_probabilities[tag] = OrderedDict() # create an empty dictionary for this tag
		for succession in tags: # go through each other tag
			tag_couple_probabilities[tag][succession] = 0 # initialize the probability that any tag is followed by any other tag to 0

	bigrams = [[words[i][1], words[i+1][1]] for i in range(0, len(words) -1)] # get all the tag bigrams in the documents
	for bigram in bigrams: # go through each bigram
		tag = bigram[0] # get the first part of the bigram
		succession = bigram[1] # get the second part of the bigram
		weight = 1 / tag_totals[tag] # the weight to be added is inversely proportional to the number of times that the tag appears to conserve probability between 0 and 1
		tag_couple_probabilities[tag][succession] += weight # add the weight to the probability

	return tag_couple_probabilities

#-------------------------------------------------------#
#                    USER FUNCTIONS                     #
#-------------------------------------------------------#

def get_input():
	print("input: ", end = "") # ask for the input
	user_input = input() # fetch the input
	return user_input.lower() # convert it to lowercase

#-------------------------------------------------------#
#                  VITERBI FUNCTIONS                    #
#-------------------------------------------------------#

# print the viterbi matrix
def print_matrix(matrix):
	tokens = matrix.keys() # get the different tokens
	print("\t", end="") # skip a tab where the tag column will come
	for token in tokens: # go through each token
		print(token, end = "\t\t") # print the token and leave space for the probability
	print("") # print an empty line

	tags = OrderedDict(list(matrix.items())[0][1]).keys() # get all the different tags from the first token
	for tag in tags: # go through each tag
		print(tag, end = "\t") # print the tag, and pad with one tab
		for token in tokens: # go through each token
			print("%.9f" % matrix[token][tag], end = "\t") # print the value of the cell with 9 decimal places
		print("") # print an empty line

	print("") # print an empty line

# get the maximum value in the column of the given token
def get_max(matrix, token, tags):
	max_value = ['.', 0] # initialize the maximum value to a default tag and 0
	for tag in tags: # go through each tag
		if (matrix[token][tag] > max_value[1]): # if the token-tag combination is greater than the maximum value
			max_value[0] = tag # set the maximum value to the cell value
			max_value[1] = matrix[token][tag] # set the maximum value to the cell value
	return max_value

# initialize the matrix
def initialize_matrix(tokens, tags):
	matrix = OrderedDict() # use an OrderedDict as order is important
	for token in tokens: # go through each token
		matrix[token] = OrderedDict() # create another OrderedDict for each token
		for tag in tags: # go through all the tags
			matrix[token][tag] = 0 # set the value of the cell to 0
	return matrix

# initialize the first column of the matrix, which is special because it uses the start state probabilities
def calculate_first_column(matrix, tokens, tags, start_probabilities, word_tag_probabilities):
	start_token = tokens[0] # get the first token to initialize the first column
	marked = False # a flag indicating whether the token has been matched with a possible tag or not
	for tag in tags: # go through each tag
		index = index_of(word_tag_probabilities, [start_token, tag]) # get the index of the first token and the tag
		if (index > -1): # if the token has been encountered before
			matrix[start_token][tag] = sqrt(start_probabilities.get(tag) * word_tag_probabilities[index][2]) # calculate the cell value
			marked = True # mark the token as having been matched with a possible tag
	if (not(marked)): # if no suitable tag has been found for the token - the token has never been encountered in training
		matrix[start_token][most_common[0]] = sqrt(start_probabilities.get(most_common[0]) * most_common[1]) # use the most common value to calculate the cell value
	return matrix

# calculate the columns of the matrix
def calculate_columns(matrix, tokens, tags, tag_probabilities, word_tag_probabilities):
	for i in range(1, len(tokens)): # skip the first token, as its column was already calculated
		previous_max = get_max(matrix, tokens[i - 1], tags) # get the maximum value from the previous column
		token = tokens[i] # get the current token, whose column will be updated
		marked = False # a flag indicating whether the token has been matched with a possible tag or not
		for tag in tags: # go through each tag
			index = index_of(word_tag_probabilities, [token, tag]) # get the index of the token and the tag
			if (index > -1): # if the token has been encountered before
				matrix[token][tag] = sqrt(previous_max[1] * tag_probabilities[previous_max[0]][tag] * word_tag_probabilities[index][2]) # calculate the cell value
				marked = True # mark the token as having been matched with a possible tag
		if (not(marked)): # if no suitable tag has been found for the token - the token has never been encountered in training
			matrix[token][most_common[0]] = sqrt(previous_max[1] * tag_probabilities[previous_max[0]][most_common[0]] * most_common[1]) # use the most common value to calculate the cell value
	return matrix

# get the sequence of tokens, their tags and probabilities
def get_sequence(matrix, tokens, tags):
	sequence = [] # the sequence is initally an empty list

	for i in range(len(tokens) - 1, -1, -1): # go through the tokens in reverse order
		token = tokens[i] # get the token
		tag = get_max(matrix, token, tags) # get the best tag for the token
		entry = [token] # put it in a list
		entry.append(tag[0]) # add to the entry the tag
		entry.append(tag[1]) # add to the entry its probability
		entry = [entry] # enclose the entry in a list
		entry.extend(sequence) # add it to the current sequence
		sequence = entry # the sequence is the extended sequence

	return sequence

# perform the Viterbi algorithm
def viterbi(tokens, start_probabilities, word_tag_probabilities, tag_probabilities):
	tags = tag_probabilities.keys() # get the different kind of tags
	matrix = initialize_matrix(tokens, tags) # get the empty, initialized matrix

	matrix = calculate_first_column(matrix, tokens, tags, start_probabilities, word_tag_probabilities) # calculate the matrix's first column
	matrix = calculate_columns(matrix, tokens, tags, tag_probabilities, word_tag_probabilities) # calculate the matrix's other columns

	return get_sequence(matrix, tokens, tags)

#-------------------------------------------------------#
#                PERFORMANCE FUNCTIONS                  #
#-------------------------------------------------------#

# compare the words tagged by Viterbi and the original
def compare(tagged, original):
	if (len(tagged) == len(original)): # if they are of the same length
		length = len(tagged) # get the length
		correct = 0
		for i in range(0, length): # go through each tagged instance
			if (not(tagged[i][0].lower() == original[i][0].lower())):
				print("Different words: " + tagged[i][0].lower() + " and " + original[i][0].lower())
				return 0
			if (tagged[i][1] == original[i][1]): # if the word was tagged correctly
				correct += 1 # increment the number of correctly-tagged words
		return correct/length
	else:
		print("Lengths are not the same: " + str(len(tagged)) + " and " + str(len(original)));
		return 0

def ncross(n, start):
	global training_set_size
	global most_common
	test_set_size = int(training_set_size / n) # calculate the size of the test set
	performance = [] # the list of performance results
	print("Corpus Size (Documents): " + str(training_set_size))
	print("Corpus Size (Tokens): " + str(len(get_tagged_words(0, training_set_size, []))))
	print("")
	for i in range(start, n): # perform validation n times
		test_set = [j for j in range(i * test_set_size, i * test_set_size + test_set_size)] # create a list of all the documents that will make up the test set
		training_set = [j for j in range(0, training_set_size) if not (j in test_set)] # the set of training documents
		print("Test Set: " + str(test_set)) # print the test set
		print("Training Set: " + str(training_set)) # print the training set

		most_common = get_most_common_tag(0, training_set_size, test_set) # get the most common tag, excluding  from training the documents in the test set
		start_probabilities = initialize_start_probabilities(0, training_set_size, test_set) # calculate the probabilities that each tag starts a sentence, excluding  from training the documents in the test set
		word_tag_probabilities = initialize_word_probabilities(0, training_set_size, test_set) # calculate the probabilities that a word is associated with a particular tag, excluding  from training the documents in the test set
		tag_probabilities = initialize_tag_probabilities(0, training_set_size, test_set)  # calculate the probabilities that a tag 'i' is followed by a tag 'j', excluding  from training the documents in the test set

		print("Learning Process Finished")

		original = get_tagged_sentences(test_set[0], len(test_set), []) # get the original, tagged sentences from the test set
		sentences = [[original[j][k][0].lower() for k in range(0, len(original[j]))] for j in range(0, len(original))] # extract just the words from each sentence in the test set
		tagged = [] # initialize the set of tagged sentences to an empty list
		original = get_tagged_words(test_set[0], len(test_set), []) # update the original sentences to be just a stream of words

		print("Sentences Extracted")

		for sentence in sentences: # go through each sentence
			tagged_sentence = viterbi(sentence, start_probabilities, word_tag_probabilities, tag_probabilities) # tag the sentence
			tagged.extend(tagged_sentence) # add the tagged sentence to the list of tagged sentences

		print("Sentences Tagged")

		performance.append(compare(tagged, original)) # add to the list of performance results the latest result
		print("Correct Rate: " + str(performance[i - start] * 100) + "%") # output the test set's percentage result
		print("") # add two empty lines
	return sum(performance) / n

#-------------------------------------------------------#

n = 10
start = 7
print("N-Cross Validation (N = " + str(n) + "): " + str(ncross(n, start) * 100) + "%")

#user_input = get_input()
#while(user_input != 'quit'):
#	tokens = word_tokenize(user_input)
#	print(viterbi(tokens, start_probabilities, word_tag_probabilities, tag_probabilities))
#	user_input = get_input()


#!/usr/bin/python

from ..text_processing import TextProcesser
from nltk.corpus import stopwords

import enchant
import itertools

class LanguageProcesser:
    """Full lang preprocessing with spaCy, Pyenchant, and NLTK."""
    def __init__(self, languages_to_eval=None,
                       use_spacy=True,
                       to_lemma=False,
                       to_stem=False,
                       word_min_len=0,
                       keep_stopwords=False,
                       keep_puncts=False,
                       keep_specials=False,
                       keep_emojis=False,
                       keep_currency=False,
                       keep_digits=False,
                       keep_spaces=False,
                       exclude_pipe=['morphologizer', "parser", 'attribute_ruler', "ner"],
                       keyword_sep="FAMVEER",
                       log_tqdm=False,
                       nltk_keep_punct=True):
        """Full text preprocessing.
        Args:
            languages_to_eval (list of str): List of languages to eval.
            nltk_keep_punct (bool): Keep punctutation (NLTK package).
        """

        self.lang_dict = {"portuguese":"pt_BR",
             "deutsch": "de_DE",
             "german": "de_DE",
             "english": "en_US",
             "russian": "ru_RU",
            }

        self.languages_to_eval = languages_to_eval
        self.use_spacy = use_spacy
        self.to_stem = to_stem
        self.keyword_sep = keyword_sep
        self.nltk_keep_punct = nltk_keep_punct
        self.txt_processer = TextProcesser(to_lemma=to_lemma,
                                           to_stem=to_stem,
                                           word_min_len=word_min_len,
                                           keep_stopwords=keep_stopwords,
                                           keep_puncts=keep_puncts,
                                           keep_specials=keep_specials,
                                           keep_emojis=keep_emojis,
                                           keep_currency=keep_currency,
                                           keep_digits=keep_digits,
                                           keep_spaces=keep_spaces,
                                           keyword_sep=keyword_sep,
                                           exclude_pipe=exclude_pipe,
                                           log_tqdm=log_tqdm,
                                           )

    # set nlp language
    def set_nlp_language(self, language_to_process):
        self.txt_processer.set_nlp_language(language_to_process=language_to_process)

    def get_nlp_pipeline(self):
        return self.txt_processer.get_nlp_pipeline()

    def spacy_process_texts(self, sentence):
        """Full text preprocessing.
        Args:
            texts (list of str): List of texts to be preprocessed.
            language_to_eval (str): language to eval.
        Returns:
            list of str: List of words.
        """
        txt_spacy = self.txt_processer.process_texts_by_language(sentence)

        split_txt = [txt_.split() for txt_ in txt_spacy]

        words_within_txt = list(set(itertools.chain(*split_txt)))
        total_words_within_txt = list(itertools.chain(*split_txt))

        return words_within_txt, total_words_within_txt, txt_spacy

    # A phrase can be from a word to a sentence
    def get_words_list(self, sentence):
        """Process a list of str.
        Args:
            texts (list of str): List of texts to be preprocessed.
            language_to_eval (str): language to eval.
        Returns:
            list of str: List of words.
        """
        if self.use_spacy:
            words_within_txt, total_words_within_txt, sentence_processed = self.spacy_process_texts(sentence)
        else:
            words_within_txt, total_words_within_txt, sentence_processed = self.nltk_process_texts(sentence)

        return words_within_txt, total_words_within_txt, sentence_processed

    # Evaluate ratios of correct and incorrect words in some language.
    def get_words_ratio(self, word_list, language_to_eval="portuguese"):
        """Process a list of str.
        Args:
            word_list: List of words.
            language_to_eval (str): language to eval.
        Returns:
            relative ratios: words that belong to some language.
            incorrect words: list of incorrect words
            correct words: list of correct words
        """
        lang_d = enchant.Dict(self.lang_dict[language_to_eval])
        total_ = len(word_list)
        relative_ = 0
        incorrect_words = []
        correct_words = []

        for word in word_list:
            if len(word)>0:
                if lang_d.check(word):
                    relative_ +=1
                    correct_words.append(word)
                else:
                    incorrect_words.append(word)
        try:
            return relative_/total_, incorrect_words, correct_words
        except: # error divided by 0
            return 0, incorrect_words, correct_words

    def get_language_dict(self, sentence):
        """Process a list of str.
        Args:
            sentence (str): text to eval.
        Returns:
            language dict: dict of languages and related info.
        """
        languages_ratios = {}

        if self.languages_to_eval is None:
            self.languages_to_eval = stopwords.fileids()

        if type(self.languages_to_eval)==str:
            self.languages_to_eval = [self.languages_to_eval]

        for language in self.languages_to_eval:

            self.set_nlp_language(language)

            languages_ratios[language] = {}

            word_list, total_word_list, text_processed = self.get_words_list(sentence)
            ratio_lang, incorrect_words_lang, correct_words_lang = self.get_words_ratio(word_list, language)
            ratio_lang_all, _, _ = self.get_words_ratio(total_word_list, language)

            languages_ratios[language]["ratio"] = ratio_lang
            languages_ratios[language]["incorrect_words"] = incorrect_words_lang
            languages_ratios[language]["correct_words"] = correct_words_lang
            languages_ratios[language]["text_processed"] = text_processed
            languages_ratios[language]["all_words"] = total_word_list
            languages_ratios[language]["ratio_all_words"] = ratio_lang_all

        return languages_ratios

    # return language ratios, language detected, list of incorrect words, and list of correct words
    def detect_language_and_word_list(self, text):
        lang_freq = self.get_language_dict(text)
        ratios = {}
        ratios_all = {}
        incorrect_words = {}
        correct_words = {}
        proc_txt = {}
        all_words = {}

        for lang in self.languages_to_eval:
            ratios[lang] = lang_freq[lang]["ratio"]
            incorrect_words[lang] = lang_freq[lang]["incorrect_words"]
            correct_words[lang] = lang_freq[lang]["correct_words"]
            proc_txt[lang] = lang_freq[lang]["text_processed"]
            all_words[lang] = lang_freq[lang]["all_words"]
            ratios_all[lang] = lang_freq[lang]["ratio_all_words"]

        if max(ratios.values())>0.0:
            language_detected = max(ratios, key=ratios.get)
            incorrect_words_list = list(incorrect_words[language_detected])
            correct_words_list = list(correct_words[language_detected])
            processed_text = proc_txt[language_detected]
            all_words_ = all_words[lang]
        else:
            language_detected = "not found"
            incorrect_words_list = list(set(itertools.chain(*incorrect_words.values())))
            correct_words_list = list(set(itertools.chain(*correct_words.values())))
            processed_text = ""
            all_words_ = all_words[lang]

        self.ratios = ratios
        self.ratios_all_words = ratios_all
        self.language_detected = language_detected
        self.incorrect_words = incorrect_words_list
        self.correct_words = correct_words_list
        self.processed_text = processed_text
        self.all_words = all_words_

    def get_processed_text(self):
        return self.processed_text

    # return language detected
    def get_language_ratios(self):
        return self.ratios

    # return language detected
    def get_all_language_ratios(self):
        return self.ratios_all_words

    # return ratio of detected language
    def get_detected_language_ratio(self):
        return max(self.ratios.values())

    # return language most rated
    def get_detected_language(self):
        return self.language_detected

    def get_all_words(self):
        return self.all_words

    def get_all_words_len(self):
        return len(self.all_words)

    # return list of correct words in the most rated language
    def get_correct_words(self):
        return self.correct_words

    # return length of correct words in the most rated language
    def get_correct_words_len(self):
        return len(self.correct_words)

    # return list of incorrect words in the most rated language
    def get_incorrect_words(self):
        return self.incorrect_words

    # return length  of incorrect words in the most rated language
    def get_incorrect_words_len(self):
        return len(self.incorrect_words)

    # NLTK procesing texts
    def nltk_process_texts(self, texts, language_to_process=None):
        """Process a list of str.
        Args:
            language_to_process (str): language to eval.
        """

        from nltk.tokenize import word_tokenize, sent_tokenize, wordpunct_tokenize
        from nltk.stem.snowball import SnowballStemmer

        from string import punctuation, digits

        palavras_txt = []

        if type(texts)==str:
            texts = [texts]

        stemmer = SnowballStemmer(language=language_to_process)

        for sentence in texts:

            if language_to_process is not None:
                palavras_inside = word_tokenize(sentence.lower(), language=language_to_process) if self.nltk_keep_punct else wordpunct_tokenize(sentence.lower())
                # avoid numbers (alone) punctuations and stopwords
                stopwords_avoid = set(stopwords.words(language_to_process)).union(set(punctuation)).union(set(digits))

            else:
                palavras_inside = word_tokenize(sentence.lower())
                # Getting StopWords for the language
                stopwords_avoid = set(punctuation).union(set(digits))

            palavras_sem_stopwords = [palavra for palavra in palavras_inside if palavra not in stopwords_avoid]

            # for NLTK, we will stemm instead
            if self.to_stem:
                palavras_sem_stopwords = [stemmer.stem(plural) for plural in palavras_sem_stopwords]

            palavras_txt.append(palavras_sem_stopwords)

        words_within_txt = list(set(itertools.chain(*palavras_txt)))

        return words_within_txt


from aiogram.types import Message, ReplyKeyboardRemove
from aiogram.dispatcher.filters import Text, Command
from config import bot_config
from func import morze_translate, get_words, check, get_records, set_record, get_lang
from aiogram.dispatcher import FSMContext
from main import dp
from states import Test, Lang
from keyboards import main_keyboard, lang_keyboard, inline_lang_keyboard
from re import split
from apscheduler.schedulers.asyncio import AsyncIOScheduler
import random

scheduler = AsyncIOScheduler()
scheduler.start()

@dp.message_handler(Command("start"), state=None)
async def start(message: Message):
    msg = f"Приветствую вас {message.from_user.first_name}, я умею кодировать ваш текст в код морзе также есть следующте команды"
    msg = "\n".join([msg, "/test - запускает тест состоящий из n-го количества слов которые нужно будет ввести на азбуке морзе а я всё внимательно проверю"])
    msg = "\n".join([msg, "/records - вывести таблицу рекордов"])
    msg = "\n".join([msg, "/lang - настройки языка"])
    await message.answer(msg , reply_markup=main_keyboard)

@dp.message_handler(Command("records"), state=None)
async def records(message: Message):
    msg = "Список ваших рекордов:\n"
    msg += "Nickname    | Score |  Language    |  Total Count  |"
    records = get_records()
    for i in records:
        msg += f"\n    {i[0]}    |       {i[1]}     |         {i[2]}          |            {i[3]}            |\x20"
    await message.answer(msg)

@dp.message_handler(Command("lang"), state=None)
async def lang(message: Message):
    language = get_lang(bot_config.lang)
    await message.answer(f"Сейчас используется язык: {language[0]}", reply_markup=lang_keyboard)
    await Lang.step1.set()

@dp.message_handler(Command("edit"), state=Lang.step1)
async def lang(message: Message, state:FSMContext):
    await message.answer("На какой язык хотите сменить?", reply_markup = inline_lang_keyboard)

@dp.callback_query_handler(lambda callback:True, state = Lang.step1)
async def editLang(callback, state:FSMContext):
    new_lang = split(":", callback.data)
    old_lang = get_lang(bot_config.lang)
    bot_config.lang = new_lang[1]

    await state.finish()
    await callback.message.answer(f"Язык успешно сменён с {old_lang[0]} на {new_lang[0]}!", reply_markup = main_keyboard)

@dp.message_handler(Command("return"), state=Lang.step1)
async def lang(message: Message, state:FSMContext):
    await state.finish()
    await message.answer("Вернёмся обратно", reply_markup=main_keyboard)

@dp.message_handler(Command("test"), state=None)
async def test(message: Message):
    await message.answer("Сколько слов?\nПрошу ввести любое положительно число кроме 1", reply_markup=ReplyKeyboardRemove())

    await Test.step1.set()

@dp.message_handler(Text)
async def send_answer(message: Message):
    words = message.text

    cod = morze_translate(words, bot_config.lang)

    await message.answer(cod)

@dp.message_handler(state=Test.step1)
async def test(message: Message, state: FSMContext):
    item = message.text
    msg = ""
    if item.isdigit() and int(item) not in (0, 1):
        msg = "Отлично тогда начнём"
    else:
        item = random.randint(5, 10)
        msg = "Я просил вас ввести число, но не получил его, я сам выберу кол-во слов в тесте"
        msg = "\n".join([msg, f"Количество слов в тесте: {item}"])

    await message.answer(msg)

    word = get_words(bot_config.lang)

    msg = await message.answer(word[0])
    await state.update_data(total_count=int(item))
    await state.update_data(count=int(item) - 2)
    await state.update_data(msg=msg)
    await state.update_data(lastWord=word[0])
    await state.update_data(score=0)

    await Test.next()

@dp.message_handler(state=Test.step2)
async def test(message:Message, state:FSMContext):
    data = await state.get_data()
    msg = data['msg']
    countN = data["count"]
    lastWord = data["lastWord"]
    score = data["score"]
    string = ""

    checked = check(lastWord, message.text)

    if checked:
        score += 1
        string = "\U00002714"
    else:
        string = "\U0000274E"

    word = get_words(bot_config.lang)
    countN -= 1

    scheduler.add_job(edit_msg, kwargs={"message": msg, "string":string})

    temp = await message.answer(word[0])
    await state.update_data(msg=temp)
    await state.update_data(count=countN)
    await state.update_data(lastWord=word[0])
    await state.update_data(score=score)
    if countN <= -1:
        await Test.next()

async def edit_msg(message:Message, string:str):
    msg = ' '.join([message.text, string])
    await message.edit_text(msg)

@dp.message_handler(state=Test.step3)
async def test(message:Message, state:FSMContext):
    data = await state.get_data()
    msg = data['msg']
    totalCount = data["total_count"]
    lastWord = data["lastWord"]
    score = data["score"]
    string = ""

    checked = check(lastWord, message.text)

    if checked:
        score += 1
        string = "\U00002714"
    else:
        string = "\U0000274E"

    scheduler.add_job(edit_msg, kwargs={"message": msg, "string":string})

    set_record(message.from_user.first_name, score, totalCount)

    await message.answer(f"Правильно введеных слов: {score}\nЗапись успешно добавлена в таблицу рекордов", reply_markup=main_keyboard)
    await state.finish()


import firebase_admin
from firebase_admin import credentials, firestore
cred = credentials.Certificate("serviceAccountKey.json")
firebase_admin.initialize_app(cred)
db = firestore.client()

from flask import Flask, render_template, request
from datetime import datetime, timezone, timedelta
app = Flask(__name__)

@app.route("/")
def index():
    homepage = "
袁家偉 Python+flask+Vercel網頁
"
    homepage += "MIS
"
    homepage += "顯示日期時間
"
    homepage += "傳送使用者暱稱
"
    homepage += "袁家偉介網頁
"
    homepage += "
讀取Firestore資料
"
    return homepage

@app.route("/read")
def read():
    Result = ""
    collection_ref = db.collection("111")
    docs = collection_ref.order_by("mail", direction=firestore.Query.DESCENDING).get()
    for doc in docs:
            dict = doc.to_dict()
    if cond in dict["Course"]:

        #print("{}老師開的{}課程,每週{}於{}上課".format(dict["Leacture"], dict["Course"],  dict["Time"],dict["Room"]))
        result += dict["Leacture"] + "老師開的" + dict["Course"] + "課程,每週"
        result += dict["Time"] + "於" + dict["Room"] + "上課\n"
        print(result)

        #Result += "文件內容：{}".format(doc.to_dict()) + "
"

    return Result

@app.route("/mis")
def course():
    return "
資訊管理導論
"

@app.route("/today")
def today():
    tz = timezone(timedelta(hours=+8))
    now = datetime.now(tz)
    return render_template("today.html", datetime = str(now))

@app.route("/welcome", methods=["GET", "POST"])
def welcome():
    user = request.values.get("nick")
    return render_template("welcome.html", name=user)

@app.route("/me")
def me():
    return render_template("aboutme.html")

#if __name__ == "__main__":
#    app.run()