# Copyright 2016 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, print_function

import collections
import re
import string

delims = {'(': ')', '[': ']', '{': '}', '': ', #\\*:'}
identifier_start = '_' + string.ascii_letters + ''.join(delims.keys())
string_delims = '"\''

cheetah_substitution = re.compile(
    r'^\$((?P<d1>\()|(?P<d2>\{)|(?P<d3>\[)|)'
    r'(?P<arg>[_a-zA-Z][_a-zA-Z0-9]*(?:\.[_a-zA-Z][_a-zA-Z0-9]*)?)(?P<eval>\(\))?'
    r'(?(d1)\)|(?(d2)\}|(?(d3)\]|)))$'
)
cheetah_inline_if = re.compile(r'#if (?P<cond>.*) then (?P<then>.*?) ?else (?P<else>.*?) ?(#|$)')


class Python(object):
    start = ''
    end = ''
    nested_start = ''
    nested_end = ''
    eval = ''
    type = str  # yaml_output.Eval


class FormatString(Python):
    start = '{'
    end = '}'
    nested_start = '{'
    nested_end = '}'
    eval = ':eval'
    type = str


class Mako(Python):
    start = '${'
    end = '}'
    nested_start = ''
    nested_end = ''
    type = str


class Converter(object):

    def __init__(self, names):
        self.stats = collections.defaultdict(int)
        self.names = set(names)
        self.extended = set(self._iter_identifiers(names))

    @staticmethod
    def _iter_identifiers(names):
        if not isinstance(names, dict):
            names = {name: {} for name in names}
        for key, sub_keys in names.items():
            yield key
            for sub_key in sub_keys:
                yield '{}.{}'.format(key, sub_key)

    def to_python(self, expr):
        return self.convert(expr=expr, spec=Python)

    def to_python_dec(self, expr):
        converted = self.convert(expr=expr, spec=Python)
        if converted and converted != expr:
            converted = '${ ' + converted.strip() + ' }'
        return converted

    def to_format_string(self, expr):
        return self.convert(expr=expr, spec=FormatString)

    def to_mako(self, expr):
        return self.convert(expr=expr, spec=Mako)

    def convert(self, expr, spec=Python):
        if not expr:
            return ''

        elif '$' not in expr:
            return expr

        try:
            return self.convert_simple(expr, spec)
        except ValueError:
            pass

        try:
            if '#if' in expr and '\n' not in expr:
                expr = self.convert_inline_conditional(expr, spec)
            return self.convert_hard(expr, spec)
        except ValueError:
            return 'Cheetah! ' + expr

    def convert_simple(self, expr, spec=Python):
        match = cheetah_substitution.match(expr)
        if not match:
            raise ValueError('Not a simple substitution: ' + expr)

        identifier = match.group('arg')
        if identifier not in self.extended:
            raise NameError('Unknown substitution {!r}'.format(identifier))
        if match.group('eval'):
            identifier += spec.eval

        out = spec.start + identifier + spec.end
        if '$' in out or '#' in out:
            raise ValueError('Failed to convert: ' + expr)

        self.stats['simple'] += 1
        return spec.type(out)

    def convert_hard(self, expr, spec=Python):
        lines = '\n'.join(self.convert_hard_line(line, spec) for line in expr.split('\n'))
        if spec == Mako:
            # no line-continuation before a mako control structure
            lines = re.sub(r'\\\n(\s*%)', r'\n\1', lines)
        return lines

    def convert_hard_line(self, expr, spec=Python):
        if spec == Mako:
            if '#set' in expr:
                ws, set_, statement = expr.partition('#set ')
                return ws + '<% ' + self.to_python(statement) + ' %>'

            if '#if' in expr:
                ws, if_, condition = expr.partition('#if ')
                return ws + '% if ' + self.to_python(condition) + ':'
            if '#else if' in expr:
                ws, elif_, condition = expr.partition('#else if ')
                return ws + '% elif ' + self.to_python(condition) + ':'
            if '#else' in expr:
                return expr.replace('#else', '% else:')
            if '#end if' in expr:
                return expr.replace('#end if', '% endif')

            if '#slurp' in expr:
                expr = expr.split('#slurp', 1)[0] + '\\'
        return self.convert_hard_replace(expr, spec)

    def convert_hard_replace(self, expr, spec=Python):
        counts = collections.Counter()

        def all_delims_closed():
            for opener_, closer_ in delims.items():
                if counts[opener_] != counts[closer_]:
                    return False
            return True

        def extra_close():
            for opener_, closer_ in delims.items():
                if counts[opener_] < counts[closer_]:
                    return True
            return False

        out = []
        delim_to_find = False

        pos = 0
        char = ''
        in_string = None
        while pos < len(expr):
            prev, char = char, expr[pos]
            counts.update(char)

            if char in string_delims:
                if not in_string:
                    in_string = char
                elif char == in_string:
                    in_string = None
                    out.append(char)
                    pos += 1
                    continue
            if in_string:
                out.append(char)
                pos += 1
                continue

            if char == '$':
                pass  # no output

            elif prev == '$':
                if char not in identifier_start:  # not a substitution
                    out.append('$' + char)  # now print the $ we skipped over

                elif not delim_to_find:  # start of a substitution
                    try:
                        delim_to_find = delims[char]
                        out.append(spec.start)
                    except KeyError:
                        if char in identifier_start:
                            delim_to_find = delims['']
                            out.append(spec.start)
                            out.append(char)

                    counts.clear()
                    counts.update(char)

                else:  # nested substitution: simply match known variable names
                    found = False
                    for known_identifier in self.names:
                        if expr[pos:].startswith(known_identifier):
                            found = True
                            break
                    if found:
                        out.append(spec.nested_start)
                        out.append(known_identifier)
                        out.append(spec.nested_end)
                        pos += len(known_identifier)
                        continue

            elif delim_to_find and char in delim_to_find and all_delims_closed():  # end of substitution
                out.append(spec.end)
                if char in delims['']:
                    out.append(char)
                delim_to_find = False

            elif delim_to_find and char in ')]}' and extra_close():  # end of substitution
                out.append(spec.end)
                out.append(char)
                delim_to_find = False

            else:
                out.append(char)

            pos += 1

        if delim_to_find == delims['']:
            out.append(spec.end)

        out = ''.join(out)
        # fix: eval stuff
        out = re.sub(r'(?P<arg>' + r'|'.join(self.extended) + r')\(\)', r'\g<arg>', out)

        self.stats['hard'] += 1
        return spec.type(out)

    def convert_inline_conditional(self, expr, spec=Python):
        if spec == FormatString:
            raise ValueError('No conditionals in format strings: ' + expr)
        matcher = r'\g<then> if \g<cond> else \g<else>'
        if spec == Python:
            matcher = '(' + matcher + ')'
        expr = cheetah_inline_if.sub(matcher, expr)
        return spec.type(self.convert_hard(expr, spec))


class DummyConverter(object):

    def __init__(self, names={}):
        pass

    def to_python(self, expr):
        return expr

    def to_format_string(self, expr):
        return expr

    def to_mako(self, expr):
        return expr
