/*
    JPC: A x86 PC Hardware Emulator for a pure Java Virtual Machine
    Release Version 2.0

    A project from the Physics Dept, The University of Oxford

    Copyright (C) 2007 Isis Innovation Limited

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License version 2 as published by
    the Free Software Foundation.

    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, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
    Details (including contact information) can be found at: 

    www.physics.ox.ac.uk/jpc
*/


package org.jpc.j2se;

import java.util.*;
import java.util.zip.*;
import java.lang.ref.*;
import java.io.*;
import java.awt.*;
import java.net.*;
import java.text.*;
import java.awt.color.*;
import java.awt.image.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.event.*;
import javax.imageio.*;

import org.jpc.emulator.processor.*;
import org.jpc.emulator.*;
import org.jpc.support.*;
import org.jpc.emulator.motherboard.*;
import org.jpc.emulator.memory.*;
import org.jpc.emulator.memory.codeblock.*;
import org.jpc.emulator.peripheral.*;
import org.jpc.emulator.pci.peripheral.*;

public class JPCApplet extends JApplet
{
    private static PC pc = null;
    private static JPCApplet owner = null;
    private static BufferedImage jpcLogo = null; 
    private static Object lock = new Object();

    static
    {
        try
        {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        }
        catch (Throwable t) {}

        String[] args = new String[] { "-fda", "mem:floppy.img", "-boot", "fda" };
        try
        {
            pc = PC.createPC(args, new VirtualClock());
        }
        catch (Throwable e)
        {
            System.out.println("Error creating PC singleton");
            e.printStackTrace();
        }
        
        try
        {
            jpcLogo = ImageIO.read(JPCApplet.class.getClassLoader().getResourceAsStream("JPCLogo.png"));
        }
        catch (Throwable t) {}
    }

    private MonitorPanel monitor;
    private Runner runner;
    private KeyTypingPanel keys;
    
    public JPCApplet()
    {
        JPanel pp = new JPanel(new BorderLayout(10, 10));
        pp.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLoweredBevelBorder(), BorderFactory.createLineBorder(new Color(0xFFA6CAF0), 15)));
        
        monitor = new MonitorPanel();
        keys = new KeyTypingPanel();

        pp.add("Center", monitor);
        pp.add("South", keys);

        getContentPane().add("Center", pp);
    }
    
    public void init()
    {
        stop();
        start();
    }

    public void start()
    {
        synchronized (lock)
        {  
            if (owner != null)
            {
                if (owner != this)
                    monitor.showMessage("Another window is currently running the JPC Applet");
            }
            else
            {
                owner = this;
                
                monitor.showMessage("Downloading Disk Image...");
                monitor.revalidate();
                monitor.requestFocus();
                
                runner = new Runner();
            }
        }

        repaint();
    }

    public void stop()
    {
        synchronized (lock)
        {
            if (owner != this)
                return;

            owner = null;
            if (runner != null)
                runner.running = false;
            runner = null;
            monitor.dispose();
        }

        repaint();
    }
   
    public void destroy()
    {
        stop();
    }

    class ImagePanel extends JPanel
    {
        public void paintComponent(Graphics g)
        {
            Dimension s = getSize();
            g.setColor(Color.blue);
            g.fillRect(0, 0, s.width, s.height);
            
            if (jpcLogo == null)
                return;
            
            int x = Math.max(0, s.width/2 - jpcLogo.getWidth()/2);
            int y = Math.max(0, s.height/2 - jpcLogo.getHeight()/2);
            int x2 = Math.min(x + jpcLogo.getWidth(), s.width);
            int y2 = Math.min(y + jpcLogo.getHeight(), s.height);
            
            g.drawImage(jpcLogo, x, y, x2-x, y2-y, null);
        }
    }
    
    class MonitorPanel extends PCMonitor implements Runnable
    {
        boolean showMonitor;
        int bytesRead, totalBytes;
        JProgressBar progress;

        MonitorPanel()
        {
            super(new BorderLayout(10, 10), pc);
            //setBackground(Color.blue);

            progress = new JProgressBar();
            progress.setStringPainted(true);
            progress.setString("Downloading Disk Image...");
            
            bytesRead = 0;
            totalBytes = 1;

            add("South", progress);
            add("Center", new ImagePanel());
        }

        void downloadComplete()
        {
            showMonitor = true;
            startUpdateThread(Thread.NORM_PRIORITY-2);
            repaint();
        }

        public void run()
        {
            if (showMonitor)
                return;

            if (totalBytes > 0)
            {
                progress.setIndeterminate(false);
                progress.setValue(100 * bytesRead / totalBytes);
            }
            else
            {
                progress.setIndeterminate(true);
                progress.setString("Downloaded "+bytesRead+" bytes");
            }
        }

        void updateValues(int read, int total)
        {
            bytesRead = read;
            totalBytes = total;
            SwingUtilities.invokeLater(this);
        }

        void showMessage(String message)
        {
            showMonitor = false;
            progress.setString(message);
            repaint();
        }

        public void paint(Graphics g)
        {
            if (showMonitor)
                super.paint(g);
            else
                defaultPaint(g);
        }
    }

    class CounterStream extends FilterInputStream
    {
        int count;
        
        CounterStream(InputStream src)
        {
            super(src);
        }

        public int getReadCount()
        {
            return count;
        }
        
        public int read() throws IOException
        {
            int r = super.read();
            if (r > 0)
                count += r;
            return r;
        }

        public int read(byte[] b)  throws IOException
        {
            return read(b, 0, b.length);
        }

        public int read(byte[] b, int off, int len) throws IOException
        {
            int r = super.read(b, off, len);
            if (r > 0)
                count += r;
            return r;
        }
    }

    class ZipDevice extends StreamBackedSeekableIODevice
    {
        private String hda;
        private InputStream raw;
        private ZipInputStream zin;
        private boolean isZip;

        ZipDevice(String hdaName, int limit, boolean isZip)
        {
            super(1024*1024, limit);
            hda = hdaName;
            this.isZip = isZip;
        }

	public void configure(String specs) throws Exception
	{
	    throw new Exception();
	}

        public void closeStream() throws IOException
        {
            if (zin != null)
                zin.close();
            if (raw != null)
                raw.close();
        }
    
        protected InputStream resetStream() throws IOException
        {
            raw = getClass().getClassLoader().getResourceAsStream(hda);
            if (isZip)
            {
                zin = new ZipInputStream(raw);
                zin.getNextEntry();
                return zin;
            }
            else
                return raw;
        }
    }

    class Runner implements Runnable
    {
        boolean running;
        Thread thread;
        ZipDevice zipDevice;
        
        Runner()
        {
            running = true;
            thread = new Thread(this);
            thread.setPriority(Thread.NORM_PRIORITY-3);
            thread.start();
        }

        private RawBlockDevice cacheDiskImage(String name) throws Exception
        {
            URL imgURL = getClass().getClassLoader().getResource(name);
            URLConnection conn = imgURL.openConnection();
            conn.setUseCaches(true);
            int length = conn.getContentLength();

            CounterStream counter = new CounterStream(conn.getInputStream());
            ZipInputStream zin = new ZipInputStream(counter);
            zin.getNextEntry();

            ByteArrayOutputStream bout = new ByteArrayOutputStream(8*1024*1024);
            byte[] buffer = new byte[4*1024];
            while (true)
            {
                if (!running)
                    return null;

                int read = zin.read(buffer);
                if (read <= 0)
                    break;

                bout.write(buffer, 0, read);
                monitor.updateValues(counter.getReadCount(), length);
            }

            ArrayBackedSeekableIODevice raw = new ArrayBackedSeekableIODevice(name, bout.toByteArray());
            System.out.println("Hard disk is "+raw.length()+" bytes long");
            return new RawBlockDevice(raw);
        }

        private RawBlockDevice downloadDiskImage(String name) throws Exception
        {
            URL imgURL = getClass().getClassLoader().getResource(name);
            URLConnection conn = imgURL.openConnection();
            conn.setUseCaches(true);
            int length = conn.getContentLength();

            boolean isZip = name.toLowerCase().endsWith(".zip");
            CounterStream counter = new CounterStream(conn.getInputStream());
            InputStream input = null;
            if (isZip)
            {
                ZipInputStream zin = new ZipInputStream(counter);
                zin.getNextEntry();
                input = zin;
            }
            else
                input = counter;

            int uncompressedLength = 0;
            byte[] buffer = new byte[4*1024];
            while (true)
            {
                if (!running)
                    return null;

                int read = input.read(buffer);
                if (read <= 0)
                    break;

                uncompressedLength += read;
                monitor.updateValues(counter.getReadCount(), length);
            }

            zipDevice = new ZipDevice(name, uncompressedLength, isZip);
            //return new RawBlockDevice(zipDevice);
            return new RawBlockDevice(new CachedWriteSeekableIODevice(zipDevice));
        }
        
        public void run()
        {
            long execCount = 0;
            String stopMessage = "";

            try
            {
                try
                {
                    System.gc();
                    Thread.sleep(2000);
                }
                catch (Throwable t) {}
                if (!running)
                    return;

                DriveSet drives = pc.getDrives();
                RawBlockDevice hda = null;
                //hda = cacheDiskImage(getParameter("hda"));
                hda = downloadDiskImage(getParameter("hda"));
                if (!running)
                    return;

                drives.setHardDrive(0, hda);
                monitor.downloadComplete();
                
                pc.reset();
                pc.getSystemClock().resume();
                
                while (running)
                {
                    try
                    {
                        while (running)
                            execCount += pc.executeRealMode();
                    }
                    catch (ModeSwitchException e) {}
                    
                    try
                    {
                        while (running)
                            execCount += pc.executeProtectedMode();
                    }
                    catch (ModeSwitchException e) {}
                }
            }
            catch (ThreadDeath e) {System.out.println("Thread Killed"); e.printStackTrace();}
            catch (Throwable t)
            {
                stopMessage = "Error in execution loop: "+t;
                t.printStackTrace();
            }
            finally
            {
                try
                {
                    zipDevice.closeStream();
                }
                catch (Throwable t) {}

                monitor.showMessage("PC Stopped "+stopMessage);
                pc.getSystemClock().pause();
            }
        }
    }

    class KeyTypingPanel extends JPanel 
    {
        KeyTypingPanel()
        {
            super(new BorderLayout(10, 10));
            JPanel p1 = new JPanel(new GridLayout(1, 0, 10, 10));
            
            KeyPress[] l1 = new KeyPress[]{new KeyPress(" : ", KeyEvent.VK_SEMICOLON, true), 
                                           new KeyPress(" \\ ", KeyEvent.VK_BACK_SLASH),
                                           new KeyPress(" / ", KeyEvent.VK_SLASH),
                                           new KeyPress("<ESC>", KeyEvent.VK_ESCAPE),
                                           new KeyPress("<DEL>", KeyEvent.VK_DELETE),
                                           new KeyPress("<TAB>", KeyEvent.VK_TAB),
                                           new KeyPress(" ; ", KeyEvent.VK_SEMICOLON),
                                           new KeyPress(" ` ", KeyEvent.VK_BACK_QUOTE),
                                           new KeyPress(" ' ", KeyEvent.VK_QUOTE),
                                           new KeyPress(" , ", KeyEvent.VK_COMMA),
                                           new KeyPress(" . ", KeyEvent.VK_PERIOD),
                                           new KeyPress(" ' ", KeyEvent.VK_QUOTE),
                                           new KeyPress(" \" ", KeyEvent.VK_QUOTE, true)};
                                           

            p1.add(new KeyPanel("Miscellaneous Keys", l1));

            KeyPress[] l2 = new KeyPress[]{new KeyPress(" F1 ", KeyEvent.VK_F1),
                                           new KeyPress(" F2 ", KeyEvent.VK_F2),
                                           new KeyPress(" F3 ", KeyEvent.VK_F3),
                                           new KeyPress(" F4 ", KeyEvent.VK_F4),
                                           new KeyPress(" F5 ", KeyEvent.VK_F5),
                                           new KeyPress(" F6 ", KeyEvent.VK_F6),
                                           new KeyPress(" F7 ", KeyEvent.VK_F7),
                                           new KeyPress(" F8 ", KeyEvent.VK_F8),
                                           new KeyPress(" F9 ", KeyEvent.VK_F9),
                                           new KeyPress(" F10 ", KeyEvent.VK_F10)};
            
            p1.add(new KeyPanel("Function Keys", l2));
            p1.add(new MouseSensitivityPanel());
            add("West", p1);   

            //JLabel help = new JLabel("Non-US Keyboards: Select character from drop-down menu and press 'Type Key'");
            //if (monitor.mouseCaptureEnabled())
            JLabel help = new JLabel("Non-US Keyboards, use overrides above. Mouse GRAB: Double Click left button. RELEASE: Double click right button");

            help.setFont(new Font(Font.DIALOG, Font.PLAIN, 12));
            help.setForeground(Color.blue);
            add("South", help);
        }

        class KeyPanel extends JPanel implements ActionListener
        {
            JComboBox choices;

            KeyPanel(String title, KeyPress[] keys)
            {
                super(new BorderLayout(10, 10));

                choices = new JComboBox(keys);
                choices.setEditable(false);
                choices.addActionListener(this);

                add("Center", choices);
                setBorder(BorderFactory.createTitledBorder(title));
                
                JButton type = new JButton("Type Key >> ");
                add("West", type);
                type.addActionListener(this);

                choices.setPreferredSize(new Dimension(80, 20));
            }

            public void actionPerformed(ActionEvent evt)
            {
                KeyPress kp = (KeyPress) choices.getSelectedItem();
                if (kp != null)
                    kp.typeKeys();
                monitor.requestFocus();
            }
        }
    }

    class KeyPress
    {
        String display;
        boolean isShifted;
        int keyCode;

        KeyPress(String display, int code)
        {
            this(display, code, false);
        }

        KeyPress(String display, int code, boolean shift)
        {
            keyCode = 0xFF & code;
            isShifted = shift;
            this.display = display;
            try
            {
                setFont(new Font(Font.DIALOG, Font.BOLD, 18));
            }
            catch (Exception e) {}
        }

        void typeKeys()
        {
            if (isShifted)
                monitor.keyPressed(KeyEvent.VK_SHIFT);
            monitor.keyPressed(keyCode);
            monitor.keyReleased(keyCode);
            if (isShifted)
                monitor.keyReleased(KeyEvent.VK_SHIFT);
        }

        public String toString()
        {
            return display;
        }
    }

    class MouseSensitivityPanel extends JPanel implements ChangeListener
    {
        JSlider slider;

        MouseSensitivityPanel()
        {
            super(new BorderLayout());
            setBorder(BorderFactory.createTitledBorder("Mouse Sensitivity"));

            //if (!monitor.mouseCaptureEnabled())
            //     add("Center", new JLabel("Mouse Disabled in Applet Mode"));
            // else
            {
                slider = new JSlider();
                slider.addChangeListener(this);
                slider.setValue(50);
                stateChanged(null);
                add("Center", slider);
            }

            setPreferredSize(new Dimension(200, 40));
        }

        public void stateChanged(ChangeEvent e) 
        {
            double val = 1.0*slider.getValue()/100.0;
            if (monitor != null)
                monitor.setMouseSensitivity(val);
        }
    }
}
