"""
Internal state of the device
"""

import logging
from utils import pp
import loxi.of13 as ofp


class GroupEntry(object):
    def __init__(self, group_desc, group_stats):
        assert isinstance(group_desc, ofp.group_desc_stats_entry)
        assert isinstance(group_stats, ofp.group_stats_entry)
        self.group_desc = group_desc
        self.group_stats = group_stats


def flow_stats_entry_from_flow_mod_message(fmm):
    assert isinstance(fmm, ofp.message.flow_mod)

    # extract a flow stats entry from a flow_mod message
    kw = fmm.__dict__

    # drop these from the object
    for k in ('xid', 'cookie_mask', 'out_port', 'buffer_id', 'out_group'):
        del kw[k]
    flow = ofp.flow_stats_entry(
        duration_sec=0,
        duration_nsec=0,
        packet_count=0,
        byte_count=0,
        **kw
    )
    return flow

def group_entry_from_group_mod_message(gmm):
    assert isinstance(gmm, ofp.message.group_mod)

    kw = gmm.__dict__

    # drop these attributes from the object:
    for k in ('xid',):
        del kw[k]

    group_desc = ofp.group_desc_stats_entry(
        group_type=gmm.group_type,
        group_id=gmm.group_id,
        buckets=gmm.buckets
    )

    group_stats = ofp.group_stats_entry(
        group_id=gmm.group_id
    )

    return GroupEntry(group_desc, group_stats)


class ObjectStore(object):

    def __init__(self):
        self.ports = []  # list of ofp.common.port_desc
        self.flows = []  # list of ofp.common.flow_stats_entry
        self.groups = {} # dict of (ofp.group_desc_stats_entry, ofp.group_stats_entry) tuples,
                         # keyed by the group_id field
        self.agent = None

    def set_agent(self, agent):
        """Set agent reference"""
        self.agent = agent

    def signal_flow_mod_error(self, code, flow_mod):
        """Forward error to agent"""
        if self.agent is not None:
            agent.signal_flow_mod_error(code, flow_mod)

    def signal_flow_removal(self, flow):
        """Forward flow removal notification to agent"""
        if self.agent is not None:
            agent.signal_flow_removal(flow)

    def signal_group_mod_error(self, code, group_mod):
        if self.agent is not None:
            agent.signal_group_mod_error(code, group_mod)

    ## <=========================== PORT HANDLERS ==================================>

    def port_list(self):
        return self.ports

    def port_add(self, port):
        self.ports.append(port)

    def port_stats(self):
        logging.warn("port_stats() not yet implemented")
        return []

    ## <=========================== FLOW HANDLERS ==================================>

    def flow_add(self, flow_add):
        assert isinstance(flow_add, ofp.message.flow_add)
        assert flow_add.cookie_mask == 0

        check_overlap = flow_add.flags & ofp.OFPFF_CHECK_OVERLAP
        if check_overlap:
            if self._flow_find_overlapping_flows(self, flow_add, return_on_first_hit=True):
                self.signal_flow_mod_error(ofp.OFPFMFC_OVERLAP, flow_add)
            else:
                # free to add as new flow
                flow = flow_stats_entry_from_flow_mod_message(flow_add)
                self.flows.append(flow)

        else:
            flow = flow_stats_entry_from_flow_mod_message(flow_add)
            idx = self._flow_find(flow)
            if idx >= 0:
                old_flow = self.flows[idx]
                assert isinstance(old_flow, ofp.common.flow_stats_entry)
                if not (flow_add.flags & ofp.OFPFF_RESET_COUNTS):
                    flow.byte_count = old_flow.byte_count
                    flow.packet_count = old_flow.packet_count
                self.flows[idx] = flow

            else:
                self.flows.append(flow)

    def flow_delete_strict(self, flow_delete_strict):
        assert isinstance(flow_delete_strict, ofp.message.flow_delete_strict)
        flow = flow_stats_entry_from_flow_mod_message(flow_delete_strict)
        idx = self._flow_find(flow)
        if (idx >= 0):
            del self.flows[idx]
            logging.info("flow removed:\n%s" % pp(flow))
        else:
            logging.error('Requesting strict delete of:\n%s\nwhen flow table is:\n\n' % pp(flow))
            for f in self.flows:
                print pp(f)

    def flow_delete(self, flow_delete):
        assert isinstance(flow_delete, ofp.message.flow_delete)

        # build a list of what to keep vs what to delete
        to_keep = []
        to_delete = []
        for f in self.flows:
            list_to_append = to_delete if self._flow_matches_spec(f, flow_delete) else to_keep
            list_to_append.append(f)

        # replace flow table with keepers
        self.flows = to_keep

        # send notifications for discarded flows as required by OF spec
        self._announce_flows_deleted(to_delete)

    def flow_modify_strict(self, flow_obj):
        raise Exception("flow_modify_strict(): Not implemented yet")

    def flow_modify(self, flow_obj):
        raise Exception("flow_modify(): Not implemented yet")

    def flow_list(self):
        return self.flows

    def _flow_find_overlapping_flows(self, flow_mod, return_on_first_hit=False):
        """
        Return list of overlapping flow(s)
        Two flows overlap if a packet may match both and if they have the same priority.
        """

    def _flow_find(self, flow):
        for i, f in enumerate(self.flows):
            if self._flows_match(f, flow):
                return i
        return -1

    def _flows_match(self, f1, f2):
        keys_matter = ('table_id', 'priority', 'flags', 'cookie', 'match')
        for key in keys_matter:
            if getattr(f1, key) != getattr(f2, key):
                return False
        return True

    def _flow_matches_spec(self, flow, flow_mod):
        """
        Return True if a given flow (flow_stats_entry) is "covered" by the wildcarded flow
        mod or delete spec (as defined in the flow_mod or flow_delete message); otherwise
        return False.
        """
        #import pdb
        #pdb.set_trace()

        assert isinstance(flow, ofp.common.flow_stats_entry)
        assert isinstance(flow_mod, (ofp.message.flow_delete, ofp.message.flow_mod))

        # Check if flow.cookie is a match for flow_mod cookie/cookie_mask
        if (flow.cookie & flow_mod.cookie_mask) != (flow_mod.cookie & flow_mod.cookie_mask):
            return False

        # Check if flow.table_id is covered by flow_mod.table_id
        if flow_mod.table_id != ofp.OFPTT_ALL and flow.table_id != flow_mod.table_id:
            return False

        # Check out_port
        if flow_mod.out_port != ofp.OFPP_ANY and not self._flow_has_out_port(flow, flow_mod.out_port):
            return False

        # Check out_group
        if flow_mod.out_group != ofp.OFPG_ANY and not self._flow_has_out_group(flow, flow_mod.out_group):
            return False

        # Priority is ignored

        # Check match condition
        # If the flow_mod match field is empty, that is a special case and indicate the flow entry matches
        match = flow_mod.match
        assert isinstance(match, ofp.common.match_v3)
        if not match.oxm_list:
            return True # If we got this far and the match is empty in the flow spec, than the flow matches
        else:
            raise NotImplementedError("_flow_matches_spec(): No flow match analysys yet")

    def _flow_has_out_port(self, flow, out_port):
        """Return True if flow has a output command with the given out_port"""
        assert isinstance(flow, ofp.common.flow_stats_entry)
        for instruction in flow.instructions:
            assert isinstance(instruction, ofp.instruction.instruction)
            if instruction.type == ofp.OFPIT_APPLY_ACTIONS:
                assert isinstance(instruction, ofp.instruction.apply_actions)
                for action in instruction.actions:
                    assert isinstance(action, ofp.action.action)
                    if action.type == ofp.OFPAT_OUTPUT:
                        assert isinstance(action, ofp.action.output)
                        if action.port == out_port:
                            return True

        # otherwise...
        return False

    def _flow_has_out_group(self, flow, out_group):
        """Return True if flow has a output command with the given out_group"""
        assert isinstance(flow, ofp.common.flow_stats_entry)
        for instruction in flow.instructions:
            assert isinstance(instruction, ofp.instruction.instruction)
            if instruction.type == ofp.OFPIT_APPLY_ACTIONS:
                assert isinstance(instruction, ofp.instruction.apply_actions)
                for action in instruction.actions:
                    assert isinstance(action, ofp.action.action)
                    if action.type == ofp.OFPAT_GROUP:
                        assert isinstance(action, ofp.action.group)
                        if action.group_id == out_group:
                            return True

        # otherwise...
        return False

    def _flows_delete_by_group_id(self, group_id):
        """Delete any flow referring to given group id"""
        to_keep = []
        to_delete = []
        for f in self.flows:
            list_to_append = to_delete if self._flow_has_out_group(f, group_id) else to_keep
            list_to_append.append(f)

        # replace flow table with keepers
        self.flows = to_keep

        # send notifications for discarded flows as required by OF spec
        self._announce_flows_deleted(to_delete)

    def _announce_flows_deleted(self, flows):
        for f in flows:
            self._announce_flow_deleted(f)

    def _announce_flow_deleted(self, flow):
        """Send notification to controller if flow is flagged with OFPFF_SEND_FLOW_REM flag"""
        if flow.flags & ofp.OFPFF_SEND_FLOW_REM:
            raise NotImplementedError("_announce_flow_deleted()")


    ## <=========================== GROUP HANDLERS =================================>

    def group_add(self, group_add):
        assert isinstance(group_add, ofp.message.group_add)
        if group_add.group_id in self.groups:
            self.signal_group_mod_error(ofp.OFPGMFC_GROUP_EXISTS, group_add)
        else:
            group_entry = group_entry_from_group_mod_message(group_add)
            self.groups[group_add.group_id] = group_entry

    def group_modify(self, group_modify):
        assert isinstance(group_modify, ofp.message.group_modify)
        if group_modify.group_id not in self.groups:
            self.signal_group_mod_error(ofp.OFPGMFC_INVALID_GROUP, group_modify)
        else:
            # replace existing group entry witj new group definition
            group_entry = group_entry_from_group_mod_message(group_modify)
            self.groups[group_modify.group_id] = group_entry

    def group_delete(self, group_delete):
        assert isinstance(group_delete, ofp.message.group_mod)
        if group_delete.group_id == ofp.OFPG_ALL:  # drop all groups
            # we must delete all flows that point to this group and signal controller as
            # requested by the flows' flag

            self.groups = {}
            logging.info("all groups deleted")

        else:
            if group_delete.group_id not in self.groups:
                # per the spec, this is silent;y ignored
                return

            else:
                self._flows_delete_by_group_id(group_delete.group_id)
                del self.groups[group_delete.group_id]
                logging.info("group %d deleted" % group_delete.group_id)

    def group_list(self):
        return [group_entry.group_desc for group_entry in self.groups.values()]

    def group_stats(self):
        return [group_entry.group_stats for group_entry in self.groups.values()]

    ## <=========================== TABLE HANDLERS =================================>

    def table_stats(self):
        """Scan through flow entries and create table stats"""
        stats = {}
        for flow in self.flows:
            table_id = flow.table_id
            entry = stats.setdefault(table_id, ofp.common.table_stats_entry(table_id))
            entry.active_count += 1
            entry.lookup_count += 1  # FIXME how to count these?
            entry.matched_count += 1  # FIXME how to count these?
            stats[table_id] = entry
        return sorted(stats.values(), key=lambda e: e.table_id)
