content: /**
   *
   * @author Andrew Hoffman
   */
  package com.grapeshot.halfnes;
  
  import com.grapeshot.halfnes.cheats.ActionReplay;
  import com.grapeshot.halfnes.mappers.BadMapperException;
  import com.grapeshot.halfnes.mappers.Mapper;
  import com.grapeshot.halfnes.ui.ControllerInterface;
  import com.grapeshot.halfnes.ui.FrameLimiterImpl;
  import com.grapeshot.halfnes.ui.FrameLimiterInterface;
  import com.grapeshot.halfnes.ui.GUIInterface;
  import javafx.application.Platform;
  
  public class NES implements ProjectInfo {
  
      private Mapper mapper;
      private APU apu;
      private CPU cpu;
      private CPURAM cpuram;
      private PPU ppu;
      private GUIInterface gui;
      private ControllerInterface controller1, controller2;
      public boolean runEmulation = false;
      private boolean dontSleep = false;
      private boolean shutdown = false;
      public long frameStartTime, framecount, frameDoneTime;
      private boolean frameLimiterOn = true;
      private String curRomPath, curRomName;
      private final FrameLimiterInterface limiter = new FrameLimiterImpl(this, 16639267);
      // Pro Action Replay device
      private ActionReplay actionReplay;
  
      public NES(GUIInterface gui) {
          if (gui != null) {
              this.gui = gui;
              gui.setNES(this);
              gui.run();
          }
      }
  
      public CPURAM getCPURAM() {
          return this.cpuram;
      }
  
      public CPU getCPU() {
          return this.cpu;
      }
  
      public void run(final String romtoload) {
          Thread.currentThread().setPriority(Thread.NORM_PRIORITY + 1);
          //set thread priority higher than the interface thread
          curRomPath = romtoload;
          gui.loadROMs(romtoload);
          run();
      }
  
      public void run() {
          while (!shutdown) {
              if (runEmulation) {
                  frameStartTime = System.nanoTime();
                  actionReplay.applyPatches();
                  runframe();
                  if (frameLimiterOn && !dontSleep) {
                      limiter.sleep();
                  }
                  frameDoneTime = System.nanoTime() - frameStartTime;
              } else {
                  limiter.sleepFixed();
                  if (ppu != null && framecount > 1) {
                      gui.render();
                  }
              }
          }
      }
  
      private synchronized void runframe() {
          //run cpu, ppu for a whole frame
          ppu.runFrame();
  
          //do end of frame stuff
          dontSleep = apu.bufferHasLessThan(1000);
          //if the audio buffer is completely drained, don't sleep for this frame
          //this is to prevent the emulator from getting stuck sleeping too much
          //on a slow system or when the audio buffer runs dry.
  
          apu.finishframe();
          cpu.modcycles();
  
  //        if (framecount == 13 * 60) {
  //            cpu.startLog();
  //            System.err.println("log on");
  //        }
          //render the frame
          ppu.renderFrame(gui);
          if ((framecount & 2047) == 0) {
              //save sram every 30 seconds or so
              saveSRAM(true);
          }
          ++framecount;
          //System.err.println(framecount);
      }
  
      public void setControllers(ControllerInterface controller1, ControllerInterface controller2) {
          this.controller1 = controller1;
          this.controller2 = controller2;
      }
  
      public void toggleFrameLimiter() {
          frameLimiterOn = !frameLimiterOn;
      }
  
      public synchronized void loadROM(final String filename) {
          loadROM(filename, null);
      }
  
      public synchronized void loadROM(final String filename, Integer initialPC) {
          runEmulation = false;
          if (FileUtils.exists(filename)
                  && (FileUtils.getExtension(filename).equalsIgnoreCase(".nes")
                  || FileUtils.getExtension(filename).equalsIgnoreCase(".nsf"))) {
              Mapper newmapper;
              try {
                  final ROMLoader loader = new ROMLoader(filename);
                  loader.parseHeader();
                  newmapper = Mapper.getCorrectMapper(loader);
                  newmapper.setLoader(loader);
                  newmapper.loadrom();
              } catch (BadMapperException e) {
                  gui.messageBox("Error Loading File: ROM is"
                          + " corrupted or uses an unsupported mapper.\n" + e.getMessage());
                  return;
              } catch (Exception e) {
                  gui.messageBox("Error Loading File: ROM is"
                          + " corrupted or uses an unsupported mapper.\n" + e.toString() + e.getMessage());
                  e.printStackTrace();
                  return;
              }
              if (apu != null) {
                  //if rom already running save its sram before closing
                  apu.destroy();
                  saveSRAM(false);
                  //also get rid of mapper etc.
                  mapper.destroy();
                  cpu = null;
                  cpuram = null;
                  ppu = null;
              }
              mapper = newmapper;
              //now some annoying getting of all the references where they belong
              cpuram = mapper.getCPURAM();
              actionReplay = new ActionReplay(cpuram);
              cpu = mapper.cpu;
              ppu = mapper.ppu;
              apu = new APU(this, cpu, cpuram);
              cpuram.setAPU(apu);
              cpuram.setPPU(ppu);
              curRomPath = filename;
              curRomName = FileUtils.getFilenamefromPath(filename);
  
              framecount = 0;
              //if savestate exists, load it
              if (mapper.hasSRAM()) {
                  loadSRAM();
              }
              //and start emulation
              cpu.init(initialPC);
              mapper.init();
              setParameters();
              runEmulation = true;
          } else {
              gui.messageBox("Could not load file:\nFile " + filename + "\n"
                      + "does not exist or is not a valid NES game.");
          }
      }
  
      private void saveSRAM(final boolean async) {
          if (mapper != null && mapper.hasSRAM() && mapper.supportsSaves()) {
              if (async) {
                  FileUtils.asyncwritetofile(mapper.getPRGRam(), FileUtils.stripExtension(curRomPath) + ".sav");
              } else {
                  FileUtils.writetofile(mapper.getPRGRam(), FileUtils.stripExtension(curRomPath) + ".sav");
              }
          }
      }
  
      private void loadSRAM() {
          final String name = FileUtils.stripExtension(curRomPath) + ".sav";
          if (FileUtils.exists(name) && mapper.supportsSaves()) {
              mapper.setPRGRAM(FileUtils.readfromfile(name));
          }
  
      }
  
      public void quit() {
          //save SRAM and quit
          //should wait for any save sram workers to be done before here
         if (cpu != null && curRomPath != null) {
              runEmulation = false;
              saveSRAM(false);
          }
          //there might be some subtle threading bug with saving?
          //System.Exit is very dirty and does NOT let the delete on exit handler
          //fire so the natives stick around...
          shutdown = true;
          Platform.exit();
      }
  
      public synchronized void reset() {
          if (cpu != null) {
              mapper.reset();
              cpu.reset();
              runEmulation = true;
              apu.pause();
              apu.resume();
          }
          //reset frame counter as well because PPU is reset
          //on Famicom, PPU is not reset when Reset is pressed
          //but some NES games expect it to be and you get garbage.
          framecount = 0;
      }
  
      public synchronized void reloadROM() {
          loadROM(curRomPath);
      }
  
      public synchronized void pause() {
          if (apu != null) {
              apu.pause();
          }
          runEmulation = false;
      }
  
      public long getFrameTime() {
          return frameDoneTime;
      }
  
      public String getrominfo() {
          if (mapper != null) {
              return mapper.getrominfo();
          }
          return null;
      }
  
      public synchronized void frameAdvance() {
          runEmulation = false;
          if (cpu != null) {
              runframe();
          }
      }
  
      public synchronized void resume() {
          if (apu != null) {
              apu.resume();
          }
          if (cpu != null) {
              runEmulation = true;
          }
      }
  
      public String getCurrentRomName() {
          return curRomName;
      }
  
      public boolean isFrameLimiterOn() {
          return frameLimiterOn;
      }
  
      public void messageBox(final String string) {
          if (gui != null) {
              gui.messageBox(string);
          }
      }
  
      public ControllerInterface getcontroller1() {
          return controller1;
      }
  
      public ControllerInterface getcontroller2() {
          return controller2;
      }
  
      public synchronized void setParameters() {
          if (apu != null) {
              apu.setParameters();
          }
          if (ppu != null) {
              ppu.setParameters();
          }
          if (limiter != null && mapper != null) {
              switch (mapper.getTVType()) {
                  case NTSC:
                  default:
                      limiter.setInterval(16639267);
                      break;
                  case PAL:
                  case DENDY:
                      limiter.setInterval(19997200);
              }
          }
      }
  
      /**
       * Access to the Pro Action Replay device.
       */
      public synchronized ActionReplay getActionReplay() {
          return actionReplay;
      }
  }
  