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

    A project from the Physics Dept, The University of Oxford

    Copyright (C) 2007-2010 The University of Oxford

    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: 

    jpc.sourceforge.net
    or the developer website
    sourceforge.net/projects/jpc/

    Conceived and Developed by:
    Rhys Newman, Ian Preston, Chris Dennis

    End of licence header
*/

package org.jpc.debugger;

import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import org.jpc.emulator.memory.AddressSpace;

import javax.swing.*;

import org.jpc.debugger.util.*;
import org.jpc.emulator.memory.PhysicalAddressSpace;

public class WatchpointsFrame extends UtilityFrame implements PCListener
{
    public static final String WATCHPOINT_FILE = "watchpoints.jpc";
    public static final long WATCHPOINT_MAGIC = 0x81057FAB7272F11l;

    private boolean edited;
    private List<Watchpoint> watchpoints;
    private WPModel model;
    private JTable wpTable;
    private String watchpointFileName;
    private AddressSpace addressSpace;

    private JCheckBoxMenuItem ignoreWP, watchPrimary;

    public WatchpointsFrame()
    {
        super("Watchpoints");

        watchpointFileName = WATCHPOINT_FILE;
        watchpoints = new Vector();
        model = new WPModel();
        edited = false;
        
        addressSpace=(AddressSpace) JPC.getObject(PhysicalAddressSpace.class);

        wpTable = new JTable(model);
        model.setupColumnWidths(wpTable);

        String delWP = "Del WP";
        InputMap in = new InputMap();
        in.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), delWP);
        ActionMap ac = new ActionMap();
        ac.setParent(wpTable.getActionMap());
        ac.put(delWP, new Deleter());

        wpTable.setInputMap(JComponent.WHEN_FOCUSED, in);
        wpTable.setActionMap(ac);

        add("Center", new JScrollPane(wpTable));

        JMenu options = new JMenu("Options");
        options.add("Set Watchpoint").addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent evt)
            {
                try {
                    String input = JOptionPane.showInputDialog(WatchpointsFrame.this, "Enter the address (in Hex) for the watchpoint: ", "Watchpoint", JOptionPane.QUESTION_MESSAGE);
                    int address = (int) Long.parseLong(input.toLowerCase(), 16);
                    setWatchpoint(address);
                } catch (Exception e) {
                }
            }
        });
        options.addSeparator();
        options.add("Remove All Watchpoints").addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent evt)
            {
                removeAllWatchpoints();
            }
        });
        
        options.addSeparator();
        ignoreWP = new JCheckBoxMenuItem("Ignore Watchpoints");
        options.add(ignoreWP);
        watchPrimary = new JCheckBoxMenuItem("Watch 'Primary' watchpoints only");
        options.add(watchPrimary);

        JMenuBar bar = new JMenuBar();
        bar.add(new WPFileMenu());
        bar.add(options);
        setJMenuBar(bar);

        setPreferredSize(new Dimension(600, 300));
        JPC.getInstance().objects().addObject(this);
        loadWatchpoints();
    }

    public boolean WatchPrimaryOnly()
    {
        return watchPrimary.getState();
    }

    public boolean ignoreWatchpoints()
    {
        return ignoreWP.getState();
    }

    public boolean isEdited()
    {
        return edited;
    }

    public void frameClosed()
    {
        if (edited)
        {
            if (JOptionPane.showConfirmDialog(this, "Do you want to save the changes to the Watchpoints?", "Save Watchpoints", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) == JOptionPane.YES_OPTION)
                saveWatchpoints();
            edited = false;
        }
        
        JPC.getInstance().objects().removeObject(this);
    }

    class WPFileMenu extends JMenu implements ActionListener
    {
        private JMenuItem load, save, saveAs, importWP;

        WPFileMenu()
        {
            super("File");
            
            load = add("Load Watchpoints");
            load.addActionListener(this);
            save = add("Save Watchpoints");
            save.addActionListener(this);
            saveAs = add("Save Watchpoints As");
            saveAs.addActionListener(this);
            addSeparator();
            importWP = add("Import Watchpoints");
            importWP.addActionListener(this);
        }
        
        private String deriveWPFileName(String name)
        {
            String nm = name.toLowerCase();
            if (nm.endsWith(".jpc"))
                return name;

            int dot = nm.indexOf('.');
            if (dot < 0)
                dot = nm.length();

            return name.substring(0, dot)+".jpc";
        }
        
        public void actionPerformed(ActionEvent evt)
        {
            JFileChooser chooser = (JFileChooser) JPC.getObject(JFileChooser.class);
            if (evt.getSource() == load)
            {
                if (chooser.showOpenDialog(JPC.getInstance()) != JFileChooser.APPROVE_OPTION)
                    return;
                
                watchpointFileName = chooser.getSelectedFile().getAbsolutePath();
                removeAllWatchpoints();
                loadWatchpoints();
            }
            else if (evt.getSource() == save)
            {
                saveWatchpoints();
            }
            else if (evt.getSource() == importWP)
            {
                if (chooser.showOpenDialog(JPC.getInstance()) != JFileChooser.APPROVE_OPTION)
                    return;
                
                removeAllWatchpoints();
                String fileName = chooser.getSelectedFile().getAbsolutePath();
                importWatchpoints(fileName, false);
            }
            else if (evt.getSource() == saveAs)
            {
                if (chooser.showSaveDialog(JPC.getInstance()) != JFileChooser.APPROVE_OPTION)
                    return;
                
                watchpointFileName = chooser.getSelectedFile().getAbsolutePath();
                saveWatchpoints();
            }
        }
    }
    
    class Deleter extends AbstractAction
    {
        public void actionPerformed(ActionEvent evt)
        {
            deleteWatchpoint(wpTable.getSelectedRow());
        }
    }
    
    public boolean isWatchpoint(int address)
    {
        Watchpoint wp = new Watchpoint(address);
        return watchpoints.contains(wp);
    }

    public void setWatchpoint(int address)
    {
        setWatchpoint(address, false);
    }

    public void setWatchpoint(int address, boolean isPrimary)
    {
        Watchpoint wp = new Watchpoint(address);
        int idx = watchpoints.indexOf(wp);
        if (idx < 0)
            watchpoints.add(wp);
        else
            wp = watchpoints.get(idx);

        if (isPrimary)
            wp.isPrimary = isPrimary;

        edited = true; 
        JPC.getInstance().refresh();
    }

    public void removeAllWatchpoints()
    {
        watchpoints.clear();
        edited = true;
        JPC.getInstance().refresh();
    }

    public void removeWatchpoint(int address)
    {
        Watchpoint wp = new Watchpoint(address);
        int idx = watchpoints.indexOf(wp);
        if (idx < 0)
            return;

        deleteWatchpoint(idx);
    }

    public Watchpoint checkForWatch()
    {
        return checkForWatch(watchPrimary.getState());
    }

    public Watchpoint checkForWatch(boolean isPrimary)
    {
        if (ignoreWP.getState())
            return null;


        for (Watchpoint wp : watchpoints) {
            byte b = addressSpace.getByte(wp.address);
            //if ((b != (byte) 0xff) && (b != 0) && (wp.value!=b))
            if (wp.value!=b)
            {
                if (isPrimary && !wp.isPrimary)
                    continue;

                return wp;
            }
        }

        return null;
    }

    private void deleteWatchpoint(int index)
    {
        try {
            watchpoints.remove(index);
        } catch (IndexOutOfBoundsException e) {
        }
        edited = true;

        JPC.getInstance().refresh();
    }

    public class Watchpoint implements Comparable<Watchpoint>
    {
        private int address;
        private int value;
        private boolean isPrimary;
        private String name;
        private boolean watchForValue;
        private byte watchValue;

        Watchpoint(int addr)
        {
            this(addr, false, (byte)0, false);
        }

        public Watchpoint(String name, int addr, byte watchValue, boolean watchForValue)
        {
            this(addr, false, watchValue, watchForValue);
            this.name = name;
        }
        
        Watchpoint(int addr, boolean primary, byte watchValue, boolean watchForValue)
        {
            address = addr;
            isPrimary = primary;
            name = "";
            value=addressSpace.getByte(addr);
            this.watchValue = watchValue;
            this.watchForValue = watchForValue;
        }

        
        public boolean equals(Object another)
        {
            if (!(another instanceof Watchpoint))
                return false;

            return (address == ((Watchpoint) another).address) && (watchValue == ((Watchpoint) another).watchValue);
        }

        public int compareTo(Watchpoint wp)
        {
            if (address != wp.address)
                return address - wp.address;
            if (watchValue == wp.watchValue)
                return 0;
            if ((0xff & watchValue) < (0xff & wp.watchValue))
                return -1;
            return 1;
        }

        public String getName()
        {
            return name;
        }
        
        public int getAddress()
        {
            return address;
        }

        public int getValue()
        {
            return value;
        }
 
        public void updateValue()
        {
            value=addressSpace.getByte(address);
        }

        public boolean isWatchingForValue()
        {
            return watchForValue;
        }

        public byte getWatchTarget()
        {
            return watchValue;
        }

        public boolean isPrimary()
        {
            return isPrimary;
        }
    }

    class WPModel extends BasicTableModel
    {
        WPModel()
        {
            super(new String[]{"Address", "Name", "Primary", "Watch target", "Watch for value"}, new int[]{80, 250, 70, 90, 90});
        }
        
        public int getRowCount()
        {
            return watchpoints.size();
        }

        public boolean isCellEditable(int row, int column)
        {
            return true;
        }

        public Class getColumnClass(int col)
        {
            if ((col == 2) || (col == 4))
                return Boolean.class;
            return String.class;
        }

        public void setValueAt(Object obj, int row, int column)
        {
            Watchpoint wp = watchpoints.get(row);

            if (column == 0)
            {
                try
                {
                    int addr = (int) Long.parseLong(obj.toString().toLowerCase(), 16);
                    wp.address = addr;
                }
                catch (Exception e) {}
            }
            else if (column == 2)
                wp.isPrimary = ((Boolean) obj).booleanValue();
            else if (column == 1)
                wp.name = obj.toString();
            else if (column == 3)
                wp.watchValue = (byte)Integer.parseInt(obj.toString(), 16);
            else if (column == 4)
                wp.watchForValue = ((Boolean) obj).booleanValue();

            int selected = sortWatchpoints(row);
            JPC.getInstance().refresh();

            if (selected >= 0)
            {
                wpTable.setRowSelectionInterval(selected, selected);
                Rectangle rect = wpTable.getCellRect(selected, 0, true);
                wpTable.scrollRectToVisible(rect);
            }
            edited = true;
        }

        public Object getValueAt(int row, int column)
        {
            Watchpoint wp = watchpoints.get(row);

            switch (column)
            {
            case 0:
                return MemoryViewPanel.zeroPadHex(wp.address, 8);
            case 1:
                return wp.name;
            case 2:
                return new Boolean(wp.isPrimary);
            case 3:
                return String.format("%02x", wp.watchValue);
            case 4:
                return new Boolean(wp.watchForValue);
            default:
                return "";
            }
        }
    }

    private int sortWatchpoints(int selectedRow)
    {
        Watchpoint selected = null;
        if (selectedRow >= 0)
            selected = watchpoints.get(selectedRow);

        Collections.sort(watchpoints);

        if (selected == null)
            return 0;
        
        for (int i = 0; i < watchpoints.size(); i++) {
            if (watchpoints.get(i) == selected)
                return i;
        }
        
        return 0;
    }

    public boolean importWatchpoints(String fileName, boolean ignoreDots)
    {
        List<Watchpoint> loaded = new ArrayList<Watchpoint>();

        File f = new File(fileName);
        if (!f.exists())
            return false;

        try {
            FileReader fin = new FileReader(f);
            try {
                BufferedReader in = new BufferedReader(fin);

                while (true) {

                    String line = in.readLine();
                    if (line == null)
                        break;

                    String[] elements = line.split("\\s", 2);

                    String name = elements[1];
                    if (name.startsWith(".") && ignoreDots)
                        continue;
                    
                    int addr = Integer.parseInt(elements[0], 16);
                    byte watchValue = (byte) 0;
                    boolean watchForValue = false;
                    if (elements.length > 2)
                    {
                        watchValue = (byte) Integer.parseInt(elements[2], 16);
                        watchForValue = true;
                    }

                    loaded.add(new Watchpoint(name, addr, watchValue, watchForValue));
                }
            } catch (IOException e) {
                return false;
            } finally {
                try {
                    fin.close();
                } catch (IOException e) {
                }
            }
        } catch (FileNotFoundException e) {
            return false;
        }

        watchpoints.clear();
        watchpoints.addAll(loaded);
        sortWatchpoints(-1);
        edited = true;
        return true;
    }

    public void loadWatchpoints()
    {
        FileInputStream fin = null;
        watchpoints.clear();

        try
        {
            File f = new File(watchpointFileName);
            if (!f.exists())
                return;

            fin = new FileInputStream(f);
            DataInputStream din = new DataInputStream(fin);
            
            if (din.readLong() != WATCHPOINT_MAGIC)
                throw new IOException("Magic number mismatch");

            while (true)
            {
                int addr = din.readInt();
                boolean primary = din.readBoolean();
                String name = din.readUTF();
                byte watchValue = din.readByte();
                boolean watchForValue = din.readBoolean();

                Watchpoint wp = new Watchpoint(addr, primary, watchValue, watchForValue);
                wp.name = name;
                watchpoints.add(wp);
            }
        }
        catch (EOFException e) 
        {
            setTitle("Watchpoints: "+watchpointFileName);
        }
        catch (Exception e)
        {
            System.out.println("Warning: failed to load watchpoints");
            e.printStackTrace();
            setTitle("Watchpoints: "+watchpointFileName+" ERROR");
            alert("Error loading watchpoints: "+e, JOptionPane.ERROR_MESSAGE);
        }
        finally
        {
            try
            {
                fin.close();
            }
            catch (Exception e) {}
            sortWatchpoints(-1);
            edited = false;
        }       
    }

    public void saveWatchpoints()
    {
        FileOutputStream out = null;
        try
        {
            out = new FileOutputStream(watchpointFileName);
            DataOutputStream dout = new DataOutputStream(out);
            dout.writeLong(WATCHPOINT_MAGIC);

            for (Watchpoint wp : watchpoints) {
                dout.writeInt(wp.address);
                dout.writeBoolean(wp.isPrimary);
                dout.writeUTF(wp.name);
                dout.writeByte(wp.watchValue);
                dout.writeBoolean(wp.watchForValue);
            }
            
            setTitle("Watchpoints: "+watchpointFileName);
        }
        catch (Exception e)
        {
            System.out.println("Warning: failed to save watchpoints");
            e.printStackTrace();
            setTitle("Watchpoints: "+watchpointFileName+" ERROR");
            alert("Error saving watchpoints: "+e, JOptionPane.ERROR_MESSAGE);
        }
        finally
        {
            try
            {
                out.close();
            }
            catch (Exception e) {}
            edited = false;
        } 
    }

    public void pcCreated() {}

    public void pcDisposed() {}
    
    public void executionStarted() {}

    public void executionStopped() {}

    public void refreshDetails()
    {
        model.fireTableDataChanged();
    }
}
