package ini.trakem2.persistence;

import ij.IJ;
import ij.gui.GenericDialog;
import ij.io.OpenDialog;
import ini.trakem2.ControlWindow;
import ini.trakem2.Project;
import ini.trakem2.display.Displayable;
import ini.trakem2.display.Patch;
import ini.trakem2.display.YesNoDialog;
import ini.trakem2.utils.Dispatcher;
import ini.trakem2.utils.IJError;
import ini.trakem2.utils.Utils;

import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.util.Collection;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;
import java.util.Vector;

import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;

/** A class to manage "file not found" problems. */
public class FilePathRepair {

	private final Project project;
	private final PathTableModel data = new PathTableModel();
	private final JTable table = new JTable(data);
	private final JFrame frame;

	private FilePathRepair(final Project project) {
		this.project = project;
		this.frame = ControlWindow.createJFrame("Repair: " + project);
	}

	private final Runnable makeGUI() {
		return new Runnable() { public void run() {
			JScrollPane jsp = new JScrollPane(table);
			jsp.setPreferredSize(new Dimension(500, 500));
			table.addMouseListener(listener);
			JLabel label = new JLabel("Double-click any to repair file path:");
			JLabel label2 = new JLabel("(Any listed with identical parent folder will be fixed as well.)");
			JPanel plabel = new JPanel();
			BoxLayout pbl = new BoxLayout(plabel, BoxLayout.Y_AXIS);
			plabel.setLayout(pbl);
			//plabel.setBorder(new LineBorder(Color.black, 1, true));
			plabel.setMinimumSize(new Dimension(400, 40));
			plabel.add(label);
			plabel.add(label2);
			JPanel all = new JPanel();
			BoxLayout bl = new BoxLayout(all, BoxLayout.Y_AXIS);
			all.setLayout(bl);
			all.add(plabel);
			all.add(jsp);
			frame.getContentPane().add(all);
			frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
			frame.addWindowListener(new WindowAdapter() {
				public void windowClosing(WindowEvent we) {
					synchronized (projects) {
						if (data.vpath.size() > 0 ) {
							Utils.logAll("WARNING: Some images remain associated to inexistent file paths.");
						}
						projects.remove(project);
					}
				}
			});
			frame.pack();
			ij.gui.GUI.center(frame);
			frame.setVisible(true);
		}};
	}

	private static class PathTableModel extends AbstractTableModel {
		final Vector<Patch> vp = new Vector<Patch>();
		final Vector<String> vpath = new Vector<String>();
		final HashSet<Patch> set = new HashSet<Patch>();
		PathTableModel() {}
		public final String getColumnName(final int col) {
			switch (col) {
				case 0: return "Image";
				case 1: return "Nonexistent file path";
				default: return "";
			}
		}
		public final int getRowCount() { return vp.size(); }
		public final int getColumnCount() { return 2; }
		public final Object getValueAt(final int row, final int col) {
			switch (col) {
				case 0: return vp.get(row);
				case 1: return vpath.get(row);
				default: return null;
			}
		}
		public final boolean isCellEditable(final int row, final int col) {
			return false;
		}
		public final void setValueAt(final Object value, final int row, final int col) {} // ignore

		synchronized public final void add(final Patch patch) {
			if (set.contains(patch)) return; // already here
			vp.add(patch);
			vpath.add(patch.getImageFilePath()); // no slice information if it's a stack
			set.add(patch);
		}

		synchronized public final String remove(final int row) {
			set.remove(vp.remove(row));
			return vpath.remove(row);
		}

		synchronized public final String remove(final Patch p) {
			final int i = vp.indexOf(p);
			if (-1 == i) return null;
			set.remove(vp.remove(i));
			return vpath.remove(i);
		}
	}

	// Static part

	static private final Hashtable<Project,FilePathRepair> projects = new Hashtable<Project,FilePathRepair>();

	static public void add(final Patch patch) {
		dispatcher.exec(new Runnable() { public void run() {
			final Project project = patch.getProject();
			FilePathRepair fpr = null;
			synchronized (projects) {
				fpr = projects.get(project);
				if (null == fpr) {
					fpr = new FilePathRepair(project);
					projects.put(project, fpr);
					SwingUtilities.invokeLater(fpr.makeGUI());
				}
				fpr.data.add(patch);
				if (!fpr.frame.isVisible()) {
					fpr.frame.setVisible(true);
				} else {
					SwingUtilities.invokeLater(new Repainter(fpr));
				}
			}
		}});
	}

	static private class Repainter implements Runnable {
		FilePathRepair fpr;
		Repainter(final FilePathRepair fpr) { this.fpr = fpr; }
		public void run() {
			try {
				fpr.table.updateUI();
				fpr.table.repaint();
				fpr.frame.pack();
			} catch (Exception e) { IJError.print(e); }
		}
	}

	static private final Dispatcher dispatcher = new Dispatcher("File path fixer");

	static private MouseAdapter listener = new MouseAdapter() {
		public void mousePressed(final MouseEvent me) {
			final JTable table = (JTable) me.getSource();
			final PathTableModel data = (PathTableModel) table.getModel();
			final int row = table.rowAtPoint(me.getPoint());
			if (-1 == row) return;

			if (2 == me.getClickCount()) {
				dispatcher.exec(new Runnable() { public void run() {
					try {
						table.setEnabled(false);
						GenericDialog gd = new GenericDialog("Fix paths");
						gd.addCheckbox("Fix other listed image files with identical parent directory", true);
						gd.addCheckbox("Fix all image files in the project with identical parent directory", true);
						gd.addCheckbox("Update mipmaps for each fixed path", false);
						gd.showDialog();
						if (!gd.wasCanceled()) {
							fixPath(table, data, row, gd.getNextBoolean(), gd.getNextBoolean(), gd.getNextBoolean());
						}
					} catch (Exception e) {
						IJError.print(e);
					} finally {
						table.setEnabled(true);
					}
				}});
			}
		}
	};

	static private void fixPath(final JTable table, final PathTableModel data, final int row, final boolean fix_similar, final boolean fix_all, final boolean update_mipmaps) throws Exception {
		synchronized (projects) {
			final Patch patch = data.vp.get(row);
			if (null == patch) return;
			final String old_path = patch.getImageFilePath();
			final File f = new File(old_path);
			if (f.exists()) {
				Utils.log("File exists for " + patch + " at " + f.getAbsolutePath() + "\n  --> not updating path.");
				data.remove(row);
				return;
			}
			// Else, pop up file dialog
			OpenDialog od = new OpenDialog("Select image file", OpenDialog.getDefaultDirectory(), null);
			String dir = od.getDirectory();
			final String filename = od.getFileName();
			if (null == dir) return; // dialog was canceled
			if (IJ.isWindows()) dir = dir.replace('\\', '/');
			if (!dir.endsWith("/")) dir += "/";
			// Compare filenames
			if ( ! filename.equals(f.getName())) {
				YesNoDialog yn = new YesNoDialog(projects.get(patch.getProject()).frame, "WARNING", "Different file names!\n  old: " + f.getName() + "\n  new: " + filename + "\nSet to new file name?");
				if ( ! yn.yesPressed()) return;

				// Remove mipmaps: would not be found with the new name and the old ones would remain behind unused
				if ( ! f.getName().equals(new File(old_path).getName())) {
					// remove mipmaps: the name wouldn't match otherwise
					patch.getProject().getLoader().removeMipMaps(patch);
				}
			}
			//
			String wrong_parent_path = new File(data.vpath.get(row)).getParent();
			wrong_parent_path = wrong_parent_path.replace('\\', '/');
			if (!wrong_parent_path.endsWith("/")) wrong_parent_path = new StringBuilder(wrong_parent_path).append('/').toString(); // not File.separatorChar, TrakEM2 uses '/' as folder separator

			final String path = new StringBuilder(dir).append(filename).toString();

			// keep track of fixed slices to avoid calling n_slices * n_slices times!
			final HashSet<Patch> fixed = new HashSet<Patch>();

			int n_fixed = 0;

			if (-1 == patch.getFilePath().lastIndexOf("-----#slice=")) {
				if (!fixPatchPath(patch, path, update_mipmaps)) {
					return;
				}
				data.remove(patch);
				fixed.add(patch);
				n_fixed += 1;
			} else {
				int n = fixStack(data, fixed, patch.getStackPatches(), old_path, path, update_mipmaps);
				if (0 == n) {
					return; // some error ocurred, no paths fixed
				}
				n_fixed += n;
				// data already cleared of removed patches by fixStack
			}

			String good_parent_path = dir;
			if (!dir.endsWith("/")) good_parent_path = new StringBuilder(good_parent_path).append('/').toString(); // not File.separatorChar, TrakEM2 uses '/' as folder separator

			// Check for similar parent paths and see if they can be fixed
			if (fix_similar) {
				for (int i=data.vp.size() -1; i>-1; i--) {
					final String wrong_path = data.vpath.get(i);
					final Patch p = data.vp.get(i);
					if (wrong_path.startsWith(wrong_parent_path)) {
						// try to fix as well
						final File file = new File(new StringBuilder(good_parent_path).append(wrong_path.substring(wrong_parent_path.length())).toString());
						if (file.exists()) {
							if (-1 == p.getFilePath().lastIndexOf("-----#slice=")) {
								if (!fixed.contains(p) && fixPatchPath(p, file.getAbsolutePath(), update_mipmaps)) {
									data.remove(p); // not by 'i' but by Patch, since if some fail the order is not the same
									n_fixed++;
									fixed.add(p);
								}
							} else {
								if (fixed.contains(p)) continue;
								n_fixed += fixStack(data, fixed, p.getStackPatches(), wrong_path, file.getAbsolutePath(), update_mipmaps);
							}
						}
					}
				}
			}
			if (fix_all) {
				// traverse all Patch from the entire project, minus those already fixed
				for (final Displayable d : patch.getLayerSet().getDisplayables(Patch.class)) {
					final Patch p = (Patch) d;
					final String wrong_path = p.getImageFilePath();
					if (wrong_path.startsWith(wrong_parent_path)) {
						File file = new File(new StringBuilder(good_parent_path).append(wrong_path.substring(wrong_parent_path.length())).toString());
						if (file.exists()) {
							if (-1 == p.getFilePath().lastIndexOf("-----#slice=")) {
								if (!fixed.contains(p) && fixPatchPath(p, file.getAbsolutePath(), update_mipmaps)) {
									data.remove(p); // not by 'i' but by Patch, since if some fail the order is not the same
									n_fixed++;
									fixed.add(p);
								}
							} else {
								if (fixed.contains(p)) continue;
								n_fixed += fixStack(data, fixed, p.getStackPatches(), wrong_path, file.getAbsolutePath(), update_mipmaps);
							}
						}
					}
				}
			}

			// if table is empty, close
			if (0 == data.vp.size()) {
				FilePathRepair fpr = projects.remove(patch.getProject());
				fpr.frame.dispose();
			}

			Utils.logAll("Fixed " + n_fixed + " image file path" + (n_fixed > 1 ? "s" : ""));
		}
	}

	static private int fixStack(final PathTableModel data, final Set<Patch> fixed, final Collection<Patch> slices, final String wrong_path, final String new_path, final boolean update_mipmaps) {
		int n_fixed = 0;
		Dimension dim = null;
		Loader loader = null;
		for (final Patch ps : slices) {
			if (fixed.contains(ps)) continue;
			final String slicepath = ps.getFilePath();
			final int isl = slicepath.lastIndexOf("-----#slice=");
			if (-1 == isl) {
				Utils.log2("Not a stack path: " + slicepath);
				continue; // someone linked an image...
			}
			final String ps_path = slicepath.substring(0, isl); // same: // ps.getImageFilePath();
			if (! ps_path.substring(0, isl).equals(wrong_path)) {
				Utils.log2("Not the same stack path:\n  i=" + ps_path + "\n  ref=" + wrong_path);
				continue; // not the same stack!
			}
			if (null == dim) {
				loader = ps.getProject().getLoader();
				loader.releaseToFit(Math.max(Loader.MIN_FREE_BYTES, ps.getOWidth() * ps.getOHeight() * 10));
				dim = loader.getDimensions(new_path);
				if (null == dim) {
					Utils.log(new StringBuilder("ERROR: could not open image at ").append(new_path).toString()); // preserving backslashes
					return n_fixed;
				}
				// Check dimensions
				if (dim.width != ps.getOWidth() || dim.height != ps.getOHeight()) {
					Utils.log("ERROR different o_width,o_height for patch " + ps + "\n  at new path " + new_path +
						"\nold o_width,o_height: " + ps.getOWidth() + "," + ps.getOHeight() +
						"\nnew o_width,o_height: " + dim.width + "," + dim.height);
					return n_fixed;
				}
			}
			// flag as good
			fixed.add(ps);
			loader.removeFromUnloadable(ps);
			// Assign new image path with slice info appended
			loader.addedPatchFrom(new_path + slicepath.substring(isl), ps);
			// submit job to regenerate mipmaps in the background
			if (update_mipmaps) loader.regenerateMipMaps(ps);
			//
			data.remove(ps);
			n_fixed++;
		}
		return n_fixed;
	}

	static private boolean fixPatchPath(final Patch patch, final String new_path, final boolean update_mipmaps) {
		try {
			// Open the image header to check that dimensions match
			final Loader loader = patch.getProject().getLoader();
			loader.releaseToFit(Math.max(Loader.MIN_FREE_BYTES, patch.getOWidth() * patch.getOHeight() * 10));
			final Dimension dim = loader.getDimensions(new_path);
			if (null == dim) {
				Utils.log(new StringBuilder("ERROR: could not open image at ").append(new_path).toString()); // preserving backslashes
				return false;
			}
			// Check and set dimensions
			if (dim.width != patch.getOWidth() || dim.height != patch.getOHeight()) {
				Utils.log("ERROR different o_width,o_height for patch " + patch + "\n  at new path " + new_path +
					"\nold o_width,o_height: " + patch.getOWidth() + "," + patch.getOHeight() +
					"\nnew o_width,o_height: " + dim.width + "," + dim.height);
				return false;
			}
			// flag as good
			loader.removeFromUnloadable(patch);
			// Assign new image path
			loader.addedPatchFrom(new_path, patch);
			// submit job to regenerate mipmaps in the background
			if (update_mipmaps) loader.regenerateMipMaps(patch);
			return true;
		} catch (Exception e) {
			IJError.print(e);
			return false;
		}
	}
}