###############################################################################
#
# Copyright (c) 2011 Ruslan Spivak
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'

import re

from slimit import ast
from slimit.lexer import Lexer

_HAS_ID_MATCH = re.compile('^%s$' % Lexer.identifier).match

def _is_identifier(value):
    return _HAS_ID_MATCH(value) and value not in Lexer.keywords_dict


class ECMAMinifier(object):

    def __init__(self):
        self.in_block = 0
        self.ifelse_stack = []

    def visit(self, node):
        method = 'visit_%s' % node.__class__.__name__
        return getattr(self, method, self.generic_visit)(node)

    def generic_visit(self, node):
        return 'GEN: %r' % node

    def visit_Program(self, node):
        return ''.join(self.visit(child) for child in node)

    def visit_Block(self, node):
        children = [self.visit(child) for child in node]
        if len(children) == 1:
            return children[0]
        else:
            return '{%s}' % ''.join(children)

    def visit_VarStatement(self, node):
        s = 'var %s;' % ','.join(self.visit(child) for child in node)
        return s

    def visit_VarDecl(self, node):
        output = []
        output.append(self.visit(node.identifier))
        if node.initializer is not None:
            output.append('=%s' % self.visit(node.initializer))
        return ''.join(output)

    def visit_Identifier(self, node):
        return node.value

    def visit_Assign(self, node):
        template = '%s%s%s'
        if getattr(node, '_parens', False):
            template = '(%s)' % template
        return template % (
            self.visit(node.left), node.op, self.visit(node.right))

    def visit_GetPropAssign(self, node):
        template = 'get %s(){%s}'
        if getattr(node, '_parens', False):
            template = '(%s)' % template
        return template % (
            self.visit(node.prop_name),
            ''.join(self.visit(element) for element in node.elements)
            )

    def visit_SetPropAssign(self, node):
        template = 'set %s(%s){%s}'
        if getattr(node, '_parens', False):
            template = '(%s)' % template
        if len(node.parameters) > 1:
            raise SyntaxError(
                'Setter functions must have one argument: %s' % node)
        return template % (
            self.visit(node.prop_name),
            ''.join(self.visit(param) for param in node.parameters),
            ''.join(self.visit(element) for element in node.elements)
            )

    def visit_Number(self, node):
        return node.value

    def visit_Comma(self, node):
        template = '%s,%s'
        if getattr(node, '_parens', False):
            template = '(%s)' % template
        return template % (self.visit(node.left), self.visit(node.right))

    def visit_EmptyStatement(self, node):
        return node.value

    def visit_If(self, node):
        has_alternative = node.alternative is not None

        def _is_singleline_block(n):
            return isinstance(n, ast.Block) and (len(n.children()) == 1)

        s = 'if('
        if node.predicate is not None:
            s += self.visit(node.predicate)
        s += ')'

        # if we are an 'if..else' statement and 'if' part contains only
        # one statement
        if has_alternative and _is_singleline_block(node.consequent):
            self.ifelse_stack.append({'if_in_ifelse': False})
            consequent = self.visit(node.consequent)
            record = self.ifelse_stack.pop()
            if record['if_in_ifelse']:
                s += '{%s}' % consequent
            else:
                s += consequent
        elif has_alternative:
            # we are an 'if..else' statement and 'if' part contains
            # myltiple statements
            s += self.visit(node.consequent)
        else:
            # 'if' without alternative - mark it so that an enclosing
            # 'if..else' can act on it and add braces around 'if' part
            if self.ifelse_stack:
                self.ifelse_stack[-1]['if_in_ifelse'] = True
            s += self.visit(node.consequent)

        if has_alternative:
            alternative = self.visit(node.alternative)
            if alternative.startswith(('(', '{')):
                s += 'else%s' % alternative
            else:
                s += 'else %s' % alternative
        return s

    def visit_Boolean(self, node):
        return node.value

    def visit_For(self, node):
        s = 'for('
        if node.init is not None:
            s += self.visit(node.init)
        if node.init is None:
            s += ';'
        elif isinstance(node.init, (ast.Assign, ast.Comma, ast.Conditional,
                                    ast.FunctionCall, ast.UnaryOp,
                                    ast.Identifier)):
            s += ';'
        else:
            s += ''
        if node.cond is not None:
            s += self.visit(node.cond)
        s += ';'
        if node.count is not None:
            s += self.visit(node.count)
        s += ')' + self.visit(node.statement)
        return s

    def visit_ForIn(self, node):
        if isinstance(node.item, ast.VarDecl):
            template = 'for(var %s in %s)'
        else:
            template = 'for(%s in %s)'
        s = template % (self.visit(node.item), self.visit(node.iterable))
        s += self.visit(node.statement)
        return s

    def visit_BinOp(self, node):
        if node.op in ('instanceof', 'in'):
            template = '%s %s %s'
        elif (node.op == '+' and
              isinstance(node.right, ast.UnaryOp) and
              node.right.op == '++' and not node.right.postfix
              ):
            # make a space between + and ++
            # https://github.com/rspivak/slimit/issues/26
            template = '%s%s %s'
        else:
            template = '%s%s%s'
        if getattr(node, '_parens', False):
            template = '(%s)' % template
        return template % (
            self.visit(node.left), node.op, self.visit(node.right))

    def visit_UnaryOp(self, node):
        s = self.visit(node.value)
        if node.postfix:
            s += node.op
        elif node.op in ('delete', 'void', 'typeof'):
            s = '%s %s' % (node.op, s)
        else:
            s = '%s%s' % (node.op, s)
        if getattr(node, '_parens', False):
            s = '(%s)' % s
        return s

    def visit_ExprStatement(self, node):
        return '%s;' % self.visit(node.expr)

    def visit_DoWhile(self, node):
        statement = self.visit(node.statement)
        if statement.startswith(('{', '(')):
            s = 'do%s' % statement
        else:
            s = 'do %s' % statement
        s += 'while(%s);' % self.visit(node.predicate)
        return s

    def visit_While(self, node):
        s = 'while(%s)' % self.visit(node.predicate)
        s += self.visit(node.statement)
        return s

    def visit_Null(self, node):
        return 'null'

    def visit_String(self, node):
        return node.value

    def visit_Continue(self, node):
        if node.identifier is not None:
            s = 'continue %s;' % self.visit_Identifier(node.identifier)
        else:
            s = 'continue;'
        return s

    def visit_Break(self, node):
        if node.identifier is not None:
            s = 'break %s;' % self.visit_Identifier(node.identifier)
        else:
            s = 'break;'
        return s

    def visit_Return(self, node):
        if node.expr is None:
            return 'return;'

        expr_text = self.visit(node.expr)
        if expr_text.startswith(('(', '{')):
            return 'return%s;' % expr_text
        else:
            return 'return %s;' % expr_text

    def visit_With(self, node):
        s = 'with(%s)' % self.visit(node.expr)
        s += self.visit(node.statement)
        return s

    def visit_Label(self, node):
        s = '%s:%s' % (
            self.visit(node.identifier), self.visit(node.statement))
        return s

    def visit_Switch(self, node):
        s = 'switch(%s){' % self.visit(node.expr)
        for case in node.cases:
            s += self.visit_Case(case)
        if node.default is not None:
            s += self.visit_Default(node.default)
        s += '}'
        return s

    def visit_Case(self, node):
        s = 'case %s:' % self.visit(node.expr)
        elements = ''.join(self.visit(element) for element in node.elements)
        if elements:
            s += elements
        return s

    def visit_Default(self, node):
        s = 'default:'
        s += ''.join(self.visit(element) for element in node.elements)
        if node.elements is not None:
            s += ''
        return s

    def visit_Throw(self, node):
        s = 'throw %s;' % self.visit(node.expr)
        return s

    def visit_Debugger(self, node):
        return '%s;' % node.value

    def visit_Try(self, node):
        result = self.visit(node.statements)
        if result.startswith('{'):
            s = 'try%s' % result
        else:
            s = 'try{%s}' % result
        if node.catch is not None:
            s += self.visit(node.catch)
        if node.fin is not None:
            s += self.visit(node.fin)
        return s

    def visit_Catch(self, node):
        ident = self.visit(node.identifier)
        result = self.visit(node.elements)
        if result.startswith('{'):
            s = 'catch(%s)%s' % (ident, result)
        else:
            s = 'catch(%s){%s}' % (ident, result)
        return s

    def visit_Finally(self, node):
        result = self.visit(node.elements)
        if result.startswith('{'):
            s = 'finally%s' % result
        else:
            s = 'finally{%s}' % result
        return s

    def visit_FuncDecl(self, node):
        elements = ''.join(self.visit(element) for element in node.elements)
        s = 'function %s(%s){%s' % (
            self.visit(node.identifier),
            ','.join(self.visit(param) for param in node.parameters),
            elements,
            )
        s += '}'
        return s

    def visit_FuncExpr(self, node):
        elements = ''.join(self.visit(element) for element in node.elements)

        ident = node.identifier
        ident = '' if ident is None else ' %s' % self.visit(ident)

        header = 'function%s(%s)'
        if getattr(node, '_parens', False):
            header = '(' + header
        s = (header + '{%s') % (
            ident,
            ','.join(self.visit(param) for param in node.parameters),
            elements,
            )
        s += '}'
        if getattr(node, '_parens', False):
            s += ')'
        return s

    def visit_Conditional(self, node):
        if getattr(node, '_parens', False):
            template = '(%s?%s:%s)'
        else:
            template = '%s?%s:%s'

        s = template % (
            self.visit(node.predicate),
            self.visit(node.consequent), self.visit(node.alternative))
        return s

    def visit_Regex(self, node):
        if getattr(node, '_parens', False):
            return '(%s)' % node.value
        else:
            return node.value

    def visit_NewExpr(self, node):
        s = 'new %s(%s)' % (
            self.visit(node.identifier),
            ','.join(self.visit(arg) for arg in node.args)
            )
        return s

    def visit_DotAccessor(self, node):
        if getattr(node, '_parens', False):
            template = '(%s.%s)'
        else:
            template = '%s.%s'
        s = template % (self.visit(node.node), self.visit(node.identifier))
        return s

    def visit_BracketAccessor(self, node):
        if isinstance(node.expr, ast.String):
            value = node.expr.value
            # remove single or double quotes around the value, but not both
            if value.startswith("'"):
                value = value.strip("'")
            elif value.startswith('"'):
                value = value.strip('"')
            if _is_identifier(value):
                s = '%s.%s' % (self.visit(node.node), value)
                return s

        s = '%s[%s]' % (self.visit(node.node), self.visit(node.expr))
        return s

    def visit_FunctionCall(self, node):
        template = '%s(%s)'
        if getattr(node, '_parens', False):
            template = '(%s)' % template

        s = template % (self.visit(node.identifier),
                        ','.join(self.visit(arg) for arg in node.args))
        return s

    def visit_Object(self, node):
        s = '{%s}' % ','.join(self.visit(prop) for prop in node.properties)
        return s

    def visit_Array(self, node):
        s = '['
        length = len(node.items) - 1
        for index, item in enumerate(node.items):
            if isinstance(item, ast.Elision):
                s += ','
            elif index != length:
                s += self.visit(item) + ','
            else:
                s += self.visit(item)
        s += ']'
        return s

    def visit_This(self, node):
        return 'this'

