package seed.minerva.cache;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.channels.FileLock;
import java.util.Map.Entry;

import otherSupport.SettingsManager;


import binaryMatrixFile.BinaryMatrixFile;

import oneLiners.OneLiners;
import seed.minerva.cache.Cache.Key;

/** Provides cache object based on seed.minerva.cache.Cache but cache is mirrored on disk 
 * Objects written to disk as they are put in the cache. Overwriting a particular key 
 * will overwrite the one in memory and just append a new one to the file but when loading
 * that file the second one will overwrite the memory one. The memory cache will therefore 
 * correctly reflect what it was on the previous run but the disk cache will waste space.
 * 
 * @deprecated Replaced by seed.minerva.cache.common.CacheService and its implmentors.
 * */
public class CacheFile extends Cache {
	private static final long serialVersionUID = 1L;

	private String fileName;
	private boolean readOnly;
			
	public CacheFile(String fileName) { this(fileName, false); }
	
	public CacheFile(String fileName, boolean readOnly) {
		if(fileName == null && readOnly == true){
			//being built from outside (see e.g. MagneticsCacheService.BufferToCache)
			return;
		}
		
		OneLiners.makePath(fileName);
		this.fileName = fileName;
		this.readOnly = readOnly;
		int i = 0;
		FileLock lock = null;
				
		/* First read what already exists in the file */		
		FileInputStream fin = null;
		DataInputStream din = null;
		boolean rewrite = false;
		try {			
			//we need to open the file for outputing while we read it in order
			//to request the file lock. we need to get the lock 
			// so we know the file is complete as we start reading it
			fin = new FileInputStream(fileName);
			din = new DataInputStream(fin);
			lock = fin.getChannel().lock(0, Long.MAX_VALUE, true);

			do{
				try {
					byte data[] = null;
					int bytesRead;
					
					//first read chunk size
					int size = din.readInt();
					if(size <= 0)
						bytesRead = 0;
					else{
						data = new byte[size];
						bytesRead = din.read(data, 0, size);
					}
					if(bytesRead != size){
						System.err.println("WARNING: Error reading from cache file '" + fileName + "'. Next chunk size is " +
								"reported as "+size+" bytes but only read " + bytesRead + " bytes. Truncating and abandoning cache.");
						rewrite = true;
						break;
					}
															
					/* Each object was written by it's own ObjectInputStream
					 * because the OIS can't cope with appending properly. */
					ByteArrayInputStream bin = new ByteArrayInputStream(data);
					ObjectInputStream oin = new ObjectInputStream(bin);
					Key key = (Key)oin.readObject();
					Object value = oin.readObject();
					super.put(key, value);
					oin.close();
					bin.close();
				} catch (InvalidClassException e){
					System.err.println("WARNING: Unable to load "+i+"'th key/value pair from cache file '" + fileName + "'\n" +
							"Class type mismatches local copy, ignoring cache entry. Exception message was: " + e.getMessage());
					rewrite = true;
					continue;
				} catch (ClassNotFoundException e) {
					System.err.println("WARNING: Unable to load "+i+"'th key/value pair from cache file '" + fileName + "'\n" +							
							"Class type does not exist locally, ignoring cache entry. Exception message was: " + e.getMessage());
					rewrite = true;
					continue;
				}
				i++;
			}while(fin.available() > 0);
		
			//fin.close();
			
		} catch (FileNotFoundException err) {			
			//We don't care - just start with an empty cache.
			if( Integer.parseInt(SettingsManager.defaultGlobal().getProperty("minerva.logLevel", "0")) > 1)
				System.out.println("No cache file '" + fileName + "', Starting new cache.");
			clear();
			rewrite = true; //write the empty new cache file
		} catch (IOException err) {
			System.err.println("WARNING: Error opening '" + fileName + "', starting new cache.");
			err.printStackTrace();
			clear();
			rewrite = true;
		}
		
		if(lock != null){
			try {
				lock.release();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
		if(din != null) 
			try{ din.close(); } catch (IOException err) { }
		
		if(fin != null) 
			try{ fin.close(); } catch (IOException err) { }
			
		if(rewrite && !readOnly){
			System.err.println("Due to cache load failue, rewriting successfully loaded items to new cache file and restarting cache from scratch.");
			clear();
			(new File(fileName)).delete();
			for(Entry<Key, Object> item : map.entrySet()){
				writeItem(item.getKey(), item.getValue());
			}
		}
	}
		
	public void put(Key key, Object value) {
		super.put(key, value);
		writeItem(key, value);	
	}
	
	private void writeItem(Key key, Object value){
		if(fileName == null || readOnly) //read-olny or no file, memory only cache
			return; 
				
		/* Re-open the file for append and write the new key/value pair */ 
		try {
			//First convert object to byte data
			ByteArrayOutputStream bos = new ByteArrayOutputStream(); 
		    ObjectOutputStream oout = new ObjectOutputStream(bos);			
			oout.writeObject(key);
			oout.writeObject(value);
			oout.flush();
			oout.close();
			bos.close();
			byte [] data = bos.toByteArray();
			
			/* OOS/OIS can't cope with appending so a new one must be created each time (groan) */
			FileOutputStream fout = new FileOutputStream(fileName, true);
			FileLock lock = fout.getChannel().lock(); //lock the file so other instances don't clash
			
			//write the data size then the data
			DataOutputStream dout = new DataOutputStream(fout);
			dout.writeInt(data.length);
			dout.write(data);
			
			//unlock and close file
			lock.release();
			dout.close();			
			fout.close();
		} catch (IOException e) {
			System.out.println("WARNING: Unable to write to cache file '" + fileName + "'. Not caching.");
			e.printStackTrace();				
		}
	}
	
	public void clear() {
		super.clear();
	}
	
	public void addProperty(String name, Object value) {
		throw new RuntimeException("Properties not supported");
	}
	
	public void removeProperty(String name) {
		throw new RuntimeException("Properties not supported");
	}
	
	public String getFileName() {
		return fileName;
	}

	/** change filName on the fly (future items only will be written to the new cachefile */
	public void setFileName(String fileName) {
		this.fileName = fileName;
	}
	
	public void setReadOnly(boolean redaOnly){ this.readOnly = redaOnly; }
	
}
