"""
Copyright 2008-2011 Free Software Foundation, Inc.
This file is part of GNU Radio

GNU Radio Companion is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

GNU Radio Companion is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
"""

from . import odict
from Element import Element
from .. gui import Messages

class FlowGraph(Element):

    def __init__(self, platform):
        """
        Make a flow graph from the arguments.
        
        Args:
            platform: a platforms with blocks and contrcutors
        
        Returns:
            the flow graph object
        """
        #initialize
        Element.__init__(self, platform)
        #inital blank import
        self.import_data()

    def _get_unique_id(self, base_id=''):
        """
        Get a unique id starting with the base id.
        
        Args:
            base_id: the id starts with this and appends a count
        
        Returns:
            a unique id
        """
        index = 0
        while True:
            id = '%s_%d'%(base_id, index)
            index = index + 1
            #make sure that the id is not used by another block
            if not filter(lambda b: b.get_id() == id, self.get_blocks()): return id

    def __str__(self): return 'FlowGraph - %s(%s)'%(self.get_option('title'), self.get_option('id'))
    def rewrite(self):
        def refactor_bus_structure():
            
            for block in self.get_blocks():
                for direc in ['source', 'sink']:
                    if direc == 'source':
                        get_p = block.get_sources;
                        get_p_gui = block.get_sources_gui;
                        bus_structure = block.form_bus_structure('source');
                    else:
                        get_p = block.get_sinks;
                        get_p_gui = block.get_sinks_gui
                        bus_structure = block.form_bus_structure('sink');
                
                    if 'bus' in map(lambda a: a.get_type(), get_p_gui()):
                    
                            
                        
                        if len(get_p_gui()) > len(bus_structure):
                            times = range(len(bus_structure), len(get_p_gui()));
                            for i in times:
                                for connect in get_p_gui()[-1].get_connections():
                                    block.get_parent().remove_element(connect);
                                get_p().remove(get_p_gui()[-1]);
                        elif len(get_p_gui()) < len(bus_structure):
                            n = {'name':'bus','type':'bus'};
                            if True in map(lambda a: isinstance(a.get_nports(), int), get_p()):
                                n['nports'] = str(1);
                            
                            times = range(len(get_p_gui()), len(bus_structure));    
                            
                            for i in times:
                                n['key'] = str(len(get_p()));
                                n = odict(n);
                                port = block.get_parent().get_parent().Port(block=block, n=n, dir=direc);
                                get_p().append(port);
                                
                                
                                
        for child in self.get_children(): child.rewrite()
        refactor_bus_structure();

    def get_option(self, key):
        """
        Get the option for a given key.
        The option comes from the special options block.
        
        Args:
            key: the param key for the options block
        
        Returns:
            the value held by that param
        """
        return self._options_block.get_param(key).get_evaluated()

    def is_flow_graph(self): return True

    ##############################################
    ## Access Elements
    ##############################################
    def get_block(self, id): return filter(lambda b: b.get_id() == id, self.get_blocks())[0]
    def get_blocks_unordered(self): return filter(lambda e: e.is_block(), self.get_elements())
    def get_blocks(self):
        blocks = self.get_blocks_unordered();
        for i in range(len(blocks)):
            if blocks[i].get_key() == 'variable':
                blk = blocks[i];
                blocks.remove(blk);
                blocks.insert(1, blk);
        return blocks;
    def get_connections(self): return filter(lambda e: e.is_connection(), self.get_elements())
    def get_children(self): return self.get_elements()
    def get_elements(self):
        """
        Get a list of all the elements.
        Always ensure that the options block is in the list (only once).
        
        Returns:
            the element list
        """
        options_block_count = self._elements.count(self._options_block)
        if not options_block_count:
            self._elements.append(self._options_block)
        for i in range(options_block_count-1):
            self._elements.remove(self._options_block)
        return self._elements

    def get_enabled_blocks(self):
        """
        Get a list of all blocks that are enabled.
        
        Returns:
            a list of blocks
        """
        return filter(lambda b: b.get_enabled(), self.get_blocks())

    def get_enabled_connections(self):
        """
        Get a list of all connections that are enabled.
        
        Returns:
            a list of connections
        """
        return filter(lambda c: c.get_enabled(), self.get_connections())

    def get_new_block(self, key):
        """
        Get a new block of the specified key.
        Add the block to the list of elements.
        
        Args:
            key: the block key
        
        Returns:
            the new block or None if not found
        """
        if key not in self.get_parent().get_block_keys(): return None
        block = self.get_parent().get_new_block(self, key)
        self.get_elements().append(block);
        if block._bussify_sink:
            block.bussify({'name':'bus','type':'bus'}, 'sink')
        if block._bussify_source:
            block.bussify({'name':'bus','type':'bus'}, 'source')
        return block;

    def connect(self, porta, portb):
        """
        Create a connection between porta and portb.
        
        Args:
            porta: a port
            portb: another port
        @throw Exception bad connection
        
        Returns:
            the new connection
        """
        connection = self.get_parent().Connection(flow_graph=self, porta=porta, portb=portb)
        self.get_elements().append(connection)
        return connection

    def remove_element(self, element):
        """
        Remove the element from the list of elements.
        If the element is a port, remove the whole block.
        If the element is a block, remove its connections.
        If the element is a connection, just remove the connection.
        """
        if element not in self.get_elements(): return
        #found a port, set to parent signal block
        if element.is_port():
            element = element.get_parent()
        #remove block, remove all involved connections
        if element.is_block():
            for port in element.get_ports():
                map(self.remove_element, port.get_connections())
        if element.is_connection():
            if element.is_bus():
                cons_list = []
                for i in map(lambda a: a.get_connections(), element.get_source().get_associated_ports()):
                    cons_list.extend(i);
                map(self.remove_element, cons_list);
        self.get_elements().remove(element)

    def evaluate(self, expr):
        """
        Evaluate the expression.
        
        Args:
            expr: the string expression
        @throw NotImplementedError
        """
        raise NotImplementedError

    ##############################################
    ## Import/Export Methods
    ##############################################
    def export_data(self):
        """
        Export this flow graph to nested data.
        Export all block and connection data.
        
        Returns:
            a nested data odict
        """
        import time
        n = odict()
        n['timestamp'] = time.ctime()
        n['block'] = [block.export_data() for block in self.get_blocks()]
        n['connection'] = [connection.export_data() for connection in self.get_connections()]
        return odict({'flow_graph': n})

    def import_data(self, n=None):
        """
        Import blocks and connections into this flow graph.
        Clear this flowgraph of all previous blocks and connections.
        Any blocks or connections in error will be ignored.
        
        Args:
            n: the nested data odict
        """
        #remove previous elements
        self._elements = list()
        #use blank data if none provided
        fg_n = n and n.find('flow_graph') or odict()
        blocks_n = fg_n.findall('block')
        connections_n = fg_n.findall('connection')
        #create option block
        self._options_block = self.get_parent().get_new_block(self, 'options')
        #build the blocks
        for block_n in blocks_n:
            key = block_n.find('key')
            if key == 'options': block = self._options_block
            else: block = self.get_new_block(key)
            #only load the block when the block key was valid
            if block: block.import_data(block_n)
            else: Messages.send_error_load('Block key "%s" not found in %s'%(key, self.get_parent()))
        #build the connections
        for connection_n in connections_n:
            #try to make the connection
            try:
                #get the block ids
                source_block_id = connection_n.find('source_block_id')
                sink_block_id = connection_n.find('sink_block_id')
                #get the port keys
                source_key = connection_n.find('source_key')
                sink_key = connection_n.find('sink_key')
                #verify the blocks
                block_ids = map(lambda b: b.get_id(), self.get_blocks())
                if source_block_id not in block_ids:
                    raise LookupError('source block id "%s" not in block ids'%source_block_id)
                if sink_block_id not in block_ids:
                    raise LookupError('sink block id "%s" not in block ids'%sink_block_id)
                #get the blocks
                source_block = self.get_block(source_block_id)
                sink_block = self.get_block(sink_block_id)
                #verify the ports
                if source_key not in source_block.get_source_keys():
                    raise LookupError('source key "%s" not in source block keys'%source_key)
                if sink_key not in sink_block.get_sink_keys():
                    raise LookupError('sink key "%s" not in sink block keys'%sink_key)
                #get the ports
                source = source_block.get_source(source_key)
                sink = sink_block.get_sink(sink_key)
                #build the connection
                self.connect(source, sink)
            except LookupError, e: Messages.send_error_load(
                'Connection between %s(%s) and %s(%s) could not be made.\n\t%s'%(
                    source_block_id, source_key, sink_block_id, sink_key, e
                )
            )
        self.rewrite() #global rewrite