###############################################################################
#
# 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"
