/**  The Emularity; easily embed emulators
  *  Copyright © 2014-2016 Daniel Brooks <db48x@db48x.net>, Jason
  *  Scott <jscott@archive.org>, Grant Galitz <grantgalitz@gmail.com>,
  *  John Vilk <jvilk@cs.umass.edu>, and Tracey Jaquith <tracey@archive.org>
  *
  *  This program 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.
  *
  *  This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
  */

var Module = null;

(function (Promise) {
   /**
    * IALoader
    */
   function IALoader(canvas, game, callbacks, scale) {
     // IA actually gives us an object here, and we really ought to be
     // looking things up from it instead.
     if (typeof game !== 'string') {
       game = game.toString();
     }
     if (!callbacks || typeof callbacks !== 'object') {
       callbacks = { before_emulator: updateLogo,
                     before_run: callbacks };
     } else {
       if (typeof callbacks.before_emulator === 'function') {
         var func = callbacks.before_emulator;
         callbacks.before_emulator = function () {
                                       updateLogo();
                                       func();
                                     };
       } else {
         callbacks.before_emulator = updateLogo;
       }
     }

     function img(src) {
       var img = new Image();
       img.src = src;
       return img;
     }

     // yea, this is a hack
     var images;
     if (/archive\.org$/.test(document.location.hostname)) {
       images = { ia: img("/images/ialogo.png"),
                  mame: img("/images/mame.png"),
                  mess: img("/images/mame.png"),
                  dosbox: img("/images/dosbox.png"),
                  sae: img("/images/sae.png"),
                  pce: img("/images/pce.png"),
                  vice: img("/images/vice.svg"),
                  np2: img("/images/nekop2.gif"),
                  xmil: img("/images/xmillenium_logo.jpg"),
                  vmac: img("/images/vmac.png"),
                  ruffle: img("/images/ruffle.png"),
                  cloudpilot: img("/images/cloudpilot.png"),
                  v86: img("/images/v86.png"),
                };
     } else {
       images = { ia: img("other_logos/ia-logo-150x150.png"),
                  mame: img("other_logos/mame.png"),
                  mess: img("other_logos/mame.png"),
                  dosbox: img("other_logos/dosbox.png"),
                  sae: img("other_logos/sae.png"),
                  pce: img("other_logos/pce.png"),
                  vice: img("other_logos/vice.svg"),
                  np2: img("other_logos/nekop2.gif"),
                  xmil: img("other_logos/xmillenium_logo.jpg"),
                  vmac: img("other_logos/vmac.png"),
                  ruffle: img("other_logos/ruffle.png"),
                  cloudpilot: img("other_logos/cloudpilot.png"),
                  v86: img("other_logos/v86.png"),
                };
     }

     function updateLogo() {
       if (emulator_logo) {
         emulator.setSplashImage(emulator_logo);
       }
     }

     var SAMPLE_RATE = (function () {
                          var audio_ctx = window.AudioContext || window.webkitAudioContext || false;
                          if (!audio_ctx) {
                            return false;
                          }
                          var sample = new audio_ctx;
                          return sample.sampleRate.toString();
                        }());

     var metadata, filelist, module, modulecfg, config_args, emulator_logo,
         emulator = new Emulator(canvas).setSplashImage(images.ia)
                                        .setLoad(loadFiles)
                                        .setCallbacks(callbacks);

     var cfgr;
     function loadFiles(fetch_file, splash) {
       splash.setTitle("Downloading game metadata...");
       return new Promise(function (resolve, reject) {
                            var loading = fetch_file('Game Metadata',
                                                     get_meta_url(game),
                                                     'document');
                            loading.then(function (data) {
                                           metadata = data;
                                           splash.setTitle("Downloading game filelist...");
                                           return fetch_file('Game File List',
                                                             get_files_url(game),
                                                             'document', true);
                                         },
                                         function () {
                                           splash.setTitle("Failed to download IA item metadata!");
                                           splash.failed_loading = true;
                                           reject(1);
                                         })
                                   .then(function (data) {
                                           if (splash.failed_loading) {
                                             return null;
                                           }
                                           filelist = data;
                                           splash.setTitle("Downloading emulator metadata...");
                                           module = metadata.getElementsByTagName("emulator")
                                                            .item(0)
                                                            .textContent;
                                           return fetch_file('Emulator Metadata',
                                                             get_emulator_config_url(module),
                                                             'text', true);
                                         },
                                         function () {
                                           if (splash.failed_loading) {
                                             return;
                                           }
                                           splash.setTitle("Failed to download file list!");
                                           splash.failed_loading = true;
                                           reject(2);
                                         })
                                   .then(function (data) {
                                           if (splash.failed_loading) {
                                             return null;
                                           }

                                           modulecfg = JSON.parse(data);
                                           var get_files;

                                           if (module && module.indexOf("dosbox") === 0) {
                                             emulator_logo = images.dosbox;
                                             cfgr = DosBoxLoader;
                                             get_files = get_dosbox_files;
                                           }
                                           else if (module && module.indexOf("sae-") === 0) {
                                             emulator_logo = images.sae;
                                             cfgr = SAELoader;
                                             get_files = get_sae_files;
                                           }
                                           else if (module && module.indexOf("pce-") === 0) {
                                             emulator_logo = images.pce;
                                             cfgr = PCELoader;
                                             get_files = get_pce_files;
                                           }
                                           else if (module && module.indexOf("vice") === 0) {
                                             emulator_logo = images.vice;
                                             cfgr = VICELoader;
                                             get_files = get_vice_files;
                                           }
                                           else if (module && module.indexOf("np2-") === 0) {
                                             emulator_logo = images.np2;
                                             cfgr = NP2Loader;
                                             get_files = get_np2_files;
                                           }
                                           else if (module && module.indexOf("ruffle-") === 0) {
                                             emulator_logo = images.ruffle;
                                             cfgr = RuffleLoader;
                                             get_files = get_ruffle_files;
                                           }
                                           else if (module && module.indexOf("xmil-") === 0) {
                                             emulator_logo = images.xmil;
                                             cfgr = NP2Loader;
                                             get_files = get_xmil_files;
                                           }
                                           else if (module && module.indexOf("vmac-") === 0) {
                                             emulator_logo = images.vmac;
                                             cfgr = vMacLoader;
                                             get_files = get_vmac_files;
                                           }
                                           else if (module && module.indexOf("cloudpilot-") === 0) {
                                             emulator_logo = images.cloudpilot;
                                             cfgr = CloudpilotLoader;
                                             get_files = get_cloudpilot_files;
                                           }
                                           else if (module && module.indexOf("v86") === 0) {
                                             emulator_logo = images.v86;
                                             cfgr = V86Loader;
                                             get_files = get_v86_files;
                                           }
                                           else if (module) {
                                             emulator_logo = images.mame;
                                             cfgr = MAMELoader;
                                             get_files = get_mame_files;
                                           }
                                           else {
                                             throw new Error("Unknown module type "+ module +"; cannot configure the emulator.");
                                           }

                                           var wantsWASM = modulecfg.wasm_filename && 'WebAssembly' in window;
                                           var nr = modulecfg['native_resolution'];
                                           config_args = [cfgr.emulatorJS(get_js_url(wantsWASM ? modulecfg.wasmjs_filename : modulecfg.js_filename)),
                                                          cfgr.emulatorWASM(wantsWASM && get_js_url(modulecfg.wasm_filename)),
                                                          cfgr.locateAdditionalEmulatorJS(locateAdditionalJS),
                                                          cfgr.fileSystemKey(game),
                                                          cfgr.nativeResolution(nr[0], nr[1]),
                                                          cfgr.aspectRatio(nr[0] / nr[1]),
                                                          cfgr.scale(scale || modulecfg.scale || 2),
                                                          cfgr.sampleRate(SAMPLE_RATE)];

                                           if ('keepAspect' in cfgr) {
                                             config_args.push(cfgr.keepAspect(modulecfg.keepAspect));
                                           }

                                           if (/archive\.org$/.test(document.location.hostname)) {
                                             config_args.push(cfgr.muted(document.cookie.indexOf('unmute=1') < 0)) // we're muted, unless cookie 'unmute' is set
                                           }

                                           var emulator_start_item = metadata.getElementsByTagName("emulator_start").item(0);
                                           if (module && module.indexOf("dosbox") === 0) {
                                             config_args.push(cfgr.startExe(metadata.getElementsByTagName("emulator_start")
                                                                                    .item(0)
                                                                                    .textContent));
                                           } else if (module && module.indexOf("vice") === 0) {
                                             var vice_fliplist = [ metadata.getElementsByTagName("vice_drive_8_fliplist").item(0),
                                                                   metadata.getElementsByTagName("vice_drive_9_fliplist").item(0),
                                                                   metadata.getElementsByTagName("vice_drive_10_fliplist").item(0),
                                                                   metadata.getElementsByTagName("vice_drive_11_fliplist").item(0) ];
                                             if (emulator_start_item) {
                                               config_args.push(cfgr.autoLoad(emulator_start_item.textContent));
                                             }
                                             var fliplists = [];
                                             vice_fliplist.forEach(function (fliplist_meta) {
                                                                     if(!fliplist_meta) {
                                                                       fliplists.push(null);
                                                                     } else {
                                                                       fliplists.push(fliplist_meta.textContent.split(";"));
                                                                     }
                                                                   });
                                             config_args.push(cfgr.fliplist(fliplists));
                                             config_args.push(cfgr.extraArgs(modulecfg.extra_args));
                                           } else if (module && module.indexOf("sae-") === 0) {
                                             config_args.push(cfgr.model(modulecfg.driver),
                                                              cfgr.rom(modulecfg.bios_filenames));
                                           } else if (module && (module.indexOf("np2-") == 0 ||
                                                                 module.indexOf("xmil-") == 0 ||
                                                                 module.indexOf("vmac-") == 0)) {
                                             if (!emulator_start_item) {
                                               throw new Exception("Error: this item does not have an 'emulator_start' metadata value; I don't know what to run.");
                                             }
                                             config_args.push(cfgr.autoLoad('/emulator/'+ emulator_start_item.textContent),
                                                              cfgr.extraArgs(modulecfg.extra_args));
                                           } else if (module && module.indexOf("pce-") === 0) {
                                             config_args.push(cfgr.model(modulecfg.driver),
                                                              cfgr.extraArgs(modulecfg.extra_args));
                                           } else if (module && module.indexOf("cloudpilot-") === 0) { // Cloudpilot
                                             config_args.push(cfgr.roms(modulecfg.bios_filenames));
                                           } else if (module && module.indexOf("ruffle-") === 0) {
                                             modulecfg.config = modulecfg.config || {};
                                             modulecfg.config.base = get_cors_url(game);
                                           } else if (module && module.indexOf("v86") === 0) {
                                             config_args.push(cfgr.memorySize(modulecfg.memory_size),
                                                              cfgr.vgaMemorySize(modulecfg.vga_memory_size),
                                                              cfgr.acpi(modulecfg.acpi),
                                                              cfgr.bootOrder(modulecfg.boot_order));
                                           } else if (module) { // MAME
                                             config_args.push(cfgr.driver(modulecfg.driver),
                                                              cfgr.extraArgs(modulecfg.extra_args));
                                             if (emulator_start_item) {
                                               config_args.push(cfgr.autoboot(emulator_start_item.textContent));
                                             }
                                           }

                                           splash.setTitle("Downloading game data...");
                                           return Promise.all(get_files(cfgr, metadata, modulecfg, filelist));
                                         },
                                         function () {
                                           if (splash.failed_loading) {
                                             return;
                                           }
                                           splash.setTitle("Failed to download emulator metadata!");
                                           splash.failed_loading = true;
                                           reject(2);
                                         })
                                   .then(function (game_files) {
                                           if (splash.failed_loading) {
                                             return;
                                           }
                                           updateLogo();
                                           resolve(cfgr.apply(null, extend(config_args, game_files)));
                                         },
                                         function (e) {
                                           if (splash.failed_loading) {
                                             return;
                                           }
                                           splash.setTitle("Failed to configure emulator!");
                                           splash.failed_loading = true;
                                           reject(3);
                                         });
                          });
     }

     function locateAdditionalJS(filename) {
       if ("file_locations" in modulecfg && filename in modulecfg.file_locations) {
         return get_js_url(modulecfg.file_locations[filename]);
       }
       return get_js_url(filename);
     }

     function get_dosbox_files(cfgr, metadata, modulecfg, filelist) {
       var default_drive = "c", // pick any drive letter as a default
           drives = {}, files = [],
           meta = dict_from_xml(metadata);
       if (game && game.endsWith(".zip")) {
         drives[default_drive] = game;
       }
       files_with_ext_from_filelist(filelist, meta.emulator_ext).forEach(function (file, i) {
                                                                           drives[default_drive] = file.name;
                                                                         });
       meta_props_matching(meta, /^dosbox_drive_([a-zA-Z])$/).forEach(function (result) {
                                                                        var key = result[0], match = result[1];
                                                                        drives[match[1]] = meta[key];
                                                                      });
       var mounts = Object.keys(drives),
           len = mounts.length;
       mounts.forEach(function (drive, i) {
                        var title = "Game File ("+ (i+1) +" of "+ len +")",
                            filename = drives[drive],
                            url = (filename.includes("/")) ? get_zip_url(filename)
                                                           : get_zip_url(filename, get_item_name(game));
                            if (filename.toLowerCase().endsWith(".zip")) {
                              files.push(cfgr.mountZip(drive,
                                                       cfgr.fetchFile(title, url)));
                            } else {
                              files.push(cfgr.mountFile('/'+ filename,
                                                        cfgr.fetchFile(title, url)));
                            }
                      });
       return files;
     }

     function get_vice_files(cfgr, metadata, modulecfg, filelist) {
       var default_drive = "8",
           drives = {}, files = [], wanted_files = [],
           meta = dict_from_xml(metadata);
       files_with_ext_from_filelist(filelist, meta.emulator_ext).forEach(function (file, i) {
                                                                           wanted_files.push(file.name);
                                                                         });
       files_with_ext_from_filelist(filelist, "conf").forEach(function (file, i) {
                                                                           wanted_files.push(file.name);
                                                                         });
       meta_props_matching(meta, /^vice_drive_([89])$/).forEach(function (result) {
                                                                  var key = result[0], match = result[1];
                                                                  drives[match[1]] = meta[key];
                                                                });

       var len = wanted_files.length;
       wanted_files.forEach(function (file, i) {
                              var title = "Game File ("+ (i+1) +" of "+ len +")",
                                  filename = file,
                                  url = (filename.includes("/")) ? get_zip_url(filename)
                                                                 : get_zip_url(filename, get_item_name(game));
                              if (filename.toLowerCase().endsWith(".zip") && false) { // TODO: Enable and fix zip support.
                                files.push(cfgr.mountZip("", // TODO: This is a hack, no drive actually applicable here
                                                         cfgr.fetchFile(title, url)));
                              } else {
                                //TODO: ensure vice_drive_8 and vice_drive_9 actually function.
                                files.push(cfgr.mountFile('/'+ filename,
                                                          cfgr.fetchFile(title, url)));
                              }
                            });
       return files;
     }

     function get_mame_files(cfgr, metadata, modulecfg, filelist) {
       var files = [],
           bios_files = modulecfg['bios_filenames'],
           already_fetched_urls = [];
       bios_files.forEach(function (fname, i) {
                            if (fname) {
                              var title = "Bios File ("+ (i+1) +" of "+ bios_files.length +")";
                              var url = get_bios_url(fname);
                              files.push(cfgr.mountFile('/'+ fname,
                                                        cfgr.fetchFile(title, url)));
                              already_fetched_urls.push(url);
                            }
                          });

       var meta = dict_from_xml(metadata),
           peripherals = {},
           game_files_counter = {};
       files_with_ext_from_filelist(filelist, meta.emulator_ext).forEach(function (file, i) {
                                                                           game_files_counter[file.name] = 1;
                                                                           if (modulecfg.peripherals && modulecfg.peripherals[i]) {
                                                                             peripherals[modulecfg.peripherals[i]] = file.name;
                                                                           }
                                                                         });
       meta_props_matching(meta, /^mame_peripheral_([a-zA-Z0-9]+)$/).forEach(function (result) {
                                                                               var key = result[0], match = result[1];
                                                                               peripherals[match[1]] = meta[key];
                                                                               game_files_counter[meta[key]] = 1;
                                                                             });

       var game_files = Object.keys(game_files_counter),
           len = game_files.length;
       game_files.forEach(function (filename, i) {
                            var title = "Game File ("+ (i+1) +" of "+ len +")",
                                url = (filename.includes("/")) ? get_zip_url(filename)
                                                               : get_zip_url(filename, get_item_name(game));
                            files.push(cfgr.mountFile('/'+ filename,
                                                      cfgr.fetchFile(title, url)));
                            already_fetched_urls.push(url);
                          });

       // add on game drive (.chd) files, if any
       // chd files must go into a subdir named after the driver for mame to find them
       var drive_files = files_with_ext_from_filelist(filelist, 'chd');  // maybe 'chd' should be meta.drive_ext?
       len = drive_files.length;
       drive_files.forEach(function (file, i) {
                             var title = "Game Drive ("+ (i+1) +" of "+ len +")";
                             var url = (file.name.includes("/")) ? get_zip_url(file.name)
                                                                 : get_zip_url(file.name, get_item_name(game));
                             if (!already_fetched_urls.includes(url)) {
                               files.push(cfgr.mountFile(modulecfg.driver + '/' + file.name,
                                                         cfgr.fetchFile(title, url)));
                             }
                           });

       Object.keys(peripherals).forEach(function (periph) {
                                          files.push(cfgr.peripheral(periph,                // we're not pushing a 'file' here,
                                                                     peripherals[periph])); // but that's ok
                                        });

       files.push(cfgr.mountFile('/'+ modulecfg['driver'] + '.cfg',
                                 cfgr.fetchOptionalFile("CFG File",
                                                        get_other_emulator_config_url(module))));
       return files;
     }

     function get_sae_files(cfgr, metadata, modulecfg, filelist) {
       var files = [],
           bios_files = modulecfg['bios_filenames'];
       bios_files.forEach(function (fname, i) {
                            if (fname) {
                              var title = "Bios File ("+ (i+1) +" of "+ bios_files.length +")";
                              files.push(cfgr.mountFile('/'+ fname,
                                                        cfgr.fetchFile(title,
                                                                       get_bios_url(fname))));
                            }
                          });

       var meta = dict_from_xml(metadata),
           game_files = files_with_ext_from_filelist(filelist, meta.emulator_ext);
       game_files.forEach(function (file, i) {
                            if (file) {
                              var title = "Game File ("+ (i+1) +" of "+ game_files.length +")",
                                  url = (file.name.includes("/")) ? get_zip_url(file.name)
                                                                  : get_zip_url(file.name, get_item_name(game));
                              files.push(cfgr.mountFile('/'+ file.name,
                                                        cfgr.fetchFile(title, url)));
                              files.push(cfgr.floppy(0,             // we're not pushing a file here
                                                     file.name));   // but that's ok
                            }
                          });
       files.push(cfgr.mountFile('/'+ modulecfg['driver'] + '.cfg',
                                 cfgr.fetchOptionalFile("Config File",
                                                        get_other_emulator_config_url(module))));
       return files;
     }

     function get_ruffle_files(cfgr, metadata, modulecfg, filelist) {
       window.RufflePlayer = window.RufflePlayer || {};
       window.RufflePlayer.config = modulecfg.config;
       var files = [];
       var meta = dict_from_xml(metadata);
       var game_files = files_with_ext_from_filelist(filelist, meta.emulator_ext);

       if (game_files.length > 0) {
         var file = game_files[0]; // only allow one .swf file to be loaded
         var title = 'Downloading Game File';
         var url = (file.name.includes('/')) ? get_zip_url(file.name)
                                             : get_zip_url(file.name, get_item_name(game));
         files.push(cfgr.mountFile('/' + file.name, cfgr.fetchFile(title, url)));
         files.push(cfgr.swf_file_name('/' + file.name));
       }
       return files;
    }

     function get_pce_files(cfgr, metadata, modulecfg, filelist) {
       var files = [],
           bios_files = modulecfg['bios_filenames'];
       bios_files.forEach(function (fname, i) {
                            if (fname) {
                              var title = "ROM File ("+ (i+1) +" of "+ bios_files.length +")";
                              files.push(cfgr.mountFile('/'+ fname,
                                                        cfgr.fetchFile(title,
                                                                       get_bios_url(fname))));
                            }
                          });

       var meta = dict_from_xml(metadata),
           game_files_counter = {};
       files_with_ext_from_filelist(filelist, meta.emulator_ext).forEach(function (file, i) {
                                                                           if (modulecfg.peripherals && modulecfg.peripherals[i]) {
                                                                             game_files_counter[file.name] = modulecfg.peripherals[i];
                                                                           }
                                                                         });
       meta_props_matching(meta, /^pce_drive_([a-zA-Z0-9]+)$/).forEach(function (result) {
                                                                         var key = result[0], periph = result[1][1];
                                                                         game_files_counter[meta[key]] = periph;
                                                                       });

       var game_files = Object.keys(game_files_counter),
           len = game_files.length;
       game_files.forEach(function (filename, i) {
                            var title = "Game File ("+ (i+1) +" of "+ len +")",
                                ext = filename.match(/\.([^.]*)$/)[1],
                                url = (filename.includes("/")) ? get_zip_url(filename)
                                                               : get_zip_url(filename, get_item_name(game));
                            files.push(cfgr.mountFile('/'+ game_files_counter[filename] +'.'+ ext,
                                                      cfgr.fetchFile(title, url)));
                          });

       files.push(cfgr.mountFile('/pce-'+ modulecfg['driver'] + '.cfg',
                                 cfgr.fetchOptionalFile("Config File",
                                                        get_other_emulator_config_url("pce-"+ modulecfg['driver']))));
       return files;
     }

     // TODO(db48x): get_{np2,xmil,vmac}_files are even more
     // duplicative than the rest; time to factor this a lot
     function get_np2_files(cfgr, metadata, modulecfg, filelist) {
       var files = [],
           bios_files = modulecfg['bios_filenames'];
       bios_files.forEach(function (fname, i) {
                            if (fname) {
                              var title = "ROM File ("+ (i+1) +" of "+ bios_files.length +")",
                                  mounter = (fname.endsWith(".zip")) ? cfgr.mountZip
                                                                     : cfgr.mountFile;
                              files.push(mounter('np2',
                                                 cfgr.fetchFile(title, get_bios_url(fname))));
                            }
                          });
       var meta = dict_from_xml(metadata),
           peripherals = {},
           game_files_counter = {};
       files_with_ext_from_filelist(filelist, meta.emulator_ext).forEach(function (file, i) {
                                                                           game_files_counter[file.name] = 1;
                                                                         });

       var game_files = Object.keys(game_files_counter),
           len = game_files.length;
       game_files.forEach(function (filename, i) {
                            var title = "Game File ("+ (i+1) +" of "+ len +")",
                                url = (filename.includes("/")) ? get_zip_url(filename)
                                                               : get_zip_url(filename, get_item_name(game));
                            files.push(cfgr.mountFile('/'+ filename,
                                                      cfgr.fetchFile(title, url)));
                          });
       return files;
     }

     function get_xmil_files(cfgr, metadata, modulecfg, filelist) {
       var files = [],
           bios_files = modulecfg['bios_filenames'];
       bios_files.forEach(function (fname, i) {
                            if (fname) {
                              var title = "ROM File ("+ (i+1) +" of "+ bios_files.length +")",
                                  mounter = (fname.endsWith(".zip")) ? cfgr.mountZip
                                                                     : cfgr.mountFile;
                              files.push(mounter('xmil',
                                                 cfgr.fetchFile(title, get_bios_url(fname))));
                            }
                          });
       var meta = dict_from_xml(metadata),
           peripherals = {},
           game_files_counter = {};
       files_with_ext_from_filelist(filelist, meta.emulator_ext).forEach(function (file, i) {
                                                                           game_files_counter[file.name] = 1;
                                                                         });

       var game_files = Object.keys(game_files_counter),
           len = game_files.length;
       game_files.forEach(function (filename, i) {
                            var title = "Game File ("+ (i+1) +" of "+ len +")",
                                url = (filename.includes("/")) ? get_zip_url(filename)
                                                               : get_zip_url(filename, get_item_name(game));
                            files.push(cfgr.mountFile('/'+ filename,
                                                      cfgr.fetchFile(title, url)));
                          });
       return files;
     }

     function get_vmac_files(cfgr, metadata, modulecfg, filelist) {
       var files = [],
           bios_files = modulecfg['bios_filenames'];
       bios_files.forEach(function (fname, i) {
                            if (fname) {
                              var title = "ROM File ("+ (i+1) +" of "+ bios_files.length +")",
                                  mounter = (fname.endsWith(".zip")) ? cfgr.mountZip
                                                                     : cfgr.mountFile;
                              files.push(mounter('minivmac',
                                                 cfgr.fetchFile(title, get_bios_url(fname))));
                            }
                          });
       var meta = dict_from_xml(metadata),
           peripherals = {},
           game_files_counter = {};
       files_with_ext_from_filelist(filelist, meta.emulator_ext).forEach(function (file, i) {
                                                                           game_files_counter[file.name] = 1;
                                                                         });
       meta_props_matching(meta, /^vmac_drive_([0-9]+)$/).forEach(function (result) {
                                                                    var key = result[0], periph = result[1][1];
                                                                    game_files_counter[meta[key]] = periph;
                                                                  });

       var game_files = Object.keys(game_files_counter),
           len = game_files.length;
       game_files.forEach(function (filename, i) {
                            var title = "Disk Image ("+ (i+1) +" of "+ len +")",
                                url = (filename.includes("/")) ? get_zip_url(filename)
                                                               : get_zip_url(filename, get_item_name(game));
                            files.push(cfgr.mountFile('/'+ filename,
                                                      cfgr.fetchFile(title, url)));
                          });
       return files;
     }

     function get_cloudpilot_files(cfgr, metadata, modulecfg, filelist) {
       var files = [];
       bios_files = modulecfg['bios_filenames'];
       bios_files.forEach(function (fname, i) {
                            if (fname) {
                              var title = "Bios File ("+ (i+1) +" of "+ bios_files.length +")";
                              files.push(cfgr.mountFile('/'+ fname,
                                                        cfgr.fetchFile(title,
                                                                       get_bios_url(fname))));
                            }
                          });
       var meta = dict_from_xml(metadata);
       var game_files = files_with_ext_from_filelist(filelist, meta.emulator_ext);
       var len = game_files.length;
       if (game_files.length > 0) {
         var file = game_files[0]; // only allow one .swf file to be loaded
         var title = 'Downloading Game File';
         var url = (file.name.includes('/')) ? get_zip_url(file.name)
                                             : get_zip_url(file.name, get_item_name(game));
         files.push(cfgr.mountFile('/' + file.name, cfgr.fetchFile(title, url)));
         files.push(cfgr.prc(file.name));
       }
       return files;
    }

     function get_v86_files(cfgr, metadata, modulecfg, filelist) {
       var files = [];

       if (modulecfg['bios_filename']) {
         files.push(cfgr.mountFile('/' + modulecfg['bios_filename'], cfgr.fetchFile("BIOS File", get_bios_url(modulecfg['bios_filename']))));
         files.push(cfgr.bios(modulecfg['bios_filename']));
       }
       if (modulecfg['vga_bios_filename']) {
         files.push(cfgr.mountFile('/' + modulecfg['vga_bios_filename'], cfgr.fetchFile("VGA BIOS File", get_bios_url(modulecfg['vga_bios_filename']))));
         files.push(cfgr.vgaBios(modulecfg['vga_bios_filename']));
       }

       var meta = dict_from_xml(metadata),
           game_files_counter = {};
       files_with_ext_from_filelist(filelist, meta.emulator_ext).forEach(function (file, i) {
                                                                           if (modulecfg.peripherals && modulecfg.peripherals[i]) {
                                                                             game_files_counter[file.name] = modulecfg.peripherals[i];
                                                                           }
                                                                         });
       meta_props_matching(meta, /^v86_drive_([a-zA-Z0-9]+)$/).forEach(function (result) {
                                                                         var key = result[0], periph = result[1][1];
                                                                         game_files_counter[meta[key]] = periph;
                                                                       });

       var game_files = Object.keys(game_files_counter),
           len = game_files.length;
       game_files.forEach(function (filename, i) {
                            var title = "Game File ("+ (i+1) +" of "+ len +")",
                                ext = filename.match(/\.([^.]*)$/)[1],
                                url = (filename.includes("/")) ? get_zip_url(filename)
                                                               : get_zip_url(filename, get_item_name(game)),
                                periph = game_files_counter[filename],
                                path = '/' + periph + '.' + ext,
                                periph_cfg = {};
                            periph_cfg[periph] = {"path": path};
                            files.push(cfgr.mountFile(path,
                                                      cfgr.fetchFile(title, url)));
                            files.push(periph_cfg);
                          });

      return files;
     }

     var get_item_name = function (game_path) {
       return game_path.split('/').shift();
     };

     var get_game_name = function (game_path) {
       return game_path.split('/').pop();
     };

     // NOTE: deliberately use cors.archive.org since this will 302 rewrite to iaXXXXX.us.archive.org/XX/items/...
     // and need to keep that "artificial" extra domain-ish name to avoid CORS issues with IE/Safari  (tracey@archive)
     var get_cors_url = function(item, path) {
       return '//cors.archive.org/cors/' + item + (path ? '/' + path : '');
     }

     var get_emulator_config_url = function (module) {
       return get_cors_url('emularity_engine_v1', module + '.json');
     };

     var get_other_emulator_config_url = function (module) {
       return get_cors_url('emularity_config_v1', module + '.cfg');
     };

     var get_meta_url = function (game_path) {
       var path = game_path.split('/');
       return get_cors_url(path[0], path[0] + "_meta.xml");
     };

     var get_files_url = function (game_path) {
       var path = game_path.split('/');
       return get_cors_url(path[0], path[0] +"_files.xml");
     };

     var get_zip_url = function (game_path, item_path) {
       if (item_path) {
         return get_cors_url(item_path, game_path);
       }
       return get_cors_url(game_path);
     };

     var get_js_url = function (js_filename) {
       return get_cors_url('emularity_engine_v1', js_filename);
     };

     var get_bios_url = function (bios_filename) {
       return get_cors_url('emularity_bios_v1', bios_filename);
     };

     function mountat (drive) {
       return function (data) {
         return { drive: drive,
                  mountpoint: "/" + drive,
                  data: data
                };
       };
     }

     return emulator;
   }

   /**
    * BaseLoader
    */
   function BaseLoader() {
     return Array.prototype.reduce.call(arguments, extend);
   }

   BaseLoader.canvas = function (id) {
     var elem = id instanceof Element ? id : document.getElementById(id);
     return { canvas: elem };
   };

   BaseLoader.emulatorJS = function (url) {
     return { emulatorJS: url };
   };

   BaseLoader.emulatorWASM = function (url) {
     return { emulatorWASM: url };
   };

   BaseLoader.locateAdditionalEmulatorJS = function (func) {
     return { locateAdditionalJS: func };
   };

   BaseLoader.fileSystemKey = function (key) {
     return { fileSystemKey: key };
   };

   BaseLoader.nativeResolution = function (width, height) {
     if (typeof width !== 'number' || typeof height !== 'number')
       throw new Error("Width and height must be numbers");
     return { nativeResolution: { width: Math.floor(width), height: Math.floor(height) } };
   };

   BaseLoader.aspectRatio = function (ratio) {
     if (typeof ratio !== 'number')
       throw new Error("Aspect ratio must be a number");
     return { aspectRatio: ratio };
   };

   BaseLoader.scale = function (scale) {
     return { scale: scale };
   };

   BaseLoader.sampleRate = function (rate) {
     return { sample_rate: rate };
   };

   BaseLoader.muted = function (muted) {
     return { muted: muted };
   };

   BaseLoader.mountZip = function (drive, file) {
     return { files: [{ drive: drive,
                        mountpoint: "/" + drive,
                        file: file
                      }] };
   };

   BaseLoader.mountFile = function (filename, file) {
     return { files: [{ mountpoint: filename,
                        file: file
                      }] };
   };

   BaseLoader.fetchFile = function (title, url) {
     return { title: title, url: url, optional: false };
   };

   BaseLoader.fetchOptionalFile = function (title, url) {
     return { title: title, url: url, optional: true };
   };

   BaseLoader.localFile = function (title, data) {
     return { title: title, data: data };
   };

   /**
    * DosBoxLoader
    */
   function DosBoxLoader() {
     var config = Array.prototype.reduce.call(arguments, extend);
     config.emulator_arguments = build_dosbox_arguments(config.emulatorStart, config.files, config.extra_dosbox_args);
     config.runner = EmscriptenRunner;
     return config;
   }
   DosBoxLoader.__proto__ = BaseLoader;

   DosBoxLoader.startExe = function (path) {
     return { emulatorStart: path };
   };

   DosBoxLoader.extraArgs = function (args) {
     return { extra_dosbox_args: args };
   };

   DosBoxLoader.mountZip = function (drive, file, drive_type) {
     //  driver type: hdd, floppy, cdrom
     return { files: [{ drive: drive,
                        mountpoint: "/" + drive,
                        file: file,
                        drive_type: drive_type || "hdd",
                      }] };
   };

   /**
    * PC98DosBoxLoader
    */
   function PC98DosBoxLoader() {
    var config = Array.prototype.reduce.call(arguments, extend);
    config.emulator_arguments = build_dosbox_arguments(config.emulatorStart, config.files, config.extra_dosbox_args);
    config.runner = PC98DosBoxRunner;
    return config;
  }
  PC98DosBoxLoader.__proto__ = DosBoxLoader;

   /**
    * MAMELoader
    */
   function MAMELoader() {
     var config = Array.prototype.reduce.call(arguments, extend);
     config.emulator_arguments = build_mame_arguments(config.muted, config.mame_driver,
                                                      config.nativeResolution, config.sample_rate,
                                                      config.peripheral, config.autoboot,
                                                      config.extra_mame_args, config.keep_aspect,
                                                      config.scale);
     config.runner = MAMERunner;
     return config;
   }
   MAMELoader.__proto__ = BaseLoader;

   MAMELoader.driver = function (driver) {
     return { mame_driver: driver };
   };

   MAMELoader.peripheral = function (peripheral, game) {
     var p = {};
     p[peripheral] = [game];
     return { peripheral: p };
   };

   MAMELoader.keepAspect = function (keep) {
     return { keep_aspect: !!keep };
   };

   MAMELoader.extraArgs = function (args) {
     return { extra_mame_args: args };
   };

   MAMELoader.autoboot = function (path) {
    return { autoboot: path };
   };

   /**
    * VICELoader
    */
    function VICELoader() {
      var config = Array.prototype.reduce.call(arguments, extend);
      if (config.fliplist) {
          VICELoader._create_fliplist_file(config.files, config.fliplist);
      }
      config.emulator_arguments = build_vice_arguments(config.emulatorStart, config.files, config.fliplist, config.extra_vice_args);
      config.runner = EmscriptenRunner;
      return config;
    }
    VICELoader.__proto__ = BaseLoader;

    VICELoader.autoLoad = function (path) {
     return { emulatorStart: path };
    };
    VICELoader.extraArgs = function (args) {
      return { extra_vice_args: args };
    };
    VICELoader.fliplist = function(fliplist) {
        return { fliplist: fliplist };
    };
    VICELoader._create_fliplist_file = function(files, fliplists) {
       var fliplist = "# Vice fliplist file\n\n";
       fliplists.forEach(function(drive_fliplist, i) {
           if(drive_fliplist) {
               drive_fliplist = drive_fliplist.reverse();
               fliplist += "UNIT " + (i + 8).toString() + "\n";
               drive_fliplist.forEach(function(disk_image) {
                   fliplist += "/emulator/" + disk_image + "\n";
               });
           }
       });
       files.push(VICELoader.mountFile('/metadata_fliplist.vfl', VICELoader.localFile("Fliplist", fliplist)).files[0]);
    };

   /**
    * SAELoader
    */

   function SAELoader() {
     var config = Array.prototype.reduce.call(arguments, extend);
     config.runner = SAERunner;
     return config;
   }
   SAELoader.__proto__ = BaseLoader;

   SAELoader.model = function (model) {
     return { amigaModel: model };
   };

   SAELoader.fastMemory = function (megabytes) {
     return { fast_memory: megabytes << 20 };
   };

   SAELoader.rom = function (filenames) {
     if (typeof filenames == "string")
       filenames = [filenames];
     return { rom: filenames[0], extRom: filenames[1] };
   };

   SAELoader.floppy = function (index, filename) {
     var f = {};
     f[index] = filename;
     return { floppy: f };
   };

   SAELoader.ntsc = function (v) {
     return { ntsc: !!v };
   };

   /**
    * PCELoader
    */

   function PCELoader() {
     var config = Array.prototype.reduce.call(arguments, extend);
     config.emulator_arguments = ["-c", "/emulator/pce-"+ config.pceModel +".cfg"];
     if (config.extra_pce_args && config.extra_pce_args.length > 0) {
       config.emulator_arguments = config.emulator_arguments.concat(config.extra_pce_args);
     }
     config.runner = EmscriptenRunner;
     return config;
   }
   PCELoader.__proto__ = BaseLoader;

   PCELoader.model = function (model) {
     return { pceModel: model };
   };

   PCELoader.extraArgs = function (args) {
     return { extra_pce_args: args };
   };

   /**
    * RuffleLoader
    */
   function RuffleLoader () {
     var config = Array.prototype.reduce.call(arguments, extend);
     config.runner = RuffleRunner;
     return config;
   }

   RuffleLoader.__proto__ = BaseLoader;

   RuffleLoader.swf_file_name = function (file_name) {
     return { swf_file_name: file_name };
   };

   /**
    * NP2Loader
    * for the NEC PC-9800
    *
    * TODO(db48x): Supposedly NP2, xmil, and Mini vMac were all by the
    * same author? Looking at it again I’m not so sure. Anyway, they
    * were all super simple to configure so this code is largely
    * shared between them.
    */
   function NP2Loader() {
     var config = Array.prototype.reduce.call(arguments, extend);
     if (!config.emulatorStart) {
       throw new Error("You must specify an autoLoad value in order to start this emulator. Try the name of the disk image.");
     }
     config.emulator_arguments = build_np2_arguments(config.emulatorStart, config.files, config.extra_np2_args);
     config.runner = NP2Runner;
     return config;
   }
   NP2Loader.__proto__ = BaseLoader;

   NP2Loader.autoLoad = function (path) {
     return { emulatorStart: path };
   };
   NP2Loader.extraArgs = function (args) {
     return { extra_np2_args: args };
   };

   /**
    * vMacLoader
    * early Mac computers on 680x0
    */
   function vMacLoader() {
     var config = Array.prototype.reduce.call(arguments, extend);
     if (!config.emulatorStart) {
       throw new Error("You must specify an autoLoad value in order to start this emulator. Try the name of the disk image.");
     }
     config.emulator_arguments = build_vmac_arguments(config.emulatorStart, config.files, config.extra_np2_args);
     config.runner = NP2Runner;
     return config;
   }
   vMacLoader.__proto__ = NP2Loader;

   /**
    * CloudpiltLoader
    */
   function CloudpilotLoader() {
     var config = Array.prototype.reduce.call(arguments, extend);
     config.runner = CloudpilotRunner;
     return config;
   }
   CloudpilotLoader.roms = function (filenames) {
     if (typeof filenames == "string") {
       filenames = [filenames];
     }
     var roms = {};
     // Assume one .bin file (Palm BIOS ROM), and one .img file (freshly booted session image).
     roms.bios = filenames.find((f) => f.match(/\.bin$/i));
     roms.session = filenames.find((f) => f.match(/\.img$/i));
     return roms;
   };
   CloudpilotLoader.prc = function (filename) {
     return { prc: filename };
   };
   CloudpilotLoader.__proto__ = BaseLoader;

   /**
    * V86Loader
    */
   function V86Loader() {
     var config = Array.prototype.reduce.call(arguments, extend);
     config.memory_size = config.memory_size || 32;
     config.vga_memory_size = config.vga_memory_size || 2;
     config.boot_order = config.boot_order || 0x213;
     config.runner = V86Runner;
     return config;
   }
   V86Loader.__proto__ = BaseLoader;

   V86Loader.bios = function (filename) {
     return {"bios": {"path": filename}};
   };

   V86Loader.vgaBios = function (filename) {
     return {"vga_bios": {"path": filename}};
   };

   V86Loader.fda = function (filename) {
     return {"fda": {"path": filename}};
   };

   V86Loader.fdb = function (filename) {
     return {"fdb": {"path": filename}};
   };

   V86Loader.hda = function (filename) {
     return {"hda": {"path": filename}};
   };

   V86Loader.hdb = function (filename) {
     return {"hda": {"path": filename}};
   };

   V86Loader.cdrom = function (filename) {
     return {"cdrom": {"path": filename}};
   };

   V86Loader.bootOrder = function (order) {
     return {"boot_order": order};
   };

   V86Loader.acpi = function (enabled) {
     return {"acpi": !!enabled};
   };

   V86Loader.memorySize = function (amount) {
     return {"memory_size": amount};
   };

   V86Loader.vgaMemorySize = function (amount) {
     return {"vga_memory_size": amount};
   };

   var build_mame_arguments = function (muted, driver, native_resolution, sample_rate, peripheral, autoboot, extra_args, keepaspect, scale) {
     scale = scale || 1;
     var args = [driver,
                 '-verbose',
                 '-rompath', 'emulator',
                 '-window',
                 keepaspect ? '-keepaspect' : '-nokeepaspect'];

     if (native_resolution && "width" in native_resolution && "height" in native_resolution) {
       args.push('-resolution', [native_resolution.width * scale, native_resolution.height * scale].join('x'));
     }

     if (sample_rate) {
       args.push('-samplerate', sample_rate);
     }

     if (autoboot) {
       args.push('-autoboot_command', autoboot+'\\n', '-autoboot_delay', '2');
     }

     if (extra_args) {
       args = args.concat(extra_args);
     }

     if (peripheral) {
       for (var p in peripheral) {
         if (Object.prototype.propertyIsEnumerable.call(peripheral, p)) {
           args.push('-' + p,
                     '/emulator/'+ (peripheral[p][0].replace(/\//g,'_')));
         }
       }
     }

     return args;
   };

   var build_dosbox_arguments = function (emulator_start, files, extra_args) {
     var args = ['-conf', '/emulator/dosbox.conf'];

     var len = files.length;
     for (var i = 0; i < len; i++) {
       if ('drive' in files[i]) {
        //  See also https://www.dosbox.com/wiki/MOUNT
         if(files[i].drive_type==='hdd'){
          args.push('-c', 'mount '+ files[i].drive +' /emulator'+ files[i].mountpoint);
         }
         else if(files[i].drive_type==='floppy'){
          args.push('-c', 'mount '+ files[i].drive +' /emulator'+ files[i].mountpoint + ' -t floppy');
         }
         else if(files[i].drive_type==='cdrom'){
          args.push('-c', 'mount '+ files[i].drive +' /emulator'+ files[i].mountpoint + ' -t cdrom');
         }
       }
     }

     if (extra_args) {
       args = args.concat(extra_args);
     }

     var path = emulator_start.split(/\\|\//); // I have LTS already
     args.push('-c', /^[a-zA-Z]:$/.test(path[0]) ? path.shift() : 'c:');
     var prog = path.pop();
     if (path && path.length)
       args.push('-c', 'cd '+ path.join('/'));
     args.push('-c', prog);

     return args;
   };

   var build_vice_arguments = function (emulator_start, files, fliplist, extra_args) {
     var args = emulator_start ? ["-autostart", "/emulator/" + emulator_start] : [];
     if (fliplist[0] || fliplist[1] || fliplist[2] || fliplist[3]) {
       args = args.concat(["-flipname", "/emulator/metadata_fliplist.vfl"]);
     }
     if (extra_args) {
       args = args.concat(extra_args);
     }
     return args;
   };

   var build_np2_arguments = function (emulator_start, files, extra_args) {
     var args = emulator_start ? [emulator_start] : [];
     if (extra_args) {
       args = args.concat(extra_args);
     }
     return args;
   };

   var build_vmac_arguments = function (emulator_start, files, extra_args) {
     var args = files.map((f) => '/emulator' + f.mountpoint)
                     .filter((f) => f != emulator_start && f != '/emulator/minivmac');
     if (emulator_start) {
       args.unshift(emulator_start);
     }
     if (extra_args) {
       args = args.concat(extra_args);
     }
     return args;
   };

   /*
    * EmscriptenRunner
    */
   function EmscriptenRunner(canvas, game_data) {
     var self = this;
     this._canvas = canvas;
     this._hooks = { start: [], reset: [] };
     // This is somewhat wrong, because our Emscripten-based emulators
     // are currently compiled to start immediately when their js file
     // is loaded.
     Module = { arguments: game_data.emulator_arguments,
                screenIsReadOnly: true,
                print: function (text) { console.log(text); },
                printErr: function (text) { console.log(text); },
                canvas: canvas,
                noInitialRun: false,
                locateFile: game_data.locateAdditionalJS,
                wasmBinary: game_data.wasmBinary,
                preInit: function () {
                           // Re-initialize BFS to just use the writable in-memory storage.
                           BrowserFS.initialize(game_data.fs);
                           var BFS = new BrowserFS.EmscriptenFS();
                           // Mount the file system into Emscripten.
                           FS.mkdir('/emulator');
                           FS.mount(BFS, {root: '/'}, '/emulator');
                         },
                preRun: [function () {
                            self._hooks.start.forEach(function (f) {
                                                        //try {
                                                          f && f();
                                                        //} catch(x) {
                                                        //  console.warn(x);
                                                        //}
                                                      });
                          }]
              };
   }

   EmscriptenRunner.prototype.start = function () {
   };

   EmscriptenRunner.prototype.pause = function () {
   };

   EmscriptenRunner.prototype.stop = function () {
   };

   EmscriptenRunner.prototype.mute = function () {
     try {
       if (!SDL_PauseAudio)
         SDL_PauseAudio = Module.cwrap('SDL_PauseAudio', '', ['number']);
       SDL_PauseAudio(true);
     } catch (x) {
       console.log("Unable to change audio state:", x);
     }
   };

   EmscriptenRunner.prototype.unmute = function () {
     try {
       if (!SDL_PauseAudio)
         SDL_PauseAudio = Module.cwrap('SDL_PauseAudio', '', ['number']);
       SDL_PauseAudio(false);
     } catch (x) {
       console.log("Unable to change audio state:", x);
     }
   };

   EmscriptenRunner.prototype.onStarted = function (func) {
     this._hooks.start.push(func);
   };

   EmscriptenRunner.prototype.onReset = function (func) {
     this._hooks.reset.push(func);
   };

   EmscriptenRunner.prototype.requestFullScreen = function () {
     this._canvas.requestFullscreen();
   };

   /*
    * PC98DosBoxRunner
    */
   function PC98DosBoxRunner() {
     return EmscriptenRunner.apply(this, arguments);
   }
   PC98DosBoxRunner.prototype = Object.create(EmscriptenRunner.prototype);
   PC98DosBoxRunner.prototype.start = function () {
     FS.symlink('/emulator/y/FONT.ROM', '/FONT.ROM');
     FS.symlink('/emulator/y/2608_bd.wav', '/2608_bd.wav');
     FS.symlink('/emulator/y/2608_hh.wav', '/2608_hh.wav');
     FS.symlink('/emulator/y/2608_sd.wav', '/2608_sd.wav');
     FS.symlink('/emulator/y/2608_rim.wav', '/2608_rim.wav');
     FS.symlink('/emulator/y/2608_tom.wav', '/2608_tom.wav');
     FS.symlink('/emulator/y/2608_top.wav', '/2608_top.wav');
   };

   /*
   * NP2Runner
   */
   function NP2Runner() {
     return EmscriptenRunner.apply(this, arguments);
   }
   NP2Runner.prototype = Object.create(EmscriptenRunner.prototype);
   NP2Runner.prototype.start = function () {
     try {
       var configFile = FS.readFile('/emulator/np2.cfg');
       FS.writeFile('/emulator/np2/np2.cfg', configFile);
     } catch (ex) {
       //If the user config file not found, NP2 will use default settings
       console.log(ex);
     }
   };

   /*
    * MAMERunner
    */
   function MAMERunner() {
     return EmscriptenRunner.apply(this, arguments);
   }
   MAMERunner.prototype = Object.create(EmscriptenRunner.prototype,
                                        { mute: { value: function () {
                                                           var machine = Module.__ZN15running_machine30emscripten_get_running_machineEv();
                                                           var soundmgr = Module.__ZN15running_machine20emscripten_get_soundEv(machine);
                                                           Module.__ZN13sound_manager4muteEbh(soundmgr, true, 0x02); // MUTE_REASON_UI
                                                         },
                                                },
                                          unmute: { value: function () {
                                                             var machine = Module.__ZN15running_machine30emscripten_get_running_machineEv();
                                                             var soundmgr = Module.__ZN15running_machine20emscripten_get_soundEv(machine);
                                                             Module.__ZN13sound_manager4muteEbh(soundmgr, false, 0x02); // MUTE_REASON_UI
                                                           },
                                                  },
                                        });

   /*
    * SAERunner
    */
   function SAERunner(canvas, game_data) {
     this._sae = new ScriptedAmigaEmulator();
     this._cfg = this._sae.getConfig();
     this._canvas = canvas;

     var model = null;
     switch (game_data.amigaModel) {
       case "A500": model = SAEC_Model_A500; break;
       case "A500P": model = SAEC_Model_A500P; break;
       case "A600": model = SAEC_Model_A600; break;
       case "A1000": model = SAEC_Model_A1000; break;
       case "A1200": model = SAEC_Model_A1200; break;
       case "A2000": model = SAEC_Model_A2000; break;
       case "A3000": model = SAEC_Model_A3000; break;
       case "A4000": model = SAEC_Model_A4000; break;
       case "A4000T": model = SAEC_Model_A4000T; break;
       /*  future. do not use. cd-emulation is not implemented yet.
       case "CDTV": model = SAEC_Model_CDTV; break;
       case "CD32": model = SAEC_Model_CD32; break; */
     }
     this._sae.setModel(model, 0);
     this._cfg.memory.z2FastSize = game_data.fastMemory || 2 << 20;
     this._cfg.floppy.speed = SAEC_Config_Floppy_Speed_Turbo;
     this._cfg.video.id = canvas.getAttribute("id");

     if (game_data.nativeResolution && game_data.nativeResolution.height == 360 && game_data.nativeResolution.width == 284)
     {
       this._cfg.video.hresolution = SAEC_Config_Video_HResolution_LoRes;
       this._cfg.video.vresolution = SAEC_Config_Video_VResolution_NonDouble;
       this._cfg.video.size_win.width = SAEC_Video_DEF_AMIGA_WIDTH; /* 360 */
       this._cfg.video.size_win.height = SAEC_Video_DEF_AMIGA_HEIGHT; /* 284 */
     }
     else if (game_data.nativeResolution && game_data.nativeResolution.height == 1440 && game_data.nativeResolution.width == 568)
     {
       this._cfg.video.hresolution = SAEC_Config_Video_HResolution_SuperHiRes;
       this._cfg.video.vresolution = SAEC_Config_Video_VResolution_Double;
       this._cfg.video.size_win.width = SAEC_Video_DEF_AMIGA_WIDTH << 2; /* 1440 */
       this._cfg.video.size_win.height = SAEC_Video_DEF_AMIGA_HEIGHT << 1; /* 568 */
     }
     else
     {
       this._cfg.video.hresolution = SAEC_Config_Video_HResolution_HiRes;
       this._cfg.video.vresolution = SAEC_Config_Video_VResolution_Double;
       this._cfg.video.size_win.width = SAEC_Video_DEF_AMIGA_WIDTH << 1; /* 720 */
       this._cfg.video.size_win.height = SAEC_Video_DEF_AMIGA_HEIGHT << 1; /* 568 */
     }

     this._cfg.memory.rom.name = game_data.rom;
     this._cfg.memory.rom.data = game_data.fs.readFileSync('/'+game_data.rom, null, flag_r);
     this._cfg.memory.rom.size = this._cfg.memory.rom.data.length;

     if (game_data.extRom) {
       this._cfg.memory.extRom.name = game_data.extRom;
       this._cfg.memory.extRom.data = game_data.fs.readFileSync('/'+game_data.extRom, null, flag_r);
       this._cfg.memory.extRom.size = this._cfg.memory.extRom.data.length;
     }

     for (var i = 0; i < Object.keys(game_data.floppy).length; i++) {
       this._cfg.floppy.drive[i].file.name = game_data.floppy[i];
       this._cfg.floppy.drive[i].file.data = game_data.fs.readFileSync('/' + game_data.floppy[i], null, flag_r);
       this._cfg.floppy.drive[i].file.size = this._cfg.floppy.drive[i].file.data.length;
     }
   }

   SAERunner.prototype.start = function () {
     var err = this._sae.start();
   };

   SAERunner.prototype.pause = function () {
     this._sae.pause();
   };

   SAERunner.prototype.stop = function () {
     this._sae.stop();
   };

   SAERunner.prototype.mute = function () {
     var err = this._sae.mute(true);
     if (err) {
       console.warn("unable to mute; SAE error number", err);
     }
   };

   SAERunner.prototype.unmute = function () {
     var err = this._sae.mute(false);
     if (err) {
       console.warn("unable to unmute; SAE error number", err);
     }
   };

   SAERunner.prototype.onStarted = function (func) {
     this._cfg.hook.event.started = func;
   };

   SAERunner.prototype.onReset = function (func) {
     this._cfg.hook.event.reseted = func;
   };

   SAERunner.prototype.requestFullScreen = function () {
     getfullscreenenabler().call(this._canvas);
   };

   /*
    * V86Runner
    */
   function V86Runner(canvas, game_data) {
     // v86 needs a specific DOM structure instead of a canvas
     var screenContainerOuterElt = document.createElement("div");
     screenContainerOuterElt.id = canvas.id;
     screenContainerOuterElt.classList = canvas.classList;
     screenContainerOuterElt.style = canvas.style;

     var screenContainerInnerElt = document.createElement("div");
     screenContainerInnerElt.classList = ["emularity-v86-screen-container"];
     screenContainerInnerElt.style = "display:flex;justify-content:center;align-items:center;background-color:#000;";

     var textDivElt = document.createElement("div");
     textDivElt.classList = ["emularity-v86-screen-text"];
     textDivElt.style = "font-size:14px;font-family:monospace;line-height:14px;white-space:pre;";
     var canvasElt = document.createElement("canvas");
     canvasElt.classList = ["emularity-v86-screen-canvas"];
     canvasElt.style = "display:none;";

     screenContainerInnerElt.appendChild(textDivElt);
     screenContainerInnerElt.appendChild(canvasElt);
     screenContainerOuterElt.appendChild(screenContainerInnerElt);
     canvas.parentNode.replaceChild(screenContainerOuterElt, canvas);

     var cfg = {};
     cfg.screen_container = screenContainerInnerElt;
     cfg.memory_size = Math.floor(game_data.memory_size * 1024 * 1024);
     cfg.vga_memory_size = Math.floor(game_data.vga_memory_size * 1024 * 1024);
     cfg.acpi = game_data.acpi;
     cfg.boot_order = game_data.boot_order;

     cfg.autostart = true;
     cfg.wasm_fn = env => {
       return new Promise(async resolve => {
         const wasm = await WebAssembly.instantiate(game_data.wasmBinary, env);
         resolve(wasm.instance.exports);
       });
     };

     ["bios", "vga_bios", "fda", "fdb", "cdrom", "hda", "hdb"].forEach(key => {
       if (game_data[key] && game_data[key]["path"]) {
         cfg[key] = { buffer: game_data.fs.readFileSync('/' + game_data[key]["path"], null, flag_r).buffer, };
       }
     });

     var emu = new V86Starter(cfg);
     this._emulator = emu;
     this.ready = null;

     if (game_data["scale"]) {
       emu.screen_set_scale(game_data["scale"], game_data["scale"]);
     }

     screenContainerInnerElt.addEventListener('click',
                                              function (e) {
                                                emu.lock_mouse();
                                              });
   }

   V86Runner.prototype.start = function () {
     this._emulator.run();
   };

   V86Runner.prototype.pause = function () {
     this._emulator.stop();
   };

   V86Runner.prototype.stop = function () {
     this._emulator.stop();
   };

   V86Runner.prototype.mute = function () {
     if (this._emulator.is_muted) {
       this._emulator.speaker_adapter.mixer.set_volume(1, undefined);
       this._emulator.is_muted = false;
     }
   };

   V86Runner.prototype.unmute = function () {
     if (!this._emulator.is_muted) {
       this._emulator.speaker_adapter.mixer.set_volume(0, undefined);
       this._emulator.is_muted = true;
     }
   };

   V86Runner.prototype.onStarted = function (func) {
     this._emulator.add_listener("emulator-started", func);
   };

   V86Runner.prototype.onReset = function (func) {
     // not supported
   };

   V86Runner.prototype.requestFullScreen = function () {
     getfullscreenenabler().call(this._canvas);
   };

   /*
    * RuffleRunner
    */
   function RuffleRunner(canvas, game_data) {
     if (!game_data.swf_file_name) {
       let url = game_data.files[0].file.url;
       game_data.swf_file_name = url.slice(url.lastIndexOf('/'));
     }
     // read game data from file system
     let gamedata = game_data.fs.readFileSync(game_data.swf_file_name, null, flag_r);
     this.ready = null;

     let ruffle = RufflePlayer.newest();
     let player = ruffle.createPlayer();
     player.addEventListener('loadedmetadata', () => {
       player.style.width = player.metadata.width + "px";
       player.style.height = player.metadata.height + "px";
     });
     this._player = player;

     // copy atributes of canvas to player div
     for (let el of canvas.attributes){
       player.setAttribute(el.localName, el.nodeValue);
     }

     canvas.parentElement.replaceChild(player, canvas);
     player.load({ data: gamedata,
                   swfFileName: game_data.swf_file_name.replace('/', ''),
                   splashScreen: false
                 })
           .then(() => {
                   this.ready(); // clear screen
                 });
   }

   RuffleRunner.prototype.requestFullScreen = function () {
     this._player.enterFullscreen();
   };

   RuffleRunner.prototype.onReset =  function (func) {
   };

   RuffleRunner.prototype.start =  function (func) {
   };

   RuffleRunner.prototype.onStarted =  function (func) {
     this.ready = func;
   };

   RuffleRunner.prototype.mute = function() {
     this._player.volume = 0;
   };

   RuffleRunner.prototype.unmute = function() {
     this._player.volume = 1;
   };

   /*
    * CloudpilotRunner
    */
   function CloudpilotRunner(canvas, game_data) {
     // Assume we have one of each: *.bin (BIOS ROM), *.img (session image), *.prc (app database)
     if (game_data.bios) {
       this._biosFile = game_data.fs.readFileSync("/" + game_data.bios, null, flag_r);
     }
     if (game_data.session) {
       this._sessionFile = game_data.fs.readFileSync("/" + game_data.session, null, flag_r);
     }
     if (game_data.prc) {
       if (game_data.prc.match(/\.zip$/i)) {
         this._prcZip = game_data.fs.readFileSync("/" + game_data.prc, null, flag_r);
         this._prcFileName = game_data.prc.replace(/\.zip$/i, '.prc');
       } else {
         this._prcFile = game_data.fs.readFileSync("/" + game_data.prc, null, flag_r);
       }
     }
     this._canvas = canvas;
   }

   CloudpilotRunner.prototype.onReset =  function (func) {
   };

   CloudpilotRunner.prototype.start =  function (func) {
     var runner = this;
     cloudpilot.createEmulator()
               .then(function(emulator) {
                       emulator
                         .setCanvas(runner._canvas)
                         .bindInput(runner._canvas, runner._canvas);

                         if (runner._sessionFile && runner._prcFile) {
                           // Load booted image and install app.
                           emulator
                             .loadSession(runner._sessionFile)
                             .installAndLaunchDatabase(runner._prcFile);
                         } else if (runner._sessionFile && runner._prcZip) {
                           // Load booted image and install app.
                           emulator
                             .loadSession(runner._sessionFile)
                             .installFromZipfileAndLaunch(runner._prcZip, runner._prcFileName);
                         } else if (runner._biosFile) {
                           // Missing app .prc; load initial BIOS directly (Palm setup process).
                           emulator.loadRom(runner._biosFile);
                         }
                       emulator.resume();
                       runner._canvas.tabIndex = 0;
                       runner._canvas.style.outline = 0;
                       runner._canvas.focus();
                       runner._emulator = emulator;
                    });
   };

   CloudpilotRunner.prototype.onStarted =  function (func) {
     func();
   };

   CloudpilotRunner.prototype.mute = function() {
     if (this._emulator) {
       this._emulator.setVolume(0);
     }
   };

   CloudpilotRunner.prototype.unmute = function() {
     if (this._emulator) {
       if (!this._emulator.isAudioInitialized()) {
         this._emulator.initializeAudio();
       }
       this._emulator.setVolume(1);
     }
   };

   CloudpilotRunner.prototype.requestFullScreen = function () {
     getfullscreenenabler().call(this._canvas);
   };

   /**
    * Emulator
    */
   function Emulator(canvas, callbacks, loadFiles) {
     if (typeof callbacks !== 'object') {
       callbacks = { before_emulator: null,
                     before_run: callbacks };
     }
     var js_url;
     var requests = [];
     var drawloadingtimer;
     // TODO: Have an enum value that communicates the current state of the emulator, e.g. 'initializing', 'loading', 'running'.
     var has_started = false;
     var loading = false;
     var defaultSplashColors = { foreground: 'white',
                                 background: 'black',
                                 failure: 'red' };
     var splash = { loading_text: "",
                    spinning: true,
                    finished_loading: false,
                    colors: defaultSplashColors,
                    table: null,
                    splashimg: new Image() };

     var runner;

     var muted = false;
     var SDL_PauseAudio;
     this.isMuted = function () { return muted; };
     this.mute = function () { return this.setMute(true); };
     this.unmute = function () { return this.setMute(false); };
     this.toggleMute = function () { return this.setMute(!muted); };
     this.setMute = function (state) {
       muted = state;
       if (runner) {
         if (state) {
           runner.mute();
         } else {
           runner.unmute();
         }
       }
       else {
         try {
           if (!SDL_PauseAudio)
             SDL_PauseAudio = Module.cwrap('SDL_PauseAudio', '', ['number']);
           SDL_PauseAudio(state);
         } catch (x) {
           console.log("Unable to change audio state:", x);
         }
       }
       return this;
     };

     // This is the bare minimum that will allow gamepads to work. If
     // we don't listen for them then the browser won't tell us about
     // them.
     // TODO: add hooks so that some kind of UI can be displayed.
     window.addEventListener("gamepadconnected",
                             function (e) {
                               console.log("Gamepad connected at index %d: %s. %d buttons, %d axes.",
                                           e.gamepad.index, e.gamepad.id,
                                           e.gamepad.buttons.length, e.gamepad.axes.length);
                             });

     window.addEventListener("gamepaddisconnected",
                             function (e) {
                               console.log("Gamepad disconnected from index %d: %s",
                                           e.gamepad.index, e.gamepad.id);
                             });

     var css_resolution, aspectRatio;
     // right off the bat we set the canvas's inner dimensions to
     // whatever it's current css dimensions are; this isn't likely to be
     // the same size that dosbox/jsmame will set it to, but it avoids
     // the case where the size was left at the default 300x150
     if (!canvas.hasAttribute("width")) {
       var style = getComputedStyle(canvas);
       canvas.width = parseInt(style.width, 10);
       canvas.height = parseInt(style.height, 10);
     }

     this.setScale = function(_scale) {
       console.warn("setScale method is deprecated; use the BaseLoader.scale method instead");
       return this;
     };

     this.setSplashImage = function(_splashimg) {
       if (_splashimg) {
         if (_splashimg instanceof Image) {
           if (splash.splashimg.parentNode) {
             splash.splashimg.src = _splashimg.src;
           } else {
             splash.splashimg = _splashimg;
           }
         } else {
           splash.splashimg.src = _splashimg;
         }
       }
       return this;
     };

     this.setCSSResolution = function(_resolution) {
       css_resolution = _resolution;
       return this;
     };

     this.setAspectRatio = function(_aspectRatio) {
       aspectRatio = _aspectRatio;
       return this;
     };

     this.setCallbacks = function(_callbacks) {
       if (typeof _callbacks !== 'object') {
         callbacks = { before_emulator: null,
                       before_run: _callbacks };
       } else {
         callbacks = _callbacks;
       }
       return this;
     };

     this.setSplashColors = function (colors) {
       splash.colors = colors;
       return this;
     };

     this.setLoad = function (loadFunc) {
       loadFiles = loadFunc;
       return this;
     };

     var start = function (options) {
       if (has_started)
         return false;
       has_started = true;
       var defaultOptions = { waitAfterDownloading: false,
                              hasCustomCSS: false };
       if (typeof options !== 'object') {
         options = defaultOptions;
       } else {
         options.__proto__ = defaultOptions;
       }

       var k, c, game_data;
       setupSplash(canvas, splash, options);
       drawsplash();

       var loading;

       if (typeof loadFiles === 'function') {
         loading = loadFiles(fetch_file, splash);
       } else {
         loading = Promise.resolve(loadFiles);
       }
       loading.then(function (_game_data) {
                      return new Promise(function(resolve, reject) {
                        var InMemoryFS = BrowserFS.FileSystem.InMemory;
                        InMemoryFS.Create(function (e, inMemory) {
                          // If the browser supports IndexedDB storage, mirror writes to that storage
                          // for persistence purposes.
                          if (BrowserFS.FileSystem.IndexedDB.isAvailable()) {
                            var AsyncMirrorFS = BrowserFS.FileSystem.AsyncMirror,
                                IndexedDBFS = BrowserFS.FileSystem.IndexedDB,
                                fileSystemKey = "fileSystemKey" in _game_data ? _game_data.fileSystemKey
                                                                              : "emularity";
                            IndexedDBFS.Create({ storeName: fileSystemKey },
                                               function(e, idbfs) {
                                                 if (e) {
                                                   finish(e, inMemory);
                                                 } else {
                                                   AsyncMirrorFS.Create({ sync: inMemory, async: idbfs },
                                                                        finish);
                                                 }
                                               });
                          } else {
                            finish(e, inMemory);
                          }
                        });

                        function finish(e, deltaFS) {
                          game_data = _game_data;

                          // Any file system writes to MountableFileSystem will be written to the
                          // deltaFS, letting us mount read-only zip files into the MountableFileSystem
                          // while being able to "write" to them.
                          var MountableFS = BrowserFS.FileSystem.MountableFileSystem,
                              OverlayFS = BrowserFS.FileSystem.OverlayFS,
                              ZipFS = BrowserFS.FileSystem.ZipFS,
                              Buffer = BrowserFS.BFSRequire('buffer').Buffer;
                          MountableFS.Create(function (e, mountable) {
                            OverlayFS.Create({ readable: mountable
                                             , writable: deltaFS
                                             },
                                             function (e, fs) {
                                               if (e) {
                                                 console.error("Failed to initialize the OverlayFS:", e);
                                                 reject();
                                               } else {
                                                 game_data.fs = fs;
                                                 function fetch(file) {
                                                   var isCached = 'cached' in file && file.cached,
                                                       hasData = 'data' in file && file.data !== null && typeof file.data !== 'undefined';
                                                   if (isCached || hasData) {
                                                     return cached_file(file.title, file.data);
                                                   } else {
                                                     return fetch_file(file.title, file.url, 'arraybuffer', file.optional);
                                                   }
                                                 }
                                                 function mountat(drive) {
                                                   return function (data) {
                                                     if (data !== null) {
                                                       drive = drive.toLowerCase();
                                                       var mountpoint = '/'+ drive;
                                                       // Mount into RO MFS.
                                                       return new Promise(function (resolve, reject) {
                                                         return new ZipFS.Create({ zipData: new Buffer(data) },
                                                                                 function (e, fs) {
                                                                                   if (e) {
                                                                                     reject();
                                                                                   } else {
                                                                                     mountable.mount(mountpoint, fs);
                                                                                     resolve();
                                                                                   }
                                                                                 });
                                                       });
                                                     }
                                                   };
                                                 }
                                                 function saveat(filename) {
                                                   return function (data) {
                                                     if (data !== null) {
                                                       if (deltaFS.existsSync(filename)) {
                                                         return;
                                                       }
                                                       if (filename.includes('/', 1)) {
                                                         var parts = filename.substring(1).split('/');
                                                         for (var i = 1; i < parts.length; i++) {
                                                           var path = '/'+ parts.slice(0, i).join('/');
                                                           if (!deltaFS.existsSync(path)) {
                                                             deltaFS.mkdirSync(path, 0777);
                                                           }
                                                         }
                                                       }
                                                       deltaFS.writeFileSync(filename, new Buffer(data), null, flag_w, 0644);
                                                     }
                                                   };
                                                 }
                                                 var promises = game_data.files
                                                                         .map(function (f) {
                                                                                if (f && f.file) {
                                                                                  if (f.drive) {
                                                                                    return fetch(f.file).then(mountat(f.drive));
                                                                                  } else if (f.mountpoint) {
                                                                                    var path = f.mountpoint[0] != '/' ? '/'+ f.mountpoint : f.mountpoint;
                                                                                    f.file.cached = deltaFS.existsSync(path);
                                                                                    return fetch(f.file).then(saveat(path));
                                                                                  }
                                                                                }
                                                                                return null;
                                                                              });
                                                 // this is kinda wrong; it really only applies when we're loading something created by Emscripten
                                                 if ('emulatorWASM' in game_data && game_data.emulatorWASM && 'WebAssembly' in window) {
                                                   promises.push(fetch({ title: "WASM Binary", url: game_data.emulatorWASM }).then(function (data) { game_data.wasmBinary = data; }));
                                                 }
                                                 Promise.all(promises).then(resolve, reject);
                                               }
                                             });
                          });
                        }
                      });
                    })
              .then(function (game_files) {
                      if (!game_data || splash.failed_loading) {
                        return null;
                      }
                      if (options.waitAfterDownloading) {
                        return new Promise(function (resolve, reject) {
                                             splash.setTitle("Press any key to continue...");
                                             splash.spinning = false;

                                             // stashes these event listeners so that we can remove them after
                                             window.addEventListener('keypress', k = keyevent(resolve));
                                             canvas.addEventListener('click', c = resolve);
                                             splash.splashElt.addEventListener('click', c);
                                           });
                      }
                      return Promise.resolve();
                    },
                    function () {
                      if (splash.failed_loading) {
                        return;
                      }
                      splash.setTitle("Failed to download game data!");
                      splash.failed_loading = true;
                    })
              .then(function () {
                      if (!game_data || splash.failed_loading) {
                        return null;
                      }
                      splash.spinning = true;
                      window.removeEventListener('keypress', k);
                      canvas.removeEventListener('click', c);
                      splash.splashElt.removeEventListener('click', c);

                      // Don't let arrow, pg up/down, home, end affect page position
                      blockSomeKeys();
                      setupFullScreen();
                      disableRightClickContextMenu(canvas);

                      // Emscripten doesn't use the proper prefixed functions for fullscreen requests,
                      // so let's map the prefixed versions to the correct function.
                      canvas.requestPointerLock = getpointerlockenabler();

                      moveConfigToRoot(game_data.fs);

                      if (callbacks && callbacks.before_emulator) {
                        try {
                          callbacks.before_emulator();
                        } catch (x) {
                          console.log(x);
                        }
                      }

                      if ("runner" in game_data) {
                        if (game_data.runner == EmscriptenRunner || game_data.runner.prototype instanceof EmscriptenRunner) {
                          // this is a stupid hack. Emscripten-based
                          // apps currently need the runner to be set
                          // up first, then we can attach the
                          // script. The others have to do it the
                          // other way around.
                          runner = setup_runner();
                        }
                      }

                      if (game_data.emulatorJS) {
                        splash.setTitle("Launching Emulator");
                        return attach_script(game_data.emulatorJS);
                      } else {
                        splash.setTitle("Non-system disk or disk error");
                      }
                      return null;
                    },
                    function () {
                      if (!game_data || splash.failed_loading) {
                        return null;
                      }
                      splash.setTitle("Invalid media, track 0 bad or unusable");
                      splash.failed_loading = true;
                    })
              .then(function () {
                      if (!game_data || splash.failed_loading) {
                        return null;
                      }
                      if ("runner" in game_data) {
                        if (!runner) {
                          runner = setup_runner();
                        }
                        runner.start();
                      }
                    });

       function setup_runner() {
         var runner = new game_data.runner(canvas, game_data);
         resizeCanvas(canvas, game_data.scale, game_data.nativeResolution, game_data.aspectRatio);
         runner.onStarted(function () {
                            splash.finished_loading = true;
                            splash.hide();
                            setTimeout(function() {
                                         if (muted) {
                                           runner.mute();
                                         }
                                         if (callbacks && callbacks.before_run) {
                                           callbacks.before_run();
                                         }
                                       },
                                       0);
                          });
         runner.onReset(function () {
                          if (muted) {
                            runner.mute();
                          }
                        });
         return runner;
       }

       return this;
     };
     this.start = start;

     var formatSize = function (event) {
       if (event.lengthComputable)
         return "("+ (event.total ? (event.loaded / event.total * 100).toFixed(0)
                                  : "100") +
                "%; "+ formatBytes(event.loaded) +
                " of "+ formatBytes(event.total) +")";
       return "("+ formatBytes(event.loaded) +")";
     };

     var formatBytes = function (bytes, base10) {
         if (bytes === 0)
           return "0 B";
         var unit = base10 ? 1000 : 1024,
             units = base10 ? ["B", "kB","MB","GB","TB","PB","EB","ZB","YB"]
                            : ["B", "KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],
             exp = parseInt((Math.log(bytes) / Math.log(unit))),
             size = bytes / Math.pow(unit, exp);
         return size.toFixed(1) +' '+ units[exp];
     };

     var fetch_file = function (title, url, rt, optional) {
       return _fetch_file(title, url, rt, optional, false);
     };

     var cached_file = function (title, data) {
       return _fetch_file(title, data, null, false, true);
     };

     var _fetch_file = function (title, url, rt, optional, cached) {
       var needsCSS = splash.table.dataset.hasCustomCSS == "false";
       var row = addRow(splash.table);
       var titleCell = row[0], statusCell = row[1];
       titleCell.textContent = title;
       return new Promise(function (resolve, reject) {
                            if (cached) {
                              success();
                              resolve(url); // second parameter reused as a pass–through
                            } else {
                              var xhr = new XMLHttpRequest();
                              xhr.open('GET', url, true);
                              xhr.responseType = rt || 'arraybuffer';
                              xhr.onprogress = function (e) {
                                                 titleCell.innerHTML = title +" <span style=\"font-size: smaller\">"+ formatSize(e) +"</span>";
                                               };
                              xhr.onload = function (e) {
                                             if (xhr.status === 200) {
                                               success();
                                               resolve(xhr.response);
                                             } else if (optional) {
                                               success();
                                               resolve(null);
                                             } else {
                                               failure();
                                               reject();
                                             }
                                           };
                              xhr.onerror = function (e) {
                                              if (optional) {
                                                success();
                                                resolve(null);
                                              } else {
                                                failure();
                                                reject();
                                              }
                                            };
                              xhr.send();
                            }
                          });
       function success() {
         statusCell.textContent = "✔";
         titleCell.parentNode.classList.add('emularity-download-success');
         titleCell.textContent = title;
         if (needsCSS) {
           titleCell.style.fontWeight = 'bold';
           titleCell.parentNode.style.backgroundColor = splash.getColor('foreground');
           titleCell.parentNode.style.color = splash.getColor('background');
         }
       }
       function failure() {
         statusCell.textContent = "✘";
         titleCell.parentNode.classList.add('emularity-download-failure');
         titleCell.textContent = title;
         if (needsCSS) {
           titleCell.style.fontWeight = 'bold';
           titleCell.parentNode.style.backgroundColor = splash.getColor('failure');
           titleCell.parentNode.style.color = splash.getColor('background');
         }
       }
     };

     function keyevent(resolve) {
       return function (e) {
                if (e.which == 32) {
                  e.preventDefault();
                  resolve();
                }
              };
     };

     var resizeCanvas = function (canvas, scale, resolution, aspectRatio) {
       if (scale && resolution) {
         // optimizeSpeed is the standardized value. different
         // browsers support different values; they will all ignore
         // values that they don't understand.
         canvas.style.imageRendering = '-moz-crisp-edges';
         canvas.style.imageRendering = '-o-crisp-edges';
         canvas.style.imageRendering = '-webkit-optimize-contrast';
         canvas.style.imageRendering = 'optimize-contrast';
         canvas.style.imageRendering = 'crisp-edges';
         canvas.style.imageRendering = 'pixelated';
         canvas.style.imageRendering = 'optimizeSpeed';

         canvas.style.width = resolution.width * scale +'px';
         canvas.style.height = resolution.height * scale +'px';
         canvas.setAttribute("width", resolution.width * scale);
         canvas.setAttribute("height", resolution.height * scale);
       }
     };

     var clearCanvas = function () {
       var context = canvas.getContext('2d');
       context.fillStyle = splash.getColor('background');
       context.fillRect(0, 0, canvas.width, canvas.height);
       console.log("canvas cleared");
     };

     function setupSplash(canvas, splash, globalOptions) {
       splash.splashElt = document.getElementById("emularity-splash-screen");
       if (!splash.splashElt) {
         splash.splashElt = document.createElement('div');
         splash.splashElt.classList.add("emularity-splash-screen");
         if (!globalOptions.hasCustomCSS) {
           splash.splashElt.style.position = 'absolute';
           splash.splashElt.style.top = '0';
           splash.splashElt.style.left = '0';
           splash.splashElt.style.right = '0';
           splash.splashElt.style.color = splash.getColor('foreground');
           splash.splashElt.style.backgroundColor = splash.getColor('background');
         }
         canvas.parentElement.appendChild(splash.splashElt);
       }

       splash.splashimg.classList.add("emularity-splash-image");
       if (!globalOptions.hasCustomCSS) {
         splash.splashimg.style.display = 'block';
         splash.splashimg.style.marginLeft = 'auto';
         splash.splashimg.style.marginRight = 'auto';
       }
       splash.splashElt.appendChild(splash.splashimg);

       splash.titleElt = document.createElement('span');
       splash.titleElt.classList.add("emularity-splash-title");
       if (!globalOptions.hasCustomCSS) {
         splash.titleElt.style.display = 'block';
         splash.titleElt.style.width = '100%';
         splash.titleElt.style.marginTop = "1em";
         splash.titleElt.style.marginBottom = "1em";
         splash.titleElt.style.textAlign = 'center';
         splash.titleElt.style.font = "24px sans-serif";
       }
       splash.titleElt.textContent = " ";
       splash.splashElt.appendChild(splash.titleElt);

       var table = document.getElementById("emularity-progress-indicator");
       if (!table) {
         table = document.createElement('table');
         table.classList.add("emularity-progress-indicator");
         table.dataset.hasCustomCSS = globalOptions.hasCustomCSS;
         if (!globalOptions.hasCustomCSS) {
           table.style.width = "75%";
           table.style.color = splash.getColor('foreground');
           table.style.backgroundColor = splash.getColor('background');
           table.style.marginLeft = 'auto';
           table.style.marginRight = 'auto';
           table.style.borderCollapse = 'separate';
           table.style.borderSpacing = "2px";
         }
         splash.splashElt.appendChild(table);
       }
       splash.table = table;
     }

     splash.setTitle = function (title) {
       splash.titleElt.textContent = title;
     };

     splash.hide = function () {
       splash.splashElt.style.display = 'none';
     };

     splash.getColor = function (name) {
       return name in splash.colors ? splash.colors[name]
                                    : defaultSplashColors[name];
     };

     var addRow = function (table) {
       var needsCSS = table.dataset.hasCustomCSS == "false";
       var row = table.insertRow(-1);
       if (needsCSS) {
         row.style.textAlign = 'center';
       }
       var cell = row.insertCell(-1);
       if (needsCSS) {
         cell.style.position = 'relative';
       }
       var titleCell = document.createElement('span');
       titleCell.classList.add("emularity-download-title");
       titleCell.textContent = '—';
       if (needsCSS) {
         titleCell.style.verticalAlign = 'center';
         titleCell.style.minHeight = "24px";
         titleCell.style.whiteSpace = "nowrap";
       }
       cell.appendChild(titleCell);
       var statusCell = document.createElement('span');
       statusCell.classList.add("emularity-download-status");
       if (needsCSS) {
         statusCell.style.position = 'absolute';
         statusCell.style.left = "0";
         statusCell.style.paddingLeft = "0.5em";
       }
       cell.appendChild(statusCell);
       return [titleCell, statusCell];
     };

     var drawsplash = function () {
       canvas.setAttribute('moz-opaque', '');
       if (!splash.splashimg.src) {
         splash.splashimg.src = "logo/emularity_color_small.png";
       }
     };

     function attach_script(js_url) {
       return new Promise(function (resolve, reject) {
                            var newScript;
                            function loaded(e) {
                              if (e.target == newScript) {
                                newScript.removeEventListener("load", loaded);
                                newScript.removeEventListener("error", failed);
                                resolve();
                              }
                            }
                            function failed(e) {
                              if (e.target == newScript) {
                                newScript.removeEventListener("load", loaded);
                                newScript.removeEventListener("error", failed);
                                reject();
                              }
                            }
                            if (js_url) {
                              var head = document.getElementsByTagName('head')[0];
                              newScript = document.createElement('script');
                              newScript.addEventListener("load", loaded);
                              newScript.addEventListener("error", failed);
                              newScript.type = 'text/javascript';
                              newScript.src = js_url;
                              head.appendChild(newScript);
                            }
                          });
     }

     function getpointerlockenabler() {
       return canvas.requestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock;
     }

     this.isfullscreensupported = function () {
        return !!(getfullscreenenabler());
     };

     function setupFullScreen() {
       var self = this;
       var fullScreenChangeHandler = function() {
                                       if (!(document.mozFullScreenElement || document.fullScreenElement)) {
                                         resizeCanvas(canvas, scale, css_resolution, aspectRatio);
                                       }
                                     };
       if ('onfullscreenchange' in document) {
         document.addEventListener('fullscreenchange', fullScreenChangeHandler);
       } else if ('onmozfullscreenchange' in document) {
         document.addEventListener('mozfullscreenchange', fullScreenChangeHandler);
       } else if ('onwebkitfullscreenchange' in document) {
         document.addEventListener('webkitfullscreenchange', fullScreenChangeHandler);
       }
     };

     this.requestFullScreen = function () {
       if (runner) {
         runner.requestFullScreen();
       }
     };

     /**
       * Prevents page navigation keys such as page up/page down from
       * moving the page while the user is playing.
       */
     function blockSomeKeys() {
       function keypress (e) {
         if (e.which >= 33 && e.which <= 40) {
           e.preventDefault();
           return false;
         }
         return true;
       }
       window.onkeydown = keypress;
     }

     /**
       * Disables the right click menu for the given element.
       */
     function disableRightClickContextMenu(element) {
       element.addEventListener('contextmenu',
                                function (e) {
                                  if (e.button == 2) {
                                    // Block right-click menu thru preventing default action.
                                    e.preventDefault();
                                  }
                                });
     }
   };

   /**
    * misc
    */
   function getfullscreenenabler() {
     return canvas.requestFullScreen || canvas.webkitRequestFullScreen || canvas.mozRequestFullScreen;
   }

   // This is such a hack. We're not calling the BrowserFS api
   // "correctly", so we have to synthesize these flags ourselves
   var flag_r = { isReadable: function() { return true; },
                  isWriteable: function() { return false; },
                  isTruncating: function() { return false; },
                  isAppendable: function() { return false; },
                  isSynchronous: function() { return false; },
                  isExclusive: function() { return false; },
                  pathExistsAction: function() { return 0; },
                  pathNotExistsAction: function() { return 1; }
                };
   var flag_w = { isReadable: function() { return false; },
                  isWriteable: function() { return true; },
                  isTruncating: function() { return false; },
                  isAppendable: function() { return false; },
                  isSynchronous: function() { return false; },
                  isExclusive: function() { return false; },
                  pathExistsAction: function() { return 0; },
                  pathNotExistsAction: function() { return 3; }
                };

   /**
    * Searches for dosbox.conf, and moves it to '/dosbox.conf' so dosbox uses it.
    */
   function moveConfigToRoot(fs) {
     var dosboxConfPath = null;
     // Recursively search for dosbox.conf.
     function searchDirectory(dirPath) {
       fs.readdirSync(dirPath).forEach(function(item) {
         if (dosboxConfPath) {
           return;
         }
         // Avoid infinite recursion by ignoring these entries, which exist at
         // the root.
         if (item === '.' || item === '..') {
           return;
         }
         // Append '/' between dirPath and the item's name... unless dirPath
         // already ends in it (which always occurs if dirPath is the root, '/').
         var itemPath = dirPath + (dirPath[dirPath.length - 1] !== '/' ? "/" : "") + item,
             itemStat = fs.statSync(itemPath);
         if (itemStat.isDirectory(itemStat.mode)) {
           searchDirectory(itemPath);
         } else if (item === 'dosbox.conf') {
           dosboxConfPath = itemPath;
         }
       });
     }

     searchDirectory('/');

     if (dosboxConfPath !== null) {
       fs.writeFileSync('/dosbox.conf',
                        fs.readFileSync(dosboxConfPath, null, flag_r),
                        null, flag_w, 0x1a4);
     }
   };

   function extend(a, b) {
     if (a === null)
       return b;
     if (b === null)
       return a;
     var ta = typeof a,
         tb = typeof b;
     if (ta !== tb) {
       if (ta === 'undefined')
         return b;
       if (tb === 'undefined')
         return a;
       throw new Error("Cannot extend an "+ ta +" with an "+ tb);
     }
     if (Array.isArray(a))
       return a.concat(b);
     if (ta === 'object') {
       Object.keys(b).forEach(function (k) {
                                a[k] = extend(k in a ? a[k] : undefined, b[k]);
                              });
       return a;
     }
     return b;
   }

   function dict_from_xml(xml) {
     if (xml instanceof XMLDocument) {
       xml = xml.documentElement;
     }
     var dict = {};
     var len = xml.childNodes.length, i;
     for (i = 0; i < len; i++) {
       var node = xml.childNodes[i];
       dict[node.nodeName] = node.textContent;
     }
     return dict;
   }

   function list_from_xml(xml) {
     if (xml instanceof XMLDocument) {
       xml = xml.documentElement;
     }
     return Array.prototype.slice.call(xml.childNodes);
   }

   function files_from_filelist(xml) {
     return list_from_xml(xml).filter(function (node) {
                                        return "getAttribute" in node;
                                      })
                              .map(function (node) {
                                     var file = dict_from_xml(node);
                                     file.name = node.getAttribute("name");
                                     return file;
                              });
   }

   function files_with_ext_from_filelist(xml, ext) {
     if (!ext) {
       return [];
     }
     if (!ext.startsWith('.')) {
       ext = '.'+ ext;
     }
     ext = ext.toLowerCase();
     return files_from_filelist(xml).filter(function (file) {
                                              return file.name.toLowerCase().endsWith(ext);
                                            });
   }

   function meta_props_matching(meta, regex) {
     if (typeof regex == "string")
       regex = RegExp(regex);
     return Object.keys(meta).map(function (k) {
                                    var match = regex.exec(k);
                                    if (match)
                                      return [k, match];
                                    return null;
                                  })
                             .filter(function (result) {
                               return !!result;
                             });
   }

   function _SDL_CreateRGBSurfaceFrom(pixels, width, height, depth, pitch, rmask, gmask, bmask, amask) {
     // TODO: Actually fill pixel data to created surface.
     // TODO: Take into account depth and pitch parameters.
     // console.log('TODO: Partially unimplemented SDL_CreateRGBSurfaceFrom called!');
     var surface = SDL.makeSurface(width, height, 0, false, 'CreateRGBSurfaceFrom', rmask, gmask, bmask, amask);

     var surfaceData = SDL.surfaces[surface];
     var surfaceImageData = surfaceData.ctx.getImageData(0, 0, width, height);
     var surfacePixelData = surfaceImageData.data;

     // Fill pixel data to created surface.
     // Supports SDL_PIXELFORMAT_RGBA8888 and SDL_PIXELFORMAT_RGB888
     var channels = amask ? 4 : 3; // RGBA8888 or RGB888
     for (var pixelOffset = 0; pixelOffset < width*height; pixelOffset++) {
       surfacePixelData[pixelOffset*4+0] = HEAPU8[pixels + (pixelOffset*channels+0)]; // R
       surfacePixelData[pixelOffset*4+1] = HEAPU8[pixels + (pixelOffset*channels+1)]; // G
       surfacePixelData[pixelOffset*4+2] = HEAPU8[pixels + (pixelOffset*channels+2)]; // B
       surfacePixelData[pixelOffset*4+3] = amask ? HEAPU8[pixels + (pixelOffset*channels+3)] : 0xff; // A
     };

     surfaceData.ctx.putImageData(surfaceImageData, 0, 0);

     return surface;
   }

   window.IALoader = IALoader;
   window.DosBoxLoader = DosBoxLoader;
   window.PC98DosBoxLoader = PC98DosBoxLoader;
   window.JSMESSLoader = MAMELoader; // depreciated; just for backwards compatibility
   window.JSMAMELoader = MAMELoader; // ditto
   window.MAMELoader = MAMELoader;
   window.SAELoader = SAELoader;
   window.PCELoader = PCELoader;
   window.VICELoader = VICELoader;
   window.NP2Loader = NP2Loader;
   window.V86Loader = V86Loader;
   window.RuffleLoader = RuffleLoader;
   window.CloudpilotLoader = CloudpilotLoader;
   window.Emulator = Emulator;
   window._SDL_CreateRGBSurfaceFrom = _SDL_CreateRGBSurfaceFrom;
 })(typeof Promise === 'undefined' ? ES6Promise.Promise : Promise);

// legacy
var JSMESS = JSMESS || {};
JSMESS.ready = function (f) { f(); };

// Local Variables:
// js-indent-level: 2
// End:
