"""
Copyright 2008-2015 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 __future__ import absolute_import

import ast
import re
import collections

from six.moves import builtins, filter, map, range, zip

from . import Constants
from .Element import Element

# Blacklist certain ids, its not complete, but should help
ID_BLACKLIST = ['self', 'options', 'gr', 'blks2', 'wxgui', 'wx', 'math', 'forms', 'firdes'] + dir(builtins)
try:
    from gnuradio import gr
    ID_BLACKLIST.extend(attr for attr in dir(gr.top_block()) if not attr.startswith('_'))
except ImportError:
    pass

_check_id_matcher = re.compile('^[a-z|A-Z]\w*$')
_show_id_matcher = re.compile('^(variable\w*|parameter|options|notebook)$')


class TemplateArg(object):
    """
    A cheetah template argument created from a param.
    The str of this class evaluates to the param's to code method.
    The use of this class as a dictionary (enum only) will reveal the enum opts.
    The __call__ or () method can return the param evaluated to a raw python data type.
    """

    def __init__(self, param):
        self._param = param

    def __getitem__(self, item):
        param = self._param
        opts = param.options_opts[param.get_value()]
        return str(opts[item]) if param.is_enum() else NotImplemented

    def __str__(self):
        return str(self._param.to_code())

    def __call__(self):
        return self._param.get_evaluated()


class Param(Element):

    is_param = True

    def __init__(self, parent, key, name, type='raw', value='', **n):
        """Make a new param from nested data"""
        super(Param, self).__init__(parent)
        self.key = key
        self._name = name
        self.value = self.default = value
        self._type = type

        self._hide = n.get('hide', '')
        self.tab_label = n.get('tab', Constants.DEFAULT_PARAM_TAB)
        self._evaluated = None

        self.options = []
        self.options_names = []
        self.options_opts = {}
        self._init_options(options_n=n.get('option', []))

        self._init = False
        self._hostage_cells = list()
        self.template_arg = TemplateArg(self)

    def _init_options(self, options_n):
        """Create the Option objects from the n data"""
        option_keys = set()
        for option_n in options_n:
            key, name = option_n['key'], option_n['name']
            # Test against repeated keys
            if key in option_keys:
                raise KeyError('Key "{}" already exists in options'.format(key))
            option_keys.add(key)
            # Store the option
            self.options.append(key)
            self.options_names.append(name)

        if self.is_enum():
            self._init_enum(options_n)

    def _init_enum(self, options_n):
        opt_ref = None
        for option_n in options_n:
            key, opts_raw = option_n['key'], option_n.get('opt', [])
            try:
                self.options_opts[key] = opts = dict(opt.split(':') for opt in opts_raw)
            except TypeError:
                raise ValueError('Error separating opts into key:value')

            if opt_ref is None:
                opt_ref = set(opts.keys())
            elif opt_ref != set(opts):
                raise ValueError('Opt keys ({}) are not identical across all options.'
                                 ''.format(', '.join(opt_ref)))
        if not self.value:
            self.value = self.default = self.options[0]
        elif self.value not in self.options:
            self.value = self.default = self.options[0]  # TODO: warn
            # raise ValueError('The value {!r} is not in the possible values of {}.'
            #                  ''.format(self.get_value(), ', '.join(self.options)))

    def __str__(self):
        return 'Param - {}({})'.format(self.get_name(), self.key)

    def get_hide(self):
        """
        Get the hide value from the base class.
        Hide the ID parameter for most blocks. Exceptions below.
        If the parameter controls a port type, vlen, or nports, return part.
        If the parameter is an empty grid position, return part.
        These parameters are redundant to display in the flow graph view.

        Returns:
            hide the hide property string
        """
        hide = self.parent.resolve_dependencies(self._hide).strip()
        if hide:
            return hide
        # Hide ID in non variable blocks
        if self.key == 'id' and not _show_id_matcher.match(self.parent.key):
            return 'part'
        # Hide port controllers for type and nports
        if self.key in ' '.join([' '.join([p._type, p._nports]) for p in self.parent.get_ports()]):
            return 'part'
        # Hide port controllers for vlen, when == 1
        if self.key in ' '.join(p._vlen for p in self.parent.get_ports()):
            try:
                if int(self.get_evaluated()) == 1:
                    return 'part'
            except:
                pass
        # Hide empty grid positions
        if self.key in ('grid_pos', 'notebook') and not self.get_value():
            return 'part'
        return hide

    def validate(self):
        """
        Validate the param.
        The value must be evaluated and type must a possible type.
        """
        Element.validate(self)
        if self.get_type() not in Constants.PARAM_TYPE_NAMES:
            self.add_error_message('Type "{}" is not a possible type.'.format(self.get_type()))

        self._evaluated = None
        try:
            self._evaluated = self.evaluate()
        except Exception as e:
            self.add_error_message(str(e))

    def get_evaluated(self):
        return self._evaluated

    def evaluate(self):
        """
        Evaluate the value.

        Returns:
            evaluated type
        """
        self._init = True
        self._lisitify_flag = False
        self._stringify_flag = False
        self._hostage_cells = list()
        t = self.get_type()
        v = self.get_value()

        #########################
        # Enum Type
        #########################
        if self.is_enum():
            return v

        #########################
        # Numeric Types
        #########################
        elif t in ('raw', 'complex', 'real', 'float', 'int', 'hex', 'bool'):
            # Raise exception if python cannot evaluate this value
            try:
                e = self.parent_flowgraph.evaluate(v)
            except Exception as e:
                raise Exception('Value "{}" cannot be evaluated:\n{}'.format(v, e))
            # Raise an exception if the data is invalid
            if t == 'raw':
                return e
            elif t == 'complex':
                if not isinstance(e, Constants.COMPLEX_TYPES):
                    raise Exception('Expression "{}" is invalid for type complex.'.format(str(e)))
                return e
            elif t == 'real' or t == 'float':
                if not isinstance(e, Constants.REAL_TYPES):
                    raise Exception('Expression "{}" is invalid for type float.'.format(str(e)))
                return e
            elif t == 'int':
                if not isinstance(e, Constants.INT_TYPES):
                    raise Exception('Expression "{}" is invalid for type integer.'.format(str(e)))
                return e
            elif t == 'hex':
                return hex(e)
            elif t == 'bool':
                if not isinstance(e, bool):
                    raise Exception('Expression "{}" is invalid for type bool.'.format(str(e)))
                return e
            else:
                raise TypeError('Type "{}" not handled'.format(t))
        #########################
        # Numeric Vector Types
        #########################
        elif t in ('complex_vector', 'real_vector', 'float_vector', 'int_vector'):
            if not v:
                # Turn a blank string into an empty list, so it will eval
                v = '()'
            # Raise exception if python cannot evaluate this value
            try:
                e = self.parent.parent.evaluate(v)
            except Exception as e:
                raise Exception('Value "{}" cannot be evaluated:\n{}'.format(v, e))
            # Raise an exception if the data is invalid
            if t == 'complex_vector':
                if not isinstance(e, Constants.VECTOR_TYPES):
                    self._lisitify_flag = True
                    e = [e]
                if not all([isinstance(ei, Constants.COMPLEX_TYPES) for ei in e]):
                    raise Exception('Expression "{}" is invalid for type complex vector.'.format(str(e)))
                return e
            elif t == 'real_vector' or t == 'float_vector':
                if not isinstance(e, Constants.VECTOR_TYPES):
                    self._lisitify_flag = True
                    e = [e]
                if not all([isinstance(ei, Constants.REAL_TYPES) for ei in e]):
                    raise Exception('Expression "{}" is invalid for type float vector.'.format(str(e)))
                return e
            elif t == 'int_vector':
                if not isinstance(e, Constants.VECTOR_TYPES):
                    self._lisitify_flag = True
                    e = [e]
                if not all([isinstance(ei, Constants.INT_TYPES) for ei in e]):
                    raise Exception('Expression "{}" is invalid for type integer vector.'.format(str(e)))
                return e
        #########################
        # String Types
        #########################
        elif t in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'):
            # Do not check if file/directory exists, that is a runtime issue
            try:
                e = self.parent.parent.evaluate(v)
                if not isinstance(e, str):
                    raise Exception()
            except:
                self._stringify_flag = True
                e = str(v)
            if t == '_multiline_python_external':
                ast.parse(e)  # Raises SyntaxError
            return e
        #########################
        # Unique ID Type
        #########################
        elif t == 'id':
            # Can python use this as a variable?
            if not _check_id_matcher.match(v):
                raise Exception('ID "{}" must begin with a letter and may contain letters, numbers, and underscores.'.format(v))
            ids = [param.get_value() for param in self.get_all_params(t)]

            # Id should only appear once, or zero times if block is disabled
            if ids.count(v) > 1:
                raise Exception('ID "{}" is not unique.'.format(v))
            if v in ID_BLACKLIST:
                raise Exception('ID "{}" is blacklisted.'.format(v))
            return v

        #########################
        # Stream ID Type
        #########################
        elif t == 'stream_id':
            # Get a list of all stream ids used in the virtual sinks
            ids = [param.get_value() for param in filter(
                lambda p: p.parent.is_virtual_sink(),
                self.get_all_params(t),
            )]
            # Check that the virtual sink's stream id is unique
            if self.parent.is_virtual_sink():
                # Id should only appear once, or zero times if block is disabled
                if ids.count(v) > 1:
                    raise Exception('Stream ID "{}" is not unique.'.format(v))
            # Check that the virtual source's steam id is found
            if self.parent.is_virtual_source():
                if v not in ids:
                    raise Exception('Stream ID "{}" is not found.'.format(v))
            return v

        #########################
        # GUI Position/Hint
        #########################
        elif t == 'gui_hint':
            if ':' in v:
                tab, pos = v.split(':')
            elif '@' in v:
                tab, pos = v, ''
            else:
                tab, pos = '', v

            if '@' in tab:
                tab, index = tab.split('@')
            else:
                index = '?'

            # TODO: Problem with this code. Produces bad tabs
            widget_str = ({
                (True, True): 'self.%(tab)s_grid_layout_%(index)s.addWidget(%(widget)s, %(pos)s)',
                (True, False): 'self.%(tab)s_layout_%(index)s.addWidget(%(widget)s)',
                (False, True): 'self.top_grid_layout.addWidget(%(widget)s, %(pos)s)',
                (False, False): 'self.top_layout.addWidget(%(widget)s)',
            }[bool(tab), bool(pos)]) % {'tab': tab, 'index': index, 'widget': '%s', 'pos': pos}

            # FIXME: Move replace(...) into the make template of the qtgui blocks
            # Return a string here
            class GuiHint(object):
                def __init__(self, ws):
                    self._ws = ws

                def __call__(self, w):
                    return (self._ws.replace('addWidget', 'addLayout') if 'layout' in w else self._ws) % w

                def __str__(self):
                    return self._ws
            return GuiHint(widget_str)
        #########################
        # Grid Position Type
        #########################
        elif t == 'grid_pos':
            if not v:
                # Allow for empty grid pos
                return ''
            e = self.parent_flowgraph.evaluate(v)
            if not isinstance(e, (list, tuple)) or len(e) != 4 or not all([isinstance(ei, int) for ei in e]):
                raise Exception('A grid position must be a list of 4 integers.')
            row, col, row_span, col_span = e
            # Check row, col
            if row < 0 or col < 0:
                raise Exception('Row and column must be non-negative.')
            # Check row span, col span
            if row_span <= 0 or col_span <= 0:
                raise Exception('Row and column span must be greater than zero.')
            # Get hostage cell parent
            try:
                my_parent = self.parent.get_param('notebook').evaluate()
            except:
                my_parent = ''
            # Calculate hostage cells
            for r in range(row_span):
                for c in range(col_span):
                    self._hostage_cells.append((my_parent, (row+r, col+c)))
            # Avoid collisions
            params = [p for p in self.get_all_params('grid_pos') if p is not self]
            for param in params:
                for parent, cell in param._hostage_cells:
                    if (parent, cell) in self._hostage_cells:
                        raise Exception('Another graphical element is using parent "{}", cell "{}".'.format(str(parent), str(cell)))
            return e
        #########################
        # Notebook Page Type
        #########################
        elif t == 'notebook':
            if not v:
                # Allow for empty notebook
                return ''

            # Get a list of all notebooks
            notebook_blocks = [b for b in self.parent_flowgraph.get_enabled_blocks() if b.key == 'notebook']
            # Check for notebook param syntax
            try:
                notebook_id, page_index = map(str.strip, v.split(','))
            except:
                raise Exception('Bad notebook page format.')
            # Check that the notebook id is valid
            try:
                notebook_block = [b for b in notebook_blocks if b.get_id() == notebook_id][0]
            except:
                raise Exception('Notebook id "{}" is not an existing notebook id.'.format(notebook_id))

            # Check that page index exists
            if int(page_index) not in range(len(notebook_block.get_param('labels').evaluate())):
                raise Exception('Page index "{}" is not a valid index number.'.format(page_index))
            return notebook_id, page_index

        #########################
        # Import Type
        #########################
        elif t == 'import':
            # New namespace
            n = dict()
            try:
                exec(v, n)
            except ImportError:
                raise Exception('Import "{}" failed.'.format(v))
            except Exception:
                raise Exception('Bad import syntax: "{}".'.format(v))
            return [k for k in list(n.keys()) if str(k) != '__builtins__']

        #########################
        else:
            raise TypeError('Type "{}" not handled'.format(t))

    def to_code(self):
        """
        Convert the value to code.
        For string and list types, check the init flag, call evaluate().
        This ensures that evaluate() was called to set the xxxify_flags.

        Returns:
            a string representing the code
        """
        v = self.get_value()
        t = self.get_type()
        # String types
        if t in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'):
            if not self._init:
                self.evaluate()
            if self._stringify_flag:
                return '"%s"' % v.replace('"', '\"')
            else:
                return v
        # Vector types
        elif t in ('complex_vector', 'real_vector', 'float_vector', 'int_vector'):
            if not self._init:
                self.evaluate()
            if self._lisitify_flag:
                return '(%s, )' % v
            else:
                return '(%s)' % v
        else:
            return v

    def get_all_params(self, type):
        """
        Get all the params from the flowgraph that have the given type.

        Args:
            type: the specified type

        Returns:
            a list of params
        """
        params = []
        for block in self.parent_flowgraph.get_enabled_blocks():
            params.extend(p for p in block.params.values() if p.get_type() == type)
        return params

    def is_enum(self):
        return self._type == 'enum'

    def get_value(self):
        value = self.value
        if self.is_enum() and value not in self.options:
            value = self.options[0]
            self.set_value(value)
        return value

    def set_value(self, value):
        # Must be a string
        self.value = str(value)

    def set_default(self, value):
        if self.default == self.value:
            self.set_value(value)
        self.default = str(value)

    def get_type(self):
        return self.parent.resolve_dependencies(self._type)

    def get_tab_label(self):
        return self.tab_label

    def get_name(self):
        return self.parent.resolve_dependencies(self._name).strip()

    ##############################################
    # Access Options
    ##############################################
    def opt_value(self, key):
        return self.options_opts[self.get_value()][key]

    ##############################################
    # Import/Export Methods
    ##############################################
    def export_data(self):
        """
        Export this param's key/value.

        Returns:
            a nested data odict
        """
        n = collections.OrderedDict()
        n['key'] = self.key
        n['value'] = self.get_value()
        return n