package net.oni2.aeinstaller.backend.packages;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.TreeSet;
import java.util.Vector;

import net.oni2.aeinstaller.backend.Paths;
import net.oni2.aeinstaller.backend.oni.management.ModInstallationList;
import net.oni2.aeinstaller.backend.oni.management.tools.ToolInstallationList;
import net.oni2.moddepot.DepotManager;
import net.oni2.moddepot.model.NodeMod;
import net.oni2.moddepot.model.TaxonomyTerm;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.XStreamException;
import com.thoughtworks.xstream.io.xml.StaxDriver;

/**
 * @author Christian Illy
 */
public class PackageManager {
	private static PackageManager instance = new PackageManager();

	private HashMap<String, Type> types = new HashMap<String, Type>();
	private HashMap<Integer, Package> mods = new HashMap<Integer, Package>();
	private HashMap<Integer, Package> tools = new HashMap<Integer, Package>();
	private Type localType = null;

	private HashMap<Integer, Package> newToolsOnDepot = new HashMap<Integer, Package>();
	private HashMap<Integer, Package> newModsOnDepot = new HashMap<Integer, Package>();

	/**
	 * @param f
	 *            Mod selection file
	 * @return Mod selection
	 */
	@SuppressWarnings("unchecked")
	public Vector<Integer> loadModSelection(File f) {
		Vector<Integer> res = new Vector<Integer>();
		try {
			if (f.exists()) {
				FileInputStream fis = new FileInputStream(f);
				XStream xs = new XStream(new StaxDriver());
				Object obj = xs.fromXML(fis);
				if (obj instanceof Vector<?>)
					res = (Vector<Integer>) obj;
				fis.close();
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return res;
	}

	/**
	 * @param f
	 *            Mod selection file
	 * @param mods
	 *            Selected mods
	 */
	public void saveModSelection(File f, TreeSet<Package> mods) {
		try {
			Vector<Integer> installed = new Vector<Integer>();
			for (Package m : mods) {
				installed.add(m.getPackageNumber());
			}
			FileOutputStream fos = new FileOutputStream(f);
			XStream xs = new XStream(new StaxDriver());
			xs.toXML(installed, fos);
			fos.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * First initialization of ModManager
	 */
	public void init() {
		HashMap<Integer, Package> oldMods = mods;
		HashMap<Integer, Package> oldTools = tools;

		types = new HashMap<String, Type>();
		mods = new HashMap<Integer, Package>();
		tools = new HashMap<Integer, Package>();

		newModsOnDepot = new HashMap<Integer, Package>();
		newToolsOnDepot = new HashMap<Integer, Package>();

		localType = new Type("-Local-");
		types.put("-Local-", localType);

		for (TaxonomyTerm tt : DepotManager.getInstance().getTypes()) {
			types.put(tt.getName(), new Type(tt.getName()));
		}

		for (NodeMod nm : DepotManager.getInstance().getModPackageNodes()) {
			if (nm.getUploads().size() == 1) {
				Package m = new Package(nm);
				if (nm.isTool()) {
					tools.put(m.getPackageNumber(), m);
					if (!oldTools.containsKey(m.getPackageNumber()))
						newToolsOnDepot.put(m.getPackageNumber(), m);
				} else {
					mods.put(m.getPackageNumber(), m);
					if (!oldMods.containsKey(m.getPackageNumber()))
						newModsOnDepot.put(m.getPackageNumber(), m);
				}
			}
		}

		updateLocalData();
	}

	/**
	 * @return Singleton instance
	 */
	public static PackageManager getInstance() {
		return instance;
	}

	Type getTypeByName(String name) {
		return types.get(name);
	}

	/**
	 * @return Collection of types which do have mods associated
	 */
	public Collection<Type> getTypesWithContent() {
		Vector<Type> res = new Vector<Type>();
		for (Type t : types.values()) {
			if (t.getEntries().size() > 0)
				res.add(t);
		}
		return res;
	}

	/**
	 * @return Collection of mods valid on this platform and not core package
	 */
	public Collection<Package> getModsValidAndNotCore() {
		Vector<Package> res = new Vector<Package>();
		for (Package m : mods.values())
			if (m.isValidOnPlatform() && !m.isCorePackage())
				res.add(m);
		return res;
	}

	/**
	 * @return Mods which are always installed and valid on this platform
	 */
	public TreeSet<Package> getCoreMods() {
		TreeSet<Package> res = new TreeSet<Package>();
		for (Package m : mods.values()) {
			if (m.isValidOnPlatform() && m.isCorePackage())
				res.add(m);
		}
		return res;
	}

	/**
	 * @return Mods which are already locally available
	 */
	public TreeSet<Package> getLocalAvailableMods() {
		TreeSet<Package> res = new TreeSet<Package>();
		for (Package m : mods.values()) {
			if (m.isLocalAvailable())
				res.add(m);
		}
		return res;
	}

	/**
	 * @return Mods which can be updated
	 */
	public TreeSet<Package> getUpdatableMods() {
		TreeSet<Package> res = new TreeSet<Package>();
		for (Package m : getLocalAvailableMods()) {
			if (m.isNewerAvailable())
				res.add(m);
		}
		return res;
	}

	/**
	 * @return Currently installed mods
	 */
	public TreeSet<Package> getInstalledMods() {
		TreeSet<Package> res = new TreeSet<Package>();
		for (int n : ModInstallationList.getInstance().getInstalledMods()) {
			res.add(getPackageByNumber(n));
		}
		return res;
	}

	/**
	 * @return Collection of tools valid on this platform and not core
	 */
	public Collection<Package> getTools() {
		Vector<Package> res = new Vector<Package>();
		for (Package m : tools.values())
			if (m.isValidOnPlatform() && !m.isCorePackage())
				res.add(m);
		return res;
	}

	/**
	 * @return Tools which are always installed and valid on this platform
	 */
	public TreeSet<Package> getCoreTools() {
		TreeSet<Package> res = new TreeSet<Package>();
		for (Package m : tools.values()) {
			if (m.isValidOnPlatform() && m.isCorePackage())
				res.add(m);
		}
		return res;
	}

	/**
	 * @return Tools which are already locally available
	 */
	public TreeSet<Package> getLocalAvailableTools() {
		TreeSet<Package> res = new TreeSet<Package>();
		for (Package m : tools.values()) {
			if (m.isLocalAvailable())
				res.add(m);
		}
		return res;
	}

	/**
	 * @return Tools which can be updated
	 */
	public TreeSet<Package> getUpdatableTools() {
		TreeSet<Package> res = new TreeSet<Package>();
		for (Package m : getLocalAvailableTools()) {
			if (m.isNewerAvailable())
				res.add(m);
		}
		return res;
	}

	/**
	 * @return Currently installed tools
	 */
	public TreeSet<Package> getInstalledTools() {
		TreeSet<Package> res = new TreeSet<Package>();
		for (int n : ToolInstallationList.getInstance().getItems().keySet()) {
			res.add(getPackageByNumber(n));
		}
		return res;
	}

	/**
	 * @return the newToolsOnDepot
	 */
	public HashMap<Integer, Package> getNewToolsOnDepot() {
		return newToolsOnDepot;
	}

	/**
	 * @return the newModsOnDepot
	 */
	public HashMap<Integer, Package> getNewModsOnDepot() {
		return newModsOnDepot;
	}

	/**
	 * Get a mod/tool by its package number
	 * 
	 * @param number
	 *            Package number
	 * @return Mod/tool or null
	 */
	private Package getPackageByNumber(int number) {
		if (mods.containsKey(number))
			return mods.get(number);
		if (tools.containsKey(number))
			return tools.get(number);
		return null;
	}

	/**
	 * Check for unresolved dependencies within the given mods
	 * 
	 * @param mods
	 *            Mods to check
	 * @return Unmet dependencies
	 */
	public HashMap<Package, HashSet<Package>> checkDependencies(
			TreeSet<Package> mods) {
		HashMap<Package, HashSet<Package>> res = new HashMap<Package, HashSet<Package>>();

		for (Package m : mods) {
			for (int depNum : m.getDependencies()) {
				Package other = getPackageByNumber(depNum);
				if (other != null) {
					if (!mods.contains(other)) {
						if (!res.containsKey(m))
							res.put(m, new HashSet<Package>());
						res.get(m).add(other);
					}
				}
			}
		}

		return res;
	}

	/**
	 * Check for incompabitilites between given mods
	 * 
	 * @param mods
	 *            Mods to check
	 * @return Incompatible mods
	 */
	public HashMap<Package, HashSet<Package>> checkIncompabitilites(
			TreeSet<Package> mods) {
		HashMap<Package, HashSet<Package>> res = new HashMap<Package, HashSet<Package>>();

		for (Package m : mods) {
			for (int confNum : m.getIncompabitilities()) {
				Package other = getPackageByNumber(confNum);
				if (other != null) {
					if (mods.contains(other)) {
						if (!res.containsKey(m))
							res.put(m, new HashSet<Package>());
						res.get(m).add(other);
					}
				}
			}
		}

		return res;
	}

	/**
	 * @param m
	 *            Mod to check
	 * @return Is mod installed?
	 */
	boolean isModInstalled(Package m) {
		return ModInstallationList.getInstance().isInstalled(
				m.getPackageNumber());
	}

	/**
	 * Rescan local packages folder for local only packages and updated
	 * Mod_Info.cfg
	 */
	public void updateLocalData() {
		if (Paths.getModsPath().exists()) {
			for (File f : Paths.getModsPath().listFiles(new FileFilter() {
				@Override
				public boolean accept(File pathname) {
					return pathname.isDirectory();
				}
			})) {
				Package m = new Package(f);
				HashMap<Integer, Package> map = null;
				if (m.isTool())
					map = tools;
				else
					map = mods;

				if (!map.containsKey(m.getPackageNumber())) {
					map.put(m.getPackageNumber(), m);
					if (!m.isCorePackage()) {
						localType.addEntry(m);
						m.getTypes().add(localType);
					}
				}
			}
			Iterator<Package> it = localType.getEntries().iterator();
			while (it.hasNext()) {
				Package p = it.next();
				if (!p.isLocalAvailable()){
					it.remove();
					mods.remove(p.getPackageNumber());
					tools.remove(p.getPackageNumber());
				}
			}
		}

		for (Package p : mods.values()) {
			p.updateLocalData();
		}
		for (Package p : tools.values()) {
			p.updateLocalData();
		}
	}

	private static XStream getXStream() {
		XStream xs = new XStream(new StaxDriver());
		xs.alias("Packages", PackageManager.class);
		xs.alias("Type", Type.class);
		xs.alias("Package", Package.class);
		return xs;
	}

	/**
	 * Save Depot cache instance to file
	 * 
	 * @param cacheFile
	 *            File to save to
	 */
	public void saveToCacheFile(java.io.File cacheFile) {
		try {
			FileOutputStream fos = new FileOutputStream(cacheFile);
			XStream xs = getXStream();
			xs.toXML(this, fos);
			fos.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Load cache from file
	 * 
	 * @param cacheFile
	 *            File to load
	 */
	public static void loadFromCacheFile(java.io.File cacheFile) {
		try {
			FileInputStream fis = new FileInputStream(cacheFile);
			XStream xs = getXStream();
			Object obj = xs.fromXML(fis);
			fis.close();
			if (obj instanceof PackageManager) {
				instance = (PackageManager) obj;
				instance.updateLocalData();
			}
		} catch (XStreamException e) {
		} catch (FileNotFoundException e) {
		} catch (IOException e) {
		}
	}
}
