/*
 * BasicMutableStore.java
 *
 * Created on May 28, 2003, 4.39 PM
 */

package hep.aida.ref.remote;

import java.io.IOException;
import java.util.*;
import java.util.logging.*;

import hep.aida.IManagedObject;
import hep.aida.IBaseHistogram;
import hep.aida.IDataPointSet;
import hep.aida.dev.IDevTree;
import hep.aida.dev.IDevMutableStore;

import hep.aida.ref.ManagedObject;
import hep.aida.ref.Annotation;
import hep.aida.ref.AidaUtils;
import hep.aida.ref.remote.interfaces.AidaUpdatable;
import hep.aida.ref.remote.interfaces.AidaTreeClient;
import hep.aida.ref.remote.interfaces.AidaUpdateEvent;

import org.freehep.util.FreeHEPLookup;
import org.openide.util.Lookup;

/**
 * This is Basic implementation of Read-Only IDevMutableStore.
 * It has extra methods that allow to change state of the tree
 * and to create IManagedObject in that tree and update its data.
 * This implementation creates appropriate subclass of RemoteClient
 * to connect to a remote server.
 *
 * All subclasses need to implement 3 methods:
 *
 *   protected RemoteClient createClient(Map options);
 *   public IManagedObject createObject(String name, String type);
 *   public void updateData(String path, String type);
 *
 * @author  serbo
 */
public abstract class RemoteMutableStore implements AidaUpdatable, IDevMutableStore {
    
    protected IDevTree tree;
    protected RemoteClient client;
    protected boolean initDone;
    protected boolean recursive;
    protected RemoteUpdateEvent[] events;
    protected Logger remoteLogger;
    protected boolean hurry; // If true, do not wait for data update,
    // just schedule data update and return.
    
    /**
     * Creates a new instance of BasicMutableStore.
     */
    public RemoteMutableStore() {
        this(false);
    }
    
    public RemoteMutableStore(boolean hurry) {
        this(null, null, hurry);
    }
    
    public RemoteMutableStore(IDevTree tree) {
        this(tree, null, false);
    }
    
    public RemoteMutableStore(IDevTree tree, boolean hurry) {
        this(tree, null, hurry);
    }
    
    private RemoteMutableStore(IDevTree tree, RemoteClient client, boolean hurry) {
        this.tree = tree;
        this.client = client;
        this.hurry = hurry;
        this.remoteLogger = Logger.getLogger("hep.aida.ref.remote");
        init();
    }
    
    
    // Service methods
    
    public void init() {
        this.recursive = true;
        this.events = new RemoteUpdateEvent[1];
        this.initDone = false;
    }
    
    public void setHurry(boolean hurry) {
        this.hurry = hurry;
        remoteLogger.fine("RemoteMutableStore.setHurry  to "+hurry);
    }
    
    // Can schedule data update for later or just directly execute it now
    public void handleDataUpdate(IManagedObject mo, String path, String type) throws IllegalArgumentException {
        if (hurry) {
            // Schedule data update here and pretend that object is up-to-date
            RemoteUpdateEvent evt = new RemoteUpdateEvent(AidaUpdateEvent.DO_DATA_UPDATE_NOW, path, type);
            RemoteUpdateEvent[] eventArray = new RemoteUpdateEvent[1];
            eventArray[0] = evt;
            if (client != null) {
                client.stateChanged(eventArray);
                if (mo instanceof RemoteManagedObject) {
                    ((RemoteManagedObject) mo).setDataValid(true);
                }
            } else {
                updateData(path, type);
            }
        } else {
            updateData(path, type);
        }
    }
    
    
    // This method must be overwritten by subclass
    protected abstract RemoteClient createClient(Map options);
    
    
    // IDevMutableStore methods
    
    public abstract void updateData(String path, String type) throws IllegalArgumentException;
    
    public abstract IManagedObject createObject(String name, String aidaType) throws IllegalArgumentException;
    
    
    // IMutableStore methods
    public void close() throws IOException {
        client.disconnect();
        client = null;
        tree = null;
        events = null;
    }
    
    public void commit(IDevTree tree, Map options) throws IOException {
        throw new UnsupportedOperationException("Can not commit changes to the Read-Only Store");
    }
    
    public boolean isReadOnly() {
        return true;
    }
    
    // Not used for now - submits single events to the queue
    public void read2(IDevTree tree, String path) throws IllegalArgumentException, IOException {
        if (this.tree == null) this.tree = tree;
        if (!client.isConnected())
            throw new IOException("Can not read from the Store: client is not connected!");
        
        remoteLogger.finest("RemoteMutableStore.read: path="+path+", recursive="+recursive);
        String[] names = client.listObjectNames(path);
        String[] types = client.listObjectTypes(path);
        if (names != null && types != null) {
            for (int i=0; i<names.length; i++) {
                events[0] = new RemoteUpdateEvent(AidaUpdateEvent.NODE_ADDED, names[i], types[i]);
                client.stateChanged(events);
                
                if (recursive && types[i].equalsIgnoreCase("dir")) read2(tree, names[i]);
            }
            events[0] = new RemoteUpdateEvent(AidaUpdateEvent.FOLDER_IS_FILLED, path, "dir");
            client.stateChanged(events);
        }
    }
    
    public void read(IDevTree tree, String path) throws IllegalArgumentException, IOException {
        if (this.tree == null) this.tree = tree;
        if (!client.isConnected())
            throw new IOException("Can not read from the Store: client is not connected!");
        
        remoteLogger.finest("RemoteMutableStore.read: path="+path+", recursive="+recursive);
        String[] names = null;
        String[] types = null;
        names = client.listObjectNames(path);
        types = client.listObjectTypes(path);
        
        if (names != null && types != null) {
            //RemoteUpdateEvent[] evts = new RemoteUpdateEvent[names.length];
            for (int i=0; i<names.length; i++) {
                //evts[i] = new RemoteUpdateEvent(AidaUpdateEvent.NODE_ADDED, names[i], types[i]);
                
                int id = AidaUpdateEvent.NODE_ADDED;
                String pathString = names[i];
                String typeString = types[i];
                String type = typeString;
                String xType = "double";
                
                // Parse out possible X Axis type
                int indexT = typeString.lastIndexOf(":");
                if (indexT > 0) {
                    type  = typeString.substring(0, indexT);
                    String tmpType = typeString.substring(indexT+1);
                    if (tmpType != null && !tmpType.equals("")) xType = tmpType;
                }
                executeStateChanged(id, pathString, type, xType);
            }
            //client.stateChanged(evts);
            tree.hasBeenFilled(path);
            
            // If "recursive", fill all sub-directories
            if (recursive) {
                for (int i=0; i<names.length; i++) {
                    if (types[i].equalsIgnoreCase("dir") || types[i].equalsIgnoreCase("mnt")) read(tree, names[i]);
                }
            }
            
            //events[0] = new RemoteUpdateEvent(AidaUpdateEvent.FOLDER_IS_FILLED, path, "dir");
            //client.stateChanged(events);
        }
    }
    
    public void read(IDevTree tree, Map options, boolean readOnly, boolean createNew) throws IOException {
        if (this.tree == null) this.tree = tree;
        //if (tree != null && tree.getLock() == null) tree.setLock(new Object());
        if (client == null) {
            client = createClient(options);
        }
        if (!client.isConnected()) {
            client.connect();
        }
        recursive = options.containsKey("recursive");
        initDone = false;
        remoteLogger.fine("RemoteMutableStore.read: initial read for the top directory, recursive="+recursive);
        read(tree, "/");
        initDone = true;
    }
    
    
    // AidaUpdatable methods
    
    /**
     * This method actually does the job of modifying the client tree.
     * If directory or node already does exist, it will not be overwritten.
     * Synchronizes with Tree lock, if the Tree has lock object set.
     */
    public void stateChanged(AidaUpdateEvent event) {
        Object lock = tree.getLock();
        remoteLogger.finest("RemoteMutableStore.stateChanged for EVENT:: id="+event.id()+", path="+event.path()+",  type="+event.nodeType());
        if (lock != null) {
            synchronized (lock) {
                executeStateChanged(event);
            }
        } else {
            executeStateChanged(event);
        }
    }
    
    protected void executeStateChanged(AidaUpdateEvent event) {
        int id = event.id();
        String path = event.path();
        String typeString = event.nodeType();
        String type = typeString;
        String xType = "double";
        
        // Parce out possible X Axis type
        int indexT = typeString.lastIndexOf(":");
        if (indexT > 0) {
            type  = typeString.substring(0, indexT);
            String tmpType = typeString.substring(indexT+1);
            if (tmpType != null && !tmpType.equals("")) xType = tmpType;
        } else if (event instanceof RemoteUpdateEvent) {
            String tmp = ((RemoteUpdateEvent) event).getXAxisType();
            if (tmp != null) xType = tmp;
        }
        executeStateChanged(id, path, type, xType);
        remoteLogger.finest("RemoteMutableStore.executeStateChanged for EVENT:: id="+id+", path="+path+",  type="+type+", xAxisType="+xType);
        if(tree instanceof RemoteTree && id != AidaUpdateEvent.NODE_UPDATED) ((RemoteTree) tree).submitEventToListeners(event);
    }
    protected void executeStateChanged(int id, String path, String type, String xType) {
        Object lock = tree.getLock();
        remoteLogger.finest("RemoteMutableStore.executeStateChanged:: id="+id+", path="+path+",  type="+type+", xAxisType="+xType+",  lock="+lock);
        if (id == AidaUpdateEvent.NODE_ADDED) {
            try {
                if (type.equalsIgnoreCase("dir") || type.equalsIgnoreCase("mnt")) {
                    if(tree instanceof RemoteTree) { 
                        ((RemoteTree) tree).addFolder(path);
                        try {
                            if (initDone) read(tree, path);
                        } catch (Exception e) {
                            remoteLogger.log(Level.INFO, "RemoteMutableStore.stateChanged: Exception while adding node: "+type+", "+path+", Skip this.", e);                     
                            remoteLogger.log(Level.FINEST, "RemoteMutableStore.stateChanged: Exception while adding node: "+type+", "+path+", Skip this.", e.getStackTrace());                     
                        }
                    } else tree.mkdirs(path);
                    
                } else {
                    // Parse object name and folder path
                    if (path.endsWith("/")) path = path.substring(0, path.length()-1);
                    if (path.endsWith("/")) path = path.substring(0, path.length()-1);
                    String name = AidaUtils.parseName(path);
                    String objDir = AidaUtils.parseDirName(path);
                    
                    IManagedObject h = null;
                    
                    // Make sure all directories in the path exist.
                    if(tree instanceof RemoteTree) ((RemoteTree) tree).addFolder(objDir);
                    else tree.mkdirs(objDir);
                    
                    if (type.equals("RemoteUnavailableObject")) h = new RemoteUnavailableObject(name);
                    else h = createObject(name, type);
                    
                    // Set X Axis Type
                    if (h instanceof RemoteManagedObject) {
                        RemoteManagedObject r = (RemoteManagedObject) h;
                        r.setFillable(true);
                        if ( h instanceof IBaseHistogram) {
                            Annotation a = (Annotation) ((IBaseHistogram) h).annotation();
                            a.setFillable(true);
                            try {
                                a.setValue("xAxisType", xType);
                            } catch (IllegalArgumentException e) {
                                a.addItem("xAxisType", xType);
                            }
                            a.setFillable(false);
                        } else if ( h instanceof IDataPointSet) {
                            Annotation a = (Annotation) ((IDataPointSet) h).annotation();
                            a.setFillable(true);
                            try {
                                a.setValue("xAxisType", xType);
                            } catch (IllegalArgumentException e) {
                                a.addItem("xAxisType", xType);
                            }
                            a.setFillable(false);
                        }
                        r.setFillable(false);
                    }
                    
                    if(tree instanceof RemoteTree) ((RemoteTree) tree).addObject(objDir, h);
                    else tree.add(objDir, h);
                    
                    if (h instanceof RemoteManagedObject) {
                        ((RemoteManagedObject) h).setTreeFolder(objDir);
                        if (h instanceof RemoteUnavailableObject) ((RemoteManagedObject) h).setDataValid(true);
                        else ((RemoteManagedObject) h).setDataValid(false);
                    } else {
                        handleDataUpdate(h, path, type);
                    }
                    
                }
            } catch (IllegalArgumentException ex) {
                remoteLogger.log(Level.INFO, "RemoteMutableStore.stateChanged: IllegalArgumentException while ADDING node. Skip this: \n\t"+ex.getMessage());
                remoteLogger.log(Level.FINEST, "", ex);
            }
            
        } else if (id == AidaUpdateEvent.FOLDER_IS_FILLED) {
            if (type.equalsIgnoreCase("dir"))
                tree.hasBeenFilled(path);
        } else if (id == AidaUpdateEvent.NODE_DELETED) {
            try {
                if (type.equalsIgnoreCase("dir")) {
                    if(tree instanceof RemoteTree) ((RemoteTree) tree).removeFolder(path);
                    else tree.rmdir(path);                    
                } else {
                    if(tree instanceof RemoteTree) ((RemoteTree) tree).removeObject(path);
                    else tree.rm(path);
                }
            } catch (IllegalArgumentException ex) {
                remoteLogger.log(Level.INFO, "RemoteMutableStore.stateChanged: IllegalArgumentException while DELETING node. Skip this.", ex);
            }
            
        } else if (id == AidaUpdateEvent.NODE_UPDATED) {
            // Here we just mark RemoteManagedObject as not valid, do not actually get new data.
            try {
                IManagedObject h = null;
                if(tree instanceof RemoteTree) h = ((RemoteTree) tree).executeFind(path);
                else h = tree.find(path);
                 if (h instanceof RemoteManagedObject) {
                    ((RemoteManagedObject) h).setDataValid(false);
                } else {
                    handleDataUpdate(h, path, type);
                }
                
            } catch (IllegalArgumentException ex) {
                remoteLogger.log(Level.INFO, "RemoteMutableStore.stateChanged: IllegalArgumentException while UPDATING node. Skip this.", ex);
            }
        } else if (id == AidaUpdateEvent.DO_DATA_UPDATE_NOW) {
            updateData(path, type);
        } else if (id == AidaUpdateEvent.NODE_TEMPORARY_UNAVAILABLE) {
            // Remove existing object first
            try {
                if (type.equalsIgnoreCase("dir")) {
                    //String[] list =  tree.listObjectNames(path);
                    if(tree instanceof RemoteTree) ((RemoteTree) tree).removeFolder(path);
                    else tree.rmdir(path);
                } else {
                    //IManagedObject obj = tree.find(path);
                    if(tree instanceof RemoteTree) ((RemoteTree) tree).removeObject(path);
                    tree.rm(path);
                }
            } catch (IllegalArgumentException ex) {
                remoteLogger.log(Level.INFO, "RemoteMutableStore.stateChanged: IllegalArgumentException while DELETING node. Skip this.", ex);
            }
            
            // Put in RemoteUnavailableObject
            String name = AidaUtils.parseName(path);
            String objDir = AidaUtils.parseDirName(path);
            
            //if(tree instanceof RemoteTree) ((RemoteTree) tree).addFolder(objDir);
            //else tree.mkdirs(objDir);
            
            RemoteUnavailableObject h = new RemoteUnavailableObject(name);
            if(tree instanceof RemoteTree) ((RemoteTree) tree).addObject(objDir, h);
            else tree.add(objDir, h);
            h.setTreeFolder(objDir);
            h.setDataValid(true);
            
        } else if (id == AidaUpdateEvent.NODE_IS_AVAILABLE_AGAIN) {
            // Remove existing RemoteUnavailableObject first
            try {
                IManagedObject obj = null;
                if(tree instanceof RemoteTree) obj = ((RemoteTree) tree).executeFind(path);
                else obj = tree.find(path);
                if (obj != null) {
                    if(tree instanceof RemoteTree) ((RemoteTree) tree).removeObject(path);
                    else tree.rm(path);
                }
            } catch (IllegalArgumentException ex) {
                remoteLogger.log(Level.INFO, "RemoteMutableStore.stateChanged: IllegalArgumentException while DELETING node. Skip this.", ex);
            }
            
            if (type.equalsIgnoreCase("dir")) {
                if(tree instanceof RemoteTree) ((RemoteTree) tree).addFolder(path);
                else tree.mkdirs(path);
                if (recursive) try {
                    read(tree, path);
                } catch (Exception ioe) {
                    remoteLogger.log(Level.INFO, "RemoteMutableStore.stateChanged: Exception while updating PATH="+path, ioe);
                    remoteLogger.log(Level.FINE, "", ioe.getStackTrace());
                }             
            } else {                
                RemoteUpdateEvent evt = new RemoteUpdateEvent(AidaUpdateEvent.NODE_ADDED, path, type);
                RemoteUpdateEvent[] eventArray = new RemoteUpdateEvent[1];
                eventArray[0] = evt;
                if (client != null) {
                    client.stateChanged(eventArray);
                }
            }
        } else {
            remoteLogger.log(Level.INFO, "RemoteMutableStore.stateChanged: Wrong ID="+id+", path="+path+",  type="+type);
        }        
    }
    
    
    // Thread that does reading updates
    public class ReadThread extends Thread {
        private String readPath;
        private long wait;
        
        public ReadThread(String readPath) {
            this(readPath, 0);
        }
        public ReadThread(String readPath, long wait) {
            this.readPath = readPath;            
            this.wait = wait;            
        }
        
        public void run() {
             try {
                if (wait > 0) Thread.sleep(wait);
                read(tree, readPath);
            } catch (InterruptedException e2) {
                remoteLogger.log(Level.INFO, "RemoteMutableStore InterruptedException.", e2);
                remoteLogger.log(Level.FINE, "", e2.getStackTrace());
	    } catch (Exception e3) {
                remoteLogger.log(Level.INFO, "Exception in RemoteMutableStore: ", e3);
                remoteLogger.log(Level.FINE, "", e3.getStackTrace());
            }            
        }
}
    
    // Do some simple tests here
    public static void main(String[] args) {
    }
    
}
