class Interpreter(object):
    
    def __init__(self, treasurer):
        self.tr = treasurer
        self.sender = None
        self.res = None
        self.log = None

    # region Services
    @staticmethod
    def _eval_daterange(dr):
        """
        :param dr: daterance tuple of 2 elements, each representing a number
            of days in the past, with 0 being today

        :return: a new tuple, with datetime instances as values (or None)

        NOTE: the first element is set at the BEGINNING of its day,
              the second at the END
              (when they're not None)
        """

        ret = []
        for i, d in enumerate(dr):
            if d is not None:
                time = utc_now() - datetime.timedelta(days=d)

                if i == 0:
                    time.replace(hour=0, minute=0, second=0)
                elif i == 1:
                    time.replace(hour=23, minute=59, second=59)

                ret.append(time)

            else:
                ret.append(d)

        return ret

    # endregion

    # region balance

    def _cmd_balance_balance(self, cmd):
        cur = self.tr.dm.cur_mng.get_by_id(cmd['currency'])
        if cmd['currency'] is not None and cur is None:
            return self.res.void_not_found_error()

        return self.res.write(user=self.sender.clone(), cur=DataEl.clone_el(cur))

    def _cmd_balance_debts(self, cmd):
        logs = self.tr.lm.retrieve(
            _type_='PaymentLog',
            user=self.sender,
            daterange=self._eval_daterange(cmd['daterange']),
            operation='debit',
            limit=IMP.OUT['n_logs'],
        )

        return self.res.write(user=self.sender.clone(), logs=list(logs))

    def _cmd_balance_credits(self, cmd):
        logs = self.tr.lm.retrieve(
            _type_='PaymentLog',
            user=self.sender,
            daterange=self._eval_daterange(cmd['daterange']),
            operation='credit',
            limit=IMP.OUT['n_logs'],
        )

        return self.res.write(user=self.sender.clone(), logs=list(logs))

    def _cmd_balance_debtors(self, cmd):
        cur = self.tr.dm.cur_mng.get_by_id(cmd['currency'])
        if cmd['currency'] is not None and cur is None:
            return self.res.void_not_found_error()

        if cur is None:
            curs = self.tr.dm.cur_mng.currencies.values()
        else:
            curs = [cur]

        data_per_cur = {cur.id: self.tr.dm.user_mng.get_all_debtors(cur)
                        for cur in curs}

        return self.res.write(data_per_cur=data_per_cur)

    def _cmd_balance_creditors(self, cmd):
        cur = self.tr.dm.cur_mng.get_by_id(cmd['currency'])
        if cmd['currency'] is not None and cur is None:
            return self.res.void_not_found_error()

        if cur is None:
            curs = self.tr.dm.cur_mng.currencies.values()
        else:
            curs = [cur]

        data_per_cur = {cur.id: self.tr.dm.user_mng.get_all_creditors(cur)
                        for cur in curs}

        return self.res.write(data_per_cur=data_per_cur)
    # endregion
    # region log

    def _cmd_log_log(self, cmd):
        logs = self.tr.lm.retrieve(
            daterange=cmd['daterange'],
            tag=cmd['tag'],
            limit=IMP.OUT['n_logs'] if cmd['n'] is None else cmd['n']
        )

        return self.res.write(logs=list(logs))

    def _cmd_log___clear___log(self, cmd):
        daterange = self._eval_daterange(cmd['daterange'])
        n_deleted = self.tr.lm.clear_logs(daterange[1])
        return self.res.commit()

    # endregion
    # region currency

    def _cmd_currency_currency(self, cmd):
        curs = {}
        missing_cids = []
        for cur_id in cmd['currencies']:
            cur = self.tr.dm.cur_mng.get_by_id(cur_id)
            if cur is None:
                missing_cids.append(cur_id)
            else:
                curs[cur_id] = cur

        if missing_cids:
            if curs:
                self.res.not_found_warning(cur=missing_cids)
            else:
                return self.res.void_not_found_error(cur=missing_cids)

        return self.res.write(
            user=self.sender.clone(),
            currencies=curs,
        )

    def _cmd_currency_add_currency(self, cmd):
        if self.tr.dm.cur_mng.get_by_id(cmd['currencies'][0]) is None:
            self.tr.dm.cur_mng.add(
                Currency(id_=cmd['currencies'][0], rate=cmd['rate'],
                         manager=self.tr.dm.cur_mng)
            )
            return self.res.commit()

        return self.res.void()

    def _cmd_currency_set_currency(self, cmd):
        cur = self.tr.dm.cur_mng.get_by_id(cmd['currencies'][0])

        # cannot modify a Currency if is __base__ or doesn't exist
        if cur is None:
            return self.res.void_not_found_error()

        changed = False
        if cmd['rate'] > 0 and cmd['rate'] != cur.rate:
            cur.rate = cmd['rate']
            changed = True

        elif cmd['val'] is not None:
            if cmd['val'] == 'auto' and not cur.auto_convert:
                cur.auto_convert = True
                changed = True
            elif cmd['val'] == 'default':
                if cmd['global'] is True:
                    if cur != self.tr.dm.cur_mng.default:
                        self.tr.dm.cur_mng.set_default(cur)
                        changed = True
                else:
                    self.sender.set_def_cur(cur)
                    changed = True

        if not changed:
            return self.res.void()

        return self.res.commit()

    def _cmd_currency_clear_currency_rate(self, cmd):
        cur = self.tr.dm.cur_mng.get_by_id(cmd['currencies'][0])
        if cur.clear_rate() is None:
            return self.res.void_not_found_error()

        return self.res.commit()

    def _cmd_currency_clear_default_currency(self, cmd):
        if cmd['global']:
            if self.tr.dm.cur_mng.clear_default():
                return self.res.commit()
        else:
            if self.sender.set_def_cur(None):
                return self.res.commit()

        return self.res.void()

    def _cmd_currency_merge_currency(self, cmd):
        curs = [self.tr.dm.cur_mng.get_by_id(cid) for cid in cmd['currencies']]

        # all currencies must exist for call to be valid
        if None in curs:
            return self.res.void_not_found_error()

        if self.tr.dm.user_mng.merge_cur(curs) == 0:
            return self.res.void()

        return self.res.commit()

    def _cmd_currency_remove_currency(self, cmd):
        """
        unlike other cases, only committed if ALL requested curs can be removed
        """

        curs = [self.tr.dm.cur_mng.get_by_id(cid) for cid in cmd['currencies']]

        if None in curs:
            return self.res.not_found_error()

        for cur in curs:
            # remove might be None or False
            if not self.tr.dm.cur_mng.remove(cur, user_mng=self.tr.dm.user_mng):
                return self.res.invalid_operation_error()

        return self.res.commit()

    def _cmd_currency___reset___currency(self, cmd):
        """
        unlike other cases, only committed if ALL requested curs can be reset
        """

        curs = [self.tr.dm.cur_mng.get_by_id(cid) for cid in cmd['currencies']]

        if None in curs:
            return self.res.not_found_error()

        # resetting a currency is obtained by removing it from all
        # users but not from the CurrencyManager
        if self.tr.dm.user_mng.remove_cur(curs) == 0:
            return self.res.void()

        return self.res.commit()

    def _cmd_currency___ground_zero___currency(self, cmd):
        """
        unlike other cases, only committed if ALL requested curs can be destroyed
        """

        curs = [self.tr.dm.cur_mng.get_by_id(cid) for cid in cmd['currencies']]

        # keep only valid currencies
        if None in curs:
            return self.res.not_found_error()

        for cur in curs:
            # remove might be None or False
            if not self.tr.dm.cur_mng.remove(
                    cur, ground_zero=True, user_mng=self.tr.dm.user_mng):
                return self.res.invalid_operation_error()

        return self.res.commit()
    # endregion
    # region undo

    def _cmd_undo_undo(self, cmd, f='do_undo'):
        res = None
        if cmd['log_id'] is None:
            assert self.sender.lm == self.tr.lm
            res = getattr(self.sender, f)()

        else:
            log = self.tr.lm.get_by_id(cmd['log_id'])
            if log:
                res = getattr(log, f)()

        if not res:
            return self.res.invalid_operation_error()

        if res is not True:
            # res might be a dict of users/currency which had been
            # deleter and hat do be restored, in order to undo
            return self.res.not_found_warning(**res)

        return self.res.commit()

    def _cmd_undo_redo(self, cmd):
        return self._cmd_undo_undo(cmd, f='do_redo')
    # endregion
    # region groups

    def _cmd_groups_groups(self, cmd):
        """
        read requested groups(s) or all existing groups
        sets groups dict in self.result
        """
        groups = {}
        missing_gids = []
        if cmd['groups']:
            for gid in cmd['groups']:
                g = self.tr.dm.group_mng.get_by_id(gid)
                if g is None:
                    missing_gids.append(gid)
                else:
                    groups[gid] = g
        else:
            groups = None

        if missing_gids:
            self.res.not_found_warning(groups=missing_gids)

        # user_spec: if True, user asked for specific group(s) (show details)
        return self.res.write(groups=groups)

    def _cmd_groups_my_groups(self, cmd):
        return self.res.write(groups=self.sender.get_groups())

    def _cmd_groups_add_group(self, cmd):
        if self.tr.dm.group_mng.get_by_id(cmd['groups'][0]) is None:
            g = Group(id_=cmd['groups'][0], manager=self.tr.dm.group_mng)
            created_uids = []
            for uid in cmd['users']:
                if uid is None:
                    uid = self.sender.id

                user, created = self.tr.dm.user_mng.get_by_id(uid, auto_create=True)
                g.add_user(user)

                if created:
                    created_uids.append(uid)

            if created_uids:
                self.res.not_found_warning(new_users=created_uids)

            self.tr.dm.group_mng.add(g)
            return self.res.commit()

        return self.res.invalid_operation_error(group_already_exist=cmd['groups'])

    def _cmd_groups_remove_groups(self, cmd):
        groups = [self.tr.dm.group_mng.get_by_id(gid) for gid in cmd['groups']]

        # remove already un-existing groups from list, remove others
        if None in groups:
            groups = [g for g in groups if g is not None]
            self.res.not_found_warning(
                groups=[gid for gid in cmd['groups']
                        if gid not in [g.id for g in groups]]
            )

        for g in groups:
            self.tr.dm.group_mng.remove(g)

        return self.res.commit()

    def _cmd_groups_remove_empty_groups(self, cmd):
        self.tr.dm.group_mng.remove_empty_groups()
        return self.res.commit()

    def _cmd_groups_add_users(self, cmd):
        group = self.tr.dm.group_mng.get_by_id(cmd['groups'][0])
        users = []
        created_uids = []
        for uid in cmd['users']:
            user, created = self.tr.dm.user_mng.get_by_id(uid, auto_create=True)
            users.append(user)
            if created:
                created_uids.append(uid)

        if group is None:
            return self.res.void_not_found_error()

        if created_uids:
            self.res.not_found_warning(new_users=created_uids)

        for u in users:
            group.add_user(u)

        return self.res.commit()

    def _cmd_groups_remove_user(self, cmd):
        users = [self.tr.dm.user_mng.get_by_id(uid) for uid in cmd['users']]
        group = self.tr.dm.group_mng.get_by_id(cmd['groups'][0])

        if group is None:
            return self.res.void_not_found_error()

        users = [u for u in users if u is not None]
        do_not_exist = [uid for uid in cmd['users']
                        if uid not in [u.id for u in users]]

        # those which exist but do not belong to the group
        not_in_group = [u.id for u in users if not group.has_user(u)]

        if do_not_exist:
            self.res.not_found_warning(do_not_exist=do_not_exist)
        if not_in_group:
            self.res.not_found_warning(not_in_group=not_in_group)

        n_removed = 0
        for u in users:
            n_removed += group.rm_user(u)

        return self.res.commit()

    def _cmd_groups_remove_users(self, cmd):
        return self._cmd_groups_remove_user(cmd)

    # endregion
    # region users

    def _cmd_users_users(self, cmd):
        if cmd['users']:
            users = [self.tr.dm.user_mng.get_by_id(uid) for uid in cmd['users']]

            if None in users:
                users = [u for u in users if u is not None]
                self.res.not_found_warning(
                    do_not_exist=[uid for uid in cmd['users']
                                  if uid not in [u.id for u in users]],
                )
        else:
            users = None

        return self.res.write(users=users)

    # endregion
    # region greetings
    def _cmd_greetings_goodbye(self, cmd):
        self.sender.status = None
        return self.res.commit()

    # endregion
    # region payment

    def _cmd_payment_pay(self, cmd):
        # properly collect all users in the cmd
        for gid in cmd['groups_order']:
            modifier = cmd['groups'][gid]['modifier']
            amount = cmd['groups'][gid]['amount']
            g_uids = self.tr.dm.group_mng.get_by_id(gid)
            if g_uids is None:
                return self.res.void_not_found_error()
            g_uids = g_uids.uids
            for uid in g_uids:
                if uid not in cmd['users']:
                    cmd['users'][uid] = {
                        'modifier': modifier,
                        'amount': amount,
                    }
        if None in cmd['users']:
            if self.sender.id not in cmd['users']:
                cmd['users'][self.sender.id] = cmd['users'][None]
            del cmd['users'][None]

        # prepare total amount
        amount = cmd['amount']

        # select currency to use and update amount if auto-converting
        cur = self.tr.dm.cur_mng.get_cur_for_user(self.sender, cid=cmd['currency'])
        self.log.cur = cur
        if cur is None:
            return self.res.not_found_error(currency=[cmd['currency']])

        # pre-process payment
        n_sharer = 0
        for uid in cmd['users']:
            u = cmd['users'][uid]
            u['payed'] = 0
            if u['modifier'] is None:
                n_sharer += 1
            elif u['modifier'] == '*':
                n_sharer += u['amount']
            elif u['modifier'] == '+':
                u['payed'] += u['amount']
                amount -= u['amount']
                n_sharer += 1
            elif u['modifier'] == '.':
                u['payed'] = u['amount']
                amount -= u['amount']

        # share and pay
        created_uids = []
        share = float(amount) / n_sharer
        for uid in cmd['users']:
            u = cmd['users'][uid]
            if u['modifier'] is None:
                u['payed'] += share
            elif u['modifier'] == '*':
                u['payed'] += u['amount'] * share
            elif u['modifier'] == '+':
                u['payed'] += share
            elif u['modifier'] == '.':
                pass

            user, created = self.tr.dm.user_mng.get_by_id(uid, auto_create=True)
            user.add_debit(cur, u['payed'])
            self.log.add_action('debit', user, u['payed'])

            if created:
                created_uids.append(uid)

        if len(created_uids):
            self.res.not_found_warning(new_users=created_uids)

        # give credit to payer
        payer_id = cmd['payer'] if cmd['payer'] is not None else self.sender.id
        payer = self.tr.dm.user_mng.get_by_id(payer_id)
        payer.add_credit(cur, cmd['amount'])
        self.log.add_action('credit', payer, cmd['amount'])

        return self.res.commit(cur_id=[cur.id])

    # endregion

    def interpret(self, message, cmd):
        """
        :param message: instance of Message
        :param cmd: the PARSED command to execute
        :return:
        """
        # make sure sender is known to Treasurer
        sender, created = self.tr.dm.user_mng.get_by_id(
            message.sender.lower(), auto_create=True)
        if sender.status is None:
            sender.status = 'confirmed'

        # fix special cases (BEFORE creating Log instance!)
        if 'currencies' in cmd and cmd['currencies'] == 'all':
            cmd['currencies'] = self.tr.dm.cur_mng.currencies.keys()
        if 'groups' in cmd and cmd['domain'] == 'groups' and cmd['groups'] == 'all':
            cmd['groups'] = self.tr.dm.group_mng.groups.keys()

        self.sender = sender
        self.res = Result(treasurer=self.tr, sender=sender.clone(), cmd=cmd)
        self.log = self.tr.lm.log_factory(message, cmd)
        try:
            f = getattr(self, '_cmd_' + cmd['domain'] + '_' + cmd['cmd'])
            f(cmd)
        except (AttributeError, TypeError):
            # includes those which need no processing. Incomplete list:
            #   easter_egg (who_is_your_daddy)
            #   error
            #   greetings, 'hi'
            #   stats / stats_plus
            #   help
            #   default
            self.res.none()
            # if IMP.DEBUG:
            #     raise

        return self.res, self.log