/*
 * 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) {
        }
    }
}