# vim: set ts=8 sts=4 et sw=4 tw=99:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import os, re
import tempfile
import subprocess
import sys, math
import datetime
import random

def realpath(k):
    return os.path.realpath(os.path.normpath(k))

class UCTNode:
    def __init__(self, loop):
        self.children = None
        self.loop = loop
        self.visits = 1
        self.score = 0

    def addChild(self, child):
        if self.children == None:
            self.children = []
        self.children.append(child)

    def computeUCB(self, coeff):
        return (self.score / self.visits) + math.sqrt(coeff / self.visits)

class UCT:
    def __init__(self, benchmark, bestTime, enableLoops, loops, fd, playouts):
        self.bm = benchmark
        self.fd = fd
        self.numPlayouts = playouts
        self.maxNodes = self.numPlayouts * 20
        self.loops = loops
        self.enableLoops = enableLoops
        self.maturityThreshold = 20
        self.originalBest = bestTime
        self.bestTime = bestTime
        self.bias = 20
        self.combos = []
        self.zobrist = { }
        random.seed()

    def expandNode(self, node, pending):
        for loop in pending:
            node.addChild(UCTNode(loop))
            self.numNodes += 1
            if self.numNodes >= self.maxNodes:
                return False
        return True

    def findBestChild(self, node):
        coeff = self.bias * math.log(node.visits)
        bestChild = None
        bestUCB = -float('Infinity')

        for child in node.children:
            ucb = child.computeUCB(coeff)
            if ucb >= bestUCB:
                bestUCB = ucb
                bestChild = child

        return child

    def playout(self, history):
        queue = []
        for i in range(0, len(self.loops)):
            queue.append(random.randint(0, 1))
        for node in history:
            queue[node.loop] = not self.enableLoops
        zash = 0
        for i in range(0, len(queue)):
            if queue[i]:
                zash |= (1 << i)
        if zash in self.zobrist:
            return self.zobrist[zash]

        self.bm.generateBanList(self.loops, queue)
        result = self.bm.treeSearchRun(self.fd, ['-m', '-j'], 3)
        self.zobrist[zash] = result
        return result

    def step(self, loopList):
        node = self.root
        pending = loopList[:]
        history = [node]

        while True:
            # If this is a leaf node...
            if node.children == None:
                # And the leaf node is mature...
                if node.visits >= self.maturityThreshold:
                    # If the node can be expanded, keep spinning.
                    if self.expandNode(node, pending) and node.children != None:
                        continue

                # Otherwise, this is a leaf node. Run a playout.
                score = self.playout(history)
                break

            # Find the best child.
            node = self.findBestChild(node)
            history.append(node)
            pending.remove(node.loop)

        # Normalize the score.
        origScore = score
        score = (self.originalBest - score) / self.originalBest

        for node in history:
            node.visits += 1
            node.score += score

        if int(origScore) < int(self.bestTime):
            print('New best score: {0:f}ms'.format(origScore))
            self.combos = [history]
            self.bestTime = origScore
        elif int(origScore) == int(self.bestTime):
            self.combos.append(history)

    def run(self):
        loopList = [i for i in range(0, len(self.loops))]
        self.numNodes = 1
        self.root = UCTNode(-1)
        self.expandNode(self.root, loopList)

        for i in range(0, self.numPlayouts):
            self.step(loopList)

        # Build the expected combination vector.
        combos = [ ]
        for combo in self.combos:
            vec = [ ]
            for i in range(0, len(self.loops)):
                vec.append(int(self.enableLoops))
            for node in combo:
                vec[node.loop] = int(not self.enableLoops)
            combos.append(vec)

        return [self.bestTime, combos]

class Benchmark:
    def __init__(self, JS, fname):
        self.fname = fname
        self.JS = JS
        self.stats = { }
        self.runList = [ ]

    def run(self, fd, eargs):
        args = [self.JS]
        args.extend(eargs)
        args.append(fd.name)
        return subprocess.check_output(args).decode()

    #    self.stats[name] = { }
    #    self.runList.append(name)
    #    for line in output.split('\n'):
    #        m = re.search('line (\d+): (\d+)', line)
    #        if m:
    #            self.stats[name][int(m.group(1))] = int(m.group(2))
    #        else:
    #            m = re.search('total: (\d+)', line)
    #            if m:
    #                self.stats[name]['total'] = m.group(1)

    def winnerForLine(self, line):
        best = self.runList[0]
        bestTime = self.stats[best][line]
        for run in self.runList[1:]:
            x = self.stats[run][line]
            if x < bestTime:
                best = run
                bestTime = x
        return best

    def chart(self):
        sys.stdout.write('{0:7s}'.format(''))
        sys.stdout.write('{0:15s}'.format('line'))
        for run in self.runList:
            sys.stdout.write('{0:15s}'.format(run))
        sys.stdout.write('{0:15s}\n'.format('best'))
        for c in self.counters:
            sys.stdout.write('{0:10d}'.format(c))
            for run in self.runList:
                sys.stdout.write('{0:15d}'.format(self.stats[run][c]))
            sys.stdout.write('{0:12s}'.format(''))
            sys.stdout.write('{0:15s}'.format(self.winnerForLine(c)))
            sys.stdout.write('\n')

    def preprocess(self, lines, onBegin, onEnd):
        stack = []
        counters = []
        rd = open(self.fname, 'rt')
        for line in rd:
            if re.search('\/\* BEGIN LOOP \*\/', line):
                stack.append([len(lines), len(counters)])
                counters.append([len(lines), 0])
                onBegin(lines, len(lines))
            elif re.search('\/\* END LOOP \*\/', line):
                old = stack.pop()
                onEnd(lines, old[0], len(lines))
                counters[old[1]][1] = len(lines)
            else:
                lines.append(line)
        return [lines, counters]

    def treeSearchRun(self, fd, args, count = 5):
        total = 0
        for i in range(0, count):
            output = self.run(fd, args)
            total += int(output)
        return total / count

    def generateBanList(self, counters, queue):
        if os.path.exists('/tmp/permabans'):
            os.unlink('/tmp/permabans')
        fd = open('/tmp/permabans', 'wt')
        for i in range(0, len(counters)):
            for j in range(counters[i][0], counters[i][1] + 1):
                fd.write('{0:d} {1:d}\n'.format(j, int(queue[i])))
        fd.close()

    def internalExhaustiveSearch(self, params):
        counters = params['counters']

        # iterative algorithm to explore every combination
        ncombos = 2 ** len(counters)
        queue = []
        for c in counters:
            queue.append(0)

        fd = params['fd']
        bestTime = float('Infinity')
        bestCombos = []

        i = 0
        while i < ncombos:
            temp = i
            for j in range(0, len(counters)):
                queue[j] = temp & 1
                temp = temp >> 1
            self.generateBanList(counters, queue)

            t = self.treeSearchRun(fd, ['-m', '-j'])
            if (t < bestTime):
                bestTime = t
                bestCombos = [queue[:]]
                print('New best time: {0:f}ms'.format(t))
            elif int(t) == int(bestTime):
                bestCombos.append(queue[:])

            i = i + 1

        return [bestTime, bestCombos]

    def internalTreeSearch(self, params):
        fd = params['fd']
        methodTime = params['methodTime']
        tracerTime = params['tracerTime']
        combinedTime = params['combinedTime']
        counters = params['counters']

        # Build the initial loop data.
        # If the method JIT already wins, disable tracing by default.
        # Otherwise, enable tracing by default.
        if methodTime < combinedTime:
            enableLoops = True
        else:
            enableLoops = False

        enableLoops = False

        uct = UCT(self, combinedTime, enableLoops, counters[:], fd, 50000)
        return uct.run()

    def treeSearch(self):
        fd, counters = self.ppForTreeSearch()

        os.system("cat " + fd.name + " > /tmp/k.js")

        if os.path.exists('/tmp/permabans'):
            os.unlink('/tmp/permabans')
        methodTime = self.treeSearchRun(fd, ['-m'])
        tracerTime = self.treeSearchRun(fd, ['-j'])
        combinedTime = self.treeSearchRun(fd, ['-m', '-j'])

        #Get a rough estimate of how long this benchmark will take to fully compute.
        upperBound = max(methodTime, tracerTime, combinedTime)
        upperBound *= 2 ** len(counters)
        upperBound *= 5    # Number of runs
        treeSearch = False
        if (upperBound < 1000):
            print('Estimating {0:d}ms to test, so picking exhaustive '.format(int(upperBound)) +
                  'search.')
        else:
            upperBound = int(upperBound / 1000)
            delta = datetime.timedelta(seconds = upperBound)
            if upperBound < 180:
                print('Estimating {0:d}s to test, so picking exhaustive '.format(int(upperBound)))
            else:
                print('Estimating {0:s} to test, so picking tree search '.format(str(delta)))
                treeSearch = True

        best = min(methodTime, tracerTime, combinedTime)

        params = {
                    'fd': fd,
                    'counters': counters,
                    'methodTime': methodTime,
                    'tracerTime': tracerTime,
                    'combinedTime': combinedTime
                 }

        print('Method JIT:  {0:d}ms'.format(int(methodTime)))
        print('Tracing JIT: {0:d}ms'.format(int(tracerTime)))
        print('Combined:    {0:d}ms'.format(int(combinedTime)))

        if 1 and treeSearch:
            results = self.internalTreeSearch(params)
        else:
            results = self.internalExhaustiveSearch(params)

        bestTime = results[0]
        bestCombos = results[1]
        print('Search found winning time {0:d}ms!'.format(int(bestTime)))
        print('Combos at this time: {0:d}'.format(len(bestCombos)))

        #Find loops that traced every single time
        for i in range(0, len(counters)):
            start = counters[i][0]
            end = counters[i][1]
            n = len(bestCombos)
            for j in bestCombos:
                n -= j[i]
            print('\tloop @ {0:d}-{1:d} traced {2:d}% of the time'.format(
                    start, end, int(n / len(bestCombos) * 100)))

    def ppForTreeSearch(self):
        def onBegin(lines, lineno):
            lines.append('GLOBAL_THINGY = 1;\n')
        def onEnd(lines, old, lineno):
            lines.append('GLOBAL_THINGY = 1;\n')

        lines = ['var JINT_START_TIME = Date.now();\n',
                 'var GLOBAL_THINGY = 0;\n']

        lines, counters = self.preprocess(lines, onBegin, onEnd)
        fd = tempfile.NamedTemporaryFile('wt')
        for line in lines:
            fd.write(line)
        fd.write('print(Date.now() - JINT_START_TIME);\n')
        fd.flush()
        return [fd, counters]

    def preprocessForLoopCounting(self):
        def onBegin(lines, lineno):
            lines.append('JINT_TRACKER.line_' + str(lineno) + '_start = Date.now();\n')

        def onEnd(lines, old, lineno):
            lines.append('JINT_TRACKER.line_' + str(old) + '_end = Date.now();\n')
            lines.append('JINT_TRACKER.line_' + str(old) + '_total += ' + \
                         'JINT_TRACKER.line_' + str(old) + '_end - ' + \
                         'JINT_TRACKER.line_' + str(old) + '_start;\n')

        lines, counters = self.preprocess(onBegin, onEnd)
        fd = tempfile.NamedTemporaryFile('wt')
        fd.write('var JINT_TRACKER = { };\n')
        for c in counters:
            fd.write('JINT_TRACKER.line_' + str(c) + '_start = 0;\n')
            fd.write('JINT_TRACKER.line_' + str(c) + '_end = 0;\n')
            fd.write('JINT_TRACKER.line_' + str(c) + '_total = 0;\n')
        fd.write('JINT_TRACKER.begin = Date.now();\n')
        for line in lines:
            fd.write(line)
        fd.write('JINT_TRACKER.total = Date.now() - JINT_TRACKER.begin;\n')
        for c in self.counters:
            fd.write('print("line ' + str(c) + ': " + JINT_TRACKER.line_' + str(c) +
                           '_total);')
        fd.write('print("total: " + JINT_TRACKER.total);')
        fd.flush()
        return fd

if __name__ == '__main__':
    script_path = os.path.abspath(__file__)
    script_dir = os.path.dirname(script_path)
    test_dir = os.path.join(script_dir, 'tests')
    lib_dir = os.path.join(script_dir, 'lib')

    # The [TESTS] optional arguments are paths of test files relative
    # to the jit-test/tests directory.

    from optparse import OptionParser
    op = OptionParser(usage='%prog [options] JS_SHELL test')
    (OPTIONS, args) = op.parse_args()
    if len(args) < 2:
        op.error('missing JS_SHELL and test argument')
    # We need to make sure we are using backslashes on Windows.
    JS = realpath(args[0])
    test = realpath(args[1])

    bm = Benchmark(JS, test)
    bm.treeSearch()
    # bm.preprocess()
    # bm.run('mjit', ['-m'])
    # bm.run('tjit', ['-j'])
    # bm.run('m+tjit', ['-m', '-j'])
    # bm.chart()

