package i2p.susi.webmail;

import i2p.susi.debug.Debug;
import i2p.susi.webmail.Messages;
import i2p.susi.util.ReadBuffer;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import net.i2p.I2PAppContext;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.util.PasswordManager;
import net.i2p.util.SecureDirectory;
import net.i2p.util.SecureFile;
import net.i2p.util.SecureFileOutputStream;


/**
 * Manage the on-disk cache.
 *
 * This is a custom format with subdirectories, gzipped files,
 * and the encoded UIDL in the file name.
 * We store either the headers or the full message.
 * No, it is not Maildir format but we could add Maildir-style
 * status suffixes (e.g. ":2.SR") later.
 *
 * Exporting to a Maildir format would be just ungzipping
 * each file to a flat directory.
 *
 * TODO draft and sent folders, cached server caps and config.
 *
 * @since 0.9.14
 */
class PersistentMailCache {
	
	/**
	 *  One lock for each user in the whole JVM, to protect against multiple sessions.
	 *  One big lock for the whole cache dir, not one for each file or subdir.
	 *  Never expired.
	 *  Sure, if we did a maildir format we wouldn't need this.
	 */
	private static final ConcurrentHashMap<String, Object> _locks = new ConcurrentHashMap<String, Object>();

	private final Object _lock;
	private final File _cacheDir;

	private static final String DIR_SUSI = "susimail";
	private static final String DIR_CACHE = "cache";
	private static final String CACHE_PREFIX = "cache-";
	private static final String DIR_FOLDER = "cur"; // MailDir-like
	private static final String DIR_PREFIX = "s";
	private static final String FILE_PREFIX = "mail-";
	private static final String HDR_SUFFIX = ".hdr.txt.gz";
	private static final String FULL_SUFFIX = ".full.txt.gz";
	private static final String B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~";

	/**
	 *  Use the params to generate a unique directory name.
	 *  @param pass ignored
	 */
	public PersistentMailCache(String host, int port, String user, String pass) throws IOException {
		_lock = getLock(host, port, user, pass);
		synchronized(_lock) {
			_cacheDir = makeCacheDirs(host, port, user, pass);
		}
	}

	/**
	 * Fetch all mails from disk.
	 * 
	 * @return a new collection
	 */
	public Collection<Mail> getMails() {
		synchronized(_lock) {
			return locked_getMails();
		}
	}

	private Collection<Mail> locked_getMails() {
		List<Mail> rv = new ArrayList<Mail>();
		for (int j = 0; j < B64.length(); j++) {
			File subdir = new File(_cacheDir, DIR_PREFIX + B64.charAt(j));
			File[] files = subdir.listFiles();
			if (files == null)
				continue;
			for (int i = 0; i < files.length; i++) {
				File f = files[i];
				if (!f.isFile())
					continue;
				Mail mail = load(f);
				if (mail != null)
			               rv.add(mail);
			}
		}
		return rv;
	}

	/**
	 * Fetch any needed data from disk.
	 * 
	 * @return success
	 */
	public boolean getMail(Mail mail, boolean headerOnly) {
		synchronized(_lock) {
			return locked_getMail(mail, headerOnly);
		}
	}

	private boolean locked_getMail(Mail mail, boolean headerOnly) {
		File f = getFullFile(mail.uidl);
		if (f.exists()) {
			ReadBuffer rb = read(f);
			if (rb != null) {
				mail.setBody(rb);
				return true;
			}
		}
		f = getHeaderFile(mail.uidl);
		if (f.exists()) {
			ReadBuffer rb = read(f);
			if (rb != null) {
				mail.setHeader(rb);
				return true;
			}
		}
		return false;
	}

	/**
	 * Save data to disk.
	 * 
	 * @return success
	 */
	public boolean saveMail(Mail mail) {
		synchronized(_lock) {
			return locked_saveMail(mail);
		}
	}

	private boolean locked_saveMail(Mail mail) {
		ReadBuffer rb = mail.getBody();
		if (rb != null) {
			File f = getFullFile(mail.uidl);
			if (f.exists())
				return true;  // already there, all good
			boolean rv = write(rb, f);
			if (rv)
				getHeaderFile(mail.uidl).delete();
			return rv;
		}
		rb = mail.getHeader();
		if (rb != null) {
			File f = getHeaderFile(mail.uidl);
			if (f.exists())
				return true;  // already there, all good
			boolean rv = write(rb, f);
			return rv;
		}
		return false;
	}

	/**
	 * 
	 * Delete data from disk.
	 */
	public void deleteMail(Mail mail) {
		deleteMail(mail.uidl);
	}

	/**
	 * 
	 * Delete data from disk.
	 */
	public void deleteMail(String uidl) {
		synchronized(_lock) {
			getFullFile(uidl).delete();
			getHeaderFile(uidl).delete();
		}
	}

	private static Object getLock(String host, int port, String user, String pass) {
		Object lock = new Object();
		Object old = _locks.putIfAbsent(user + host + port, lock);
		return (old != null) ? old : lock;
	}

	/**
	 *   ~/.i2p/susimail/cache/cache-xxxxx/cur/s[a-z]/mail-xxxxx.full.txt.gz
	 *   folder1 is the base.
	 */
	private static File makeCacheDirs(String host, int port, String user, String pass) throws IOException {
		File f = new SecureDirectory(I2PAppContext.getGlobalContext().getConfigDir(), DIR_SUSI);
		if (!f.exists() && !f.mkdir())
			throw new IOException("Cannot create " + f);
		f = new SecureDirectory(f, DIR_CACHE);
		if (!f.exists() && !f.mkdir())
			throw new IOException("Cannot create " + f);
		f = new SecureDirectory(f, CACHE_PREFIX + Base64.encode(user + host + port));
		if (!f.exists() && !f.mkdir())
			throw new IOException("Cannot create " + f);
		File base = new SecureDirectory(f, DIR_FOLDER);
		if (!base.exists() && !base.mkdir())
			throw new IOException("Cannot create " + base);
		for (int i = 0; i < B64.length(); i++) {
			f = new SecureDirectory(base, DIR_PREFIX + B64.charAt(i));
			if (!f.exists() && !f.mkdir())
				throw new IOException("Cannot create " + f);
		}
		return base;
	}

	private File getHeaderFile(String uidl) {
		return getFile(uidl, HDR_SUFFIX);
	}

	private File getFullFile(String uidl) {
		return getFile(uidl, FULL_SUFFIX);
	}

	private File getFile(String uidl, String suffix) {
		byte[] raw = DataHelper.getASCII(uidl);
		byte[] md5 = PasswordManager.md5Sum(raw);
		String db64 = Base64.encode(md5);
		File dir = new File(_cacheDir, DIR_PREFIX + db64.charAt(0));
		String b64 = Base64.encode(uidl);
		return new SecureFile(dir, FILE_PREFIX + b64 + suffix);
	}

	/**
	 * Save data to disk.
	 * 
	 * @return success
	 */
	private static boolean write(ReadBuffer rb, File f) {
		OutputStream out = null;
		try {
			out = new BufferedOutputStream(new GZIPOutputStream(new SecureFileOutputStream(f)));
			out.write(rb.content, rb.offset, rb.length);
			return true;
		} catch (IOException ioe) {
			Debug.debug(Debug.ERROR, "Error writing: " + f + ": " + ioe);
			return false;
		} finally {
			if (out != null) 
				try { out.close(); } catch (IOException ioe) {}
		}
	}

	/**
	 *  @return null on failure
	 */
	private static ReadBuffer read(File f) {
		InputStream in = null;
		try {
			long len = f.length();
			if (len > 16 * 1024 * 1024) {
				throw new IOException("too big");
			}
			in = new GZIPInputStream(new BufferedInputStream(new FileInputStream(f)));
			ByteArrayOutputStream out = new ByteArrayOutputStream((int) len);
			int read = 0;
			byte buf[] = new byte[4*1024];
			while ( (read = in.read(buf)) != -1)  {
				out.write(buf, 0, read);
			}
			ReadBuffer rb = new ReadBuffer(out.toByteArray(), 0, out.size());
			return rb;
		} catch (IOException ioe) {
			Debug.debug(Debug.ERROR, "Error reading: " + f + ": " + ioe);
			return null;
		} catch (OutOfMemoryError oom) {
			Debug.debug(Debug.ERROR, "Error reading: " + f + ": " + oom);
			return null;
		} finally {
			if (in != null) 
				try { in.close(); } catch (IOException ioe) {}
		}
	}

	/**
	 *  @return null on failure
	 */
	private static Mail load(File f) {
		String name = f.getName();
		String uidl;
		boolean headerOnly;
		if (name.endsWith(FULL_SUFFIX)) {
			uidl= Base64.decodeToString(name.substring(FILE_PREFIX.length(), name.length() - FULL_SUFFIX.length()));
			headerOnly = false;
		} else if (name.endsWith(HDR_SUFFIX)) {
			uidl= Base64.decodeToString(name.substring(FILE_PREFIX.length(), name.length() - HDR_SUFFIX.length()));
			headerOnly = true;
		} else {
			return null;
		}
		if (uidl == null)
			return null;
		ReadBuffer rb = read(f);
		if (rb == null)
			return null;
		Mail mail = new Mail(uidl);
		if (headerOnly)
			mail.setHeader(rb);
		else
			mail.setBody(rb);
		return mail;
	}
}
