/* Author: Clauirton Siebra <c.siebra@ed.ac.uk>
 * Updated: Sat May 20 07:56:20 2006 by Clauirton Siebra
 * Copyright: (c) 2006, AIAI, University of Edinburgh
 */

package ix.ip2.map;

import javax.swing.*;
import java.awt.Color;
import java.awt.event.*;
import java.awt.BorderLayout;
import java.awt.Component;
import java.util.*;
import java.io.FileInputStream;
import java.io.File;

import ix.util.*;
import ix.util.lisp.*;
import ix.util.xml.XML;
import ix.util.xml.FileSyntaxManager;
import ix.ip2.StateViewer;
import ix.ip2.Ip2;
import ix.ip2.PanelFrame;
import ix.ip2.Ip2ModelManager;
import ix.ip2.ObjectView;
import ix.iface.ui.HelpFrame;
import ix.iface.ui.AboutFrame;
import ix.iface.util.CatchingActionListener;
import ix.iface.util.ToolFrame;
import ix.iface.util.IconImage;
import ix.icore.process.event.*;
import ix.icore.domain.ObjectProperty;
import ix.icore.domain.Constraint;
import ix.icore.domain.PatternAssignment;

import com.bbn.openmap.*;
import com.bbn.openmap.util.ColorFactory;
import com.bbn.openmap.gui.*;
import com.bbn.openmap.layer.shape.ShapeLayer;
import com.bbn.openmap.event.SelectMouseMode;
import com.bbn.openmap.event.CenterEvent;
import com.bbn.openmap.proj.*;

public class MapWhiteboard extends ToolFrame implements StateViewer, ActionListener, KeyListener, MouseListener {

    protected Ip2 ip2;

    MapBean map = null;
    WorldStateLayer wLayer;
    JLabel coordinate = new JLabel("coordinates");
    boolean ctrlDown = false;
    Vector layersV = new Vector();

    SortedMap viewsNames = null;
    ObjectView view = null;    
    SortedSet objects = null; 
    List properties = null;

    private Function1 stateLookupFn = new Function1() {
        public Object funcall(Object obj) {
            LList llist = (LList)obj;
            return ((Ip2ModelManager)(ip2.getModelManager())).getWorldStateValue(llist);
        }
    };

    public MapWhiteboard(Ip2 ip2) {
	super("Map View");
        this.ip2 = ip2;	

	String viewDirName = Parameters.getParameter("object-view-directory");
	if(viewDirName != null) {
	    loadViews(viewDirName);
	}

        getContentPane().setLayout(new BorderLayout());
        setJMenuBar(makeMenuBar());
        setIconImage(IconImage.getIconImage("ip2-map-icon.gif"));
	
        addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
            setVisible(false);
            }
        });

	//setSize(530, 410);

	setMapView(loadProperties(null));
	stateChange(new ProcessStatusEvent(ip2.getModelManager()),((Ip2ModelManager)(ip2.getModelManager())).getWorldStateMap());
    }

    protected void setMapView(Properties properties) {
	(ip2.getModelManager()).addProcessStatusListener(this);
	ip2.addResetHook(new ResetHook());

	wLayer = new WorldStateLayer(this);

        try {

	    if (map != null) {
		layersV.clear();
		getContentPane().removeAll();
	    }

            //Allows all the components to find each other if they are added to it
            MapHandler mapHandler = new MapHandler();

            // Create the map
            map = new MapBean();

            //Display the coordinates via mouse event
            map.addMouseListener(new MouseAdapter() {
                public void mouseClicked(MouseEvent e) {
                    coordinate.setText((map.getCoordinates(e)).toString());
                }
            });

            map.addKeyListener(this);
            map.addMouseListener(this);
    
            // Add the MapBean to the MapHandler.
            mapHandler.add(map);

            // Add the map to the JFrame
            getContentPane().add(map, BorderLayout.CENTER);

            //Creating Layers
            Layer[] layers = getLayers(properties);
            LayerHandler layerHandler = new LayerHandler();
            for (int i = 0; i < layers.length; i++) {
                layers[i].setVisible(true);
                layerHandler.addLayer(layers[i]);
            }
        
            //Set the properties of the map: center, lat, log, ...
            setMapProperties(properties,layers);

            // Mouse delegator to handle mouse events on the map
            mapHandler.add(new MouseDelegator());
            mapHandler.add(new SelectMouseMode());

            // Manages all layers, on or off.
            mapHandler.add(layerHandler);

            //add a GUI to control the layers
            mapHandler.add(new LayersPanel());

            // Some pre-built tools
            ToolPanel toolPanel = new ToolPanel();
            mapHandler.add(toolPanel);
            OMToolSet ots = new OMToolSet();
            ots.getNavigatePanel().setDefaultCenter(map.getCenter().getLatitude(),
                                                    map.getCenter().getLongitude());

            mapHandler.add(ots);   //mapHandler.add(new OMToolSet());

            toolPanel.add(coordinate);
            getContentPane().add(toolPanel, BorderLayout.SOUTH);
	    //validate();

	    //stateChange(new ProcessStatusEvent(ip2.getModelManager()),((Ip2ModelManager)(ip2.getModelManager())).getWorldStateMap());

        } catch(MultipleSoloMapComponentException e){}
    }

    protected Layer[] getLayers(Properties p) {
        String layersValue = p.getProperty("map.layers");
        if (layersValue == null) {
            System.err.println("No property <<map.layers>> found in properties file.");
            return null;
        }

        StringTokenizer tokens = new StringTokenizer(layersValue, " ");
        Vector layerNames = new Vector();
        while (tokens.hasMoreTokens()) {
            layerNames.addElement(tokens.nextToken());
        }
        int nLayerNames = layerNames.size();
        //Vector layers = new Vector(); //Vector layers = new Vector(nLayerNames);
        layersV.addElement(wLayer);  

        // For each layer marker name, find that layer's properties.
        // The marker name is used to scope those properties that
        // apply to a particular layer.  If you parse the layers'
        // properties from a file, you can add/remove layers from the
        // application without re-compiling.  You could hard-code all
        // the properties being set if you'd rather...

        for (int i = 0; i < nLayerNames; i++) {
            String layerName = (String)layerNames.elementAt(i);

            // Find the .class property to know what kind of layer to create.
            String classProperty = layerName + ".class";
            String className = p.getProperty(classProperty);
            if (className == null) {
                // Skip it if you don't find it.
                System.err.println("Failed to locate property \""+ classProperty + "\"");
                System.err.println("Skipping layer \"" + layerName + "\"");
                continue;
            }
            try {
                // Create it if you do...
                Object obj = java.beans.Beans.instantiate(null, className);
                if (obj instanceof Layer) {
                    Layer l = (Layer) obj;
                    // All layers have a setProperties method, and
                    // should intialize themselves with proper
                    // settings here.  If a property is not set, a
                    // default should be used, or a big, graceful
                    // complaint should be issued.
                    l.setProperties(layerName, p);
                    layersV.addElement(l);
                }
            } catch (java.lang.ClassNotFoundException e) {
                System.err.println("Layer class not found: \""+ className + "\"");
                System.err.println("Skipping layer \"" + layerName + "\"");
            } catch (java.io.IOException e) {
                System.err.println("IO Exception instantiating class \""   + className + "\"");
                System.err.println("Skipping layer \"" + layerName + "\"");
            }
        }
        int nLayers = layersV.size();

        if (nLayers == 0) {
            return null;
        } else {
            Layer[] value = new Layer[nLayers];
            layersV.copyInto(value);
            return value;
        }

    }

    public void reset() {
	if(wLayer != null) 
	    wLayer.reset();
    }   

    //Methods in ProcessStatusListener interface
     
    /** Ignored by this viewer. */
    public void statusUpdate(ProcessStatusEvent event) { }

    /** Ignored by this viewer. */
    public void newBindings(ProcessStatusEvent event, Map bindings) { }

    public void stateChange(ProcessStatusEvent event, Map delta) {

        if(view == null) {
	    for (Iterator i = delta.entrySet().iterator(); i.hasNext();) {
		Map.Entry e = (Map.Entry)i.next();
		LList pattern = (LList)e.getKey();
		Object value = e.getValue();
		wLayer.addPatternValue(pattern, value);
		wLayer.changePatternValue(pattern, value);
	    }
	}

	else {
	    objects = view.getInitialObjects(((Ip2ModelManager)(ip2.getModelManager())).getWorldStateMap(), stateLookupFn);   
	    //SortedSet sortedset = view.getNewObjects(delta, objects, stateLookupFn);

	    for (Iterator i = delta.entrySet().iterator(); i.hasNext();) {
		Map.Entry e = (Map.Entry)i.next();
		LList pattern = (LList)e.getKey();

		String attribute = (pattern.get(0)).toString();

		if(attribute.equals("type")) {
		    ensureValidType(pattern.get(1),e.getValue());
		}

		if(objects.contains(pattern.get(1)) && isAttributeInProperties(attribute)) {
		    Object value = e.getValue();
		    wLayer.addPatternValue(pattern, value);
		    wLayer.changePatternValue(pattern, value);
		}
	    }
	}
    }

    public void stateDeletion(ProcessStatusEvent event, Map delta) {
	if(wLayer != null) {
	    for (Iterator i = delta.entrySet().iterator(); i.hasNext();) {
		Map.Entry e = (Map.Entry)i.next();
		LList pattern = (LList)e.getKey();
		Object value = e.getValue();
		wLayer.deletePatternValue(pattern,value);
	    }
	}
    }

    private boolean isAttributeInProperties(String attribute) {
	ObjectProperty op;
	for(Iterator i = properties.iterator();i.hasNext();) {
	    op = (ObjectProperty) i.next();
	    if(Symbol.intern(attribute) == op.getName() || attribute.equals("type")) {
		return true;
	    }
	}	
	return false;
    }

    private void ensureValidType(Object obj,Object value) {
	LList pattern = Lisp.cons(obj,Lisp.NIL);
	if(!((view.getTypes()).contains(value))) {
	    try {
		wLayer.deletePatternValue(Lisp.cons(Symbol.intern("latitude"),pattern),null);
	    }catch(Exception e){} // if the object doesn't exist yet in the map.   
	}
	else {
	    ObjectProperty op;
	    for(Iterator i = properties.iterator();i.hasNext();) {
		op = (ObjectProperty) i.next();
		pattern = Lisp.cons(obj,Lisp.NIL);
		pattern = Lisp.cons(op.getName(),pattern);
		Object ob = (((Ip2ModelManager)(ip2.getModelManager())).getWorldStateMap()).get(pattern);
		if(ob!=null) wLayer.addPatternValue(pattern,ob);
	    }
	}
    }    

    private Properties loadProperties(String vName){
        Properties properties = new Properties();
        try{
            if(Parameters.haveParameter("map-view-directory") && vName!=null) {
		try {
		    properties.load(new FileInputStream(Parameters.getParameter("map-view-directory")+"/"+vName+".props"));
		    Debug.noteln("File used to map configuration: "+Parameters.getParameter("map-view-directory")+vName+".props");
		    return properties;
		}
		catch (Exception e){}
	    }
	  
	    if(Parameters.haveParameter("map-view-default-properties")) {
		properties.load(new FileInputStream(Parameters.getParameter("map-view-default-properties")));
		Debug.noteln("File used to map configuration (from map-view-default-properties parameter): "+Parameters.getParameter("map-view-default-properties"));
	    }

	    else {
		//properties.load(new FileInputStream("../addon/map/resources/map/world-map/map.props"));
		String s = (Util.class.getClassLoader().getResource("resources/map/world-map-default/map.props")).getFile();
		properties.load(new FileInputStream(s));
		Debug.noteln("File used to map configuration: "+s);
            }
        }
        catch (Exception e) {
            System.err.println("Error in loading property map file " +e.toString());
         }
        return properties;
    }

    protected void setMapProperties(Properties properties,Layer[] layers){
    
        // Set the map's size property
        if(properties.containsKey("map.Width") && properties.containsKey("map.Height"))
            setSize(Integer.parseInt(properties.getProperty("map.Width")),Integer.parseInt(properties.getProperty("map.Height")));
        //else
            setSize(800,600);

        // Set the map's center property
        if(properties.containsKey("map.Latitude") && properties.containsKey("map.Longitude"))
            map.setCenter(new LatLonPoint(Float.parseFloat(properties.getProperty("map.Latitude")),
                                          Float.parseFloat(properties.getProperty("map.Longitude"))));
	else
	    map.setCenter(new LatLonPoint(0f,0f));
    
        // Set a projection type for the map
        if(properties.containsKey("map.Projection")) {    
            String projName = properties.getProperty("map.Projection");
            int projType = ProjectionFactory.getProjType(projName);
            map.setProjectionType(projType);
        }       

	// Set the map' scale
        if(properties.containsKey("map.Scale")) {
	    float sc = Float.parseFloat(properties.getProperty("map.Scale"));
	    if(sc<(map.getProjection()).getMinScale())
		//sc = (map.getProjection()).getMinScale();
		sc = ((map.getProjection()).getMaxScale() - (map.getProjection()).getMinScale())/2;
	    else if (sc>(map.getProjection()).getMaxScale())
		//sc = (map.getProjection()).getMaxScale();
		sc = ((map.getProjection()).getMaxScale() - (map.getProjection()).getMinScale())/2;
            map.setScale(sc);
	}
	else
	    map.setScale(((map.getProjection()).getMaxScale() - (map.getProjection()).getMinScale())/2);

        // An ARGB integer to use for the background (see) color.
        if(properties.containsKey("map.BackgroundColor"))
            map.setBackgroundColor(ColorFactory.parseColor(properties.getProperty("map.BackgroundColor")));
    
        // Set the layers that will be visible
        if(properties.containsKey("map.startUpLayers")) {
            for (int i = 1; i < layers.length; i++)
                layers[i].setVisible(false);

            String layersOn = properties.getProperty("map.startUpLayers");
            if (layersOn != null) {
                StringTokenizer tokens = new StringTokenizer(layersOn, " ");
                Layer l;
                while (tokens.hasMoreTokens()) {
                    l = getLayer(tokens.nextToken(),layers);
                    if (l!= null) l.setVisible(true);
                }
            }
        }
    }

    public void sendConstraint(PatternAssignment pa){
        Vector v = new Vector();
        v.add(pa);
        Constraint c = new Constraint(Symbol.intern("world-state"),Symbol.intern("effect"),v);
        ((Ip2ModelManager)ip2.getModelManager()).addConstraint(c);
    }

    protected Layer getLayer(String name, Layer[] l) {
        for (int i = 1; i < l.length; i++)
            if ((l[i].getPropertyPrefix()).equals(name))
                return l[i];
        return null;
    }

   public Component getView(PanelFrame frame) {
        return this;
    }

    protected JMenuBar makeMenuBar() {
        JMenuBar bar = new JMenuBar();

        JMenuItem closeI = new JMenuItem("Close");
        closeI.addActionListener(CatchingActionListener.listener(MapWhiteboard.this));

        JMenuItem refreshI = new JMenuItem("Refresh");
        refreshI.addActionListener(CatchingActionListener.listener(MapWhiteboard.this));

	JMenuItem newViewI = new JMenuItem("New view window"); 
	newViewI.addActionListener(CatchingActionListener.listener(MapWhiteboard.this));

        JMenu fileMenu = new JMenu("File");
        bar.add(fileMenu);
	fileMenu.add(newViewI);
        fileMenu.add(refreshI);
        fileMenu.addSeparator();
        fileMenu.add(closeI);

        JMenu viewMenu = new JMenu("View");
        bar.add(viewMenu);

        JMenu projectionMenu = new JMenu("Projection");
        viewMenu.add(projectionMenu);

        ButtonGroup group = new ButtonGroup();
        JRadioButtonMenuItem meI,cyI,azI,orI;
        meI = new JRadioButtonMenuItem("Mercator");
        meI.addActionListener(CatchingActionListener.listener(MapWhiteboard.this));
        meI.setSelected(true);
        group.add(meI);
        projectionMenu.add(meI);
        cyI = new JRadioButtonMenuItem("Cylindrical (CADRG)");
        cyI.setActionCommand("CADRG");
        cyI.addActionListener(CatchingActionListener.listener(MapWhiteboard.this));
        group.add(cyI);
        projectionMenu.add(cyI);

        azI = new JRadioButtonMenuItem("Azimuth (Gnomonic)");
        azI.setActionCommand("Gnomonic");
        azI.addActionListener(CatchingActionListener.listener(MapWhiteboard.this));
        group.add(azI);
        projectionMenu.add(azI);
        orI = new JRadioButtonMenuItem("Orthographic");
        orI.addActionListener(CatchingActionListener.listener(MapWhiteboard.this));
        group.add(orI);
        projectionMenu.add(orI);

        JMenu selectViewMenu = new JMenu("Select");
	selectViewMenu.setEnabled(false);
	JMenuItem jmenuitem;

	jmenuitem = new JMenuItem("All");
	jmenuitem.setActionCommand("All");        
	jmenuitem.addActionListener(CatchingActionListener.listener(MapWhiteboard.this));
	selectViewMenu.add(jmenuitem);

	if(viewsNames!=null) {
	    for(Iterator iterator = viewsNames.keySet().iterator(); iterator.hasNext(); selectViewMenu.add(jmenuitem)) {
		String s = (String)iterator.next();
		jmenuitem = new JMenuItem(s);
		jmenuitem.setActionCommand("Select View");        
		jmenuitem.addActionListener(CatchingActionListener.listener(MapWhiteboard.this));
		selectViewMenu.add(jmenuitem);
	    }
	}
	
	selectViewMenu.setEnabled(true);

        viewMenu.add(selectViewMenu);

        JMenuItem helpI = new JMenuItem("Help");
        helpI.addActionListener(CatchingActionListener.listener(MapWhiteboard.this));

        JMenuItem aboutI = new JMenuItem("About");
        aboutI.addActionListener(CatchingActionListener.listener(MapWhiteboard.this));

        JMenu helpMenu = new JMenu("Help");
        bar.add(helpMenu);
        helpMenu.add(helpI);
        helpMenu.add(aboutI);

        return bar;
    }

    public void actionPerformed(ActionEvent e) {
        String command = e.getActionCommand();
        Debug.noteln("Map Tool action:", command);
        if (command.equals("Close")) {
            this.setVisible(false);
        }
        else if (command.equals("Refresh")) {
            if(wLayer != null)
                wLayer.refresh();
        }
        else if (command.equals("Help")) {
            (new HelpFrame(Util.class.getClassLoader().getResource("resources/html/ix-map-help.html"))).setVisible(true);
        }
        else if (command.equals("About")) {
            (new AboutFrame("I-X Map Tool version 4.4, 26-June-06")).setVisible(true);
        }
	else if (command.equals("New view window")) {
	    (new MapWhiteboard(ip2)).setVisible(true);
	}
	else if (command.equals("Select View")) {
	    JMenuItem jmenuitem = (JMenuItem)e.getSource();
            String viewN = jmenuitem.getText();
	    setTitle(viewN);
            setMapView(loadProperties(viewN));	    
	    view = (ObjectView) viewsNames.get(viewN);
	    properties = Collect.ensureList(view.getProperties());
	    stateChange(new ProcessStatusEvent(ip2.getModelManager()),((Ip2ModelManager)(ip2.getModelManager())).getWorldStateMap());
	}
	else if(command.equals("All")) {
	    setTitle("Map View");
	    view = null;
	    stateChange(new ProcessStatusEvent(ip2.getModelManager()),((Ip2ModelManager)(ip2.getModelManager())).getWorldStateMap());
	}
        else
            map.setProjectionType(ProjectionFactory.getProjType(command));
        //Debug.noteln("Nothing to do", command);
    }

    public LatLonPoint getCoordinates(MouseEvent e){
        coordinate.setText((map.getCoordinates(e)).toString());
        return map.getCoordinates(e);
    }

    public void setCenterView(MouseEvent e){
       LatLonPoint ll = map.getCoordinates(e);
       map.center(new CenterEvent(this,ll.getLatitude(),ll.getLongitude()));
    }


    void loadViews(String s) {
        try {    
            viewsNames = readViews(s);
        }
        catch(Exception e) {
	    System.out.println(e.toString());
        }
    }

    public SortedMap readViews(String s)
    {
        FileSyntaxManager filesyntaxmanager = XML.fileSyntaxManager();
        SortedMap sortedmap = filesyntaxmanager.readAllObjects(ix.ip2.ObjectView.class, s);
        TreeMap treemap = new TreeMap();
        ObjectView objectview;
        String s1;

        for(Iterator iterator = sortedmap.entrySet().iterator(); iterator.hasNext(); treemap.put(s1, objectview))
        {
            java.util.Map.Entry entry = (java.util.Map.Entry)iterator.next();
            File file = (File)entry.getKey();
            objectview = (ObjectView)entry.getValue();
            s1 = objectview.getName();
            if(s1 == null)
            {
                s1 = Strings.beforeLast(".", file.getName());
                objectview.setName(s1);
            }
            if(treemap.get(s1) != null)
                throw new IllegalArgumentException("There are two views named " + Strings.quote(s1) + " in " + s);
        }

        return treemap;
    }

    class ResetHook implements Runnable {
        public void run() {
            reset();
        }
        ResetHook(){}
    }

    // KEYBOARD AND MOUSE STUFF

    public void keyPressed(KeyEvent e){
    if (KeyEvent.getKeyModifiersText(e.getModifiers()).equals("Ctrl"))
        ctrlDown = true;
    }

    public void keyReleased(KeyEvent e){
        ctrlDown = false;
    }

    public void keyTyped(KeyEvent e){}
    public void mouseExited(MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {/*map.requestFocus();*/}
    public void mouseClicked(MouseEvent e) {}
    public void mouseReleased(MouseEvent e) {}

    public void mousePressed(MouseEvent e) {
        map.requestFocus();
        if(e.getButton()==e.BUTTON2)
            setCenterView(e);
        else if (ctrlDown && e.getButton()==e.BUTTON1)
            setCenterView(e);
    }

}

// javac -classpath ../../../../ix.jar:../imports/openmap.jar:. ix/ip2/map/MapWhiteboard.java
