package net.y3n20u.aeszip;

import static net.y3n20u.aeszip.CommonValues.*;
import static net.y3n20u.aeszip.CommonValues.MESSAGE_ALREADY_ARCHIVING;
import static net.y3n20u.aeszip.CommonValues.METHOD_DEFLATED;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;

public class AesZipEncrypter {
	// file name char set
	
	
	private static final String MESSAGE_INVALID_DESTINATION = "destination is invalid, maybe null or directory.";

	// TODO tune // 2^20
	private static final int BUFFER_SIZE = 1048576;

	public static int METHOD_DEFAULT = METHOD_DEFLATED;

	private OutputStream destination;
	private boolean alreadyUsed;
	private Charset fileNameCharset;
	private Charset commentCharset;
	private Charset passwordCharset;
	private final Map<String, File> filesToBeArchived = new LinkedHashMap<String, File>();
	private final Map<String, String> passwords = new LinkedHashMap<String, String>();
	private boolean shouldClose = false;

	// fields for progress listener.
	private ArchiveProgressListener progressListener;
	private ArchiveProgressStatus previousStatus;

	public AesZipEncrypter(OutputStream destination) {
		init(destination);
		shouldClose = false;
	}

	public AesZipEncrypter(File destination) {
		if (destination == null || destination.isDirectory()) {
			throw new IllegalArgumentException(MESSAGE_INVALID_DESTINATION);
		}
		OutputStream destinationStream;
		try {
			destinationStream = new FileOutputStream(destination);
		} catch (FileNotFoundException e) {
			// TODO: more comprehensive message.
			throw new IllegalArgumentException(e);
		}
		init(destinationStream);
		shouldClose = true;
	}

	protected void init(OutputStream destination) {
		this.ensureState();
		if (destination == null) {
			// TODO: more comprehensive message.
			throw new IllegalArgumentException(new NullPointerException());
		}
		this.destination = destination;
		alreadyUsed = false;
		fileNameCharset = Charset.defaultCharset();
		commentCharset = Charset.defaultCharset();
		passwordCharset = Charset.defaultCharset();
	}

	public void setProgressListener(ArchiveProgressListener progressListener) {
		this.ensureState();
		this.progressListener = progressListener;
	}

	protected boolean isAlreadyUsed() {
		return alreadyUsed;
	}

	public void setFileNameCharset(Charset charset) {
		this.ensureState();
		if (charset == null) {
			// TODO: more comprehensive message.
			throw new IllegalArgumentException(new NullPointerException());
		}
		fileNameCharset = charset;
	}
	
	protected Charset getFileNameCharset() {
		return fileNameCharset;
	}
	
	public void setCommentCharset(Charset charset) {
		this.ensureState();
		if (charset == null) {
			// TODO: more comprehensive message.
			throw new IllegalArgumentException(new NullPointerException());
		}
		commentCharset = charset;
	}
	
	protected Charset getCommentCharset() {
		return commentCharset;
	}
	
	public void setPasswordCharset(Charset charset) {
		this.ensureState();
		if (charset == null) {
			// TODO: more comprehensive message.
			throw new IllegalArgumentException(new NullPointerException());
		}
		this.passwordCharset = charset;
	}

	protected Charset getPasswordCharset() {
		return passwordCharset;
	}

	public void add(String name, File source, String password) {
		this.ensureState();
		if (name == null || source == null || password == null) {
			// TODO
			throw new IllegalArgumentException(new NullPointerException());
		}
		
		filesToBeArchived.put(name, source);
		passwords.put(name, password);
	}

	public void archive() throws IOException {
		ensureState();
		alreadyUsed = true;
		AesZipOutputStream zipOutputStream = null;
		if (progressListener != null) {
			progressListener.start();
			this.generateProgressStatus();
		}
		try {
			zipOutputStream = new AesZipOutputStream(destination);
			zipOutputStream.setFileNameCharset(fileNameCharset);
			zipOutputStream.setCommentCharset(commentCharset);
			for (String name : filesToBeArchived.keySet()) {
				File sourceFile = filesToBeArchived.get(name);
				byte[] passwordBytes = passwords.get(name).getBytes(this.getPasswordCharset());
				this.archiveImplForEachFile(zipOutputStream, sourceFile, name, passwordBytes);
			}
			zipOutputStream.finish();
			if (progressListener != null) {
				progressListener.finishedAll();
			}
		} finally {
			if (shouldClose) {
				try {
					if (zipOutputStream != null) {
						zipOutputStream.close();
					}
				} finally {
					if (destination != null) {
						destination.close();
					}
				}
			}
		}
	}

	private void generateProgressStatus() {
		long totalSize = 0L;
		for (File file : filesToBeArchived.values()) {
			totalSize += file.length();
		}
		previousStatus = new ArchiveProgressStatus(filesToBeArchived.size(), 0, totalSize, 0, -1, 0);
	}

	private void archiveImplForEachFile(AesZipOutputStream zipOut, File file, String name, byte[] password)
			throws IOException {
		boolean shouldAppendSeparator = (file.isDirectory() && !name.endsWith(FILE_NAME_SEPARATOR));
		String checkedName = name + (shouldAppendSeparator ? FILE_NAME_SEPARATOR : "");
		AesZipEntry entry = new AesZipEntry(checkedName, password);
		entry.setActualCompressionMethod(METHOD_DEFAULT);
		entry.setTime(new Date().getTime());
		zipOut.putNextZipEntry(entry);

		long size = file.length();
		String sourceName = file.getCanonicalPath();
		if (progressListener != null) {
			previousStatus = previousStatus.generateUpdatedStatusWithNextEntry(size, sourceName, checkedName);
			progressListener.notifyStatus(previousStatus);
		}
		if (file.isDirectory()) {
			return;
		}
		readAndWriteFileContents(file, zipOut);
	}

	private void readAndWriteFileContents(File sourceFile, AesZipOutputStream zipOut) throws IOException {
		InputStream inputStream = null;
		try {
			inputStream = new BufferedInputStream(new FileInputStream(sourceFile));
			byte[] buf = new byte[BUFFER_SIZE];
			int readChunkSize = inputStream.read(buf, 0, BUFFER_SIZE);
			while (readChunkSize != -1) {
				zipOut.write(buf, 0, readChunkSize);
				if (progressListener != null) {
					previousStatus = previousStatus.generateUpdatedStatus(readChunkSize);
					progressListener.notifyStatus(previousStatus);
				}
				readChunkSize = inputStream.read(buf, 0, BUFFER_SIZE);
			}
		} finally {
			if (inputStream != null) {
				inputStream.close();
			}
		}
	}

	private void ensureState() {
		if (this.isAlreadyUsed()) {
			throw new IllegalStateException(MESSAGE_ALREADY_ARCHIVING);
		}
	}
}
