/* Copyright (c) 2010, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */

package com.cburch.logisim.file;

import java.awt.Component;
import java.awt.Dimension;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.filechooser.FileFilter;

import com.cburch.logisim.std.Builtin;
import com.cburch.logisim.tools.Library;
import com.cburch.logisim.util.JFileChoosers;
import com.cburch.logisim.util.MacCompatibility;
import com.cburch.logisim.util.StringUtil;
import com.cburch.logisim.util.ZipClassLoader;

public class Loader implements LibraryLoader {
	public static final String LOGISIM_EXTENSION = ".circ";
	public static final FileFilter LOGISIM_FILTER = new LogisimFileFilter();
	public static final FileFilter JAR_FILTER = new JarFileFilter();

	private static class LogisimFileFilter extends FileFilter {
		@Override
		public boolean accept(File f) {
			return f.isDirectory()
				|| f.getName().endsWith(LOGISIM_EXTENSION);
		}

		@Override
		public String getDescription() {
			return Strings.get("logisimFileFilter");
		}
	}

	private static class JarFileFilter extends FileFilter {
		@Override
		public boolean accept(File f) {
			return f.isDirectory()
				|| f.getName().endsWith(".jar");
		}

		@Override
		public String getDescription() {
			return Strings.get("jarFileFilter");
		}
	}
	
	// fixed
	private Component parent;
	private Builtin builtin = new Builtin();

	// to be cleared with each new file
	private File mainFile = null;
	private Stack<File> filesOpening = new Stack<File>();
	private Map<File,File> substitutions = new HashMap<File,File>();

	public Loader(Component parent) {
		this.parent = parent;
		clear();
	}
	
	public Builtin getBuiltin() {
		return builtin;
	}
	
	public void setParent(Component value) {
		parent = value;
	}
	
	private File getSubstitution(File source) {
		File ret = substitutions.get(source);
		return ret == null ? source : ret;
	}

	//
	// file chooser related methods
	//
	public File getMainFile() {
		return mainFile;
	}

	public JFileChooser createChooser() {
		return JFileChoosers.createAt(getCurrentDirectory());
	}

	// used here and in LibraryManager only
	File getCurrentDirectory() {
		File ref;
		if (!filesOpening.empty()) {
			ref = filesOpening.peek();
		} else {
			ref = mainFile;
		}
		return ref == null ? null : ref.getParentFile();
	}

	private void setMainFile(File value) {
		mainFile = value;
	}

	//
	// more substantive methods accessed from outside this package
	//
	public void clear() {
		filesOpening.clear();
		mainFile = null;
	}
	
	public LogisimFile openLogisimFile(File file, Map<File,File> substitutions)
			throws LoadFailedException {
		this.substitutions = substitutions;
		try {
			return openLogisimFile(file);
		} finally {
			this.substitutions = Collections.emptyMap();
		}
	}

	public LogisimFile openLogisimFile(File file) throws LoadFailedException {
		try {
			LogisimFile ret = loadLogisimFile(file);
			if (ret != null) setMainFile(file);
			showMessages(ret);
			return ret;
		} catch (LoaderException e) {
			throw new LoadFailedException(e.getMessage(), e.isShown());
		}
	}
	
	public LogisimFile openLogisimFile(InputStream reader)
			throws LoadFailedException, IOException {
		LogisimFile ret = null;
		try {
			ret = LogisimFile.load(reader, this);
		} catch (LoaderException e) {
			return null;
		}
		showMessages(ret);
		return ret;
	}

	public Library loadLogisimLibrary(File file) {
		File actual = getSubstitution(file);
		LoadedLibrary ret = LibraryManager.instance.loadLogisimLibrary(this, actual);
		if (ret != null) {
			LogisimFile retBase = (LogisimFile) ret.getBase();
			showMessages(retBase);
		}
		return ret;
	}
	
	public Library loadJarLibrary(File file, String className) {
		File actual = getSubstitution(file);
		return LibraryManager.instance.loadJarLibrary(this, actual, className);
	}
	
	public void reload(LoadedLibrary lib) {
		LibraryManager.instance.reload(this, lib);
	}
	
	public boolean save(LogisimFile file, File dest) {
		Library reference = LibraryManager.instance.findReference(file, dest);
		if (reference != null) {
			JOptionPane.showMessageDialog(parent,
					StringUtil.format(Strings.get("fileCircularError"), reference.getDisplayName()),
					Strings.get("fileSaveErrorTitle"),
					JOptionPane.ERROR_MESSAGE);
			return false;
		}
		
		File backup = determineBackupName(dest);
		boolean backupCreated = backup != null && dest.renameTo(backup);
		
		FileOutputStream fwrite = null;
		try {
			try {
				MacCompatibility.setFileCreatorAndType(dest, "LGSM", "circ");
			} catch (IOException e) { }
			fwrite = new FileOutputStream(dest);
			file.write(fwrite, this);
			file.setName(toProjectName(dest));

			File oldFile = getMainFile();
			setMainFile(dest);
			LibraryManager.instance.fileSaved(this, dest, oldFile, file);
		} catch (IOException e) {
			if (backupCreated) recoverBackup(backup, dest);
			if (dest.exists() && dest.length() == 0) dest.delete();
			JOptionPane.showMessageDialog(parent,
				StringUtil.format(Strings.get("fileSaveError"),
					e.toString()),
				Strings.get("fileSaveErrorTitle"),
				JOptionPane.ERROR_MESSAGE);
			return false;
		} finally {
			if (fwrite != null) {
				try {
					fwrite.close();
				} catch (IOException e) {
					if (backupCreated) recoverBackup(backup, dest);
					if (dest.exists() && dest.length() == 0) dest.delete();
					JOptionPane.showMessageDialog(parent,
						StringUtil.format(Strings.get("fileSaveCloseError"),
							e.toString()),
						Strings.get("fileSaveErrorTitle"),
						JOptionPane.ERROR_MESSAGE);
					return false;
				}
			}
		}
		
		if (!dest.exists() || dest.length() == 0) {
			if (backupCreated && backup != null && backup.exists()) {
				recoverBackup(backup, dest);
			} else {
				dest.delete();
			}
			JOptionPane.showMessageDialog(parent,
					Strings.get("fileSaveZeroError"),
					Strings.get("fileSaveErrorTitle"),
					JOptionPane.ERROR_MESSAGE);
			return false;
		}
		
		if (backupCreated && backup.exists()) {
			backup.delete();
		}
		return true;
	}
	
	private static File determineBackupName(File base) {
		File dir = base.getParentFile();
		String name = base.getName();
		if (name.endsWith(LOGISIM_EXTENSION)) {
			name = name.substring(0, name.length() - LOGISIM_EXTENSION.length());
		}
		for (int i = 1; i <= 20; i++) {
			String ext = i == 1 ? ".bak" : (".bak" + i);
			File candidate = new File(dir, name + ext);
			if (!candidate.exists()) return candidate;
		}
		return null;
	}
	
	private static void recoverBackup(File backup, File dest) {
		if (backup != null && backup.exists()) {
			if (dest.exists()) dest.delete();
			backup.renameTo(dest);
		}
	}

	//
	// methods for LibraryManager
	//  
	LogisimFile loadLogisimFile(File request) throws LoadFailedException {
		File actual = getSubstitution(request);
		for (File fileOpening : filesOpening) {
			if (fileOpening.equals(actual)) {
				throw new LoadFailedException(StringUtil.format(Strings.get("logisimCircularError"),
						toProjectName(actual)));
			}
		}

		LogisimFile ret = null;
		filesOpening.push(actual);
		try {
			ret = LogisimFile.load(actual, this);
		} catch (IOException e) {
			throw new LoadFailedException(StringUtil.format(Strings.get("logisimLoadError"),
					toProjectName(actual), e.toString()));
		} finally {
			filesOpening.pop();
		}
		ret.setName(toProjectName(actual));
		return ret;
	}

	Library loadJarFile(File request, String className) throws LoadFailedException {
		File actual = getSubstitution(request);
		// Up until 2.1.8, this was written to use a URLClassLoader, which
		// worked pretty well, except that the class never releases its file
		// handles. For this reason, with 2.2.0, it's been switched to use
		// a custom-written class ZipClassLoader instead. The ZipClassLoader
		// is based on something downloaded off a forum, and I'm not as sure
		// that it works as well. It certainly does more file accesses.
		
		// Anyway, here's the line for this new version:
		ZipClassLoader loader = new ZipClassLoader(actual);
		
		// And here's the code that was present up until 2.1.8, and which I
		// know to work well except for the closing-files bit. If necessary, we
		// can revert by deleting the above declaration and reinstating the below.
		/*
		URL url;
		try {
			url = new URL("file", "localhost", file.getCanonicalPath());
		} catch (MalformedURLException e1) {
			throw new LoadFailedException("Internal error: Malformed URL");
		} catch (IOException e1) {
			throw new LoadFailedException(Strings.get("jarNotOpenedError"));
		}
		URLClassLoader loader = new URLClassLoader(new URL[] { url });
		*/
		
		// load library class from loader
		Class<?> retClass;
		try {
			retClass = loader.loadClass(className);
		} catch (ClassNotFoundException e) {
			throw new LoadFailedException(StringUtil.format(Strings.get("jarClassNotFoundError"), className));
		}
		if (!(Library.class.isAssignableFrom(retClass))) {
			throw new LoadFailedException(StringUtil.format(Strings.get("jarClassNotLibraryError"), className));
		}
		
		// instantiate library
		Library ret;
		try {
			ret = (Library) retClass.newInstance();
		} catch (Exception e) {
			throw new LoadFailedException(StringUtil.format(Strings.get("jarLibraryNotCreatedError"), className));
		}
		return ret;
	}

	//
	// Library methods
	//
	public Library loadLibrary(String desc) {
		return LibraryManager.instance.loadLibrary(this, desc);
	}

	public String getDescriptor(Library lib) {
		return LibraryManager.instance.getDescriptor(this, lib);
	}

	public void showError(String description) {
		if (!filesOpening.empty()) {
			File top = filesOpening.peek();
			String init = toProjectName(top) + ":";
			if (description.contains("\n")) {
				description = init + "\n" + description;
			} else {
				description = init + " " + description;
			}
		}
		
		if (description.contains("\n") || description.length() > 60) {
			int lines = 1;
			for (int pos = description.indexOf('\n'); pos >= 0;
					pos = description.indexOf('\n', pos + 1)) {
				lines++;
			}
			lines = Math.max(4, Math.min(lines, 7));

			JTextArea textArea = new JTextArea(lines, 60);
			textArea.setEditable(false);
			textArea.setText(description);
			textArea.setCaretPosition(0);
			
			JScrollPane scrollPane = new JScrollPane(textArea);		
			scrollPane.setPreferredSize(new Dimension(350, 150));
			JOptionPane.showMessageDialog(parent, scrollPane,
					Strings.get("fileErrorTitle"), JOptionPane.ERROR_MESSAGE);
		} else {
			JOptionPane.showMessageDialog(parent, description,
					Strings.get("fileErrorTitle"), JOptionPane.ERROR_MESSAGE);
		}
	}

	private void showMessages(LogisimFile source) {
		if (source == null) return;
		String message = source.getMessage();
		while (message != null) {
			JOptionPane.showMessageDialog(parent,
				message, Strings.get("fileMessageTitle"),
				JOptionPane.INFORMATION_MESSAGE);
			message = source.getMessage();
		}
	}

	//
	// helper methods
	//
	File getFileFor(String name, FileFilter filter) {
		// Determine the actual file name.
		File file = new File(name);
		if (!file.isAbsolute()) {
			File currentDirectory = getCurrentDirectory();
			if (currentDirectory != null) file = new File(currentDirectory, name);
		}
		while (!file.canRead()) {
			// It doesn't exist. Figure it out from the user.
			JOptionPane.showMessageDialog(parent,
				StringUtil.format(Strings.get("fileLibraryMissingError"),
					file.getName()));
			JFileChooser chooser = createChooser();
			chooser.setFileFilter(filter);
			chooser.setDialogTitle(StringUtil.format(Strings.get("fileLibraryMissingTitle"), file.getName()));
			int action = chooser.showDialog(parent, Strings.get("fileLibraryMissingButton"));
			if (action != JFileChooser.APPROVE_OPTION) {
				throw new LoaderException(Strings.get("fileLoadCanceledError"));
			}
			file = chooser.getSelectedFile();
		}
		return file;
	}

	private String toProjectName(File file) {
		String ret = file.getName();
		if (ret.endsWith(LOGISIM_EXTENSION)) {
			return ret.substring(0, ret.length() - LOGISIM_EXTENSION.length());
		} else {
			return ret;
		}
	}

}
