"""
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 numbers
import re
import collections

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

from . import Constants, blocks
from .base import Element
from .utils.descriptors import Evaluated, EvaluatedEnum, setup_names

# Blacklist certain ids, its not complete, but should help
ID_BLACKLIST = ['self', 'options', 'gr', 'math', '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, AttributeError):
    pass


class TemplateArg(str):
    """
    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 __new__(cls, param):
        value = param.to_code()
        instance = str.__new__(cls, value)
        setattr(instance, '_param', param)
        return instance

    def __getitem__(self, item):
        return str(self._param.get_opt(item)) if self._param.is_enum() else NotImplemented

    def __getattr__(self, item):
        if not self._param.is_enum():
            raise AttributeError()
        try:
            return str(self._param.get_opt(item))
        except KeyError:
            raise AttributeError()

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

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


@setup_names
class Param(Element):

    is_param = True

    name = Evaluated(str, default='no name')
    dtype = EvaluatedEnum(Constants.PARAM_TYPE_NAMES, default='raw')
    hide = EvaluatedEnum('none all part')

    # region init
    def __init__(self, parent, id, label='', dtype='raw', default='',
                 options=None, option_labels=None, option_attributes=None,
                 category='', hide='none', **_):
        """Make a new param from nested data"""
        super(Param, self).__init__(parent)
        self.key = id
        self.name = label.strip() or id.title()
        self.category = category or Constants.DEFAULT_PARAM_TAB

        self.dtype = dtype
        self.value = self.default = str(default)

        self.options = self._init_options(options or [], option_labels or [],
                                          option_attributes or {})
        self.hide = hide or 'none'
        # end of args ########################################################

        self._evaluated = None
        self._stringify_flag = False
        self._lisitify_flag = False
        self._init = False

    @property
    def template_arg(self):
        return TemplateArg(self)

    def _init_options(self, values, labels, attributes):
        """parse option and option attributes"""
        options = collections.OrderedDict()
        options.attributes = collections.defaultdict(dict)

        padding = [''] * max(len(values), len(labels))
        attributes = {key: value + padding for key, value in six.iteritems(attributes)}

        for i, option in enumerate(values):
            # Test against repeated keys
            if option in options:
                raise KeyError('Value "{}" already exists in options'.format(option))
            # get label
            try:
                label = str(labels[i])
            except IndexError:
                label = str(option)
            # Store the option
            options[option] = label
            options.attributes[option] = {attrib: values[i] for attrib, values in six.iteritems(attributes)}

        default = next(iter(options)) if options else ''
        if not self.value:
            self.value = self.default = default

        if self.is_enum() and self.value not in options:
            self.value = self.default = default  # TODO: warn
            # raise ValueError('The value {!r} is not in the possible values of {}.'
            #                  ''.format(self.get_value(), ', '.join(self.options)))
        return options
    # endregion

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

    def __repr__(self):
        return '{!r}.param[{}]'.format(self.parent, self.key)

    def is_enum(self):
        return self.get_raw('dtype') == 'enum'

    def get_value(self):
        value = self.value
        if self.is_enum() and value not in self.options:
            value = self.default
            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 rewrite(self):
        Element.rewrite(self)
        del self.name
        del self.dtype
        del self.hide

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

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

    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
        dtype = self.dtype
        expr = self.get_value()

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

        #########################
        # Numeric Types
        #########################
        elif dtype in ('raw', 'complex', 'real', 'float', 'int', 'hex', 'bool'):
            # Raise exception if python cannot evaluate this value
            try:
                value = self.parent_flowgraph.evaluate(expr)
            except Exception as value:
                raise Exception('Value "{}" cannot be evaluated:\n{}'.format(expr, value))
            # Raise an exception if the data is invalid
            if dtype == 'raw':
                return value
            elif dtype == 'complex':
                if not isinstance(value, Constants.COMPLEX_TYPES):
                    raise Exception('Expression "{}" is invalid for type complex.'.format(str(value)))
                return value
            elif dtype in ('real', 'float'):
                if not isinstance(value, Constants.REAL_TYPES):
                    raise Exception('Expression "{}" is invalid for type float.'.format(str(value)))
                return value
            elif dtype == 'int':
                if not isinstance(value, Constants.INT_TYPES):
                    raise Exception('Expression "{}" is invalid for type integer.'.format(str(value)))
                return value
            elif dtype == 'hex':
                return hex(value)
            elif dtype == 'bool':
                if not isinstance(value, bool):
                    raise Exception('Expression "{}" is invalid for type bool.'.format(str(value)))
                return value
            else:
                raise TypeError('Type "{}" not handled'.format(dtype))
        #########################
        # Numeric Vector Types
        #########################
        elif dtype in ('complex_vector', 'real_vector', 'float_vector', 'int_vector'):
            default = []

            if not expr:
                return default   # Turn a blank string into an empty list, so it will eval

            try:
                value = self.parent.parent.evaluate(expr)
            except Exception as value:
                raise Exception('Value "{}" cannot be evaluated:\n{}'.format(expr, value))

            if not isinstance(value, Constants.VECTOR_TYPES):
                self._lisitify_flag = True
                value = [value]

            # Raise an exception if the data is invalid
            if dtype == 'complex_vector' and not all(isinstance(item, numbers.Complex) for item in value):
                raise Exception('Expression "{}" is invalid for type complex vector.'.format(value))
            elif dtype in ('real_vector', 'float_vector') and not all(isinstance(item, numbers.Real) for item in value):
                raise Exception('Expression "{}" is invalid for type float vector.'.format(value))
            elif dtype == 'int_vector' and not all(isinstance(item, Constants.INT_TYPES) for item in value):
                raise Exception('Expression "{}" is invalid for type integer vector.'.format(str(value)))
            return value
        #########################
        # String Types
        #########################
        elif dtype in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'):
            # Do not check if file/directory exists, that is a runtime issue
            try:
                value = self.parent.parent.evaluate(expr)
                if not isinstance(value, str):
                    raise Exception()
            except:
                self._stringify_flag = True
                value = str(expr)
            if dtype == '_multiline_python_external':
                ast.parse(value)  # Raises SyntaxError
            return value
        #########################
        # Unique ID Type
        #########################
        elif dtype == 'id':
            self.validate_block_id()
            return expr

        #########################
        # Stream ID Type
        #########################
        elif dtype == 'stream_id':
            self.validate_stream_id()
            return expr

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

            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)
        #########################
        # Import Type
        #########################
        elif dtype == 'import':
            # New namespace
            n = dict()
            try:
                exec(expr, n)
            except ImportError:
                raise Exception('Import "{}" failed.'.format(expr))
            except Exception:
                raise Exception('Bad import syntax: "{}".'.format(expr))
            return [k for k in list(n.keys()) if str(k) != '__builtins__']

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

    def validate_block_id(self):
        value = self.value
        # Can python use this as a variable?
        if not re.match(r'^[a-z|A-Z]\w*$', value):
            raise Exception('ID "{}" must begin with a letter and may contain letters, numbers, '
                            'and underscores.'.format(value))
        if value in ID_BLACKLIST:
            raise Exception('ID "{}" is blacklisted.'.format(value))
        block_names = [block.name for block in self.parent_flowgraph.iter_enabled_blocks()]
        # Id should only appear once, or zero times if block is disabled
        if self.key == 'id' and block_names.count(value) > 1:
            raise Exception('ID "{}" is not unique.'.format(value))
        elif value not in block_names:
            raise Exception('ID "{}" does not exist.'.format(value))
        return value

    def validate_stream_id(self):
        value = self.value
        stream_ids = [
            block.params['stream_id'].value
            for block in self.parent_flowgraph.iter_enabled_blocks()
            if isinstance(block, blocks.VirtualSink)
            ]
        # Check that the virtual sink's stream id is unique
        if isinstance(self.parent_block, blocks.VirtualSink) and stream_ids.count(value) >= 2:
            # Id should only appear once, or zero times if block is disabled
            raise Exception('Stream ID "{}" is not unique.'.format(value))
        # Check that the virtual source's steam id is found
        elif isinstance(self.parent_block, blocks.VirtualSource) and value not in stream_ids:
            raise Exception('Stream ID "{}" is not found.'.format(value))

    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
        """
        self._init = True
        v = self.get_value()
        t = self.dtype
        # String types
        if t in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'):
            if not self._init:
                self.evaluate()
            return repr(v) if self._stringify_flag else 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_opt(self, item):
        return self.options.attributes[self.get_value()][item]