/*
 * Copyright (C) 2009-2015 FBReader.ORG Limited <contact@fbreader.org>
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

package org.geometerplus.fbreader.book;

import java.util.*;

import org.geometerplus.zlibrary.core.util.MiscUtil;
import org.geometerplus.zlibrary.core.filesystem.*;

public final class FileInfoSet {
	private static final class Pair {
		private final String myName;
		private final FileInfo myParent;

		Pair(String name, FileInfo parent) {
			myName = name;
			myParent = parent;
		}

		@Override
		public int hashCode() {
			return (myParent == null) ? myName.hashCode() : myParent.hashCode() + myName.hashCode();
		}

		@Override
		public boolean equals(Object o) {
			if (this == o) {
				return true;
			}
			if (!(o instanceof Pair)) {
				return false;
			}
			Pair p = (Pair)o;
			return myName.equals(p.myName) && MiscUtil.equals(myParent, p.myParent);
		}
	}

	private final HashMap<ZLFile,FileInfo> myInfosByFile = new HashMap<ZLFile,FileInfo>();
	private final HashMap<FileInfo,ZLFile> myFilesByInfo = new HashMap<FileInfo,ZLFile>();
	private final HashMap<Pair,FileInfo> myInfosByPair = new HashMap<Pair,FileInfo>();
	private final HashMap<Long,FileInfo> myInfosById = new HashMap<Long,FileInfo>();

	private final LinkedHashSet<FileInfo> myInfosToSave = new LinkedHashSet<FileInfo>();
	private final LinkedHashSet<FileInfo> myInfosToRemove = new LinkedHashSet<FileInfo>();

	private final BooksDatabase myDatabase;

	public FileInfoSet(BooksDatabase database) {
		myDatabase = database;
		load(database.loadFileInfos());
	}

	public FileInfoSet(BooksDatabase database, ZLFile file) {
		myDatabase = database;
		load(database.loadFileInfos(file));
	}

	FileInfoSet(BooksDatabase database, long fileId) {
		myDatabase = database;
		load(database.loadFileInfos(fileId));
	}

	private void load(Collection<FileInfo> infos) {
		for (FileInfo info : infos) {
			myInfosByPair.put(new Pair(info.Name, info.Parent), info);
			myInfosById.put(info.Id, info);
		}
	}

	public void save() {
		myDatabase.executeAsTransaction(new Runnable() {
			public void run() {
				for (FileInfo info : myInfosToRemove) {
					myDatabase.removeFileInfo(info.Id);
					myInfosByPair.remove(new Pair(info.Name, info.Parent));
				}
				myInfosToRemove.clear();
				for (FileInfo info : myInfosToSave) {
					myDatabase.saveFileInfo(info);
				}
				myInfosToSave.clear();
			}
		});
	}

	public boolean check(ZLPhysicalFile file, boolean processChildren) {
		if (file == null) {
			return true;
		}
		final long fileSize = file.size();
		FileInfo info = get(file);
		if (info.FileSize == fileSize) {
			return true;
		} else {
			info.FileSize = fileSize;
			if (processChildren && !"epub".equals(file.getExtension())) {
				removeChildren(info);
				myInfosToSave.add(info);
				addChildren(file);
			} else {
				myInfosToSave.add(info);
			}
			return false;
		}
	}

	public List<ZLFile> archiveEntries(ZLFile file) {
		final FileInfo info = get(file);
		if (!info.hasChildren()) {
			return Collections.emptyList();
		}
		final LinkedList<ZLFile> entries = new LinkedList<ZLFile>();
		for (FileInfo child : info.subtrees()) {
			if (!myInfosToRemove.contains(child)) {
				entries.add(ZLArchiveEntryFile.createArchiveEntryFile(file, child.Name));
			}
		}
		return entries;
	}

	private FileInfo get(String name, FileInfo parent) {
		final Pair pair = new Pair(name, parent);
		FileInfo info = myInfosByPair.get(pair);
		if (info == null) {
			info = new FileInfo(name, parent);
			myInfosByPair.put(pair, info);
			myInfosToSave.add(info);
		}
		return info;
	}


	private FileInfo get(ZLFile file) {
		if (file == null) {
			return null;
		}
		FileInfo info = myInfosByFile.get(file);
		if (info == null) {
			info = get(file.getLongName(), get(file.getParent()));
			myInfosByFile.put(file, info);
		}
		return info;
	}

	public long getId(ZLFile file) {
		final FileInfo info = get(file);
		if (info == null) {
			return -1;
		}
		if (info.Id == -1) {
			save();
		}
		return info.Id;
	}

	private ZLFile getFile(FileInfo info) {
		if (info == null) {
			return null;
		}
		ZLFile file = myFilesByInfo.get(info);
		if (file == null) {
			file = ZLFile.createFile(getFile(info.Parent), info.Name);
			myFilesByInfo.put(info, file);
		}
		return file;
	}

	public ZLFile getFile(long id) {
		return getFile(myInfosById.get(id));
	}

	private void removeChildren(FileInfo info) {
		for (FileInfo child : info.subtrees()) {
			if (myInfosToSave.contains(child)) {
				myInfosToSave.remove(child);
			} else {
				myInfosToRemove.add(child);
			}
			removeChildren(child);
		}
	}

	private void addChildren(ZLFile file) {
		for (ZLFile child : file.children()) {
			final FileInfo info = get(child);
			if (myInfosToRemove.contains(info)) {
				myInfosToRemove.remove(info);
			} else {
				myInfosToSave.add(info);
			}
			addChildren(child);
		}
	}
}
