content: /*
   * HalfNES by Andrew Hoffman
   * Licensed under the GNU GPL Version 3. See LICENSE file
   */
  package com.grapeshot.halfnes.ui;
  
  import com.grapeshot.halfnes.FileUtils;
  import com.grapeshot.halfnes.NES;
  import com.grapeshot.halfnes.PrefsSingleton;
  import com.grapeshot.halfnes.video.RGBRenderer;
  import com.grapeshot.halfnes.cheats.ActionReplay;
  import com.grapeshot.halfnes.cheats.ActionReplayGui;
  import com.grapeshot.halfnes.video.NTSCRenderer;
  import com.grapeshot.halfnes.video.Renderer;
  import java.awt.*;
  import java.awt.datatransfer.DataFlavor;
  import java.awt.datatransfer.Transferable;
  import java.awt.datatransfer.UnsupportedFlavorException;
  import java.awt.event.*;
  import java.awt.image.BufferStrategy;
  import java.awt.image.BufferedImage;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.Enumeration;
  import java.util.List;
  import java.util.zip.ZipEntry;
  import java.util.zip.ZipFile;
  import java.util.zip.ZipInputStream;
  import javax.swing.*;
  
  public class SwingUI extends JFrame implements GUIInterface {
  
      private Canvas canvas;
      private BufferStrategy buffer;
      private NES nes;
      private static final long serialVersionUID = 6411494245530679723L;
      private final AL listener = new AL();
      private int screenScaleFactor;
      private final long[] frametimes = new long[60];
      private int frametimeptr = 0;
      private boolean smoothScale, inFullScreen = false;
      private GraphicsDevice gd;
      private int NES_HEIGHT, NES_WIDTH;
      private Renderer renderer;
      private final ControllerImpl padController1, padController2;
  
      public SwingUI() {
          nes = new NES(this);
          screenScaleFactor = PrefsSingleton.get().getInt("screenScaling", 2);
          padController1 = new ControllerImpl(this, 0);
          padController2 = new ControllerImpl(this, 1);
          nes.setControllers(padController1, padController2);
          padController1.startEventQueue();
          padController2.startEventQueue();
      }
  
      @Override
      public NES getNes() {
          return nes;
      }
  
      @Override
      public void setNES(NES nes) {
          this.nes = nes;
      }
  
      public synchronized void setRenderOptions() {
          if (canvas != null) {
              this.remove(canvas);
          }
          screenScaleFactor = PrefsSingleton.get().getInt("screenScaling", 2);
          smoothScale = PrefsSingleton.get().getBoolean("smoothScaling", false);
          if (PrefsSingleton.get().getBoolean("TVEmulation", false)) {
              renderer = new NTSCRenderer();
              NES_WIDTH = 302;
          } else {
              renderer = new RGBRenderer();
              NES_WIDTH = 256;
          }
          if (PrefsSingleton.get().getInt("region", 0) > 1) {
              NES_HEIGHT = 240;
              renderer.setClip(0);
          } else {
              NES_HEIGHT = 224;
              renderer.setClip(8);
          }
  
          // Create canvas for painting
          canvas = new Canvas();
          canvas.setSize(NES_WIDTH * screenScaleFactor, NES_HEIGHT * screenScaleFactor);
          canvas.setEnabled(false); //otherwise it steals input events.
          // Add canvas to game window
          this.add(canvas);
          this.pack();
          canvas.createBufferStrategy(2);
          buffer = canvas.getBufferStrategy();
      }
      
  	public void start(String[] args) {
          if (args == null || args.length < 1 || args[0] == null) {
              nes.run();
          } else {
              nes.run(args[0]);
          }
  	}
  
      @Override
      public synchronized void run() {
          //construct window
          this.setTitle("HalfNES " + NES.VERSION);
          this.setResizable(false);
          buildMenus();
          setRenderOptions();
          this.getRootPane().registerKeyboardAction(listener, "Escape",
                  KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
          this.getRootPane().registerKeyboardAction(listener, "Toggle Fullscreen",
                  KeyStroke.getKeyStroke(KeyEvent.VK_F11, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
          this.getRootPane().registerKeyboardAction(listener, "Quit",
                  KeyStroke.getKeyStroke(KeyEvent.VK_F4, KeyEvent.ALT_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW);
          this.setLocation(PrefsSingleton.get().getInt("windowX", 0),
                  PrefsSingleton.get().getInt("windowY", 0));
          this.addWindowListener(listener);
          this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
  
          this.setVisible(true);
          // Create BackBuffer
  
          //now add the drag and drop handler.
          TransferHandler handler = new TransferHandler() {
              @Override
              public boolean canImport(final TransferHandler.TransferSupport support) {
                  if (!support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
                      return false;
                  }
  
                  return true;
              }
  
              @Override
              public boolean importData(final TransferHandler.TransferSupport support) {
                  if (!canImport(support)) {
                      return false;
                  }
                  Transferable t = support.getTransferable();
                  try {
                      //holy typecasting batman (this interface predates generics)
                      File toload = (File) ((java.util.List) t.getTransferData(DataFlavor.javaFileListFlavor)).get(0);
                      loadROM(toload.getCanonicalPath());
                  } catch (UnsupportedFlavorException e) {
                      return false;
                  } catch (IOException e) {
                      return false;
                  }
                  return true;
              }
          };
          this.setTransferHandler(handler);
      }
  
      public void buildMenus() {
          JMenuBar menus = new JMenuBar();
          JMenu file = new JMenu("File");
          JMenuItem item;
          file.add(item = new JMenuItem("Open ROM"));
          item.addActionListener(listener);
          item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O,
                  Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
  
          file.addSeparator();
  
          file.add(item = new JMenuItem("Preferences"));
          item.addActionListener(listener);
          item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P,
                  Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
  
          file.addSeparator();
  
          file.add(item = new JMenuItem("Toggle Fullscreen"));
          item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F11, 0));
          item.addActionListener(listener);
          menus.add(file);
  
          file.add(item = new JMenuItem("Quit"));
          item.addActionListener(listener);
          menus.add(file);
  
          JMenu nesmenu = new JMenu("NES");
          nesmenu.add(item = new JMenuItem("Reset"));
          item.addActionListener(listener);
          item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R,
                  Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
  
          nesmenu.add(item = new JMenuItem("Hard Reset"));
          item.addActionListener(listener);
          item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T,
                  Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
  
          nesmenu.add(item = new JMenuItem("Pause"));
          item.addActionListener(listener);
          item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0));
  
          nesmenu.add(item = new JMenuItem("Resume"));
          item.addActionListener(listener);
          item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0));
  
          nesmenu.add(item = new JMenuItem("Fast Forward"));
          item.addActionListener(listener);
          item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,
                  Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
  
          nesmenu.add(item = new JMenuItem("Frame Advance"));
          item.addActionListener(listener);
          item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PERIOD,
                  Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
  
          nesmenu.addSeparator();
  
          nesmenu.add(item = new JMenuItem("Controller Settings"));
          item.addActionListener(listener);
          item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
                  Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
  
          nesmenu.add(item = new JMenuItem("Cheat Codes"));
          item.addActionListener(listener);
          item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F10,
                  Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
  
          nesmenu.addSeparator();
  
          nesmenu.add(item = new JMenuItem("ROM Info"));
          item.addActionListener(listener);
          item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I,
                  Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
  
          menus.add(nesmenu);
  
          JMenu help = new JMenu("Help");
          help.add(item = new JMenuItem("About"));
          item.addActionListener(listener);
          item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0));
          menus.add(help);
          this.setJMenuBar(menus);
      }
  
      public void loadROM() {
          FileDialog fileDialog = new FileDialog(this);
          fileDialog.setMode(FileDialog.LOAD);
          fileDialog.setTitle("Select a ROM to load");
          //should open last folder used, and if that doesn't exist, the folder it's running in
          final String path = PrefsSingleton.get().get("filePath", System.getProperty("user.dir", ""));
          final File startDirectory = new File(path);
          if (startDirectory.isDirectory()) {
              fileDialog.setDirectory(path);
          }
          //and if the last path used doesn't exist don't set the directory at all
          //and hopefully the jFileChooser will open somewhere usable
          //on Windows it does - on Mac probably not.
          fileDialog.setFilenameFilter(new NESFileFilter());
          boolean wasInFullScreen = false;
          if (inFullScreen) {
              wasInFullScreen = true;
              //load dialog won't show if we are in full screen, so this fixes for now.
              toggleFullScreen();
          }
          fileDialog.setVisible(true);
          if (fileDialog.getFile() != null) {
              PrefsSingleton.get().put("filePath", fileDialog.getDirectory());
              loadROM(fileDialog.getDirectory() + fileDialog.getFile());
          }
          if (wasInFullScreen) {
              toggleFullScreen();
          }
      }
  
      private void loadROM(String path) {
          if (path.endsWith(".zip") || path.endsWith(".ZIP")) {
              try {
                  loadRomFromZip(path);
              } catch (IOException ex) {
                  this.messageBox("Could not load file:\nFile does not exist or is not a valid NES game.\n" + ex.getMessage());
              }
          } else {
              nes.loadROM(path);
          }
      }
  
      private void loadRomFromZip(String zipName) throws IOException {
          final String romName = selectRomInZip(listRomsInZip(zipName));
          if (romName != null) {
              final File extractedFile = extractRomFromZip(zipName, romName);
              if (extractedFile != null) {
                  extractedFile.deleteOnExit();
                  nes.loadROM(extractedFile.getCanonicalPath());
              }
          }
      }
  
      private List<String> listRomsInZip(String zipName) throws IOException {
          final ZipFile zipFile = new ZipFile(zipName);
          final Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
          final List<String> romNames = new ArrayList<>();
          while (zipEntries.hasMoreElements()) {
              final ZipEntry entry = zipEntries.nextElement();
              if (!entry.isDirectory() && (entry.getName().endsWith(".nes")
                      || entry.getName().endsWith(".fds")
                      || entry.getName().endsWith(".nsf"))) {
                  romNames.add(entry.getName());
              }
          }
          zipFile.close();
          if (romNames.isEmpty()) {
              throw new IOException("No NES games found in ZIP file.");
          }
          return romNames;
      }
  
      private String selectRomInZip(List<String> romNames) {
          if (romNames.size() > 1) {
              return (String) JOptionPane.showInputDialog(this,
                      "Select ROM to load", "Select ROM to load",
                      JOptionPane.PLAIN_MESSAGE, null,
                      romNames.toArray(), romNames.get(0));
          } else if (romNames.size() == 1) {
              return romNames.get(0);
          }
          return null;
      }
  
      private File extractRomFromZip(String zipName, String romName) throws IOException {
          final ZipInputStream zipStream = new ZipInputStream(new FileInputStream(zipName));
          ZipEntry entry;
          do {
              entry = zipStream.getNextEntry();
          } while ((entry != null) && (!entry.getName().equals(romName)));
          if (entry == null) {
              zipStream.close();
              throw new IOException("Cannot find file " + romName + " inside archive " + zipName);
          }
          //name temp. extracted file after parent zip and file inside
  
          //note: here's the bug, when it saves the temp file if it's in a folder 
          //in the zip it's trying to put it in the same folder outside the zip
          final File outputFile = new File(new File(zipName).getParent()
                  + File.separator + FileUtils.stripExtension(new File(zipName).getName())
                  + " - " + romName);
          if (outputFile.exists()) {
              this.messageBox("Cannot extract file. File " + outputFile.getCanonicalPath() + " already exists.");
              zipStream.close();
              return null;
          }
          final byte[] buf = new byte[4096];
          final FileOutputStream fos = new FileOutputStream(outputFile);
          int numBytes;
          while ((numBytes = zipStream.read(buf, 0, buf.length)) != -1) {
              fos.write(buf, 0, numBytes);
          }
          zipStream.close();
          fos.close();
          return outputFile;
      }
  
      public synchronized void toggleFullScreen() {
          if (inFullScreen) { // disable
              this.dispose();
              gd.setFullScreenWindow(null);
              canvas.setSize(NES_HEIGHT * screenScaleFactor, NES_WIDTH * screenScaleFactor);
              this.setUndecorated(false);
              this.setVisible(true);
              inFullScreen = false;
              buildMenus();
              // nes.resume();
          } else { // enable
              setJMenuBar(null);
              gd = getGraphicsConfiguration().getDevice();
              if (!gd.isFullScreenSupported()) {
                  //then fullscreen will give a window the size of the screen instead
                  messageBox("Fullscreen is not supported by your OS or version of Java.");
                  return;
              }
              this.dispose();
              this.setUndecorated(true);
  
              gd.setFullScreenWindow(this);
              this.setVisible(true);
              
              DisplayMode dm = gd.getDisplayMode();
              canvas.setSize(dm.getWidth(), dm.getHeight());
  
              inFullScreen = true;
          }
      }
  
      @Override
      public void messageBox(final String message) {
          JOptionPane.showMessageDialog(this, message);
      }
      int bgcolor;
      BufferedImage frame;
      double fps;
      int frameskip = 0;
  
      @Override
      public final synchronized void setFrame(final int[] nextframe, final int[] bgcolors, boolean dotcrawl) {
          //todo: stop running video filters while paused!
          //also move video filters into a worker thread because they
          //don't really depend on emulation state at all. Yes this is going to
          //cause more lag but it will hopefully get back up to playable speed with NTSC filter
  
          frametimes[frametimeptr] = nes.getFrameTime();
          ++frametimeptr;
          frametimeptr %= frametimes.length;
  
          if (frametimeptr == 0) {
              long averageframes = 0;
              for (long l : frametimes) {
                  averageframes += l;
              }
              averageframes /= frametimes.length;
              fps = 1E9 / averageframes;
              this.setTitle(String.format("HalfNES %s - %s, %2.2f fps"
                      + ((frameskip > 0) ? " frameskip " + frameskip : ""),
                      NES.VERSION,
                      nes.getCurrentRomName(),
                      fps));
          }
          if (nes.framecount % (frameskip + 1) == 0) {
              frame = renderer.render(nextframe, bgcolors, dotcrawl);
              render();
          }
      }
  
      @Override
      public final synchronized void render() {
          final Graphics graphics = buffer.getDrawGraphics();
          if (smoothScale) {
              ((Graphics2D) graphics).setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
          }
          if (inFullScreen) {
              graphics.setColor(Color.BLACK);
              DisplayMode dm = gd.getDisplayMode();
              int scrnheight = dm.getHeight();
              int scrnwidth = dm.getWidth();
              graphics.fillRect(0, 0, scrnwidth, scrnheight);
              if (PrefsSingleton.get().getBoolean("maintainAspect", true)) {
                  double scalefactor = getmaxscale(scrnwidth, scrnheight);
                  int height = (int) (NES_HEIGHT * scalefactor);
                  int width = (int) (256 * scalefactor * 1.1666667);
                  graphics.drawImage(frame, ((scrnwidth / 2) - (width / 2)),
                          ((scrnheight / 2) - (height / 2)),
                          width,
                          height,
                          null);
              } else {
                  graphics.drawImage(frame, 0, 0,
                          scrnwidth,
                          scrnheight,
                          null);
              }
              graphics.setColor(Color.DARK_GRAY);
              graphics.drawString(this.getTitle(), 16, 16);
  
          } else {
              graphics.drawImage(frame, 0, 0, NES_WIDTH * screenScaleFactor, NES_HEIGHT * screenScaleFactor, null);
          }
  
          graphics.dispose();
          buffer.show();
  
      }
  
      private void showOptions() {
          final PreferencesDialog dialog = new PreferencesDialog(this);
          dialog.setVisible(true);
          if (dialog.okClicked()) {
              setRenderOptions();
              nes.setParameters();
          }
      }
  
      private void showControlsDialog() {
          final ControlsDialog dialog = new ControlsDialog(this);
          dialog.setVisible(true);
          if (dialog.okClicked()) {
              padController1.setButtons();
              padController2.setButtons();
          }
      }
  
      private void showActionReplayDialog() {
          nes.pause();
          final ActionReplay actionReplay = nes.getActionReplay();
          if (actionReplay != null) {
              final ActionReplayGui dialog = new ActionReplayGui(this, false, actionReplay);
              dialog.setVisible(true);
          } else {
              JOptionPane.showMessageDialog(this, "You have to load a game first.", "No ROM", JOptionPane.ERROR_MESSAGE);
          }
          nes.resume();
      }
  
      public void savewindowposition() {
          PrefsSingleton.get().putInt("windowX", this.getX());
          PrefsSingleton.get().putInt("windowY", this.getY());
      }
  
      private double getmaxscale(final int width, final int height) {
          return Math.min(height / (double) NES_HEIGHT, width / (double) NES_WIDTH);
      }
  
      @Override
      public void loadROMs(String path) {
          throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
      }
  
      public class AL implements ActionListener, WindowListener {
  
          @Override
          public void actionPerformed(final ActionEvent arg0) {
              // placeholder for more robust handler
              if (arg0.getActionCommand().equals("Quit")) {
                  close();
              } else if (arg0.getActionCommand().equals("Reset")) {
                  nes.reset();
              } else if (arg0.getActionCommand().equals("Hard Reset")) {
                  nes.reloadROM();
              } else if (arg0.getActionCommand().equals("Pause")) {
                  nes.pause();
              } else if (arg0.getActionCommand().equals("Resume")) {
                  nes.resume();
              } else if (arg0.getActionCommand().equals("Preferences")) {
                  showOptions();
              } else if (arg0.getActionCommand().equals("Fast Forward")) {
                  nes.toggleFrameLimiter();
              } else if (arg0.getActionCommand().equals("About")) {
                  messageBox("HalfNES " + NES.VERSION
                          + "\n"
                          + "Get the latest version and report any bugs at " + NES.URL + " \n"
                          + "\n"
                          + "This program is free software licensed under the GPL version 3, and comes with \n"
                          + "NO WARRANTY of any kind. (but if something's broken, please report it). \n"
                          + "See the license.txt file for details.");
              } else if (arg0.getActionCommand().equals("ROM Info")) {
                  String info = nes.getrominfo();
                  if (info != null) {
                      messageBox(info);
                  }
              } else if (arg0.getActionCommand().equals("Open ROM")) {
                  loadROM();
              } else if (arg0.getActionCommand().equals("Toggle Fullscreen")) {
                  toggleFullScreen();
              } else if (arg0.getActionCommand().equals("Frame Advance")) {
                  nes.frameAdvance();
              } else if (arg0.getActionCommand().equals("Escape")) {
                  if (inFullScreen) {
                      toggleFullScreen();
                  } else {
                      close();
                  }
              } else if (arg0.getActionCommand().equals("Controller Settings")) {
                  showControlsDialog();
              } else if (arg0.getActionCommand().equals("Cheat Codes")) {
                  showActionReplayDialog();
              }
          }
  
          @Override
          public void windowOpened(WindowEvent e) {
          }
  
          @Override
          public void windowClosing(WindowEvent e) {
              close();
          }
  
          private void close() {
              dispose();
              savewindowposition();
              padController1.stopEventQueue();
              padController2.stopEventQueue();
              nes.quit();
          }
  
          @Override
          public void windowClosed(WindowEvent e) {
              //we don't care about these events
          }
  
          @Override
          public void windowIconified(WindowEvent e) {
              //but java wants us to implement something for all of them
          }
  
          @Override
          public void windowDeiconified(WindowEvent e) {
              //so we can use the interface.
          }
  
          @Override
          public void windowActivated(WindowEvent e) {
          }
  
          @Override
          public void windowDeactivated(WindowEvent e) {
          }
      }
  }
  