###############################################################################
# PyDial: Multi-domain Statistical Spoken Dialogue System Software
###############################################################################
#
# Copyright 2015 - 2019
# Cambridge University Engineering Department Dialogue Systems Group
#
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
###############################################################################

'''
UserModel.py - goal, agenda inventor for sim user 
====================================================

Copyright CUED Dialogue Systems Group 2015 - 2017

.. seealso:: CUED Imports/Dependencies: 
    
    import :mod:`utils.DiaAct` |.|
    import :mod:`utils.dact` |.|
    import :mod:`usersimulator.UMHdcSim` |.|
    import :mod:`ontology.Ontology` |.|
    import :mod:`utils.Settings` |.|
    import :mod:`utils.ContextLogger`

************************

'''

__author__ = "cued_dialogue_systems_group"
import copy

from utils import DiaAct, dact, Settings, ContextLogger
from usersimulator import UMHdcSim
from ontology import Ontology 
logger = ContextLogger.getLogger('')


class UMAgenda(object):
    '''Agenda of :class:`DiaAct` acts corresponding to a goal.
    
    :param domain: domain tag (ie CamRestaurants)
    :type domain: str
    '''
    def __init__(self, dstring):
        self.dstring = dstring
        self.agenda_items = []  # Stack of DiaAct
        self.rules = None
        # HDC probs on how simuser will conditionally behave: values are upper limits of ranges uniform sample falls in
        self.NOT_MENTION = 1.0 # default - this value doesnt actually matter
        self.LEAVE_INFORM = 0.4
        self.CONFIRM = -1.0  # doesn't make sense actually to change inform(X=Y) to a confirm() act.
        # ONE OTHER OPTION IS TO APPEND constraint into another inform act so it is inform(X=Y,A=B)

    def init(self, goal):
        """
        Initialises the agenda by creating DiaActs corresponding to the
        constraints in the goal G. Uses the default order for the
        dialogue acts on the agenda: an inform act is created for
        each constraint. Finally a bye act is added at the bottom of the agenda.
        
        :param goal: 
               
        .. Note::
            No requests are added to the agenda.
        """
        self.agenda_items = []
        self.append_dact_to_front(DiaAct.DiaAct('inform(type="%s")' % goal.request_type))

        for const in goal.constraints:
            slot = const.slot
            value = const.val
            if slot == 'method':
                continue

            do_not_add = False
            dia_act = DiaAct.DiaAct('inform()')
            slots = Ontology.global_ontology.getSlotsToExpress(self.dstring, slot, value)

            for s in slots:
                val = goal.get_correct_const_value(s)
                if val is not None:
                    dia_act.append(slot, val)
                if len(slots) == 1 and self.contains(s, val):
                    do_not_add = True
                elif len(slots) > 1:
                    # Slot value pair might already be in other act on agenda: remove that one
                    self.filter_acts_slot(s)

            if not do_not_add:
                self.append_dact_to_front(dia_act)

        # CONDITIONALLY INIT THE AGENDA:
        if len(goal.copied_constraints): # dont need self.CONDITIONAL_BEHAVIOUR - list will be empty as appropriate
            self._conditionally_init_agenda(goal)
          
        # Finally append a bye() act to complete agenda:
        self.append_dact_to_front(DiaAct.DiaAct('bye()'))
        return

    def _conditionally_init_agenda(self, goal):
        """Use goal.copied_constraints -- to conditionally init Agenda.
        Probabilistically remove/alter the agenda for this constraint then:

        :param:
        :returns:
        """

        for dact in goal.copied_constraints:
            #print dact.slot, dact.op, dact.val   # TODO - delete debug prints
            uniform_sample = Settings.random.uniform()
            #print uniform_sample
            if uniform_sample < self.CONFIRM:
                # TODO - remove/change this - 
                # Decided this doesn't make sense - (so set self.CONFIRM=0) - info is in belief state, that is enough.
                logger.info("changing inform() act to a confirm() act")
                self.replace_acts_slot(dact.slot, replaceact="confirm") 
            elif uniform_sample < self.LEAVE_INFORM:
                pass
            else:
                logger.info("removing the inform() act and not replacing with anything.") 
                self.filter_acts_slot(dact.slot)
        return


    def contains(self, slot, value, negate=False):
        '''Check if slot, value pair is in an agenda dialogue act

        :param slot:
        :param value:
        :param negate: None
        :type negate: bool
        :returns: (bool) slot, value is in an agenda dact?
        '''
        for dact in self.agenda_items:
            if dact.contains(slot, value, negate):
                return True
        return False

    def get_agenda_with_act(self, act):
        '''agenda items with this act
        :param act: dialogue act 
        :type act: str
        :return: (list) agenda items
        '''
        items = []
        for ait in self.agenda_items:
            if ait.act == act:
                items.append(ait)
        return items

    def get_agenda_with_act_slot(self, act, slot):
        '''
        :param act: dialogue act
        :type act: str
        :param slot: slot name
        :type slot: str
        :return: (list) of agenda items
        '''
        items = []
        for ait in self.agenda_items:
            if ait.act == act:
                for item in ait.items:
                    if item.slot == slot:
                        items.append(ait)
                        break
        return items

    def replace_acts_slot(self, slot, replaceact="confirm"):
        """
        """
        for ait in self.agenda_items:
            if len(ait.items) == 1:
                if ait.items[0].slot == slot:
                    print(ait.act)
                    print(ait.items)
                    ait.act = replaceact
                    print(ait)
                    input('going to change this to confirm')


    def filter_acts_slot(self, slot):
        '''
        Any acts related to the given slot are removed from the agenda.
        :param slot: slot name
        :type slot: str
        :return: None
        '''
        deleted = []
        for ait in self.agenda_items:
            if ait.act in ['inform', 'confirm', 'affirm'] and len(ait.items) > 0:
                if len(ait.items) > 1:
                    pass
#                     logger.error('Assumes all agenda items have only one semantic items: {}'.format(ait))
                for only_item in ait.items:
                    if only_item.slot == slot:
                        deleted.append(ait)

        for ait in deleted:
            self.agenda_items.remove(ait)

    def filter_constraints(self, dap):
        '''Filters out acts on the agenda that convey the constraints mentioned in the given dialogue act. 
        Calls :meth:`filter_acts_slot` to do so. 

        :param dap:
        :returns: None
        '''
        if dap.act in ['inform', 'confirm'] or \
            (dap.act in ['affirm', 'negate'] and len(dap.items) > 0):
            for item in dap.items:
                self.filter_acts_slot(item.slot)

    def size(self):
        '''Utility func to get size of agenda_items list
        
        :returns: (int) length
        '''
        return len(self.agenda_items)

    def clear(self):
        '''
        Erases all acts on the agenda (empties list)
        
        :return: None
        '''
        self.agenda_items = []

    def append_dact_to_front(self, dact):
        '''Adds the given dialogue act to the front of the agenda

        :param (instance): dact
        :returns: None
        '''
        self.agenda_items = [dact] + self.agenda_items

    def push(self, dact):
        # if dact.act == 'null':
        #     logger.warning('null() in agenda')
        # if dact.act == 'bye' and len(self.agenda_items) > 0:
        #     logger.warning('bye() in agenda')
        self.agenda_items.append(dact)

    def pop(self):
        return self.agenda_items.pop()

    def remove(self, dact):
        self.agenda_items.remove(dact)


class UMGoal(object):
    '''Defines a goal within a domain

    :param patience: user patience 
    :type patience: int
    '''
    def __init__(self, patience, domainString):
        self.constraints = []
        self.copied_constraints = []  # goals copied over from other domains. Used to conditionally create agenda.
        self.requests = {}
        self.prev_slot_values = {}
        self.patience = patience
        self.request_type = Ontology.global_ontology.get_type(domainString)  

        self.system_has_informed_name_none = False
        self.no_relaxed_constraints_after_name_none = False

    def clear(self, patience, domainString):
        self.constraints = []
        self.requests = {}
        self.prev_slot_values = {}
        self.patience = patience
        self.request_type = Ontology.global_ontology.get_type(domainString)

        self.system_has_informed_name_none = False
        self.no_relaxed_constraints_after_name_none = False

    '''
    Methods for constraints.
    '''
    def set_copied_constraints(self, all_conditional_constraints):
        """Creates a list of dacts, where the constraints have come from earlier domains in the dialog.

        :param all_conditional_constraints: of all previous constraints (over all domains in dialog)
        :type all_conditional_constraints: dict
        :returns: None
        """
        for dact in self.constraints:
            slot,op,value = dact.slot,dact.op,dact.val
            if slot in list(all_conditional_constraints.keys()):
                if len(all_conditional_constraints[slot]):
                    if value in all_conditional_constraints[slot]:
                        self.copied_constraints.append(dact)
        return

    def add_const(self, slot, value, negate=False):
        """
        """
        if not negate:
            op = '='
        else:
            op = '!='
        item = dact.DactItem(slot, op, value)
        self.constraints.append(item)

    def replace_const(self, slot, value, negate=False):
        self.remove_slot_const(slot, negate)
        self.add_const(slot, value, negate)

    def contains_slot_const(self, slot):
        for item in self.constraints:
            # an error introduced here by dact.py __eq__ method: 
            #if item.slot == slot:
            if str(item.slot) == slot:
                return True
        return False

    def remove_slot_const(self, slot, negate=None):
        copy_consts = copy.deepcopy(self.constraints)
        
        if negate is not None:
            if not negate:
                op = '='
            else:
                op = '!='
        
            for item in copy_consts:
                if item.slot == slot:
                    if item.op == op:
                        self.constraints.remove(item)
        else:
            for item in copy_consts:
                if item.slot == slot:
                    self.constraints.remove(item)

    def get_correct_const_value(self, slot, negate=False):
        '''
        :return: (list of) value of the given slot in user goal constraint.

        '''
        values = []
        for item in self.constraints:
            if item.slot == slot:
                if item.op == '!=' and negate or item.op == '=' and not negate:
                    values.append(item.val)

        if len(values) == 1:
            return values[0]
        elif len(values) == 0:
            return None
        logger.error('Multiple values are found for %s in constraint: %s' % (slot, str(values)))
        return values

    def get_correct_const_value_list(self, slot, negate=False):
        '''
        :return: (list of) value of the given slot in user goal constraint.
        '''
        values = []
        for item in self.constraints:
            if item.slot == slot:
                if (item.op == '!=' and negate) or (item.op == '=' and not negate):
                    values.append(item.val)
        return values

    def add_prev_used(self, slot, value):
        '''
        Adds the given slot-value pair to the record of previously used slot-value pairs.
        '''
        if slot not in self.prev_slot_values:
            self.prev_slot_values[slot] = set()
        self.prev_slot_values[slot].add(value)

    def add_name_constraint(self, value, negate=False):
        if value in [None, 'none']:
            return

        wrong_venues = self.get_correct_const_value_list('name', negate=True)
        correct_venue = self.get_correct_const_value('name', negate=False)

        if not negate:
            # Adding name=value but there is name!=value.
            if value in wrong_venues:
                logger.error('Failed to add name=%s: already got constraint name!=%s.' %
                             (value, value))
                return
            
            # Can have only one name= constraint.
            if correct_venue is not None:
                #logger.debug('Failed to add name=%s: already got constraint name=%s.' %
                #             (value, correct_venue))
                self.replace_const('name', value) # ic340: added to override previously informed venues, to avoid
                                                    # simuser getting obsessed with a wrong venue
                return

            # Adding name=value, then remove all name!=other.
            self.replace_const('name', value)
            return

        # if not negate and not self.is_suitable_venue(value):
        #     logger.debug('Failed to add name=%s: %s is not a suitable venue for goals.' % (value, value))
        #     return

        if negate:
            # Adding name!=value but there is name=value.
            if correct_venue == value:
                logger.error('Failed to add name!=%s: already got constraint name=%s.' % (value, value))
                return

            # Adding name!=value, but there is name=other. No need to add.
            if correct_venue is not None:
                return

            self.add_const('name', value, negate=True)
            return

    # def is_correct(self, item):
    #     '''
    #     Check if the given items are correct in goal constraints.
    #     :param item: set[(slot, op, value), ...]
    #     :return:
    #     '''
    #     if type(item) is not set:
    #         item = set([item])
    #     for it in item:
    #         for const in self.constraints:
    #             if const.match(it):

    def is_satisfy_all_consts(self, item):
        '''
        Check if all the given items set[(slot, op, value),..] satisfies all goal constraints (conjunction of constraints).
        '''
        if type(item) is not set:
            item = set([item])
        for it in item:
            for const in self.constraints:
                if not const.match(it):
                    return False
        return True

    def is_completed(self):
        # If the user has not specified any constraints, return True
        if not self.constraints:
            return True
        if (self.system_has_informed_name_none and not self.no_relaxed_constraints_after_name_none) or\
                (self.is_venue_recommended() and self.are_all_requests_filled()):
            return True
        return False

    '''
    Methods for requests.
    '''
    def reset_requests(self):
        for info in self.requests:
            self.requests[info] = None

    def fill_requests(self, dact_items):
        for item in dact_items:
            if item.op != '!=':
                self.requests[item.slot] = item.val

    def are_all_requests_filled(self):
        '''
        :return: True if all request slots have a non-empty value.
        '''
        return None not in list(self.requests.values())

    def is_venue_recommended(self):
        '''
        Returns True if the request slot 'name' is not empty.
        :return:
        '''
        if 'name' in self.requests and self.requests['name'] is not None:
            return True
        return False

    def get_unsatisfied_requests(self):
        results = []
        for info in self.requests:
            if self.requests[info] is None:
                results.append(info)
        return results

    def __str__(self):
        result = 'constraints: ' + str(self.constraints) + '\n'
        result += 'requests:    ' + str(self.requests) + '\n'
        if self.patience is not None:
            result += 'patience:    ' + str(self.patience) + '\n'
        return result


class GoalGenerator(object):
    '''
    Master class for defining a goal generator to generate domain specific goals for the simulated user.
    
    This class also defines the interface for new goal generators.
    
    To implement domain-specific behaviour, derive from this class and override init_goals.
    '''
    def __init__(self, dstring):
        '''
        The init method of the goal generator reading the config and setting the default parameter values.
        
        :param dstring: domain name tag
        :type dstring: str
        '''
        self.dstring = dstring
        configlist = []
        
        self.CONDITIONAL_BEHAVIOUR = False
        if Settings.config.has_option("conditional","conditionalsimuser"):
            self.CONDITIONAL_BEHAVIOUR = Settings.config.getboolean("conditional","conditionalsimuser")
        
        self.MAX_VENUES_PER_GOAL = 4
        if Settings.config.has_option('goalgenerator','maxvenuespergoal'):
            configlist.append('maxvenuespergoal')
            self.MAX_VENUES_PER_GOAL = Settings.config.getint('goalgenerator','maxvenuespergoal')
        self.MIN_VENUES_PER_GOAL = 1

        self.MAX_CONSTRAINTS = 3
        if Settings.config.has_option('goalgenerator','maxconstraints'):
            configlist.append('maxconstraints')
            self.MAX_CONSTRAINTS = Settings.config.getint('goalgenerator','maxconstraints')

        self.MAX_REQUESTS = 3
        if Settings.config.has_option('goalgenerator','maxrequests'):
            configlist.append('maxrequests')
            self.MAX_REQUESTS = Settings.config.getint('goalgenerator','maxrequests')
        self.MIN_REQUESTS = 1
        if Settings.config.has_option('goalgenerator','minrequests'):
            configlist.append('minrequests')
            self.MIN_REQUESTS = Settings.config.getint('goalgenerator','minrequests')
        assert(self.MIN_REQUESTS > 0)
        if Settings.config.has_option('goalgenerator','minvenuespergoal'):
            self.MIN_VENUES_PER_GOAL = int(Settings.config.get('goalgenerator','minvenuespergoal'))
#        self.PERCENTAGE_OF_ZERO_SOLUTION_TASKS = 50
#        if Settings.config.has_option('GOALGENERATOR','PERCZEROSOLUTION'):
#            self.PERCENTAGE_OF_ZERO_SOLUTION_TASKS = int(Settings.config.get('GOALGENERATOR','PERCZEROSOLUTION'))
#        self.NO_REQUESTS_WITH_VALUE_NONE = False
#        if Settings.config.has_option('GOALGENERATOR','NOREQUESTSWITHVALUENONE'):
#            self.NO_REQUESTS_WITH_VALUE_NONE

        if self.MIN_VENUES_PER_GOAL > self.MAX_VENUES_PER_GOAL:
            logger.error('Invalid config: MIN_VENUES_PER_GOAL > MAX_VENUES_PER_GOAL')
        
        self.conditional_constraints = []
        self.conditional_constraints_slots = []
        
       
    def init_goal(self, otherDomainsConstraints, um_patience):
        '''
        Initialises the goal g with random constraints and requests

        :param otherDomainsConstraints: of constraints from other domains in this dialog which have already had goals generated.
        :type otherDomainsConstraints: list
        :param um_patience: the patiance value for this goal
        :type um_patience: int
        :returns: (instance) of :class:`UMGoal`
        '''
        
        # clean/parse the domainConstraints list - contains other domains already generated goals:
        self._set_other_domains_constraints(otherDomainsConstraints)

        # Set initial goal status vars
        goal = UMGoal(um_patience, domainString=self.dstring)
        logger.debug(str(goal))
        num_attempts_to_resample = 2000
        while True:
            num_attempts_to_resample -= 1
            # Randomly sample a goal (ie constraints):
            self._init_consts_requests(goal, um_patience)
            # Check that there are venues that satisfy the constraints:
            venues = Ontology.global_ontology.entity_by_features(self.dstring, constraints=goal.constraints)
            #logger.info('num_venues: %d' % len(venues))
            if self.MIN_VENUES_PER_GOAL < len(venues) < self.MAX_VENUES_PER_GOAL:
                break
            if num_attempts_to_resample == 0:
                logger.warning('Maximum number of goal resampling attempts reached.')
                
        if self.CONDITIONAL_BEHAVIOUR:
            # now check self.generator.conditional_constraints list against self.goal -assume any values that are the same
            # are because they are conditionally copied over from earlier domains goal. - set self.goal.copied_constraints
            goal.set_copied_constraints(all_conditional_constraints=self.conditional_constraints)

        # logger.warning('SetSuitableVenues is deprecated.')
        return goal
        
    def _set_other_domains_constraints(self, otherDomainsConstraints):
        """Simplest approach for now: just look for slots with same name 
        """
        # Get a list of slots that are valid for this task.
        valid_const_slots = Ontology.global_ontology.getValidSlotsForTask(self.dstring) 
        self.conditional_constraints = {slot: [] for slot in valid_const_slots}

        if not self.CONDITIONAL_BEHAVIOUR:
            self.conditional_constraint_slots = []
            return

        for const in otherDomainsConstraints:
            if const.slot in valid_const_slots and const.val != "dontcare": #TODO think dontcare should be dealt with diff 
                # issue is that first domain may be dontcare - but 2nd should be generated conditioned on first.
                if const.op == "!=":
                    continue
                
                #TODO delete: if const.val in Ontology.global_ontology.ontology['informable'][const.slot]: #make sure value is valid for slot
                if Ontology.global_ontology.is_value_in_slot(self.dstring, value=const.val, slot=const.slot):
                    self.conditional_constraints[const.slot] += [const.val]  
        self.conditional_constraint_slots = [s for s,v in self.conditional_constraints.items() if len(v)]
        return


    def _init_consts_requests(self, goal, um_patience):
        '''
        Randomly initialises constraints and requests of the given goal.
        '''
        goal.clear(um_patience, domainString=self.dstring)

        # Randomly pick a task: bar, hotel, or restaurant (in case of TownInfo)
        goal.request_type = Ontology.global_ontology.getRandomValueForSlot(self.dstring, slot='type')

        # Get a list of slots that are valid for this task.
        valid_const_slots = Ontology.global_ontology.getValidSlotsForTask(self.dstring)
        
        # First randomly sample some slots from those that are valid: 
        sampling_probs = Ontology.global_ontology.get_sample_prob(self.dstring, 
                                                                  candidate=valid_const_slots,
                                                                  conditional_values=self.conditional_constraint_slots)
        random_slots = list(Settings.random.choice(valid_const_slots,
                                size=min(self.MAX_CONSTRAINTS, len(valid_const_slots)),
                                replace=False,
                                p=sampling_probs))


        # Now randomly fill in some constraints for the sampled slots:
        for slot in random_slots:
            goal.add_const(slot, Ontology.global_ontology.getRandomValueForSlot(self.dstring, slot=slot, 
                                                            nodontcare=False,
                                                            conditional_values=self.conditional_constraints[slot]))
        
        # Add requests. Assume that the user always wants to know the name of the place
        goal.requests['name'] = None
        
        if self.MIN_REQUESTS == self.MAX_REQUESTS:
            n = self.MIN_REQUESTS -1  # since 'name' is already included
        else:
            n = Settings.random.randint(low=self.MIN_REQUESTS-1,high=self.MAX_REQUESTS)
        valid_req_slots = Ontology.global_ontology.getValidRequestSlotsForTask(self.dstring)
        if n > 0 and len(valid_req_slots) >= n:   # ie more requests than just 'name'
            choosen = Settings.random.choice(valid_req_slots, n,replace=False)
            for reqslot in choosen:
                goal.requests[reqslot] = None


class UM(object):
    '''Simulated user
    
    :param None:
    '''
    def __init__(self, domainString):
        self.max_patience = 5
        self.sample_patience = None
        if Settings.config.has_option('goalgenerator', 'patience'): # only here for backwards compatibility; should actually be in um
            self.max_patience = Settings.config.get('goalgenerator', 'patience')
        if Settings.config.has_option('usermodel', 'patience'):
            self.max_patience = Settings.config.get('usermodel', 'patience')
        if isinstance(self.max_patience,str) and len(self.max_patience.split(',')) > 1:
            self.sample_patience = [int(x.strip()) for x in self.max_patience.split(',')]
            if len(self.sample_patience) != 2 or self.sample_patience[0] > self.sample_patience[1]:
                logger.error('Patience should be either a single int or a range between 2 ints.')
        else:
            self.max_patience = int(self.max_patience)
        
        self.patience_old_style = False
        if Settings.config.has_option('usermodel', 'oldstylepatience'):
            self.patience_old_style = Settings.config.getboolean('usermodel', 'oldstylepatience')

        self.old_style_parameter_sampling = True
        if Settings.config.has_option('usermodel', 'oldstylesampling'):
            self.old_style_parameter_sampling = Settings.config.getboolean('usermodel', 'oldstylesampling')
            
        self.sampleParameters = False
        if Settings.config.has_option('usermodel', 'sampledialogueprobs'):
            self.sampleParameters = Settings.config.getboolean('usermodel', 'sampledialogueprobs')
            
        self.generator = self._load_domain_goal_generator(domainString)
        self.goal = None
        self.prev_goal = None
        self.hdcSim = self._load_domain_simulator(domainString)
        self.lastUserAct = None
        self.lastSysAct = None
        
    def init(self, otherDomainsConstraints):
        '''
        Initialises the simulated user.
        1. Initialises the goal G using the goal generator.
        2. Populates the agenda A using the goal G.
        Resets all UM status to their defaults.

        :param otherDomainsConstraints: of domain goals/constraints (slot=val) from other domains in dialog for which goal has already been generated.
        :type otherDomainsConstraints: list
        :returns None:
        '''
        if self.sampleParameters:
            self._sampleParameters()

        if self.sample_patience:
            self.max_patience = Settings.random.randint(self.sample_patience[0], self.sample_patience[1])
        
        self.goal = self.generator.init_goal(otherDomainsConstraints, self.max_patience)
        logger.debug(str(self.goal))

        self.lastUserAct = None
        self.lastSysAct = None
        self.hdcSim.init(self.goal, self.max_patience)  #uses infor in self.goal to do conditional generation of agenda as well.

    def receive(self, sys_act):
        '''
        This method is called to transmit the machine dialogue act to the user.
        It updates the goal and the agenda.
        :param sys_act: System action.
        :return:
        '''
        # Update previous goal.
        self.prev_goal = copy.deepcopy(self.goal)

        # Update the user patience level.
        if self.lastUserAct is not None and self.lastUserAct.act == 'repeat' and\
                        self.lastSysAct is not None and self.lastSysAct.act == 'repeat' and\
                        sys_act.act == 'repeat':
            # Endless cycle of repeating repeats: reduce patience to zero.
            logger.info("Endless cycle of repeating repeats. Setting patience to zero.")
            self.goal.patience = 0

        elif sys_act.act == 'badact' or sys_act.act == 'null' or\
                (self.lastSysAct is not None and self.lastUserAct is not None and self.lastUserAct.act != 'repeat' and self.lastSysAct == sys_act):
            # Same action as last turn. Patience decreased.
            self.goal.patience -= 1
        elif self.patience_old_style:
            # not same action as last time so patience is restored
            self.goal.patience = self.max_patience

        if self.goal.patience < 1:
            logger.debug(str(self.goal))
            logger.debug('All patience gone. Clearing agenda.')
            self.hdcSim.agenda.clear()
            # Pushing bye act onto agenda.
            self.hdcSim.agenda.push(DiaAct.DiaAct('bye()'))
            return

        # Update last system action.
        self.lastSysAct = sys_act

        # Process the sys_act
        self.hdcSim.receive(sys_act, self.goal)

        # logger.warning('should update goal information')

    def respond(self):
        '''
        This method is called after receive() to get the user dialogue act response.
        The method first increments the turn counter, then pops n items off the agenda to form
        the response dialogue act. The agenda and goal are updated accordingly.

        :param None:
        :returns: (instance) of :class:`DiaAct` 
        '''
        user_output = self.hdcSim.respond(self.goal)

        if user_output.act == 'request' and len(user_output.items) > 0:
            # If there is a goal constraint on near, convert act type to confirm
            # TODO-  do we need to add more domain dependent type rules here for other domains beyond CamRestaurants?
            # this whole section mainly seems to stem from having "area" and "near" in ontology... 
            if user_output.contains_slot('near') and self.goal.contains_slot_const('near'):
                for const in self.goal.constraints:
                    if const.slot == 'near':
                        near_const = const.val
                        near_op = const.op
                        break
                # original: bug. constraints is a list --- near_const = self.goal.constraints['near']
                if near_const != 'dontcare':
                    if near_op == "=":   # should be true for 'dontcare' value
                        #TODO - delete-WRONG-user_output.dact['act'] = 'confirm'
                        #TODO-delete-user_output.dact['slots'][0].val = near_const
                        user_output.act = 'confirm'
                        user_output.items[0].val = near_const
                    
        self.lastUserAct = user_output
        #self.goal.update_au(user_output)
        return user_output
    
    def _sampleParameters(self):
        if not self.old_style_parameter_sampling:
            self.max_patience = Settings.random.randint(2,10)
            
    def _load_domain_goal_generator(self, domainString):
        '''
        Loads and instantiates the respective goal generator object as configured in config file. The new object is returned.
        
        Default is GoalGenerator.
        
        .. Note:
            To dynamically load a class, the __init__() must take two arguments: domainString (str), conditional_behaviour (bool)
        
        :param domainString: the domain the goal generator will be loaded for.
        :type domainString: str
        :returns: goal generator object
        '''
        
        generatorClass = None
        
        if Settings.config.has_option('usermodel_' + domainString, 'goalgenerator'):
            generatorClass = Settings.config.get('usermodel_' + domainString, 'goalgenerator')
        
        if generatorClass is None:
            return GoalGenerator(domainString)
        else:
            try:
                # try to view the config string as a complete module path to the class to be instantiated
                components = generatorClass.split('.')
                packageString = '.'.join(components[:-1]) 
                classString = components[-1]
                mod = __import__(packageString, fromlist=[classString])
                klass = getattr(mod, classString)
                return klass(domainString)
            except ImportError:
                logger.error('Unknown domain ontology class "{}" for domain "{}"'.format(generatorClass, domainString))
                
    def _load_domain_simulator(self, domainString):
        '''
        Loads and instantiates the respective simulator object as configured in config file. The new object is returned.
        
        Default is UMHdcSim.
        
        .. Note:
            To dynamically load a class, the __init__() must take one argument: domainString (str)
        
        :param domainString: the domain the simulator will be loaded for.
        :type domainString: str
        :returns: simulator object
        '''
        
        simulatorClass = None
        
        if Settings.config.has_option('usermodel_' + domainString, 'usersimulator'):
            simulatorClass = Settings.config.get('usermodel_' + domainString, 'usersimulator')
        
        if simulatorClass is None:
            return UMHdcSim.UMHdcSim(domainString)
        else:
            try:
                # try to view the config string as a complete module path to the class to be instantiated
                components = simulatorClass.split('.')
                packageString = '.'.join(components[:-1]) 
                classString = components[-1]
                mod = __import__(packageString, fromlist=[classString])
                klass = getattr(mod, classString)
                return klass(domainString)
            except ImportError:
                logger.error('Unknown domain ontology class "{}" for domain "{}"'.format(simulatorClass, domainString))
                
                

#END OF FILE