#!/usr/bin/env python3
# Copyright 2017 The Emscripten Authors.  All rights reserved.
# Emscripten is available under two separate licenses, the MIT license and the
# University of Illinois/NCSA Open Source License.  Both these licenses can be
# found in the LICENSE file.

'''
Reproducer Rewriter
===================

Processes a project and rewrites it so as to generate deterministic,
reproducible automatic results. For example, you can run this on a
game, and then when the game is run it will record user input and
sources of nondeterminism like Math.random(). You can then run
that recording as a benchmark or as a demo, it should give nearly
identical results every time it is run to the data that was
recorded.

Usage:

1. Run this script as

    reproduceriter.py IN_DIR OUT_DIR FIRST_JS [WINDOW_LOCATION] [ON_IDLE]

   IN_DIR should be the project directory, and OUT_DIR will be
   created with the instrumented code (OUT_DIR will be overwritten
   if it exists). FIRST_JS should be a path (relative to IN_DIR) to
   the first JavaScript file loaded by the project (this tool
   will add code to that). The last two parameters, WINDOW_LOCATION
   and ON_IDLE, are relevant for shell builds. If WINDOW_LOCATION is
   specified, we will make a build that runs in the shell and not in
   a browser. WINDOW_LOCATION is the fake window.location we set in the
   fake DOM, and ON_IDLE is code that runs when the fake main browser
   event loop runs out of actions. (Note that only a browser build can
   do recording, shell builds just replay.)

   You will need to call

    Recorder.start();

   at the right time to start the relevant event loop. For
   example, if your application is a game that starts receiving
   events when in fullscreen, add something like

    if (typeof Recorder !== 'undefined') Recorder.start();

   in the button that launches fullscreen. start() will start
   either recording when in record mode, or replaying when
   in replay mode, so you need this in both modes.

2. Run the instrumented project in OUR_DIR and interact with
   the program. When you are done recording, open the web
   console and run

    Recorder.finish();

   This will write out the recorded data into the current tab.
   Save it as

    repro.data

   in OUT_DIR.

3. To re-play the recorded data, run the instrumented build
   with

    &reproduce=repro.data

   Note that as mentioned above you need to call

    Recorder.start();

   when the recorded event loop should start to replay.

Notes:

 * When we start to replay events, the assumption is that
   there is nothing asynchronous that affects execution. So
   asynchronous loading of files should have already
   completed.

   TODO: start running recorded events with some trigger, for example the fullscreen button in BananaBread

Examples

 * BananaBread: Unpack into a directory called bb, then one
   directory up, run

    emscripten/tools/reproduceriter.py bb bench js/game-setup.js game.html?low,low,reproduce=repro.data "function(){ print('triggering click'); document.querySelector('.fullscreen-button.low-res').callEventListeners('click'); window.onIdle = null; }"

   for a shell build, or

    emscripten/tools/reproduceriter.py bb bench js/game-setup.js

   for a browser build. Since only a browser build can do recording, you would normally
   make a browser build, record a trace, then make a shell build and copy the trace
   there so you can run it.

   The last parameter specifies what to do when the event loop is idle: We fire an event and then set onIdle (which was this function) to null, so this is a one-time occurence.

Notes

 * Replay can depend on browser state. One case is if you are replaying a fullscreen
   game with pointer lock, then you will need to manually allow pointer lock if it
   isn't already on for the machine. If you do it too early or too late, the replay
   can be different, since mousemove events mean different things depending on
   whether the pointer is locked or not.

'''

from __future__ import print_function
import os
import re
import sys
import shutil
from pathlib import Path

assert len(sys.argv) >= 4, 'Usage: reproduceriter.py IN_DIR OUT_DIR FIRST_JS [WINDOW_LOCATION]'

# Process input args

in_dir = sys.argv[1]
out_dir = sys.argv[2]
first_js = sys.argv[3]
window_location = sys.argv[4] if len(sys.argv) >= 5 else ''
on_idle = sys.argv[5] if len(sys.argv) >= 6 else ''

shell = not not window_location

dirs_to_drop = 0 if not os.path.dirname(first_js) else len(os.path.dirname(first_js).split('/'))

if os.path.exists(out_dir):
  shutil.rmtree(out_dir)
assert os.path.exists(os.path.join(in_dir, first_js))

# Copy project

print('copying tree...')

shutil.copytree(in_dir, out_dir)

# Add customizations in all JS files

print('add customizations...')

for parent, _, files in os.walk(out_dir):
  for filename in files:
    if filename.endswith('.js'):
      fullname = os.path.join(parent, filename)
      print('   ', fullname)
      with open(fullname) as fh:
        js = fh.read()
      js = re.sub(r'document\.on(\w+) ?= ?([\w.$]+)', lambda m: 'Recorder.onEvent("' + m.group(1) + '", ' + m.group(2) + ')', js)
      js = re.sub(r'''([\w.'"\[\]]+)\.addEventListener\(([\w,. $]+)\)''', lambda m: 'Recorder.addListener(' + m.group(1) + ', ' + m.group(2) + ')', js)
      Path(fullname).write_text(js)

# Add our boilerplate

print('add boilerplate...')

with open(os.path.join(out_dir, first_js), 'w') as fh1:
  fh1.write(
    (Path(os.path.dirname(os.path.dirname(__file__)), 'src', 'headless.js').read_text() % (
      window_location, window_location.split('?')[-1], on_idle or 'null', dirs_to_drop
    ) if shell else '') +
    Path(os.path.dirname(__file__), 'reproduceriter.js').read_text() +
    Path(in_dir, first_js).read_text() + ('\nwindow.runEventLoop();\n' if shell else '')
  )

print('done!')
