/*******************************************************************************
 * Bunshin : DHT Replication & Caching
 * Copyright (C) 2004-2005 Ruben Mondejar
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 ******************************************************************************/

package bunshin.storage;

import java.io.*;
import java.util.*;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.extended.DynamicProxyConverter;
import com.thoughtworks.xstream.io.xml.DomDriver;
import com.thoughtworks.xstream.mapper.DynamicProxyMapper;
import com.thoughtworks.xstream.mapper.DynamicProxyMapper.DynamicProxy;

import org.jdom.*; 
import org.jdom.input.*; 
import org.jdom.output.*; 
import org.w3c.dom.Document;

import bunshin.util.*;
import rice.p2p.commonapi.*;

/**
 *
 * @author Ruben Mondejar
 */
public class XMLStorage implements StorageManager {

    private String ROOT_PATH = "storage";	

	private RescueTask rescueTask;
	private Timer timer = new Timer();
	
	private Hashtable paths = new Hashtable(); //context --> path
	private Hashtable maps = new Hashtable(); //context -> key --> filename
	private Hashtable memTable = new Hashtable();//key-->value
	
	XStream xstream = new XStream(new DomDriver());
	SAXBuilder builder = new SAXBuilder(); 
	
	private long max = 0;
	public XMLStorage() {}

	public XMLStorage (String root_path) {
		ROOT_PATH = root_path;
		loadInfoDir();
		try {
			loadAll();
		} catch (JDOMException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public XMLStorage (String root_path, long max_size) {

		ROOT_PATH = root_path;
		long max = max_size;
		loadInfoDir();
		try {
			loadAll();
		} catch (JDOMException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public XMLStorage (long max_size)  {
		long max = max_size;
		loadInfoDir();
		try {
			loadAll();
		} catch (JDOMException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void init (Properties prop) {
	
		String root_path = (String)prop.get ("BUNSHIN_STORAGE_ROOT_DIR");
		if (root_path!=null) {

			ROOT_PATH = root_path+ROOT_PATH;
			System.out.println("ROOT_PATH : "+ROOT_PATH);
		}
		loadInfoDir();
		try {
			loadAll();
		} catch (JDOMException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	public void setPath(String context, String path) {
		
		String oldPath = (String) paths.get(context);       
       
        if (oldPath!= null && !oldPath.equals(path)) {
        
		  File f = new File(oldPath);
		  File f2 = new File(path);

		  if (f!=null) 	{    
		     
		    //exists new directory?
   	        //if (!f.exists())  
		    
            //move all files to new directory??
  		    f2.renameTo(f);		             

  		  }
  		  else f2.mkdirs();   		 

		}   
		else {
			File f2 = new File(path);
			f2.mkdirs();
		}
		
	    paths.put(context,path);
		saveInfoDir();
	}

	public void add(String context, Bucket newBucket) throws StorageException {

		if (memTable.containsKey(context)) {

			Bucket mem = (Bucket) memTable.get(context);
			// mapping all values
			Iterator it = newBucket.keySet().iterator();
			while(it.hasNext()) {
			  Id key = (Id) it.next();
			  Values values = newBucket.extract(key);
			  mapping(context,key,values,null);
			}
			
			mem.add(newBucket);	 
			memTable.put(context,mem); 
		}
		else {			
			
			// mapping all values
			Iterator it = newBucket.keySet().iterator();
			while(it.hasNext()) {
			  Id key = (Id) it.next();
			  Values values = newBucket.extract(key);
			  mapping(context,key,values,null);
			}
			
			memTable.put(context,newBucket);	
		}


		if (max==0 || memSize()<max) {
			try 
			{
				saveAll(context);
			} 
			catch(Exception ex) 
			{
				throw new StorageException(ex);
			}
		}

	}

   
    
    
	public void put(String context, Bucket newBucket) throws StorageException 	{		


        // mapping all values
		Iterator it = newBucket.keySet().iterator();
		while(it.hasNext()) {
		  Id key = (Id) it.next();
		  Values values = newBucket.extract(key);
		  mapping(context,key,values,null);
		}
			 
		memTable.put(context,newBucket);	
         
		if (max==0 || memSize()<max) 
		{
			try 
			{
				removeAll(context);
				saveAll(context);
			} 
			catch(Exception ex) 
			{
				throw new StorageException(ex);
			}
		}
	}


   private boolean mapping(String context, Id key,Values values, String field) {
   	  
   	  boolean successful = false;
   	
   	  try {
   	  
      String filename = (String) values.get("#filename");		
	  if (filename!=null) {
	    Hashtable map = (Hashtable) maps.get(context);    	  
    	if (map==null) map = new Hashtable();
    	map.put(key,filename);       
    	maps.put(context,map);		  		
			
		if (field==null && values.containsKey("#field")) field = (String) values.get("#field");	
		
		if (field!=null) {
		  byte[] bytes = (byte[]) values.remove(field);			  
			    			  
		  if (bytes!=null) saveMapFile(context,key,filename,bytes);
		   //values.remove("#filename");
		  	values.put(field,'#'+filename);
	    }	
	    
	    successful = true; 
	    System.out.println("mapping :"+values);   
      }
      } catch(IOException ex) {
		        ex.printStackTrace();
	  }   
      return successful;
    }  
    
    
	public void write(String context, Id key, Object value, String field) throws StorageException {  

		Bucket mem;
		if (memTable.containsKey(context))  
		{
			mem = (Bucket) memTable.get(context);			  
		}
		else  
		{
			mem = new Bucket();
		}
		
		boolean overwrite = false;
		
		if (value instanceof Values)   overwrite = mapping(context,key,(Values)value,field);          
	           
		 

		
		if (overwrite || field==null) {
			mem.overwrite(key,value);
		}	
		else {
			if (!(value instanceof Values))mem.insert(key,value,field);
			else mem.overwrite(key,value);
		}	
		
		
		memTable.put(context,mem);

		if (max==0 || memSize()<max) {
			try {
				save(context,key);
			} 
			catch(IOException ex) 
			{
				throw new StorageException(ex);
			}
		}
		
	
	}
	

	public Object remove(String context, Id key) throws StorageException {
		
		Object obj = null;
		if (memTable.containsKey(context)) 	
		{
			Bucket mem = (Bucket) memTable.get(context);
			obj = mem.remove(key);		
		
			if (max==0 || memSize()<max) 
			{
				try 
				{
					Hashtable map = (Hashtable) maps.get(context);
					if (map!=null && map.containsKey(key)) {
					  String filename = (String)map.remove(key);
					  removeMapFile(context,key,filename);	
					}
					delete(context,key);
					
				} 
				catch(IOException ex) 
				{
					throw new StorageException(ex);
				}
			} 
		}
		return obj;
	}

	public Object delete(String context, Id key, String field) 	throws StorageException {
		System.out.println("delete in"+context+" & key "+key+" & field "+field);
		Object obj = null;
		if (memTable.containsKey(context)) 	{
			Bucket mem = (Bucket) memTable.get(context);
		
			
			obj = mem.delete(key,field);		
			try {
			if (obj!=null) {
					
			  if (max==0 || memSize()<max) 	{
				save(context,key);				
			  } 
			}
			else delete(context,key);  
			} 
			catch(IOException ex) 	{
				throw new StorageException(ex);
			}
		}
	
 		return obj;
	
	}
	
	
	public Object retrieve(String context, Id key,String field) { 	

		if (memTable.containsKey(context))  {
			Bucket mem = (Bucket) memTable.get(context);	
		    Object obj = mem.load(key,field);
		        
		    //System.out.println("1) obj :"+obj);	
		    if (obj!=null && obj instanceof String) {
		      String s = (String) obj;
		      if (s.charAt(0)=='#') {
		      	Hashtable map = (Hashtable) maps.get(context);
		      	//System.out.println("2) map :"+map);	
		      	if (map!=null) {
		      	  String filename = (String) map.get(key); 
		      	  //System.out.println("3) filename :"+filename);	      
		      	  try {		      	  	    
		      	    byte[] bytes = loadMapFile(context,key,filename);
		      	    return bytes;
		      	  } catch(IOException ex){
		      	  	ex.printStackTrace();
		      	  }  
		      	}		      	
		      }
		      else return obj;
		    }
		    else return obj;
		}
		return null;
	}  
  
  
    private Values unmapping(String context, Id key,Values values) {
		
	  //check if values are mapping	
	  Values values2 = (Values) values.clone();
	  Iterator it = values2.keySet().iterator();
			  
	  while(it.hasNext()) {
	    String field = (String) it.next();
		Object value = values.get(field);
		if (value!=null && value instanceof String) {
		  String s = (String) value;
			  	  
		  //unmapping 			  	
		  if (s.charAt(0)=='#') { 
		            	
		    Hashtable map = (Hashtable) maps.get(context);
		    if (map!=null) {
		      String filename = (String) map.get(key); 
		      try {
		        byte[] bytes = loadMapFile(context,key,filename);
		      	       
		      	//replace map value		      	        
		      	values.put("#filename",filename);
		      	values.put("#field",field);	
 		        values.put(field,bytes);	                	      	        
		      	        
		      	        		      	      
		      } catch(IOException ex) {
		        ex.printStackTrace();
		      }   	      	      
		      	      				  		
		    }  
		  }
		}
	  }
	  
	  return values;
	  	
	}
	
	  
	public synchronized Values extract(String context, Id key) { 	
		
		if (memTable.containsKey(context)) {
			
			Bucket mem = (Bucket) memTable.get(context);	
			Values values = mem.extract(key);
			if (values!=null) values = unmapping(context,key,values);	
			return values;
		}
		return null;
	}  
  
	public synchronized Bucket getBucket(String context) {
		
		if (memTable.containsKey(context))	{
		  
		  Bucket mem1 = (Bucket) memTable.get(context);
		  Bucket mem = new Bucket(mem1);
		  
		  Iterator it = mem.keySet().iterator();	
			
	      while(it.hasNext()) {
		    Id key = (Id) it.next();
		    Values values = mem.extract(key);
		    if (values!=null) {
		      values = unmapping(context,key,values);
		      mem.overwrite(key,values);
		    }		    
		  }	
			
		  return mem;
		}
		return null;
	}   

	public synchronized Hashtable getBuckets() {
        
        Hashtable buckets = new Hashtable();
        Iterator it = memTable.keySet().iterator();
        
        while (it.hasNext()) {
          String context = (String)	it.next();
          Bucket mem = getBucket(context);  
          buckets.put(context,mem);
        }              
        
		return buckets;
	}   
	

	public boolean exists(String context, Id key) 	{
		if (memTable.containsKey(context)) {
			
			Bucket mem = (Bucket) memTable.get(context);
			return mem.containsKey(key);	    	  
		}
		return false;
	}





    private void saveInfoDir() {    	
	
	  try {	  				
		File f = new File(ROOT_PATH);
		
		if (!f.exists()) f.mkdirs();

		f = new File(ROOT_PATH+"/"+"infoDir");
		f.createNewFile();
		PrintWriter pw = new PrintWriter(f);		
		xstream.toXML(paths,pw);
	
		//ObjectOutputStream oos = new ObjectOutputStream(out);
		//oos.writeObject(paths);
		pw.close();
	  }
	  catch (IOException ex) {
		ex.printStackTrace();
	  }
    }
    
    private void loadInfoDir() {    	
	    	
	    try {
	    		
		File dir = new File(ROOT_PATH);
		
		if (dir.exists()) {
		
		  File f = new File(ROOT_PATH+"/"+"infoDir");
		  if (f.exists()) {
				
		    FileInputStream in = new FileInputStream(f);		    
		    //paths =(Hashtable) ois.readObject();
		    paths =(Hashtable) xstream.fromXML(in);
		    in.close();
		    if (paths==null) paths = new Hashtable();
		  } 
		  
		  //temp dirs
		  File[] subdirs = dir.listFiles();
		  //for each subdir
		  for (int i=0;i<subdirs.length;i++) {
		    if (subdirs[i].isDirectory())
		     // paths.put(subdirs[i].getName(),subdirs[i].getName());
		      paths.put(subdirs[i].getName(),subdirs[i].getPath());
		     //System.out.println("name : "+subdirs[i].getName()+" path : "+subdirs[i].getPath());
		  }
		  		  
		}  
		
		}
		catch (IOException ex) 
		{
			ex.printStackTrace();
		}
		   
    }
    
	private void save(String context,Id key) throws IOException {

		//File dir = new File(ROOT_PATH);
        String dir = (String) paths.get(context);		

        if (dir==null) dir = ROOT_PATH+"/"+context;			
		
		File f = new File(dir);
		
		if (!f.exists()) f.mkdirs();
		
		Bucket mem = (Bucket) memTable.get(context);
		Values values = mem.extract(key);
		Pair pair = new Pair(key,values);
		String name = ((rice.pastry.NodeId)key).toStringFull();
		f = new File(dir+"/"+name+".xml");
		f.createNewFile();
				
		PrintWriter pw = new PrintWriter(f);		
	    xstream.toXML(pair, pw);
		pw.close();

	}
	
    private void saveMapFile(String context,Id key, String filename, byte[] bytes) throws IOException {

  	
        String dir = (String) paths.get(context);		

        if (dir==null) dir = ROOT_PATH+"/"+context;			
		
		File f = new File(dir);
	 	
		if (!f.exists()) f.mkdirs();
		
	    File out = new File(dir+"/mapping");				
		PrintWriter pw = new PrintWriter(out);
		Hashtable map = (Hashtable) maps.get(context);
		if (map!=null) xstream.toXML(map, pw);
		pw.close();
		
		FileOutputStream fout = new FileOutputStream(dir+'/'+filename);
		fout.write(bytes);
		fout.flush();
		fout.close();
		
	}
	
	 private void removeMapFile(String context,Id key, String filename) throws IOException {

  	
        String dir = (String) paths.get(context);		

        if (dir==null) dir = ROOT_PATH+"/"+context;			
		
		File f = new File(dir);
		
		if (!f.exists()) f.mkdirs();
		
		File file = new File(dir+"/mapping");				
		Hashtable map = (Hashtable) maps.get(context);					
		if (map!=null) {
			if (map.size()>0) {
			
			  File out = file;			
		      PrintWriter pw = new PrintWriter(out);
		      xstream.toXML(map,pw);
			  pw.close();
			}
			else file.delete(); 
		}	
		else file.delete();
			
		
		File file2 = new File(dir+'/'+filename);
		file2.delete();
		
	}
	
	private byte[] loadMapFile(String context,Id key, String filename) throws IOException {

		
        String dir = (String) paths.get(context);		

        if (dir==null) dir = ROOT_PATH+"/"+context;			
		
		File f = new File(dir+'/'+filename);
		
		if (!f.exists()) f.mkdirs();
				
		FileInputStream fin = new FileInputStream(f);
		
		byte[] bytes = new byte[(int)f.length()];
		
		fin.read(bytes);		
		fin.close();
		
		return bytes;
		
	}

	private void delete(String context, Id key) throws IOException {

		//File dir = new File(ROOT_PATH);
		String dir = (String) paths.get(context);
	    
	    if (dir==null) dir = ROOT_PATH+"/"+context;			
		
		File f = new File(dir);
		
		if (!f.exists()) f.mkdirs();
		
		//TODO --> delete mapping & file if exists...

		String name = ((rice.pastry.NodeId)key).toStringFull();
		f = new File(dir+"/"+name+".xml");
		f.delete();
		//System.out.println("removing file "+name);
		
	}

	public void removeBucket(String context) throws StorageException {		
		memTable.remove(context);	
        try {
		  removeAll(context);
	    }  catch(IOException ex) 	{
	      throw new StorageException(ex);
        }
	}

	private void removeAll(String context) throws IOException 
	{
		String dir = (String) paths.get(context);	
	
	    if (dir==null) dir = ROOT_PATH+"/"+context;			
		
		File f = new File(dir);		

		if (f.exists()) 	
		{
			File[] files = f.listFiles();
			for (int i=0;i<files.length;i++) {
				files[i].delete();
			}
			f.delete();
		}
	}

	private void saveAll(String context) throws IOException, JDOMException {

			String dir = (String) paths.get(context);
						
			if (dir==null) dir = ROOT_PATH+"/"+context;			
		
		    File f = new File(dir);
		
		    if (!f.exists()) f.mkdirs();
		

			Bucket mem = (Bucket) memTable.get(context);
			Collection c = mem.keySet();
			Iterator it2 = c.iterator();
			while(it2.hasNext()) 
			{
				Id key = (Id)it2.next();				
				Values values = mem.extract(key);
				Pair pair = new Pair(key,values);
				String name = ((rice.pastry.NodeId)key).toStringFull();
				f = new File(dir+"/"+name+".xml");
				f.createNewFile();
				PrintWriter pw = new PrintWriter(f);
				xstream.toXML(pair, pw);
				pw.close();
			}
		
	}

	private void saveAll() throws IOException, JDOMException {

		Iterator it = paths.keySet().iterator();
		//for each directory
		while(it.hasNext())	{

			String context = (String) it.next();
			String dir = (String) paths.get(context);
			
            if (dir==null) dir = ROOT_PATH+"/"+context;			
		
		    File f = new File(dir);
		
   		    if (!f.exists()) f.mkdirs();
		

			Bucket mem = (Bucket) memTable.get(context);
			Collection c = mem.keySet();
			Iterator it2 = c.iterator();
			while(it2.hasNext()) 
			{
				Id key = (Id)it2.next();				
				Values values = mem.extract(key);
				Pair pair = new Pair(key,values);
				String name = ((rice.pastry.NodeId)key).toStringFull();
				f = new File(dir+"/"+name+".xml");
				f.createNewFile();
				FileOutputStream out = new FileOutputStream(f);
				PrintWriter pw = new PrintWriter(f);
				
				xstream.toXML(pair, pw);
				pw.close();
			}
		}
	}

	private void loadAll() throws JDOMException 	{
        
		Vector crashes = new Vector();
		
		Iterator it = paths.keySet().iterator();
		//for each directory
		while(it.hasNext())	{

			String context = (String) it.next();
			Bucket mem = (Bucket) memTable.get(context);
			if (mem==null) mem = new Bucket();
			String dir = (String) paths.get(context);
			System.out.println("context dir :"+dir);
			
			if (dir==null) dir = ROOT_PATH+"/"+context;			
		
		    File f = new File(dir);
		
		    if (!f.exists()) f.mkdirs();
		
			if (f.isDirectory())  	{
				try 
				{
					File[] files = f.listFiles();
					for (int i=0;i<files.length;i++) 
					{
										
					    //System.out.println("llegint "+files[i].getName());				
						 FileInputStream in = new FileInputStream(files[i]);
						//DomReader dr = new DomReader((Document) builder.build(f));
						 //ObjectInputStream ois = new ObjectInputStream (in);
						 						
						
						String name = files[i].getName(); 
						if (name.equals("mapping")) {
								
				
						
				
						  //ObjectInputStream ois =  xstream.createObjectInputStream(dr);
						  
							
					      //read pairs info										     
					      //Hashtable map =(Hashtable) ois.readObject();
							Hashtable map =(Hashtable) xstream.fromXML(in);
					      in.close();
					     					
					      if (map==null) 	map = new Hashtable();				
					      else System.out.println("map loaded -> context "+context+" map : "+map);
					      maps.put(context,map);
						}
						else if (name.substring(name.length()-3).equals("xml")) {						
						
						 
						  //ObjectInputStream ois = xstream.createObjectInputStream(dr);
						  System.out.println("TRYING :"+name);  
						  try {
						    //Pair pair=(Pair) ois.readObject();
							Pair pair=(Pair) xstream.fromXML(in);  
						    Id key = pair.getKey();
						    Values values= pair.getValues();
						  
						    mem.overwrite(key,values);
						    memTable.put(context,mem);
						  
						  } catch(Exception e) {
							  System.out.println("CRASHING :"+name); 
							  crashes.add(files[i]);
							  crashes.add(context);
							  crashes.add(mem);
							  e.printStackTrace();
						  }
						  in.close();
						  //System.out.println("load file "+name);
						  //System.out.println("mem"+mem+"key"+key+","+" values"+values);
						}  
					}
				}
				catch (IOException ex) 
				{
					ex.printStackTrace();
				}
			}
			//else no files...			
		}
		System.out.println("Size "+memSize());
		if (!crashes.isEmpty()) {
			rescueTask = new RescueTask(crashes);
			timer.schedule(rescueTask,5000);
			System.out.println("\n\n\nNew Timer, rescue files : "+crashes.size()/3);
		}
	}

	private long memSize() 	{
        
		long size=0;

		Iterator it = paths.keySet().iterator();
		//for each directory
		while(it.hasNext())	{

			String context = (String) it.next();
			String dir = (String) paths.get(context);
            
		    if (dir==null) dir = ROOT_PATH+"/"+context;			
		
   		    File f = new File(dir);
		
		    if (!f.exists()) f.mkdirs();			
		    
			if (f.isDirectory()) {
				File[] files = f.listFiles();
				//for each file
				for (int i=0;i<files.length;i++) {
					//System.out.println("dir :"+f.getName()+" file "+files[i].getName());
				
					size+=files[i].length();
				}
			}
		}
		return size;
	}

	public boolean isFull() {
		return (memSize()<max);
	}
	
	class RescueTask extends TimerTask	{

		private Vector crashes;
		
		public RescueTask(Vector crashes) {
			this.crashes=crashes;
		}
		
		public void run()
		{
			Vector newCrashes = new Vector();
			try 
			{
				
				Iterator it = crashes.iterator();
				while (it.hasNext()) 
				{
									
				    //System.out.println("llegint "+files[i].getName());	
					File file = (File) it.next();
					String context = (String) it.next();
					Bucket mem = (Bucket) it.next();
					
					FileInputStream in = new FileInputStream(file);
					//ObjectInputStream ois = new ObjectInputStream(in);
					//DomReader dr = new DomReader((Document) builder.build(file));
					//DomWriter dw = new DomWriter((Document) builder.build(file));
					 
					String name = file.getName(); 
					if (name.equals("mapping")) {
							
					 
					  //ObjectInputStream ois = xstream.createObjectInputStream(dr);
					  
				      //read pairs info										     
				      //Hashtable map =(Hashtable) ois.readObject();
					  Hashtable map = (Hashtable) xstream.fromXML(in);
				      in.close();
				     					
				      if (map==null) 	map = new Hashtable();				
				      else System.out.println("map loaded -> context "+context+" map : "+map);
				      maps.put(context,map);
					}
					else if (name.substring(name.length()-3).equals("xml")) {						
					 
					  //ObjectInputStream ois =  xstream.createObjectInputStream(dr);
						
					  System.out.println("TRYING :"+name);  
					  try {
					    //Pair pair=(Pair) ois.readObject();
				        Pair pair=(Pair) xstream.fromXML(in);
					    Id key = pair.getKey();
					    Values values= pair.getValues();
					  
					    mem.overwrite(key,values);
					    memTable.put(context,mem);
					  
					  } catch(Exception e) {
						  System.out.println("CRASHING :"+name); 
						  newCrashes.add(file);
						  newCrashes.add(context);
						  newCrashes.add(mem);
						  e.printStackTrace();
					  }
					  in.close();
					  //System.out.println("load file "+name);
					  //System.out.println("mem"+mem+"key"+key+","+" values"+sues);
					}  
				}
			}
			catch (IOException ex) 
			{
				ex.printStackTrace();
			}
						
			if (!newCrashes.isEmpty()) {
				rescueTask = new RescueTask(newCrashes);
				timer.schedule(rescueTask,5000);
				System.out.println("\n\n\nOther Timer, rescue files : "+newCrashes.size()/3);
			}
		}
	}	

}