#    (C) Copyright 2015-2024 Sei Lisa. All rights reserved.
#
#    This file is part of LSL PyOptimizer.
#
#    LSL PyOptimizer 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 3 of the
#    License, or (at your option) any later version.
#
#    LSL PyOptimizer 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 LSL PyOptimizer. If not, see <http://www.gnu.org/licenses/>.

# Convert an abstract syntax tree + symbol table back to a script as text.

from lslopt import lslfuncs
from lslopt import lslcommon
from lslopt.lslcommon import Key, Vector, Quaternion, warning
from math import copysign
from strutil import *

debugScopes = False

class outscript(object):

    binary_operators = frozenset({'||','&&','^','|','&','==','!=','<','<=','>',
        '>=','<<','>>','+','-','*','/','%', '=', '+=', '-=', '*=', '/=','%=',
        })
    extended_assignments = frozenset({'&=', '|=', '^=', '<<=', '>>='})
    unary_operators = frozenset({'NEG', '!', '~'})
    op_priority = {'=':0, '+=':0, '-=':0, '*=':0, '/=':0, '%=':0, '&=':0,
        '|=':0, '^=':0, '<<=':0, '>>=':0,
        '||':1, '&&':1, '|':2, '^':3, '&':4, '==':5, '!=':5,
        '<':6, '<=':6, '>':6, '>=':6, '<<':7, '>>':7, '+':8, '-':8,# 'NEG':8,
        '*':9, '/':9, '%':9}#, '!':10, '~':10, '++':10, '--':10, }
    assignment_ops = frozenset({'=', '+=', '-=', '*=', '/=','%='})

    def Value2LSL(self, value):
        tvalue = type(value)
        if tvalue in (Key, unicode):
            pfx = sfx = ''
            if type(value) == Key:
                # Constants of type key can not be represented
                #raise lslfuncs.ELSLTypeMismatch
                # Actually they can be the result of folding.
                # On second thought, if we report the error, the location info
                # is lost. So we emit a warning instead, letting the compiler
                # report the error in the generated source.
                if self.globalmode and self.listmode:
                    warning(u"Illegal combo: Key type inside a global list")
                if self.listmode or not self.globalmode:
                    if self.globalmode:
                        pfx = '(key)'
                    else:
                        pfx = '((key)'
                        sfx = ')'
            if u'\t' in value and self.warntabs:
                warning(u"A string contains a tab. Tabs are expanded to four"
                         " spaces by the viewer when copy-pasting the code"
                         " (disable this warning by disabling the 'warntabs'"
                         " option).")
            return (pfx + '"' + u2str(unicode(value), 'utf8')
                .replace('\\','\\\\').replace('"','\\"').replace('\n','\\n')
                + '"' + sfx)
        if tvalue == int:
            if value < 0 and not self.globalmode and self.optsigns:
                #return '0x%X' % (value + 4294967296)
                return '((integer)' + str(value) + ')'
            return str(value)
        if tvalue == float:
            if self.optfloats and value.is_integer() and -2147483648.0 <= value < 2147483648.0:
                if self.globalmode and not self.listmode:
                    if value == 0 and copysign(1, value) == -1:
                        return '-0.'
                    return str(int(value))
                elif not self.globalmode:
                    # Important inside lists!!
                    if value == 0 and copysign(1, value) == -1:
                        return '(-(float)0)'
                    return '((float)' + str(int(value)) + ')'
            s = repr(value)
            if s == 'nan':
                return '(1e40*0)' if copysign(1, value) < 0 else '(-1e40*0)'
            if s == 'inf':
                return '1e40'
            if s == '-inf':
                return '-1e40' if self.globalmode else '((float)-1e40)'
            # Try to remove as many decimals as possible but keeping the F32 value intact
            exp = s.find('e')
            if ~exp:
                s, exp = s[:exp], s[exp:]
                if exp[1] == '+':
                    exp = exp[:1] + exp[2:]
                if '.' not in s:
                    # I couldn't produce one but it's assumed that if it happens,
                    # this code deals with it correctly
                    s += '.' # pragma: no cover
            else:
                if '.' not in s:
                    # This should never happen (Python should always return a point or exponent)
                    return s + '.' # pragma: no cover
                exp = ''

            # Shorten the float as much as possible.
            while s[-1] != '.' and lslfuncs.F32(float(s[:-1]+exp)) == value:
                s = s[:-1]
            if s[-1] != '.':
                news = s[:-1]
                neg = ''
                if s[0] == '-':
                    news = news[1:]
                    neg = '-'
                # Try harder
                point = news.index('.') + 1 - len(news)
                if point:
                    news = str(int(news[:point-1] + news[point:]) + 1).zfill(len(news)-1) # Increment
                else:
                    news = str(int(news[:-1])+1).zfill(len(news)-1)
                news = news[:point + len(news)] + '.' + news[point + len(news):] # Reinsert point
                # Repeat the operation with the incremented number
                while news[-1] != '.' and lslfuncs.F32(float(neg+news[:-1]+exp)) == value:
                    news = news[:-1]
                if len(neg+news) < len(s) and lslfuncs.F32(float(neg+news+exp)) == value:
                    # Success! But we try even harder. We may have converted
                    # 9.9999e3 into 10.e3; that needs to be turned into 1.e4.
                    if exp != '':
                        if news[2:3] == '.': # we converted 9.9... into 10.
                            newexp = 'e' + str(int(exp[1:])+1) # increase exponent
                            news2 = news[0] + '.' + news[1] + news[3:] # move dot to the left
                            while news2[-1] == '0': # remove trailing zeros
                                news2 = news2[:-1]
                            if len(neg+news2) < len(s) and lslfuncs.F32(float(neg+news2+newexp)) == value:
                                news = news2
                                exp = newexp
                    s = neg+news
            if exp and s[-1] == '.':
                s = s[:-1] # transfrom e.g. 1.e-30 into 1e-30
            if value >= 0 or self.globalmode or not self.optsigns:
                return s + exp
            return '((float)' + s + exp + ')'
        if tvalue == Vector:
            return '<' + self.Value2LSL(value[0]) + ', ' + self.Value2LSL(value[1]) \
                + ', ' + self.Value2LSL(value[2]) + '>'
        if tvalue == Quaternion:
            return '<' + self.Value2LSL(value[0]) + ', ' + self.Value2LSL(value[1]) \
                + ', ' + self.Value2LSL(value[2]) + ', ' + self.Value2LSL(value[3]) + '>'
        if tvalue == list:
            if value == []:
                return '[]'
            if len(value) < 5:
                save_listmode = self.listmode
                self.listmode = True
                ret = '[' + self.Value2LSL(value[0])
                for elem in value[1:]:
                    ret += ', ' + self.Value2LSL(elem)
                ret += ']'
                self.listmode = save_listmode
                return ret
            ret = '' if lslcommon.IsCalc else '\n'
            first = True
            self.indentlevel += 0 if lslcommon.IsCalc else 1
            for entry in value:
                ret += self.dent() + ('[ ' if first else ', ')
                save_listmode = self.listmode
                self.listmode = True
                ret += self.Value2LSL(entry) + '\n'
                self.listmode = save_listmode
                first = False
            ret += self.dent()
            self.indentlevel -= 0 if lslcommon.IsCalc else 1
            return ret + ']'

        assert False, u'Value of unknown type in Value2LSL: ' + repr(value)

    def dent(self):
        return self.indent * self.indentlevel

    def FindName(self, node, scope = None):
        if scope is None:
            # node is a node
            if (hasattr(node, 'scope')
                    and 'NewName' in self.symtab[node.scope][node.name]):
                return self.symtab[node.scope][node.name]['NewName']
            if node.nt == 'FNCALL' and 'NewName' in self.symtab[0][node.name]:
                return self.symtab[0][node.name]['NewName']

            return node.name
        # node is a name
        if 'NewName' in self.symtab[scope][node]:
            return self.symtab[scope][node]['NewName']
        return node

    def LeadMinus(self, node):
        """Tells whether a node starts with a minus sign"""
        # TODO: This may add redundant parentheses around --V.
        # More exactly, the line marked "!!!".
        # They are necessary to prevent wrong output (---v instead of -(--v))
        # but not always are they actually necessary. See regr/issue-31.lsl
        return (node.nt == 'NEG'
                or node.nt == '--V'  # !!!
                or not self.optsigns
                   and node.nt == 'CONST'
                   and node.t in {'integer', 'float'}
                   and copysign(1, node.value) == -1
               )

    def OutIndented(self, node):
        if node.nt != '{}':
            self.indentlevel += 1
        ret = self.OutCode(node)
        if node.nt != '{}':
            self.indentlevel -= 1
        return ret

    def OutExprList(self, L):
        ret = ''
        if L:
            First = True
            for item in L:
                if not First:
                    ret += ', '
                ret += self.OutExpr(item)
                First = False
        return ret

    def OutExpr(self, expr):
        # Handles expression nodes (as opposed to statement nodes)
        nt = expr.nt
        child = expr.ch

        if nt in self.binary_operators:
            lnt = child[0].nt
            lparen = False
            rnt = child[1].nt
            rparen = False
            if nt in self.assignment_ops and nt in self.op_priority:
                # Assignment is right-associative, so it needs to be dealt with
                # separately.
                base_pri = self.op_priority[nt]
                if rnt in self.op_priority:
                    if self.op_priority[rnt] < base_pri: # should never happen
                        rparen = True
            elif nt in self.op_priority:
                base_pri = self.op_priority[nt]
                if lnt in self.op_priority:
                    if self.op_priority[lnt] < base_pri:
                        lparen = True
                elif (self.LeadMinus(child[0]) and base_pri >
                      self.op_priority['-']
                     ):
                    lparen = True

                # This situation has ugly cases due to the strange precedence
                # of unary minus. Consider the following two statements, paying
                # attention to the binding power of the binary not ~ operator:
                #     (~-a) * a
                #     a * (~-a) * a
                # In one case, the (~-a) is a left child; in the other, it's
                # part of a right child. In both, cases, the parentheses are
                # mandatory, or they would be interpreted respectively as:
                #     ~-(a * a)
                #     a * ~-(a * a)
                # Yet the tree structure makes it quite hard to detect these.
                # We have to descend down the left nodes as we keep finding
                # binary operators or unary operators, to find whether the
                # symbol is followed by unary minus. If that's the case, we
                # need to act as if there was a negation right here.
                elif lnt in ('~', '!'):
                    lnode = child[0]
                    while True:
                        lnode = lnode.ch[0]
                        if (lnode.nt not in self.op_priority
                            and lnode.nt not in ('~', '!')
                           ):
                            break
                    if (self.LeadMinus(lnode) and base_pri >
                        self.op_priority['-']
                       ):
                        lparen = True

                if rnt in self.op_priority:
                    if self.op_priority[rnt] <= base_pri:
                        rparen = True
                elif (self.LeadMinus(child[1]) and self.op_priority['-'] <
                      base_pri
                     ):
                    rparen = True
                # see above
                elif rnt in ('~', '!'):
                    lnode = child[1]
                    while True:
                        # Descend down the left nodes of the right node, to
                        # find whether it's immediately followed by -
                        lnode = lnode.ch[0]
                        if lnode.nt not in self.op_priority and lnode.nt not in ('~', '!'):
                            break
                    if (self.LeadMinus(lnode) and base_pri >
                        self.op_priority['-']
                       ):
                        # TODO: Improve right operand parenthesis removal
                        #       for minus signs.
                        # Shouldn't this look into the RHS node?
                        #       a * -b * c
                        # is currently translated to
                        #       a * (-b * c)
                        # which is correct but not optimal.
                        rparen = True

            if lparen:
                ret = '(' + self.OutExpr(child[0]) + ')'
            else:
                ret = self.OutExpr(child[0])
            ret += ' ' + nt + ' '
            if rparen:
                ret += '(' + self.OutExpr(child[1]) + ')'
            else:
                ret += self.OutExpr(child[1])
            return ret

        if nt == 'IDENT':
            if debugScopes:
                return self.FindName(expr) + '\\' + str(expr.scope) + '/'
            return self.FindName(expr)

        if nt == 'CONST':
            if (self.foldconst and expr.t == 'list' and len(expr.value) == 1
                    and not self.globalmode):
                return '(list)' + self.Value2LSL(expr.value[0])
            return self.Value2LSL(expr.value)

        if nt == 'CAST' or self.foldconst and nt in ('LIST', 'CONST') and len(child)==1 and not self.globalmode:
            ret =  '(' + expr.t + ')'
            expr = child[0]
            if expr.nt in ('CONST', 'IDENT', 'V++', 'V--', 'VECTOR',
               'ROTATION', 'LIST', 'FIELD', 'PRINT', 'FNCALL'):
                if expr.nt != 'LIST' or len(expr.ch) != 1:
                    return ret + self.OutExpr(expr)
            return ret + '(' + self.OutExpr(expr) + ')'

        if nt == 'LIST':
            self.listmode = True
            if len(child) < 5:
                ret = '[' + self.OutExprList(child) + ']'
            else:
                self.indentlevel += 0 if lslcommon.IsCalc else 1
                ret = '' if lslcommon.IsCalc else '\n'
                first = True
                for elem in child:
                    ret += self.dent() + ('[ ' if first else ', ')
                    ret += self.OutExpr(elem) + '\n'
                    first = False
                ret += self.dent() + ']'
                self.indentlevel -= 0 if lslcommon.IsCalc else 1
            self.listmode = False
            return ret

        if nt in ('VECTOR', 'ROTATION'):
            ret = ('<' + self.OutExpr(child[0]) + ', '
                   + self.OutExpr(child[1]) + ', ')
            if nt == 'ROTATION':
                ret += self.OutExpr(child[2]) + ', '
            lnt = child[-1].nt
            if lnt in self.op_priority \
               and self.op_priority[lnt] <= self.op_priority['>']:
                ret += '(' + self.OutExpr(child[-1]) + ')'
            else:
                ret += self.OutExpr(child[-1])
            return ret + '>'

        if nt == 'FNCALL':
            return self.FindName(expr) + '(' + self.OutExprList(child) + ')'

        if nt == 'PRINT':
            return 'print(' + self.OutExpr(child[0]) + ')'

        if nt in self.unary_operators:
            ret = nt
            lnt = child[0].nt
            paren = False
            if nt == 'NEG':
                ret = '-'
                if (lnt == 'CONST' and child[0].t == 'integer'
                    and child[0].value < 0
                   ):
                    # shortcut
                    ret += str(child[0].value + 4294967296)
                    return ret
                if lnt in self.op_priority:
                    paren = self.op_priority[lnt] <= self.op_priority['-']
                elif (lnt == 'NEG' or lnt == '--V'
                      or lnt == 'CONST'
                         and child[0].t == 'float'
                         and child[0].value < 0
                     ):
                    ret += ' ' # don't output "--" as that's a different token
            else:
                if lnt in self.op_priority:
                    paren = True

            if paren:
                ret += '(' + self.OutExpr(child[0]) + ')'
            else:
                ret += self.OutExpr(child[0])
            return ret

        if nt == 'FLD':
            return self.OutExpr(child[0]) + '.' + expr.fld

        if nt in ('V--', 'V++'):
            return self.OutExpr(child[0]) + ('++' if nt == 'V++' else '--')

        if nt in ('--V', '++V'):
            return ('++' if nt == '++V' else '--') + self.OutExpr(child[0])

        if nt in self.extended_assignments:
            lvalue = self.OutExpr(child[0])
            return lvalue + ' = ' + lvalue + ' ' + nt[:-1] + ' (' + self.OutExpr(child[1]) + ')'

        if nt == 'EXPRLIST':
            return self.OutExprList(child)

        if nt == 'SUBIDX':
            return '(MISSING TYPE)' + self.OutExpr(child[0]) + '[' + self.OutExprList(child[1:]) + ']'

        assert False, 'Internal error: expression type "' + nt + '" not handled' # pragma: no cover

    def OutCode(self, node):
        nt = node.nt
        child = node.ch

        if nt == 'IF':
            ret = self.dent()
            while True:
                ret += 'if (' + self.OutExpr(child[0]) + ')\n'
                # Do we need to add braces around the THEN side?
                needs_braces = False
                if len(child) == 3:
                    testnode = child[1]
                    # Find last IF in an ELSE IF chain
                    while testnode.nt == 'IF' and len(testnode.ch) == 3:
                        testnode = testnode.ch[2]
                    if testnode.nt == 'IF':
                        # hit an IF without ELSE at the end of the chain
                        needs_braces = True
                if needs_braces:
                    ret += self.dent() + '{\n'
                    ret += self.OutIndented(child[1])
                    ret += self.dent() + '}\n'
                else:
                    ret += self.OutIndented(child[1])
                if len(child) < 3:
                    return ret
                if child[2].nt != 'IF':
                    ret += self.dent() + 'else\n' + self.OutIndented(child[2])
                    return ret
                ret += self.dent() + 'else '
                node = child[2]
                child = node.ch
        if nt == 'WHILE':
            ret = self.dent() + 'while (' + self.OutExpr(child[0]) + ')\n'
            ret += self.OutIndented(child[1])
            return ret
        if nt == 'DO':
            ret = self.dent() + 'do\n'
            ret += self.OutIndented(child[0])
            return ret + self.dent() + 'while (' + self.OutExpr(child[1]) + ');\n'
        if nt == 'FOR':
            ret = self.dent() + 'for ('
            ret += self.OutExpr(child[0])
            ret += '; ' + self.OutExpr(child[1]) + '; '
            ret += self.OutExpr(child[2])
            ret += ')\n'
            ret += self.OutIndented(child[3])
            return ret
        if nt == '@':
            if debugScopes:
                return self.dent() + '@' + self.FindName(node) \
                       + '\\' + str(node.scope) + '/;\n'
            return self.dent() + '@' + self.FindName(node) + ';\n'
        if nt == 'JUMP':
            if debugScopes:
                return self.dent() + 'jump ' + self.FindName(node) \
                       + '\\' + str(node.scope) + '/;\n'
            return self.dent() + 'jump ' + self.FindName(node) + ';\n'
        if nt == 'STSW':
            return self.dent() + 'state ' + self.FindName(node) + ';\n'
        if nt == 'RETURN':
            if child:
                return self.dent() + 'return ' + self.OutExpr(child[0]) + ';\n'
            return self.dent() + 'return;\n'
        if nt == 'DECL':
            ret = self.dent() + node.t + ' ' + self.FindName(node)
            if debugScopes:
                ret += '\\' + str(node.scope) + '/'
            if child:
                if hasattr(child[0], 'orig') and (child[0].orig.nt != 'IDENT'
                        or child[0].orig.name
                           in self.symtab[child[0].orig.scope]):
                    ret += ' = ' + self.OutExpr(child[0].orig)
                else:
                    ret += ' = ' + self.OutExpr(child[0])
            return ret + ';\n'
        if nt == ';':
            return self.dent() + ';\n'

        if nt in ('STDEF', '{}'):
            ret = ''
            if nt == 'STDEF':
                if node.name == 'default':
                    ret = self.dent() + 'default\n'
                else:
                    ret = self.dent() + 'state ' + self.FindName(node) + '\n'

            ret += self.dent() + '{\n'
            self.indentlevel += 1
            firstnode = True
            for stmt in node.ch:
                if stmt.nt == 'LAMBDA':
                    continue
                if nt == 'STDEF' and not firstnode:
                    ret += '\n'
                ret += self.OutCode(stmt)
                firstnode = False
            self.indentlevel -= 1
            return ret + self.dent() + '}\n'

        if nt == 'FNDEF':
            ret = self.dent()
            if node.t is not None:
                ret += node.t + ' '
            ret += self.FindName(node) + '('
            scope = node.pscope
            ret += ', '.join(typ + ' ' + self.FindName(name, scope)
                             for typ, name in zip(node.ptypes, node.pnames))
            return ret + ')\n' + self.OutCode(child[0])

        if nt == 'EXPR':
            return self.dent() + self.OutExpr(child[0]) + (
                ';\n' if not lslcommon.IsCalc else '')

        if nt == 'LAMBDA':
            return ''

        assert False, "Internal error: node type not handled: " + nt # pragma: no cover

    def output(self, treesymtab, options = ('optimize',
            'optsigns','optfloats','warntabs')):
        # Build a sorted list of dict entries
        self.tree, self.symtab = treesymtab

        # Grab options
        self.optimize = 'optimize' in options
        # These are optimization options that depend on the above:
        self.optsigns = self.optimize and 'optsigns' in options
        self.optfloats = self.optimize and 'optfloats' in options
        self.foldconst = self.optimize and 'constfold' in options

        self.warntabs = 'warntabs' in options

        ret = ''
        self.indent = '    '
        self.indentlevel = 0
        self.globalmode = False
        self.listmode = False
        firstnode = True
        prevnt = None
        for node in self.tree:
            if node.nt == 'LAMBDA':
                # these don't produce output, skip
                continue
            if not firstnode and (node.nt != 'DECL' or prevnt != 'DECL'):
                ret += '\n'
            self.globalmode = node.nt == 'DECL'
            ret += self.OutCode(node)
            self.globalmode = False
            firstnode = False
            prevnt = node.nt

        return ret
