/* $Id: CacheManager.java,v 1.1 2004/01/09 16:55:42 burkhard Exp $
 * Created on 21.05.2003 by sell
 *
 */
package de.skyrix.zsp.logic.cache;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

import de.skyrix.zsp.conf.ZSPConfig;
import de.skyrix.zsp.event.ConfigChangeEvent;
import de.skyrix.zsp.event.ConfigChangeListener;
import de.skyrix.zsp.logic.DAVItem;
import de.skyrix.zsp.logic.IdVersionPair;

/**Verwaltet den Cache des Proxies.
 * 
 * @author sell
 * @version 0.2
 */
public class CacheManager {
	private static final String HEADER =
		"ZidestoreProxy cache file - (c) 2003 Skyrix Software AG";

	private String rootDir = null;
	private static CacheManager selfInstance = null;
	private static boolean clearingCache = false;

	private CacheManager() {
	}

	private CacheManager(String rootDir) throws CacheManageException {
		this();

		if (rootDir == null)
			throw new IllegalArgumentException("NULL is not a valid root directory.");

		setRootDir(rootDir);
		selfInstance = this;

		//Register ConfigChangeListener
		ZSPConfig.addConfigChangeListener(new ConfigChangeListener() {

			public void configurationChanged(ConfigChangeEvent e) {
				String oldRootDir = new String(getRootDir());
				try {
					setRootDir(ZSPConfig.getProperty("cache_dir"));
				}
				catch (Exception ex) {
					try {
						System.err.println("Invalid cache directory, using previous.");
						setRootDir(oldRootDir);
					}
					catch (Exception exc) {
					}
				}
			}
		});
	}

	public void putIDsAndVersions(String folder, ArrayList idvList) {
		if (folder == null || idvList == null)
			return;

		try {
			String dir = determineFilePath(folder);

			Properties idvProps = new Properties();

			Iterator idvIterator = idvList.iterator();
			while (idvIterator.hasNext()) {
				try {
					IdVersionPair pair = (IdVersionPair) idvIterator.next();
					idvProps.setProperty("" + pair.getID(), "" + pair.getVersion());
				}
				catch (Exception e) {
				}
			}

			idvProps.store(new FileOutputStream(dir + "idsAndVersion.prop"), HEADER);
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}

	public synchronized void addIDVPair(String folder, IdVersionPair idvPair)
		throws CacheManageException {
		if (folder == null || idvPair == null)
			return;

		Properties idvProps = null;
		try {
			idvProps = loadIDVList(folder);
		}
		catch (Exception e) {
		}

		if (idvProps == null)
			idvProps = new Properties();

		idvProps.setProperty("" + idvPair.getID(), "" + idvPair.getVersion());

		try {
			String dir = determineFilePath(folder);
			idvProps.store(new FileOutputStream(dir + "idsAndVersion.prop"), HEADER);
		}
		catch (Exception e) {
			e.printStackTrace();
			throw new CacheManageException("A cache file does not exists and can't be created.");
		}
	}

	public synchronized void removeIDVPair(String folder, IdVersionPair idvPair)
		throws CacheManageException {
		if (folder == null || idvPair == null)
			return;

		Properties idvProps = null;
		boolean useSpoolDir = false;
		try {
			idvProps = loadIDVList(folder);
			if (!idvProps.containsKey(idvPair.getID())) {
				idvProps = loadIDVList(folder + "/_spool");
				useSpoolDir = true;
			}
		}
		catch (Exception e) {
		}

		if (!idvProps.containsKey(idvPair.getID()))
			return;

		if (idvProps == null)
			idvProps = new Properties();

		idvProps.remove("" + idvPair.getID());

		try {
			String dir = determineFilePath(folder);
			if (useSpoolDir)
				dir += "_spool/";

			idvProps.store(new FileOutputStream(dir + "idsAndVersion.prop"), HEADER);
		}
		catch (Exception e) {
			e.printStackTrace();
			throw new CacheManageException("A cache file does not exists and can't be created.");
		}
	}

	public List getIDsAndVersions(String folder) throws CacheManageException {
		List retList = new ArrayList();

		List idvList = getIDsAndVersions(folder, null);
		if (idvList != null)
			retList.addAll(idvList);

		idvList = getIDsAndVersions(folder, "_spool");

		if (idvList != null)
			retList.addAll(idvList);

		System.out.println("getidsandversions returns: " + retList);

		return retList;
	}

	//
	//
	//
	//  !!!!!    SCHAUEN WEGEN SPIDER ALREADY RUNNING     !!!!!
	//
	//
	//

	private Properties loadIDVList(String folder) throws CacheManageException {
		if (folder == null)
			return null;

		String dir = determineFilePath(folder);
		if (!dir.endsWith("/"))
			dir += "/";

		try {
			File fdir = new File(dir);
			if (fdir.exists() && fdir.isDirectory()) {

				File idVersionFile = new File(dir + "idsAndVersion.prop");
				if (!idVersionFile.exists() || !idVersionFile.isFile())
					return null;

				if (!idVersionFile.canRead())
					throw new CacheManageException(
						"a cache file exists but is not readable: "
							+ idVersionFile.getAbsolutePath());

				Properties idvProps = new Properties();
				idvProps.load(new FileInputStream(idVersionFile));
				return idvProps;
			}
		}
		catch (CacheManageException e) {
			throw e;
		}
		catch (Exception e) {
			e.printStackTrace();
		}

		return null;
	}

	private String determineFilePath(String folder) {
		String dir;
		if (folder.startsWith(rootDir))
			dir = folder;
		else {
			dir = rootDir;
			if (!folder.startsWith(File.separator))
				dir += File.separator;
			dir += folder;
			if (!folder.endsWith(File.separator))
				dir += File.separator;
		}
		return dir;
	}

	private List getIDsAndVersions(String messageFolder, String subFolder)
		throws CacheManageException {
		if (messageFolder == null)
			return null;

		String folder = messageFolder;
		if (subFolder != null)
			folder += "/" + subFolder;

		ArrayList aRet = null;
		Properties idvProps = loadIDVList(folder);

		//System.out.println("loaded idv props = " + idvProps);

		if (idvProps == null)
			return new ArrayList();

		try {
			aRet = new ArrayList();
			Enumeration keys = idvProps.keys();
			while (keys.hasMoreElements()) {
				String id = (String) keys.nextElement();
				String version = idvProps.getProperty(id);

				try {
					IdVersionPair ivPair =
						new IdVersionPair(id, Integer.parseInt(version));
					aRet.add(ivPair);
				}
				catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		catch (Exception e) {
			e.printStackTrace();
		}

		//System.out.println("returning idvlist: " + aRet);

		return aRet;
	}

	public static CacheManager getInstance(String rootDir)
		throws CacheManageException {
		if (selfInstance == null)
			new CacheManager(rootDir);
		return selfInstance;
	}

	public static CacheManager getInstance() {
		return selfInstance;
	}

	private void createFolder(String folder) throws CacheManageException {
		if (folder == null)
			return;

		File folderDir = new File(rootDir + folder);
		if (!folderDir.exists()) {
			if (!folderDir.mkdirs())
				throw new CacheManageException(
					"Unable to create cache directory \"" + folderDir + "\"");
		}

		if (!folderDir.canRead()) {
			throw new CacheManageException(
				"Unable to read from cache directory \"" + folderDir + "\"");
		}

		if (!folderDir.canWrite()) {
			throw new CacheManageException(
				"Unable to write to cache directory \"" + folderDir + "\"");
		}

		if (!folderDir.isDirectory()) {
			throw new CacheManageException(
				"Cache directory \""
					+ folderDir
					+ "\" exists, but is not a valid directory.");
		}
	}

	private String getPath(String url) throws MalformedURLException {
		if (url == null)
			return null;

		return new URL(url).getPath();
	}

	private String getMessageFileName(DAVItem message, String folder) {
		try {
			String idStr = folder.substring(folder.lastIndexOf("/") + 1);
			String path = folder.substring(0, folder.lastIndexOf("/"));
			String subPath = idStr.substring(0, 2);

			createFolder(path + File.separator + subPath);

			return path
				+ File.separator
				+ subPath
				+ File.separator
				+ idStr
				+ ".prop."
				+ message.getVersion();
		}
		catch (Exception e) {
			System.err.println("getMessageFileName");
			e.printStackTrace();
		}
		return null;
	}

	private String getMessageFileName(String folder, String id) {
		try {
			String path = folder;
			if (!folder.startsWith(File.separator))
				path = File.separator + folder;

			if (!folder.endsWith(File.separator))
				path += File.separator;

			/*if (isSpooled) {
				path += "_spool/";
			}*/

			String subDir = "" + id;
			subDir = subDir.substring(0, 2);

			return path
				+ subDir
				+ File.separator
				+ id
				+ ".prop."
				+ getLatestMessageVersion(folder, id);
		}
		catch (Exception e) {
			//e.printStackTrace();
		}
		return null;
	}

	private void saveItem(DAVItem item, String filePath) throws IOException {
		String dir = rootDir;

		if (!filePath.startsWith(File.separator))
			dir += File.separator;
		dir += filePath;

		FileOutputStream output = new FileOutputStream(dir);
		item.store(output, HEADER);
		output.close();
	}

	public void putFolder(DAVItem folder) throws CacheManageException {
		if (folder == null)
			return;

		try {
			String path = getPath(folder.getLocation());
			if (path == null)
				throw new CacheManageException("NULL is not a valid location.");
			createFolder(path);
			saveItem(folder, path + File.separator + "folder_att.prop");
		}
		catch (MalformedURLException e) {
			throw new CacheManageException(
				folder.getLocation() + " is not a valid location.");
		}
		catch (FileNotFoundException e) {
			e.printStackTrace();
			throw new CacheManageException("A cache file does not exists and can't be created.");
		}
		catch (IOException e) {
			throw new CacheManageException("Unable to write to a cache file.");
		}
	}

	public DAVItem getFolder(String folderName) {
		if (folderName == null)
			return null;

		try {
			File folderAttribFile =
				new File(rootDir + folderName + "folder_att.prop");
			if (!folderAttribFile.exists()) {
				//System.out.println("attrib file doesn't exists.");
				return null;
			}

			DAVItem item = new DAVItem();
			item.load(new FileInputStream(folderAttribFile));

			return item;

		}
		catch (Exception e) {
			e.printStackTrace();
		}

		return null;
	}

	public DAVItem getMessage(String uri, String id) {
		if (uri == null)
			return null;

		try {
			String dir = determineFilePath(uri);
			//det machen
			File messageFile = new File(getMessageFileName(dir, id));
			if (!messageFile.exists()) {
				messageFile = new File(getMessageFileName(dir + "/_spool", id));
			}

			if (!messageFile.exists()) {
				System.err.println(
					"ATTENTION: message (" + dir + "/" + id + ") not found!!!");
				return null;
			}

			DAVItem item = new DAVItem();
			item.load(new FileInputStream(messageFile));

			System.out.println("location = " + item.getLocation());

			return item;

		}
		catch (Exception e) {
			e.printStackTrace();
		}

		return null;
	}

	public List getAttributes(String uri, String range)
		throws MalformedURLException {

		if (range == null)
			return null;

		LinkedList items = new LinkedList();
		ArrayList messages = new ArrayList();

		//Determine the item(s)
		if (range.indexOf("_range_") > -1) {
			//more than one item
			StringTokenizer rangeTokenizer =
				new StringTokenizer(
					range.substring(range.indexOf("_range_") + 7),
					"_",
					false);
			while (rangeTokenizer.hasMoreElements()) {
				try {
					String token = rangeTokenizer.nextToken();
					items.add(token);
				}
				catch (Exception e) {
				}
			}
		}
		else {
			//only one item
			try {
				items.add(new Integer(range));
			}
			catch (Exception e) {
			}
		}

		//no valid message ids found
		if (items.isEmpty())
			return null;

		//Iterate over items (message ids)
		Iterator itemIterator = items.iterator();
		while (itemIterator.hasNext()) {
			try {
				Object id = itemIterator.next();

				//get the message an add it to result list
				messages.add(getMessage(uri, id.toString()));
			}
			catch (Exception e) {
			}

		}

		return messages;
	}

	public void putMessage(DAVItem message) throws CacheManageException {
		try {
			String folder = new URL(message.getLocation()).getPath();
			putMessage(message, folder);
		}
		catch (Exception e) {
		}
	}

	public void putMessage(DAVItem message, String folder)
		throws CacheManageException {
		if (message == null)
			return;

		try {
			String fileName = getMessageFileName(message, folder);

			if (fileName == null)
				throw new CacheManageException(fileName + " is not a valid location.");
			saveItem(message, fileName);
		}
		catch (MalformedURLException e) {
			throw new CacheManageException(
				message.getLocation() + " is not a valid location.");
		}
		catch (FileNotFoundException e) {
			e.printStackTrace();
			throw new CacheManageException("A cache file does not exists and can't be created.");
		}
		catch (IOException e) {
			throw new CacheManageException("Unable to write to a cache file.");
		}
		catch (Exception e) {
			e.printStackTrace();
			throw new CacheManageException("Unable to actualize a cache file.");
		}
	}

	public boolean hasVersion() {

		return false;
	}

	public void setRootDir(String rootDir) throws CacheManageException {
		if (rootDir == null)
			throw new IllegalArgumentException("NULL is not a valid root directory.");

		this.rootDir = rootDir;
		createFolder(File.separator);
	}

	public String getRootDir() {
		return rootDir;
	}

	public void removeFolder(String path) {
	}

	/**Removes a message from file system.
	 * <strong>ATTENTION: this method removes the message only from
	 * file system, not from the server. In those cases use
	 * {@link #deleteMessage(String, String) deleteMessage()} instead.</strong>
	 * 
	 * @param folder - folder to remove the message from
	 * @param id     - ID of the message
	 */
	public void removeMessage(String folder, String id) {
		if (folder == null || id == null)
			return;

		File messageFile = new File(rootDir + getMessageFileName(folder, id));
		if (!messageFile.exists()) {
			messageFile =
				new File(rootDir + getMessageFileName(folder + "/_spool", id));
		}
		System.out.println(
			"removing message " + id + " from " + messageFile.getAbsolutePath());

		try {
			//delete message from filesystem
			messageFile.delete();

			//remove message from IDVersion file
			IdVersionPair idvPair = new IdVersionPair(id, 0);

			String idvFolder = folder;

			removeIDVPair(idvFolder, idvPair);
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void deleteMessage(String folder, String id) {
	}

	public List listFolderItems(String folder) {
		//Get ids and versions list
		File folderFile = new File(rootDir + folder);
		if (!folderFile.exists()
			|| !folderFile.isDirectory()
			|| !folderFile.canRead())
			return null;

		ArrayList items = new ArrayList();
		File[] folderEntries = folderFile.listFiles();
		for (int i = 0; i < folderEntries.length; i++) {
			try {
				File itemFile = folderEntries[i];
				if (itemFile.isDirectory()) {
					File attribFile = new File(itemFile, "folder_att.prop");
					if (attribFile.exists()
						&& attribFile.isFile()
						&& attribFile.canRead()) {
						DAVItem davItem = new DAVItem();
						davItem.load(new FileInputStream(attribFile));

						items.add(davItem);
						davItem.setProperty("resourcetype", "collection", "D", "DAV:");
					}
				}
			}
			catch (Exception e) {
				e.printStackTrace();
			}

		}
		return items;
	}

	public int getLatestMessageVersion(String folder, String messageID)
		throws CacheManageException {
		List idv = getIDsAndVersions(folder);

		if (idv != null && !idv.isEmpty()) {
			Iterator idvIterator = idv.iterator();
			while (idvIterator.hasNext()) {
				try {
					IdVersionPair idvPair = (IdVersionPair) idvIterator.next();
					if (idvPair.getID().equals(messageID))
						return idvPair.getVersion();
				}
				catch (Exception e) {
				}
			}
		}

		return 0;
	}

	/**Compares a list of remote versions with the local versions in
	 * cache and return the items to remove/update.
	 * 
	 * @param folder         - folder to check
	 * @param remoteIDVs     - list of remote versiona
	 * @param returnOboletes - if <code>true</code> return the
	 *    items to remove otherwise return the items to update.
	 * 
	 * @return items to remove/update
	 */
	public List checkMessageVersion(
		String folder,
		List remoteIDVs,
		boolean returnObsoletes)
		throws CacheManageException {

		if (remoteIDVs == null)
			return null;

		List localIDVs = getIDsAndVersions(folder);

		ArrayList updateList = new ArrayList();

		if (localIDVs == null)
			return remoteIDVs;

		Iterator idpIterator = remoteIDVs.iterator();
		while (idpIterator.hasNext()) {
			try {
				IdVersionPair remotePair = (IdVersionPair) idpIterator.next();
				if (!localIDVs.contains(remotePair)
					|| (remotePair.getVersion()
						> ((IdVersionPair) localIDVs.get(localIDVs.indexOf(remotePair)))
							.getVersion())) {
					updateList.add(remotePair);
				}
				localIDVs.remove(remotePair);
			}
			catch (Exception e) {
			}
		}

		return (
			returnObsoletes
				? (localIDVs != null && !localIDVs.isEmpty() ? localIDVs : null)
				: (updateList != null && !updateList.isEmpty() ? updateList : null));
	}

	public synchronized boolean clearCache() {
		if (rootDir == null)
			return false;

		File rootFile = new File(rootDir);

		if (!rootFile.exists() || !rootFile.canWrite() || !rootFile.isDirectory())
			return false;

		try {
			clearingCache = true;
			boolean result = deleteDirectory(rootFile);
			clearingCache = false;

			return result;
		}
		catch (Exception e) {
			e.printStackTrace();
		}

		return false;
	}

	private boolean deleteDirectory(File dir) throws IOException {
		if (dir == null || !dir.isDirectory())
			return false;

		File[] entries = dir.listFiles();
		int size = entries.length;

		for (int i = 0; i < size; i++) {
			if (entries[i].isDirectory()) {
				if (!deleteDirectory(entries[i]))
					return false;

			}
			else {
				if (!entries[i].delete())
					return false;

			}
		}

		return dir.delete();
	}

	public boolean createMessage(DAVItem item, String folder) {
		System.out.println("creating item " + item + " in folder " + folder);

		if (item == null || folder == null)
			return false;

		try {
			createFolder(folder + "/_spool");
			System.out.println("spool folder created");
		}
		catch (Exception e) {
			System.out.println("cant create spool folder for new messages");
			e.printStackTrace();
			return false;
		}

		try {
			String messageID = "new-msg-" + new Date().getTime();

			System.out.println("setting location and status");
			item.setLocation(folder + "/" + messageID);
			item.setStatus("HTTP/1.1 200 OK");
			item.setVersion(1);
			item.setSpooled(true);
			item.setNew(true);

			System.out.println("saving message");
			item.setProperty("message_id", messageID);
			putMessage(item, folder + "/_spool/" + messageID);

			addIDVPair(folder + "/_spool", new IdVersionPair(messageID, 1));

			System.out.println("well done ;)");
		}
		catch (Exception e) {
			System.err.println("unable to store new message");
			e.printStackTrace();
			return false;
		}

		return true;
	}

	public boolean updateMessage(DAVItem item, String folder) {
		if (item == null || folder == null)
			return false;

		//increment version
		//item.setve

		return false;
	}

	public static boolean isClearingCache() {
		return clearingCache;
	}

	private void addMessagesFromSpoolDir(String spoolDir) {
		if (spoolDir == null)
			return;

		try {
			List idvList = getIDsAndVersions(spoolDir + "/_spool");
			SpoolerQueue spoolerQueue = SpoolerQueue.getInstance();
			System.out.println("idv of spool = " + idvList);

			Iterator idvIterator = idvList.iterator();
			while (idvIterator.hasNext()) {
				try {
					IdVersionPair idvPair = (IdVersionPair) idvIterator.next();
					DAVItem spooledItem = getMessage(spoolDir, idvPair.getID());

					//System.out.println("adding item " + spooledItem);

					//TODO: updated/deleted items will not work!!!
					if (spooledItem.isNew()) {
						System.out.println("found new item");
						spoolerQueue.createMessage(spooledItem);
					}
					else {
						System.out.println("found updated item");
						spoolerQueue.updateMessage(spooledItem);
					}

				}
				catch (Exception e) {
				}

			}

			//spoolerQueue.createMessage();

		}
		catch (Exception e) {
		}
	}

	private void findSpoolDir(String startupDir) {
		File rootDirFile = new File(startupDir);

		if (rootDirFile.exists()
			&& rootDirFile.isDirectory()
			&& rootDirFile.canRead()) {
			String[] dirEntries = rootDirFile.list();
			for (int i = 0; i < dirEntries.length; i++) {
				File entryFile = new File(startupDir + "/" + dirEntries[i]);
				if (entryFile.exists()
					&& entryFile.isDirectory()
					&& entryFile.canRead()) {
					if ("_spool".equals(entryFile.getName())) {
						addMessagesFromSpoolDir(startupDir);
					}
					else
						findSpoolDir(entryFile.getAbsolutePath());
				}
			}
		}
	}

	/**Searches all spooled messages an put them
	 * into Spooler Q.
	 */
	public void initSpoolerQueue() {
		System.out.println("initializing Q with rootDir " + rootDir);

		findSpoolDir(rootDir);
	}
}
