/*
    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.io.*;
import java.awt.*;
import java.text.*;
import java.awt.color.*;
import java.awt.image.*;
import java.awt.event.*;

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

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 PCMonitor extends KeyHandlingPanel implements GraphicsDisplay
{
    public static final int WIDTH = 720;
    public static final int HEIGHT = 400;
    public static final int DEPTH = 32;

    public static int thisMachineClockSpeedMHz = 3000;
    public static boolean showSpeedDisplay;

    private PC pc;
    private Keyboard keyboard;
    private VGACard vgaCard;

    private BufferedImage buffer;
    private int[] rawImageData;

    private Updater updater;
    private int xmin, xmax, ymin, ymax, width, height, mouseX, mouseY;
    private boolean resized, doubleSize;

    public PCMonitor(PC pc)
    {
        this(null, pc);
    }

    public PCMonitor(LayoutManager mgr, PC pc)
    {
        super(mgr);

        this.pc = pc;
        setDoubleBuffered(false);
        requestFocusInWindow();
        doubleSize = false;
        mouseX = mouseY = 0;

        vgaCard = pc.getGraphicsCard();
        keyboard = pc.getKeyboard();
	resizeDisplay(WIDTH, HEIGHT);
        setInputMap(WHEN_FOCUSED, null);
    }

    public void repeatedKeyPress(int keyCode)
    {
        keyboard.keyPressed(KeyMapping.getScancode(new Integer(keyCode)));
    }

    public void keyPressed(int keyCode)
    {
        keyboard.keyPressed(KeyMapping.getScancode(new Integer(keyCode)));
    }

    public void keyReleased(int keyCode)
    {
        keyboard.keyReleased(KeyMapping.getScancode(new Integer(keyCode)));
    }

    public void mouseEventReceived(int dx, int dy, int dz, int buttons)
    {
        if (doubleSize)
            keyboard.putMouseEvent(dx/2, dy/2, dz, buttons);
        else
            keyboard.putMouseEvent(dx, dy, dz, buttons);
    }

    public void startUpdateThread()
    {
        startUpdateThread(Thread.currentThread().getPriority());
    }

    public void startUpdateThread(int vgaUpdateThreadPriority)
    {
        updater = new Updater();
        updater.setPriority(vgaUpdateThreadPriority);
        updater.start();
    }

    public void dispose()
    {
        try
        {
            updater.running = false;
            updater.interrupt();
        }
        catch (Throwable t) {}
    }

    public void setDoubleSize(boolean value)
    {
        if (doubleSize == value)
            return;
        
        Dimension d = getPreferredSize();
        if (value)
            setPreferredSize(new Dimension(d.width*2, d.height*2));
        else
            setPreferredSize(new Dimension(d.width/2, d.height/2));
        doubleSize = value;

        revalidate();
        repaint();
    }

    class Updater extends Thread
    {
        boolean running = true;

        public void run()
        {
            while (running)
            {
                try
                {
                    Thread.sleep(30);
                    prepareUpdate();
                    vgaCard.updateDisplay(PCMonitor.this);

                    if (doubleSize)
                        repaint(2*xmin, 2*ymin, 2*(xmax - xmin + 1), 2*(ymax - ymin + 1));
                    else
                        repaint(xmin, ymin, xmax - xmin + 1, ymax - ymin + 1);
                }
                catch (InterruptedException e) {}
                catch (ThreadDeath d)
                {
                    running = false;
                }
                catch (Throwable t) 
                {
                    System.err.println("Warning: error in video display update "+t);
		    t.printStackTrace();
                }
            }
        }
    }

    public int rgbToPixel(int red, int green, int blue)
    {
        return 0xFF000000 | ((0xFF & red) << 16) | ((0xFF & green) << 8) | (0xFF & blue);
    }

    private final void prepareUpdate()
    {
        xmin = width;
        xmax = 0;
        ymin = height;
        ymax = 0;
    }

    public void resizeDisplay(int width, int height)
    {
        resized = true;
        this.width = width;
        this.height = height;

        buffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        WritableRaster raster = buffer.getRaster();
        DataBufferInt buf = (DataBufferInt) raster.getDataBuffer();
        rawImageData = buf.getData();
        
        int pink = Color.pink.getRGB();
        for (int i=0; i<rawImageData.length; i++)
            rawImageData[i] = pink;

        setPreferredSize(new Dimension(width, height));
        revalidate();
        repaint();
    }

    public int[] getDisplayBuffer()
    {
	return rawImageData;
    }

    public final void dirtyDisplayRegion(int x, int y, int w, int h)
    {
        xmin = Math.min(x, xmin);
        xmax = Math.max(x+w, xmax);
        ymin = Math.min(y, ymin);
        ymax = Math.max(y+h, ymax);
    }

    public void update(Graphics g)
    {
        paint(g);
    }
 
    protected void paintPCMonitor(Graphics g)
    {
        int w = width;
        int h = height;

        if (doubleSize)
        {
            w *= 2;
            h *= 2;
            g.drawImage(buffer, 0, 0, w, h, null);
        }
        else
            g.drawImage(buffer, 0, 0, null);
        
        Dimension s = getSize();
        g.setColor(getBackground());
        g.fillRect(w, 0, s.width - w, h);
        g.fillRect(0, h, s.width, s.height - h);
    }

    protected void defaultPaint(Graphics g)
    {
        super.paint(g);
    }

    public void paint(Graphics g)
    {
        paintPCMonitor(g);
    }

    public static class PCMonitorFrame extends JFrame implements ActionListener, Runnable
    {
        private boolean running;
        private PC pc;
        private PCMonitor monitor;
        private JMenuItem quit, stop, start, reset;
        private JCheckBoxMenuItem doubleSize;
        private Thread runner;

        private long markTime;
        private DecimalFormat fmt, fmt2;
        private double mhz;
        private JProgressBar speedDisplay;

        public PCMonitorFrame(String title, PC pc)
        {
            super(title);
            monitor = new PCMonitor(pc);
            monitor.startUpdateThread();

            this.pc = pc;
            mhz = 0;
            fmt = new DecimalFormat("0.00");
            fmt2 = new DecimalFormat("0.000");
            markTime = System.currentTimeMillis();
            getContentPane().add("Center", new JScrollPane(monitor));

            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            JMenuBar bar = new JMenuBar();
            JMenu file = new JMenu("File");
            stop = file.add("Stop");
            stop.addActionListener(this);
            start = file.add("Start");
            start.addActionListener(this);
	    reset = file.add("Reset");
	    reset.addActionListener(this);
	    file.addSeparator();
            doubleSize = new JCheckBoxMenuItem("Double Size");
            file.add(doubleSize);
            doubleSize.addActionListener(this);
            file.addSeparator();
            quit = file.add("Quit");
            quit.addActionListener(this);
            bar.add(file);
            
            bar.add(Box.createHorizontalGlue());
            speedDisplay = new JProgressBar();
            speedDisplay.setStringPainted(true);
            speedDisplay.setString(" 0.00 Mhz");
            speedDisplay.setPreferredSize(new Dimension(100, 20));
	    if (showSpeedDisplay)
		bar.add(speedDisplay);

            setJMenuBar(bar);
            start();
        }

        private boolean updateMHz(long i)
        {
            long t2 = System.currentTimeMillis();

            if (t2 - markTime < 1000)
                return false;

            if (t2 == markTime)
                mhz = 0;
            else
                mhz = i * 1000.0 / (t2 - markTime) / 1000000;

            markTime = t2;
            
            double clockSpeed = 17.25/770*mhz/7.5*2.790;
            int percent = (int) (clockSpeed / thisMachineClockSpeedMHz * 1000 * 100 * 10);
            speedDisplay.setValue(percent);
            speedDisplay.setString(fmt.format(mhz)+" MHz or "+fmt2.format(clockSpeed)+" GHz Clock");
            //System.err.println("Speed (MHz): "+mhz);
            return true;
        }

        private synchronized void stop()
        {
            running = false;
            try
            {
                runner.join(5000);
            }
            catch (Throwable t){}

            try
            {
                runner.stop();
            }
            catch (Throwable t) {}
            runner = null;
        }

        private synchronized void start()
        {
            if (running)
                return;

            running = true;
            runner = new Thread(this);
            runner.setPriority(Thread.NORM_PRIORITY-1);
            runner.start();
        }

	private void reset()
	{
	    stop();
	    pc.reset();
	    start();
	}

        public void actionPerformed(ActionEvent evt)
        {
            if (evt.getSource() == quit)
                System.exit(0);
            else if (evt.getSource() == stop)
                stop();
            else if (evt.getSource() == start)
                start();
	    else if (evt.getSource() == reset)
		reset();
            else if (evt.getSource() == doubleSize)
                monitor.setDoubleSize(doubleSize.isSelected());
        }

        public void run()
        {
            markTime = System.currentTimeMillis();
	    pc.getSystemClock().resume();
            long execCount = 0;
            try
            {
                while (running)
                {
                    try
                    {
                        while (running)
                        {
                            execCount += pc.executeRealMode();
                            
                            if (execCount >= 50000)
                            {
                                if (updateMHz(execCount))
                                    execCount = 0;
                            }
                        }
                    }
                    catch (ModeSwitchException e) {}
                    
                    try
                    {
                        while (running)
                        {
                            execCount += pc.executeProtectedMode();
                            
                            if (execCount >= 50000)
                            {
                                if (updateMHz(execCount))
                                    execCount = 0;
                            }
                        }
                    }
                    catch (ModeSwitchException e) {}
                }
            }
            catch (Exception e)
            {
		System.err.println("Caught exception @ Address:0x" + Integer.toHexString(pc.getProcessor().getInstructionPointer()));
                System.err.println(e);
                e.printStackTrace();
            }
            finally
            {
                pc.getSystemClock().pause();
                System.err.println("PC Stopped");
            }
        }
    }

    public static PCMonitorFrame createMonitor(PC pc)
    {
        return createMonitor("JPC Monitor", pc);
    }

    public static PCMonitorFrame createMonitor(String title, PC pc)
    {
        PCMonitorFrame result = new PCMonitorFrame(title, pc);
        result.setBounds(100, 100, WIDTH+20, HEIGHT+70);
        result.validate();
        result.setVisible(true);
        
        return result;
    }

    private static String findArg(String[] args, String key, String defaultValue)
    {
        if (key.startsWith("-"))
            key = key.substring(1);

        for (int i=0; i<args.length-1; i++)
        {
            String arg = args[i];
            if (arg.startsWith("-"))
                arg = arg.substring(1);

            if (arg.equalsIgnoreCase(key))
                return args[i+1];
        }

        return defaultValue;
    }

    private static boolean argExists(String[] args, String key)
    {
        if (key.startsWith("-"))
            key = key.substring(1);
        
        for (int i=0; i<args.length; i++)
        {
            String arg = args[i];
            if (arg.startsWith("-"))
                arg = arg.substring(1);

            if (arg.equalsIgnoreCase(key))
                return true;
        }

        return false;
    }

    public static void main(String[] args) throws Exception
    {
        try
        {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        }
        catch (Exception e) {}

       if (args.length == 0)
	    args = new String[] { "-fda", "mem:floppy.img", "-hda", "mem:dosgames.img", "-boot", "fda" };

       try
       {
	   showSpeedDisplay = true;
           thisMachineClockSpeedMHz = Integer.parseInt(findArg(args, "mhz", "-1"));
	   if (thisMachineClockSpeedMHz == -1) {
	       thisMachineClockSpeedMHz = 3000;
	       showSpeedDisplay = false;
	   } else if (thisMachineClockSpeedMHz < 500)
	       thisMachineClockSpeedMHz = 3000;	   
       }
       catch (Exception e) {}
       
        PC pc = PC.createPC(args, new VirtualClock()); 
        PCMonitorFrame frame = createMonitor(pc);
    }
}
