#!/usr/bin/env python3
#
# Copyright 2016 WebAssembly Community Group participants
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

'''
This fuzzes the relooper using the C API.
'''

from __future__ import print_function

import difflib
import os
import random
import subprocess
import time

seed_init = int(time.time())
seed_init *= seed_init
seed_init %= (2**32)

if os.environ.get('LD_LIBRARY_PATH'):
    os.environ['LD_LIBRARY_PATH'] += os.pathsep + 'lib'
else:
    os.environ['LD_LIBRARY_PATH'] = 'lib'

counter = 0

while True:
    # Random decisions
    seed = seed_init
    random.seed(seed)
    seed_init += 1
    num = random.randint(2, 250)
    density = random.random() * random.random()
    code_likelihood = random.random()
    code_max = random.randint(0, num if random.random() < 0.5 else 3)
    max_printed = random.randint(1, num if random.random() < 0.5 else 3)
    max_decision = num * 20
    decisions = [random.randint(1, max_decision) for x in range(num * 3)]
    branches = [0] * num
    defaults = [0] * num
    branch_codes = [0] * num  # code on the branch, which may alter the global state

    # with some probability print the same id for different blocks,
    # as the printing is the block contents - allow merging etc. opts
    def printed_id(i):
        if random.random() < 0.5:
            return i
        return i % max_printed

    printed_ids = [printed_id(i) for i in range(num)]

    def random_code():
        if code_max == 0 or random.random() > code_likelihood:
            return 0    # no code
        # A random number to perturb/increment the global state
        return random.randint(1, code_max)

    for i in range(num):
        b = set([])
        bs = random.randint(1, max(1,
                            round(density * random.random() * (num - 1))))
        for j in range(bs):
            b.add(random.randint(1, num - 1))
        b = list(b)
        defaults[i] = random.choice(b)
        b.remove(defaults[i])
        branches[i] = b
        branch_codes[i] = [random_code() for item in range(len(b) + 1)]  # one for each branch, plus the default
    optimize = random.random() < 0.5
    print(counter, ':', num, density, optimize, code_likelihood, code_max, max_printed, ', seed =', seed)
    counter += 1

    for temp in ['fuzz.wasm', 'fuzz.wast', 'fast.txt', 'fuzz.slow.js',
                 'fuzz.c']:
        try:
            os.unlink(temp)
        except OSError:
            pass

    # parts
    entry = '''
var label = 0;
var state;
var decisions = %s;
var index = 0;
function check() {
  if (index >= decisions.length) throw 'HALT';
  console.log('(i32.const ' + (-decisions[index]) + ')');
  return decisions[index++];
}
''' % str(decisions)

    slow = entry + '\n'
    slow += 'label = 0;\n'

    slow += '''
while(1) switch(label) {
'''

    fast = '''

#include <assert.h>
#include <stdio.h>

#include "binaryen-c.h"

// globals: address 4 is index
// decisions are at address 8+

int main() {
  BinaryenModuleRef module = BinaryenModuleCreate();

  // check()

  // if the end, halt
  BinaryenExpressionRef halter = BinaryenIf(module,
    BinaryenBinary(module,
      BinaryenGeUInt32(),
      BinaryenLoad(module, 4, 0, 0, 0, BinaryenTypeInt32(),
                   BinaryenConst(module, BinaryenLiteralInt32(4))),
      BinaryenConst(module, BinaryenLiteralInt32(4 * %d)) // jumps of 4 bytes
    ),
    BinaryenUnreachable(module),
    NULL
  );
  // increment index
  BinaryenExpressionRef incer = BinaryenStore(module,
    4, 0, 0,
    BinaryenConst(module, BinaryenLiteralInt32(4)),
    BinaryenBinary(module,
      BinaryenAddInt32(),
      BinaryenLoad(module, 4, 0, 0, 0, BinaryenTypeInt32(),
                   BinaryenConst(module, BinaryenLiteralInt32(4))),
      BinaryenConst(module, BinaryenLiteralInt32(4))
    ),
    BinaryenTypeInt32()
  );

  // optionally, print the return value
  BinaryenExpressionRef args[] = {
    BinaryenBinary(module,
      BinaryenSubInt32(),
      BinaryenConst(module, BinaryenLiteralInt32(0)),
      BinaryenLoad(module,
        4, 0, 4, 0, BinaryenTypeInt32(),
        BinaryenLoad(module, 4, 0, 0, 0, BinaryenTypeInt32(),
                     BinaryenConst(module, BinaryenLiteralInt32(4)))
      )
    )
  };
  BinaryenExpressionRef debugger;
  if (1) debugger = BinaryenCall(module, "print", args, 1,
                                 BinaryenTypeNone());
  else debugger = BinaryenNop(module);

  // return the decision. need to subtract 4 that we just added,
  // and add 8 since that's where we start, so overall offset 4
  BinaryenExpressionRef returner = BinaryenLoad(module,
    4, 0, 4, 0, BinaryenTypeInt32(),
    BinaryenLoad(module, 4, 0, 0, 0, BinaryenTypeInt32(),
    BinaryenConst(module, BinaryenLiteralInt32(4)))
  );
  BinaryenExpressionRef checkBodyList[] = { halter, incer, debugger,
                                            returner };
  BinaryenExpressionRef checkBody = BinaryenBlock(module,
    NULL, checkBodyList, sizeof(checkBodyList) / sizeof(BinaryenExpressionRef),
    BinaryenTypeInt32()
  );
  BinaryenFunctionTypeRef i = BinaryenAddFunctionType(module, "i",
                                                      BinaryenTypeInt32(),
                                                      NULL, 0);
  BinaryenAddFunction(module, "check", i, NULL, 0, checkBody);

  // contents of main() begin here

  RelooperRef relooper = RelooperCreate(module);

''' % len(decisions)

    for i in range(num):
        slow += '  case %d: console.log("(i32.const %d)"); state = check(); \n' % (
            i, printed_ids[i])
        b = branches[i]
        bc = branch_codes[i]

        def get_phi(j):
            phi = ''
            if bc[j]:
                phi = 'index += %d; ' % bc[j]
            return phi

        for j in range(len(b)):
            slow += '    if (state %% %d == %d) { %s label = %d; break }\n' % (
                len(b) + 1, j, get_phi(j), b[j])    # TODO: split range 1-n into these options
        slow += '    %slabel = %d; break\n' % (get_phi(-1), defaults[i])

    use_switch = [random.random() < 0.5 for i in range(num)]

    for i in range(num):
        fast += '''
  RelooperBlockRef b%d;
  {
    BinaryenExpressionRef args[] = {
      BinaryenConst(module, BinaryenLiteralInt32(%d))
    };
    BinaryenExpressionRef list[] = {
      BinaryenCall(module, "print", args, 1, BinaryenTypeNone()),
      BinaryenLocalSet(module, 0, BinaryenCall(module, "check", NULL, 0,
                                               BinaryenTypeInt32()))
    };
''' % (i, printed_ids[i])
        if use_switch[i]:
            fast += '''
    b%d = RelooperAddBlockWithSwitch(relooper,
      BinaryenBlock(module, NULL, list, 2, BinaryenTypeNone()),
      BinaryenBinary(module,
        BinaryenRemUInt32(),
        BinaryenLocalGet(module, 0, BinaryenTypeInt32()),
        BinaryenConst(module, BinaryenLiteralInt32(%d))
      )
    );
''' % (i, len(branches[i]) + 1)
        else:    # non-switch
            fast += '''
    b%d = RelooperAddBlock(relooper, BinaryenBlock(module, NULL, list, 2, BinaryenTypeNone()));
''' % i
        fast += '''
  }
'''

    for i in range(num):
        b = branches[i]
        bc = branch_codes[i]

        def get_phi(j):
            phi = 'NULL'
            if bc[j]:
                # increment the index of global state
                phi = '''
    BinaryenStore(module,
      4, 0, 0,
      BinaryenConst(module, BinaryenLiteralInt32(4)),
      BinaryenBinary(module,
        BinaryenAddInt32(),
        BinaryenLoad(module, 4, 0, 0, 0, BinaryenTypeInt32(),
                     BinaryenConst(module, BinaryenLiteralInt32(4))),
        BinaryenConst(module, BinaryenLiteralInt32(4 * %d))
      ),
      BinaryenTypeInt32()
    )''' % bc[j]
            return phi

        for j in range(len(b)):
            if use_switch[i]:
                total = len(b) + 1
                values = ','.join([str(x) for x in range(random.randint(len(b) + 1,
                                   max_decision + 2)) if x % total == j])
                fast += '''
  {
    BinaryenIndex values[] = { %s };
    RelooperAddBranchForSwitch(b%d, b%d, values,
                               sizeof(values) / sizeof(BinaryenIndex), %s);
  }
''' % (values, i, b[j], get_phi(j))
            else:    # non-switch
                fast += '''
  RelooperAddBranch(b%d, b%d, BinaryenBinary(module,
    BinaryenEqInt32(),
    BinaryenBinary(module,
      BinaryenRemUInt32(),
      BinaryenLocalGet(module, 0, BinaryenTypeInt32()),
      BinaryenConst(module, BinaryenLiteralInt32(%d))
    ),
    BinaryenConst(module, BinaryenLiteralInt32(%d))
  ), %s);
''' % (i, b[j], len(b) + 1, j, get_phi(j))
        # default branch
        if use_switch[i]:
            fast += '''
  RelooperAddBranchForSwitch(b%d, b%d, NULL, 0, %s);
''' % (i, defaults[i], get_phi(-1))
        else:
            fast += '''
  RelooperAddBranch(b%d, b%d, NULL, %s);
''' % (i, defaults[i], get_phi(-1))

    fast += '''
  BinaryenExpressionRef body = RelooperRenderAndDispose(relooper, b0, 1);

  int decisions[] = { %s };
  int numDecisions = sizeof(decisions)/sizeof(int);

  // write out all the decisions, then the body of the function
  BinaryenExpressionRef full[numDecisions + 1];

  {
    int i;
    for (i = 0; i < numDecisions; i++) {
      full[i] = BinaryenStore(module,
        4, 0, 0,
        BinaryenConst(module, BinaryenLiteralInt32(8 + 4 * i)),
        BinaryenConst(module, BinaryenLiteralInt32(decisions[i])),
        BinaryenTypeInt32()
      );
    }
  }
  full[numDecisions] = body;
  BinaryenExpressionRef all = BinaryenBlock(module, NULL, full,
                                            numDecisions + 1,
                                            BinaryenTypeNone());

  BinaryenFunctionTypeRef v = BinaryenAddFunctionType(module, "v",
                                                      BinaryenTypeNone(),
                                                      NULL, 0);
  // locals: state, free-for-label
  BinaryenType localTypes[] = { BinaryenTypeInt32(), BinaryenTypeInt32() };
  BinaryenFunctionRef theMain = BinaryenAddFunction(module, "main", v,
                                                    localTypes, 2, all);
  BinaryenSetStart(module, theMain);

  // import

  BinaryenType iparams[] = { BinaryenTypeInt32() };
  BinaryenFunctionTypeRef vi = BinaryenAddFunctionType(module, "vi",
                                                       BinaryenTypeNone(),
                                                       iparams, 1);
  BinaryenAddFunctionImport(module, "print", "spectest", "print", vi);

  // memory
  BinaryenSetMemory(module, 1, 1, "mem", NULL, NULL, NULL, 0, 0);

  // optionally, optimize
  if (%d) BinaryenModuleOptimize(module);

  assert(BinaryenModuleValidate(module));

  // write it out

  BinaryenModulePrint(module);

  BinaryenModuleDispose(module);

  return 0;
}
''' % (', '.join(map(str, decisions)), optimize)

    slow += '}'

    open('fuzz.slow.js', 'w').write(slow)
    open('fuzz.c', 'w').write(fast)

    print('.')
    cmd = [os.environ.get('CC') or 'gcc', 'fuzz.c', '-Isrc',
           '-lbinaryen', '-lasmjs',
           '-lsupport', '-Llib/.', '-pthread', '-o', 'fuzz']
    subprocess.check_call(cmd)
    print('^')
    subprocess.check_call(['./fuzz'], stdout=open('fuzz.wast', 'w'))
    print('*')
    fast_out = subprocess.Popen(['bin/wasm-shell', 'fuzz.wast'],
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE).communicate()[0]
    print('-')
    node = os.getenv('NODE', 'nodejs')
    slow_out = subprocess.Popen([node, 'fuzz.slow.js'],
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE).communicate()[0]
    print('_')

    if slow_out != fast_out:
        print(''.join([a.rstrip() + '\n' for a in difflib.unified_diff(
            slow_out.split('\n'),
            fast_out.split('\n'),
            fromfile='slow',
            tofile='fast')]))
        assert False
