/*
 *  Copyright (C) 2006-2020  Ronald Blankendaal
 *
 *  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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */
package org.dbgl.model.entity;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.commons.lang3.StringUtils;
import org.dbgl.exception.InvalidMountstringException;
import org.dbgl.model.GenericStats;
import org.dbgl.model.NativeCommand;
import org.dbgl.model.aggregate.DosboxVersion;
import org.dbgl.model.conf.Configuration;
import org.dbgl.model.conf.GenerationAwareConfiguration.Generation;
import org.dbgl.model.conf.mount.Mount;
import org.dbgl.model.factory.MountFactory;
import org.dbgl.service.ITextService;
import org.dbgl.service.TextService;


public abstract class TemplateProfileBase extends Configurable {

	private DosboxVersion dosboxVersion_;
	private List<NativeCommand> nativeCommands_;
	private GenericStats stats_;

	public TemplateProfileBase() {
		super();
	}

	public abstract void setConfigurationFileLocationByIdentifiers();

	public abstract void setBooter(boolean booter);

	public DosboxVersion getDosboxVersion() {
		return dosboxVersion_;
	}

	public void setDosboxVersion(DosboxVersion dosboxVersion) {
		dosboxVersion_ = dosboxVersion;
	}

	public List<NativeCommand> getNativeCommands() {
		return nativeCommands_;
	}

	public void setNativeCommands(List<NativeCommand> nativeCommands) {
		nativeCommands_ = nativeCommands;
	}

	public void resetNativeCommands() {
		List<NativeCommand> nativeCommands = new ArrayList<>();
		NativeCommand.insertDosboxCommand(nativeCommands);
		setNativeCommands(nativeCommands);
	}

	public GenericStats getStats() {
		return stats_;
	}

	public void setStats(GenericStats stats) {
		stats_ = stats;
	}

	public String getConfigurationString() {
		return configuration_.toString(getNettoMountingPoints());
	}

	public Configuration getCombinedConfiguration() {
		ITextService text = TextService.getInstance();
		Configuration conf = new Configuration();
		try {
			StringBuffer warnings = new StringBuffer();
			warnings.append(conf.loadDataWithAutoexec(text, dosboxVersion_.getConfigurationString(), dosboxVersion_.getConfigurationFile(), null));
			warnings.append(conf.loadDataWithAutoexec(text, getConfigurationString(), configuration_.getFileLocation() != null ? getConfigurationFile(): null, null));
			conf.getAutoexec().setBooterByDefault(configuration_.getAutoexec().getBooterByDefault());
			conf.getAutoexec().setGameMain(configuration_.getAutoexec().getGameMain());
			if (StringUtils.isNotEmpty(warnings)) {
				System.err.println(warnings);
			}
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
		return conf;
	}

	public String resetAndLoadConfiguration() throws IOException {
		return resetAndLoadConfiguration(null);
	}

	public String resetAndLoadConfiguration(File cwd) throws IOException {
		StringBuffer warningsLog = new StringBuffer();
		warningsLog.append(dosboxVersion_.resetAndLoadConfiguration());
		if (configuration_.getFileLocation() != null) {
			warningsLog.append(configuration_.reloadWithAutoexec(TextService.getInstance(), getDosboxNettoMountingPoints(), cwd));
			configuration_.removeDuplicateValuesIn(dosboxVersion_.getConfiguration());
		}
		return warningsLog.toString();
	}

	public String loadConfiguration(File cwd) throws IOException {
		StringBuffer warningsLog = new StringBuffer();
		if (configuration_.getFileLocation() != null) {
			warningsLog.append(configuration_.loadWithAutoexec(TextService.getInstance(), getDosboxNettoMountingPoints(), cwd));
			configuration_.removeDuplicateValuesIn(dosboxVersion_.getConfiguration());
		}
		return warningsLog.toString();
	}

	public String loadConfigurationData(ITextService text, String data, File file) throws IOException {
		return loadConfigurationData(text, data, file, null);
	}

	public String loadConfigurationData(ITextService text, String data, File file, File cwd) throws IOException {
		return configuration_.loadDataWithAutoexec(text, data, file, getDosboxNettoMountingPoints(), cwd);
	}

	public void saveConfiguration() throws IOException {
		configuration_.save(false, getNettoMountingPoints());
	}

	public void saveConfiguration(boolean prepareOnly) throws IOException {
		configuration_.save(prepareOnly, getNettoMountingPoints());
	}

	public void setValue(String sectionTitle, String sectionItem, String value) {
		if (configuration_.hasValue(sectionTitle, sectionItem))
			configuration_.removeValue(sectionTitle, sectionItem);

		if (dosboxVersion_.getConfiguration().hasValue(sectionTitle, sectionItem)) {
			String dbValue = dosboxVersion_.getConfiguration().getValue(sectionTitle, sectionItem);
			if (!value.equals(dbValue))
				configuration_.setValue(sectionTitle, sectionItem, value);
		}
	}

	/**
	 * set to another DOSBox version but try to maintain as many settings as possible
	 */
	public String setToDosboxVersion(DosboxVersion dstDosboxVersion) throws IOException {
		if (dosboxVersion_.getId() == dstDosboxVersion.getId())
			return StringUtils.EMPTY;

		configuration_.setSections(getCombinedConfiguration().getSections());
		return alterToDosboxVersionGeneration(dosboxVersion_.getGeneration(), dstDosboxVersion);
	}

	/**
	 * set to another DOSBox version while maintaining only the changes from the defaults
	 */
	public String switchToDosboxVersion(DosboxVersion dstDosboxVersion) throws IOException {
		if (dosboxVersion_.getId() == dstDosboxVersion.getId())
			return StringUtils.EMPTY;

		return alterToDosboxVersionGeneration(dosboxVersion_.getGeneration(), dstDosboxVersion);
	}

	/**
	 * reloads all DOSBox settings, but keeps exit and unique mounts and booter/executable data
	 */
	public String reloadDosboxVersion(DosboxVersion newDosboxVersion) throws IOException {
		String warningsLog = newDosboxVersion.resetAndLoadConfiguration();
		dosboxVersion_ = newDosboxVersion;
		configuration_.clearSections();
		configuration_.removeUnnecessaryMounts(dosboxVersion_.getConfiguration());
		return warningsLog;
	}

	public String alterToDosboxVersionGeneration(Generation src, DosboxVersion dstDosboxVersion) throws IOException {
		String warningsLog = dstDosboxVersion.resetAndLoadConfiguration();
		int srcGeneration = src.ordinal();
		int dstGeneration = dstDosboxVersion.getGeneration().ordinal();
		for (int i = 0; i < (srcGeneration - dstGeneration); i++)
			configuration_.downgradeOneGeneration(Generation.values()[srcGeneration - i]);
		for (int i = 0; i < (dstGeneration - srcGeneration); i++)
			configuration_.upgradeOneGeneration(Generation.values()[srcGeneration + i]);
		configuration_.removeValuesNotSetIn(dstDosboxVersion.getConfiguration());
		configuration_.removeDuplicateValuesIn(dstDosboxVersion.getConfiguration());
		dosboxVersion_ = dstDosboxVersion;
		return warningsLog;
	}

	private List<Mount> getDosboxNettoMountingPoints() {
		List<Mount> result = new ArrayList<>();
		if (dosboxVersion_ != null) {
			for (Mount mount: dosboxVersion_.getConfiguration().getAutoexec().getMountingpoints()) {
				if (result.stream().anyMatch(x -> x.getDrive() == mount.getDrive())) {
					if (mount.isUnmounted())
						result.removeIf(x -> x.getDrive() == mount.getDrive());
				} else {
					if (!mount.isUnmounted())
						result.add(mount);
				}
			}
		}
		return result;
	}

	public List<Mount> getNettoMountingPoints() {
		List<Mount> result = new ArrayList<>(getDosboxNettoMountingPoints());
		for (Mount mount: configuration_.getAutoexec().getMountingpoints()) {
			if (result.stream().anyMatch(x -> x.getDrive() == mount.getDrive())) {
				if (mount.isUnmounted())
					result.removeIf(x -> x.getDrive() == mount.getDrive());
			} else {
				if (!mount.isUnmounted())
					result.add(mount);
			}
		}
		return result;
	}

	public List<Mount> getMountingPointsForUI() {
		List<Mount> result = new ArrayList<>();
		try {
			for (Mount mount: getDosboxNettoMountingPoints())
				result.add(MountFactory.createCopy(mount));
			for (Mount mount: configuration_.getAutoexec().getMountingpoints()) {
				Optional<Mount> m = result.stream().filter(x -> x.getDrive() == mount.getDrive()).findFirst();
				if (m.isPresent()) {
					Mount existing = m.get();
					if (!existing.isUnmounted() && mount.isUnmounted())
						existing.setUnmounted(true);
					else if (existing.isUnmounted() && !mount.isUnmounted())
						result.add(MountFactory.createCopy(mount));
				} else {
					result.add(MountFactory.createCopy(mount));
				}
			}
		} catch (InvalidMountstringException e) {
			e.printStackTrace();
		}
		return result;
	}

	public Set<Character> getNettoMountedDrives() {
		return getNettoMountingPoints().stream().map(x -> x.getDrive()).collect(Collectors.toSet());
	}

	public void unmountDosboxMounts() {
		for (char drive: getNettoMountedDrives())
			addMount(MountFactory.createUnmount(drive).toString());
	}

	/**
	 * Set the mountingpoints so that it reaches the same nettoMounts with the minimum required mounts. In other words, unnecessary mounts are removed. The order of mounts is likely to be changed,
	 * unless dosboxAutoexec has no mounts, then the original order will be kept.
	 */
	public void removeUnnecessaryMounts() {
		List<Mount> dbList = getDosboxNettoMountingPoints();
		List<Mount> fullList = getNettoMountingPoints();
		List<Mount> result = new ArrayList<>();

		// determine mounts from dosbox that have been removed - unmounts, or have remained the same, or have changed
		for (Mount dbMount: dbList) {
			Optional<Mount> finalMount = fullList.stream().filter(x -> x.getDrive() == dbMount.getDrive()).findFirst();
			if (!finalMount.isPresent()) {
				result.add(MountFactory.createUnmount(dbMount));
			} else {
				if (!finalMount.get().toString().equals(dbMount.toString())) {
					result.add(MountFactory.createUnmount(dbMount));
					try {
						result.add(MountFactory.createCopy(finalMount.get()));
					} catch (InvalidMountstringException e) {
						e.printStackTrace();
					}
				}
			}
		}
		// determine added mounts
		for (Mount mnt: fullList) {
			if (dbList.stream().noneMatch(x -> x.getDrive() == mnt.getDrive()))
				result.add(mnt);
		}

		getConfiguration().getAutoexec().setMountingpoints(result);
	}

	public void editMountBasedOnIndexUI(int index, String mount) {
		List<Mount> uiMounts = getMountingPointsForUI();
		int dosboxMounts = dosboxVersion_.getConfiguration().getAutoexec().getMountingpoints().size();
		int unmountedDosboxMountsBeforeIndex = (int)IntStream.range(0, Math.min(index, dosboxMounts)).filter(x -> uiMounts.get(x).isUnmounted()).count();

		if (index < dosboxMounts) {
			Mount mnt = uiMounts.get(index);
			if (!mnt.isUnmounted()) {
				removeMountBasedOnIndexUI(index);
				configuration_.getAutoexec().addMount(mount);
			}
		} else {
			try {
				configuration_.getAutoexec().getMountingpoints().set(index - (dosboxMounts - unmountedDosboxMountsBeforeIndex), MountFactory.create(mount));
			} catch (InvalidMountstringException e) {
				// nothing we can do
			}
		}
	}

	public void removeMountBasedOnIndexUI(int index) {
		List<Mount> uiMounts = getMountingPointsForUI();
		int dosboxMounts = dosboxVersion_.getConfiguration().getAutoexec().getMountingpoints().size();
		int unmountedDosboxMountsBeforeIndex = (int)IntStream.range(0, Math.min(index, dosboxMounts)).filter(x -> uiMounts.get(x).isUnmounted()).count();

		if (index < dosboxMounts) {
			Mount mnt = uiMounts.get(index);
			if (mnt.isUnmounted()) {
				if (!getNettoMountedDrives().contains(mnt.getDrive()))
					configuration_.getAutoexec().getMountingpoints().remove(unmountedDosboxMountsBeforeIndex);
			} else {
				configuration_.getAutoexec().getMountingpoints().add(unmountedDosboxMountsBeforeIndex, MountFactory.createUnmount(mnt));
			}
		} else {
			configuration_.getAutoexec().getMountingpoints().remove(index - (dosboxMounts - unmountedDosboxMountsBeforeIndex));
		}
	}

	public void removeFloppyMounts() {
		for (Iterator<Mount> it = configuration_.getAutoexec().getMountingpoints().iterator(); it.hasNext();)
			if (it.next().getMountAs().equals("floppy"))
				it.remove();
	}

	public String[] getMountStringsForUI() {
		return getMountingPointsForUI().stream().map(x -> x.toString(true)).toArray(String[]::new);
	}
}
