/*
 * This file is part of Nuts Framework.
 * Copyright (C) 2009 Nuts Develop Team.
 *
 * Nuts Framework 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 any later version.
 * 
 * Nuts Framework 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 Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.exts.vfs.ndfs;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.List;

import nuts.core.orm.dao.DataAccessSession;
import nuts.core.orm.dao.DataAccessUtils;
import nuts.core.orm.dao.DataHandler;
import nuts.exts.vfs.ndfs.dao.NdfsData;
import nuts.exts.vfs.ndfs.dao.NdfsDataDAO;
import nuts.exts.vfs.ndfs.dao.NdfsDataExample;
import nuts.exts.vfs.ndfs.dao.NdfsFile;
import nuts.exts.vfs.ndfs.dao.NdfsFileDAO;
import nuts.exts.vfs.ndfs.dao.NdfsFileExample;

import org.apache.commons.vfs.FileName;
import org.apache.commons.vfs.FileObject;
import org.apache.commons.vfs.FileSystemException;
import org.apache.commons.vfs.FileType;
import org.apache.commons.vfs.NameScope;
import org.apache.commons.vfs.RandomAccessContent;
import org.apache.commons.vfs.provider.AbstractFileObject;
import org.apache.commons.vfs.util.FileObjectUtils;
import org.apache.commons.vfs.util.RandomAccessMode;

/**
 */
public class NdfsFileObject extends AbstractFileObject implements FileObject {
	private NdfsFileSystem fs;

	private NdfsFile file;

	/**
	 * @param name
	 * @param fs
	 */
	protected NdfsFileObject(FileName name, NdfsFileSystem fs) {
		super(name, fs);
		this.fs = fs;

		if (name.getDepth() == 0) {
			file = new NdfsFile();
			file.setId(0L);
			file.setParentId(null);
			file.setName("");
			file.setFolder(true);
		}
	}

	/**
	 * @return the ndfsFile
	 */
	protected NdfsFile getFile() {
		return file;
	}

	private static class FileDataHandler implements DataHandler<NdfsData> {

		private byte[] buf;
		private int len = 0;

		public FileDataHandler(byte[] buf) {
			this.buf = buf;
		}

		public boolean handleData(NdfsData data) {
			System.arraycopy(data.getData(), 0, buf, len, data.getData().length);
			len += data.getData().length;
			return true;
		}
	}

	/**
	 * @return the data
	 * @throws IOException
	 */
	private void loadData() throws IOException {
		if (file.getData() != null) {
			return;
		}

		if (file.getId() == null || file.getLength() < 1) {
			file.setData(new byte[file.getLength().intValue()]);
		}
		else {
			DataAccessSession das = fs.getDataAccessClient().openSession();
			try {
				NdfsDataDAO<NdfsData, NdfsDataExample> dataDAO = fs.createDataDAO(das);
				NdfsDataExample dexp = dataDAO.createExample();
				dexp.fileId().equalTo(file.getId()).itemNo().asc();
				
				byte[] buf = new byte[file.getLength().intValue()];
				
				dataDAO.selectByExampleWithDataHandler(dexp,
						new FileDataHandler(buf));
				
				file.setData(buf);
			}
			finally {
				DataAccessUtils.closeQuietly(das);
			}
		}
	}

	/**
	 * Attaches this file object to its file resource.
	 */
	protected void doAttach() throws Exception {
		if (file != null) {
			return;
		}

		// String[] fns = StringUtils.split(path, FileName.SEPARATOR_CHAR);
		//
		// NdfsFileObject fo =
		// (NdfsFileObject)this.resolveFile(FileName.ROOT_PATH);
		// for (int i = 0; i < fns.length - 1; i++) {
		// if (!fo.exists()) {
		// break;
		// }
		// fo = (NdfsFileObject)fo.resolveFile(fns[i], NameScope.CHILD);
		// }

		NdfsFileObject parent = (NdfsFileObject)getParent();
		if (parent.exists()) {
			DataAccessSession das = fs.getDataAccessClient().openSession();
			try {
				NdfsFileDAO<NdfsFile, NdfsFileExample> fileDAO = fs.createFileDAO(das);
				NdfsFileExample exp = fileDAO.createExample();
				exp.parentId().equalTo(parent.getFile().getId()).and().name().equalTo(
						getName().getBaseName());
				file = fileDAO.selectOneByExample(exp);
			}
			finally {
				DataAccessUtils.closeQuietly(das);
			}
		}
		if (file == null) {
			file = new NdfsFile();
			file.setName(getName().getBaseName());
		}
	}

	/**
	 * Detaches this file object from its file resource.
	 * <p/>
	 * <p>
	 * Called when this file is closed. Note that the file object may be reused
	 * later, so should be able to be reattached.
	 * <p/>
	 * This implementation does nothing.
	 */
	protected void doDetach() throws Exception {
		if (getName().getDepth() > 0) {
			file = null;
		}
	}

	/**
	 * Returns the file's type.
	 */
	protected FileType doGetType() throws Exception {
		if (file == null || !file.exists()) {
			return FileType.IMAGINARY;
		}

		return file.getFolder() ? FileType.FOLDER : FileType.FILE;
	}

	/**
	 * Returns the children of the file.
	 */
	protected String[] doListChildren() throws Exception {
		String[] children = null;
		if (file != null && file.exists()) {
			DataAccessSession das = fs.getDataAccessClient().openSession();
			try {
				NdfsFileDAO<NdfsFile, NdfsFileExample> fileDAO = fs.createFileDAO(das);
				NdfsFileExample exp = fileDAO.createExample();
				exp.parentId().equalTo(file.getId());

				List<NdfsFile> files = fileDAO.selectByExample(exp);
				children = new String[files.size()];
				for (int i = 0; i < files.size(); i++) {
					children[i] = files.get(i).getName();
				}
			}
			finally {
				DataAccessUtils.closeQuietly(das);
			}
		}
		return children;
	}

	/**
	 * Returns the children of the file.
	 */
	protected FileObject[] doListChildrenResolved() throws Exception {
		FileObject[] children = null;
		if (file != null && file.exists()) {
			DataAccessSession das = fs.getDataAccessClient().openSession();
			try {
				NdfsFileDAO<NdfsFile, NdfsFileExample> fileDAO = fs.createFileDAO(das);
				NdfsFileExample exp = fileDAO.createExample();
				exp.parentId().equalTo(file.getId());

				List<NdfsFile> files = fileDAO.selectByExample(exp);
				children = new FileObject[files.size()];
				for (int i = 0; i < files.size(); i++) {
					NdfsFile nf = files.get(i);

					FileName fn = fs.getFileSystemManager().resolveName(getName(),
							nf.getName(), NameScope.CHILD);
					NdfsFileObject fo = (NdfsFileObject)fs.resolveFile(fn);
					fo.file = nf;

					children[i] = fo;
				}
			}
			finally {
				DataAccessUtils.closeQuietly(das);
			}
		}
		return children;
	}

	/**
	 * Deletes this file.
	 */
	protected void doDelete() throws Exception {
		if (file != null && file.getId() != null && file.getId() > 0) {
			DataAccessSession das = fs.getDataAccessClient().openSession();
			try {
				NdfsFileDAO<NdfsFile, NdfsFileExample> fileDAO = fs.createFileDAO(das);
				NdfsDataDAO<NdfsData, NdfsDataExample> dataDAO = fs.createDataDAO(das);

				NdfsDataExample dexp = dataDAO.createExample();
				dexp.fileId().equalTo(file.getId());
				dataDAO.deleteByExample(dexp);

				fileDAO.deleteByPrimaryKey(file.getId());
				
				das.commit();
			}
			finally {
				DataAccessUtils.closeQuietly(das);
			}
		}
	}

	/**
	 * Called when this file is deleted. Updates cached info and notifies
	 * subclasses, parent and file system.
	 */
	protected void handleDelete() throws Exception {
		file = new NdfsFile();
		file.setName(getName().getBaseName());
		super.handleDelete();
	}

	/**
	 * rename this file
	 */
	protected void doRename(FileObject newfile) throws Exception {
		if (!exists()) {
			return;
		}

		NdfsFileObject parent = (NdfsFileObject)newfile.getParent();
		if (!parent.exists()) {
			parent.createFolder();
		}

		file.setLastModified(new Date(System.currentTimeMillis()));
		file.setParentId(parent.getFile().getId());
		file.setFolder(false);

		DataAccessSession das = fs.getDataAccessClient().openSession();
		try {
			NdfsFileDAO<NdfsFile, NdfsFileExample> fileDAO = fs.createFileDAO(das);
			if (file.getId() == null) {
				fileDAO.insert(file);
			}
			else {
				fileDAO.updateByPrimaryKey(file);
			}
			das.commit();
		}
		finally {
			DataAccessUtils.closeQuietly(das);
		}
	}

	/**
	 * Creates this file, if it does not exist.
	 */
	public void createFile() throws FileSystemException {
		synchronized (fs) {
			try {
				if (exists() && !FileType.FILE.equals(getType())) {
					throw new FileSystemException(
							"vfs.provider/create-file.error", getName());
				}

				final FileObject parent = getParent();
				if (!parent.exists()) {
					parent.createFolder();
				}

				if (!exists()) {
					saveFile(true, System.currentTimeMillis());
					handleCreate(FileType.FILE);
				}
			}
			catch (RuntimeException re) {
				throw re;
			}
			catch (Exception e) {
				throw new FileSystemException("vfs.provider/create-file.error",
						getName(), e);
			}
		}
	}

	/**
	 * Creates this folder.
	 */
	protected void doCreateFolder() throws Exception {
		saveFile(true, System.currentTimeMillis());
	}

	private void saveFile(boolean folder, long lastModified) throws IOException {
		file.setLastModified(new Date(lastModified));
		file.setParentId(((NdfsFileObject)getParent()).getFile().getId());
		file.setFolder(folder);

		DataAccessSession das = fs.getDataAccessClient().openSession();
		try {
			NdfsFileDAO<NdfsFile, NdfsFileExample> fileDAO = fs.createFileDAO(das);

			if (file.getId() == null) {
				fileDAO.insert(file);
			}
			else {
				fileDAO.updateByPrimaryKey(file);
			}
			das.commit();
		}
		finally {
			DataAccessUtils.closeQuietly(das);
		}
	}

	private void deleteData() throws IOException {
		if (file.getId() == null) {
			return;
		}

		DataAccessSession das = fs.getDataAccessClient().openSession();
		try {
			NdfsDataDAO<NdfsData, NdfsDataExample> dataDAO = fs.createDataDAO(das);
			NdfsDataExample dexp = dataDAO.createExample();
			dexp.fileId().equalTo(file.getId());

			dataDAO.deleteByExample(dexp);
			das.commit();
		}
		finally {
			DataAccessUtils.closeQuietly(das);
		}
	}

	protected void saveData(boolean dirty) throws IOException {
		saveFile(false, System.currentTimeMillis());

		if (dirty) {
			deleteData();

			int len = file.getLength().intValue();
			if (len > 0) {
				DataAccessSession das = fs.getDataAccessClient().openSession();
				try {
					NdfsDataDAO<NdfsData, NdfsDataExample> dataDAO = fs.createDataDAO(das);

					for (int i = 0; i < file.getLength(); i += fs.getBlockSize()) {
						NdfsData nd = new NdfsData();
						nd.setFileId(file.getId());
						nd.setItemNo(i);
						int bs = fs.getBlockSize();
						if (i + bs > len) {
							bs = len - i;
						}
						byte[] buf = new byte[bs];
						System.arraycopy(file.getData(), i, buf, 0, bs);
						nd.setData(buf);
						dataDAO.insert(nd);
					}

					das.commit();
				}
				finally {
					DataAccessUtils.closeQuietly(das);
				}
			}
		}
	}

	/**
	 * Resize the buffer
	 * 
	 * @param newSize
	 * @throws Exception
	 */
	protected void resize(int newSize) throws IOException {
		byte[] buf = file.getData();
		int size = buf.length < newSize ? buf.length : newSize;
		byte[] newBuf = new byte[newSize];
		System.arraycopy(buf, 0, newBuf, 0, size);
		file.setData(newBuf);
		file.setLength((long)newSize);
	}

	protected boolean doIsSameFile(FileObject destFile)
			throws FileSystemException {
		if (!FileObjectUtils.isInstanceOf(destFile, NdfsFileObject.class)) {
			return false;
		}

		return this.getName().equals(destFile.getName());
	}

	/**
	 * Determines if this file can be written to.
	 */
	protected boolean doIsWriteable() throws FileSystemException {
		return file.getWriteable();
	}

	/**
	 * Determines if this file is hidden.
	 */
	protected boolean doIsHidden() {
		return file.getHidden();
	}

	/**
	 * Determines if this file can be read.
	 */
	protected boolean doIsReadable() throws FileSystemException {
		return file.getReadable();
	}

	/**
	 * Gets the last modified time of this file.
	 */
	protected long doGetLastModifiedTime() throws FileSystemException {
		return file.getLastModified() == null ? 0 : file.getLastModified().getTime();
	}

	/**
	 * Sets the last modified time of this file.
	 */
	protected void doSetLastModifiedTime(final long modtime) throws IOException {
		if (exists()) {
			saveFile(getType().hasChildren(), modtime);
		}
		else {
			file.setLastModified(new Date(modtime));
		}
	}

	/**
	 * Creates an input stream to read the content from.
	 */
	protected InputStream doGetInputStream() throws Exception {
		loadData();
		return new ByteArrayInputStream(file.getData());
	}

	/**
	 * Returns the size of the file content (in bytes).
	 */
	protected long doGetContentSize() throws Exception {
		return file.getLength();
	}

	/**
	 * Creates an output stream to write the file content to.
	 */
	protected OutputStream doGetOutputStream(boolean append) throws Exception {
		return new NdfsFileOutputStream(this, append);
	}

	/**
	 * Creates an random access content to write the file content to.
	 */
	protected RandomAccessContent doGetRandomAccessContent(RandomAccessMode mode)
			throws Exception {
		loadData();
		return new NdfsFileRandomAccessContent(this, mode);
	}

}
