/*
 * 
 *               Panbox - encryption for cloud storage 
 *      Copyright (C) 2014-2015 by Fraunhofer SIT and Sirrix AG 
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Additonally, third party code may be provided with notices and open source
 * licenses from communities and third parties that govern the use of those
 * portions, and any licenses granted hereunder do not alter any rights and
 * obligations you may have under such open source licenses, however, the
 * disclaimer of warranty and limitation of liability provisions of the GPLv3 
 * will apply to all the product.
 * 
 */
package org.panbox.desktop.common.vfs.backend.generic;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Hashtable;

import javax.crypto.SecretKey;

import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.panbox.core.crypto.io.AESGCMRandomAccessFileCompat;
import org.panbox.core.exception.FileEncryptionException;
import org.panbox.core.exception.FileIntegrityException;
import org.panbox.core.vfs.backend.VirtualFile;
import org.panbox.core.vfs.backend.VirtualVolume;
import org.panbox.desktop.common.ex.PanboxEncryptionException;
import org.panbox.desktop.common.ex.PanboxIntegrityException;
import org.panbox.desktop.common.vfs.backend.VirtualRandomAccessFile;

public class GenericVirtualFileImpl extends VirtualRandomAccessFile {

	protected File file;
	protected AESGCMRandomAccessFileCompat aesRandomAccessFile;

	protected boolean deleteFileOnClose = false;
	
	private final static Logger logger = Logger
			.getLogger(GenericVirtualFileImpl.class);

	/**
	 * stores number of open file handles for each opened file
	 */
	final static Hashtable<AESGCMRandomAccessFileCompat, Integer> filehandleCtrMap = new Hashtable<AESGCMRandomAccessFileCompat, Integer>();

	protected GenericVirtualFileImpl(String fileName, VirtualVolume volume) {
		super(fileName, volume);
		file = new File(fileName);
		try {
			if (!isDirectory()) {
				if (exists()) {
					aesRandomAccessFile = file.canWrite() ? AESGCMRandomAccessFileCompat
							.getInstance(file, true)
							: AESGCMRandomAccessFileCompat.getInstance(file,
									false);
					// System.err.println("getInstance(" + file.getName() + ","
					// + aesRandomAccessFile.writable + ") -> "
					// + aesRandomAccessFile.toString());
				} else {
					aesRandomAccessFile = file.getParentFile().canWrite() ? AESGCMRandomAccessFileCompat
							.getInstance(file, true)
							: AESGCMRandomAccessFileCompat.getInstance(file,
									false);
					// System.err.println("getInstance(" + file.getName() + ","
					// + aesRandomAccessFile.writable + ") -> "
					// + aesRandomAccessFile.toString());
				}
			}
		} catch (IOException | FileEncryptionException e) {
			logger.error("Unable to initialize AES backend", e);
		}
	}

	@Override
	public boolean isDirectory() {
		return file.isDirectory();
	}

	@Override
	public boolean delete() {
		if (file.isDirectory()) {
			try {
				FileUtils.deleteDirectory(file);
				return true;
			} catch (IOException e) {
				e.printStackTrace();
				return false;
			}
		} else {
			return file.delete();
		}
	}

	@Override
	public boolean createNewDirectory() {
		return file.mkdirs();
	}

	@Override
	public boolean exists() {
		return file.exists();
	}

	@Override
	public long length() throws IOException {
		// return aesRandomAccessFile.length();
		return AESGCMRandomAccessFileCompat
				.realToVirtualFileSize(file.length());
	}

	@Override
	public VirtualFile[] list() {
		String[] files = file.list();

		GenericVirtualFileImpl[] fileList = new GenericVirtualFileImpl[files.length];

		for (int i = 0; i < files.length; ++i) {
			fileList[i] = new GenericVirtualFileImpl(
					new File(file, files[i]).getPath(), volume);
		}

		return fileList;
	}

	@Override
	public String getPath() {
		return file.getPath();
	}

	@Override
	public String getRelativePath() {
		return volume.getRelativePathForFile(this);
	}

	@Override
	public boolean renameTo(VirtualFile newFile) {
		if (isDirectory()) {
			return file.renameTo(newFile.getFile());
		} else {
			// use backend rename function
			GenericVirtualFileImpl newDFile = (GenericVirtualFileImpl) newFile;
			if (aesRandomAccessFile.renameTo(newDFile.getFile())) {
				this.file = newDFile.getFile();
				return true;
			} else {
				return false;
			}
		}
	}

	@Override
	public File getFile() {
		return file;
	}

	@Override
	public String toString() {
		return file.getName() + " at " + file.getAbsoluteFile();
	}

	@Override
	public long getCreationTime() {
		try {
			BasicFileAttributes attr = Files.readAttributes(file.toPath(),
					BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
			return attr.creationTime().toMillis();
		} catch (IOException e) {
			logger.error("Error in getCreationTime()", e);
			return 0;
		}
	}

	@Override
	public long getLastAccessTime() {
		try {
			BasicFileAttributes attr = Files.readAttributes(file.toPath(),
					BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
			return attr.lastAccessTime().toMillis();
		} catch (IOException e) {
			logger.error("Error in getLastAccessTime()", e);
			return 0;
		}
	}

	@Override
	public long getLastWriteTime() {
		// return file.lastModified();
		try {
			BasicFileAttributes attr = Files.readAttributes(file.toPath(),
					BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
			return attr.lastModifiedTime().toMillis();
		} catch (IOException e) {
			logger.error("Error in getLastWriteTime()", e);
			return 0;
		}
	}

	@Override
	public void setLastAccessTime(long atime) throws IOException {
		if (atime > 0) {
			FileTime fileTime = FileTime.fromMillis(atime);
			Files.setAttribute(file.toPath(), "lastAccessTime", fileTime);
		}
	}

	@Override
	public void setModifiedTime(long mtime) throws IOException {
		if (mtime > 0) {
			FileTime fileTime = FileTime.fromMillis(mtime);
			Files.setLastModifiedTime(file.toPath(), fileTime);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#seek(long)
	 */
	@Override
	public void seek(long pos) throws IOException, PanboxEncryptionException {
		try {
			aesRandomAccessFile.seek(pos);
		} catch (IOException e) {
			throw new IOException(e.getMessage(), e);
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#getFilePointer()
	 */
	@Override
	public long getFilePointer() throws IOException {
		try {
			return aesRandomAccessFile.getFilePointer();
		} catch (IOException e) {
			throw new IOException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#skipBytes(int)
	 */
	@Override
	public int skipBytes(int n) throws IOException, PanboxEncryptionException {
		try {
			return aesRandomAccessFile.skipBytes(n);
		} catch (IOException e) {
			throw new IOException(e.getMessage(), e);
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#write(byte[])
	 */
	@Override
	public void write(byte[] b) throws IOException, PanboxEncryptionException {
		try {
			aesRandomAccessFile.write(b);
		} catch (IOException e) {
			throw new IOException(e.getMessage(), e);
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		} catch (FileIntegrityException e) {
			throw new PanboxIntegrityException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#write(long,
	 * java.nio.ByteBuffer)
	 */
	@Override
	public int write(long seekpos, ByteBuffer b) throws IOException,
			PanboxEncryptionException {
		try {
			aesRandomAccessFile.seek(seekpos);
			byte[] buf;
			if (b.hasArray()) {
				buf = b.array();
				this.write(buf, 0, buf.length);
			} else {
				buf = new byte[b.remaining()];
				b.get(buf);
				this.write(buf, 0, buf.length);
			}
			return buf.length;
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#write(long,
	 * java.nio.ByteBuffer)
	 */
	@Override
	public int write(long seekpos, byte[] b) throws IOException,
			PanboxEncryptionException {
		try {
			aesRandomAccessFile.seek(seekpos);
			this.write(b, 0, b.length);
			return b.length;
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#write(byte[], int,
	 * int)
	 */
	@Override
	public void write(byte[] b, int off, int len) throws IOException,
			PanboxEncryptionException {
		try {
			aesRandomAccessFile.write(b, off, len);
		} catch (IOException e) {
			throw new IOException(e.getMessage(), e);
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		} catch (FileIntegrityException e) {
			throw new PanboxIntegrityException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#write(int)
	 */
	@Override
	public void write(int b) throws IOException, PanboxEncryptionException {
		try {
			aesRandomAccessFile.write(b);
		} catch (IOException e) {
			throw new IOException(e.getMessage(), e);
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		} catch (FileIntegrityException e) {
			throw new PanboxIntegrityException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#read(byte[], int,
	 * int)
	 */
	@Override
	public int read(byte[] b, int off, int len) throws IOException,
			PanboxEncryptionException {
		try {
			return aesRandomAccessFile.read(b, off, len);
		} catch (IOException e) {
			throw new IOException(e.getMessage(), e);
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		} catch (FileIntegrityException e) {
			throw new PanboxIntegrityException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#read(byte[])
	 */
	@Override
	public int read(byte[] b) throws IOException, PanboxEncryptionException {
		try {
			return aesRandomAccessFile.read(b);
		} catch (IOException e) {
			throw new IOException(e.getMessage(), e);
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		} catch (FileIntegrityException e) {
			throw new PanboxIntegrityException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#read(long,
	 * java.nio.ByteBuffer)
	 */
	@Override
	public int read(long seekpos, ByteBuffer b) throws IOException,
			PanboxEncryptionException {
		byte[] buf = new byte[b.remaining()]; // raf.array() does not
		// work, because backend
		// byte[] is missing!
		try {
			aesRandomAccessFile.seek(seekpos);
			int reallyRead = aesRandomAccessFile.read(buf, 0, buf.length);
			if (reallyRead != -1) {
				// Trim NUL-Bytes from file
				b.put(buf, 0, reallyRead);
				return reallyRead;
			} else {
				// EOF is indicated by -1; returning -1 will be misinterpreted
				// as an ERRNO (i.e. EPERM)
				return 0;
			}
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		} catch (FileIntegrityException e) {
			throw new PanboxIntegrityException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#read(long,
	 * java.nio.ByteBuffer)
	 */
	@Override
	public int read(long seekpos, byte[] b) throws IOException,
			PanboxEncryptionException {
		try {
			aesRandomAccessFile.seek(seekpos);
			int reallyRead = aesRandomAccessFile.read(b);
			if (reallyRead == -1) {
				// EOF is indicated by -1; returning -1 will be misinterpreted
				// as an ERRNO (i.e. EPERM)
				return 0;
			} else {
				return reallyRead;
			}
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		} catch (FileIntegrityException e) {
			throw new PanboxIntegrityException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#read()
	 */
	@Override
	public int read() throws IOException, PanboxEncryptionException {
		try {
			return aesRandomAccessFile.read();
		} catch (IOException e) {
			throw new IOException(e.getMessage(), e);
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		} catch (FileIntegrityException e) {
			throw new PanboxIntegrityException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#open(java.io.File,
	 * boolean)
	 */
	@Override
	public void open() throws IOException, PanboxEncryptionException {
		try {
			// windows verifies file existance by calling create(OPEN_EXISTING)
			// - thus check if file exists at an earlypoint in time
			if (!file.exists()) {
				throw new IOException("File " + file + "does not exist!");
			} else {

				// TODO: support read-only mode, use fileinfo-objects
				// check file handle counter maps and create/increase values
				synchronized (filehandleCtrMap) {
					Integer fhctr = filehandleCtrMap.get(aesRandomAccessFile);
					// System.err.println("open(" + file.getName() + ","
					// + aesRandomAccessFile.writable + ", ctr=" + fhctr
					// + ") -> " + aesRandomAccessFile.toString());
					if (fhctr != null) {
						// file has already been opened - only increase counter
						filehandleCtrMap.put(aesRandomAccessFile, ++fhctr);
					} else {
						aesRandomAccessFile.open();
						filehandleCtrMap.put(aesRandomAccessFile, 1);
					}
				}
			}
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#create(int,
	 * javax.crypto.SecretKey, java.io.File)
	 */
	@Override
	public void create(int shareKeyVersion, SecretKey shareKey)
			throws IOException, PanboxEncryptionException {
		try {
			// don't check if file has been opened - multiple create() calls on
			// the same file should cause an exception
			// TODO: check error handling + mapping to error codes
			aesRandomAccessFile.create(shareKeyVersion, shareKey);
			// System.err.println("create(" + file.getName() + ","
			// + aesRandomAccessFile.writable + ") -> "
			// + aesRandomAccessFile.toString());

			// check file handle counter maps and create/increase values
			synchronized (filehandleCtrMap) {
				Integer fhctr = filehandleCtrMap.get(aesRandomAccessFile);
				if (fhctr == null) {
					filehandleCtrMap.put(aesRandomAccessFile, 1);
				}
			}

		} catch (IOException e) {
			throw new IOException(e.getMessage(), e);
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#flush()
	 */
	@Override
	public void flush() throws IOException {
		aesRandomAccessFile.flush();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#close()
	 */
	@Override
	public void close() throws IOException {
		if (!file.exists()) {
			throw new IOException("File " + file + "does not exist!");
		} else {
			synchronized (filehandleCtrMap) {
				Integer fhctr = filehandleCtrMap.get(aesRandomAccessFile);
				if ((fhctr == null) || (fhctr < 1)) {
					throw new IOException(
							"Invalid value of file handle counter!");
				} else {
					if (fhctr == 1) {
						// last reference, close file and remove entry
						aesRandomAccessFile.close();
						// System.err.println("close(" + file.getName() + ","
						// + aesRandomAccessFile.writable + ") -> "
						// + aesRandomAccessFile.toString());
						filehandleCtrMap.remove(aesRandomAccessFile);
						
						if(deleteFileOnClose) {
							delete();
						}
					} else if (fhctr > 1) {
						// decrease counter value, but don't close file
						filehandleCtrMap.put(aesRandomAccessFile, --fhctr);
					}
				}
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#setDeleteOnClose()
	 */
	@Override
	public void setDeleteOnClose() {
		deleteFileOnClose = true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.panbox.desktop.common.vfs.backend.VirtualFile#getShareKeyVersion()
	 */
	@Override
	public int getShareKeyVersion() throws PanboxEncryptionException,
			IOException {
		try {
			return aesRandomAccessFile.getShareKeyVersion();
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.panbox.desktop.common.vfs.backend.VirtualFile#initWithShareKey(javax
	 * .crypto .SecretKey)
	 */
	@Override
	public void initWithShareKey(SecretKey shareKey)
			throws PanboxEncryptionException, IOException {
		try {
			aesRandomAccessFile.initWithShareKey(shareKey);
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		} catch (FileIntegrityException e) {
			throw new PanboxIntegrityException(e.getMessage(), e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.panbox.desktop.common.vfs.backend.VirtualFile#setLength(long)
	 */
	@Override
	public void setLength(long newLength) throws IOException,
			PanboxEncryptionException {
		try {
			aesRandomAccessFile.setLength(newLength);
		} catch (FileEncryptionException e) {
			throw new PanboxEncryptionException(e.getMessage(), e);
		} catch (FileIntegrityException e) {
			throw new PanboxIntegrityException(e.getMessage(), e);
		}
	}

	@Override
	public boolean isOpened() {
		return aesRandomAccessFile.isOpen();
	}

	@Override
	public boolean isSymbolic() {
		return Files.isSymbolicLink(getFile().toPath());
	}

	@Override
	public void setAttr(long attr) throws IOException {
		super.setAttr(attr);
	}

	@Override
	public boolean canExecute() {
		return file.canExecute();
	}

	@Override
	public boolean canRead() {
		return file.canRead();
	}

	@Override
	public boolean canWrite() {
		return file.canWrite();
	}
}
