// Copyright 2005 Konrad Twardowski
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// CREDITS: http://java.sun.com/docs/books/tutorial/uiswing/components/filechooser.html

package org.makagiga.commons.swing;

import static org.makagiga.commons.UI.i18n;

import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Window;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.JFileChooser;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.filechooser.FileSystemView;

import org.makagiga.commons.Config;
import org.makagiga.commons.FS;
import org.makagiga.commons.Globals;
import org.makagiga.commons.Kiosk;
import org.makagiga.commons.MActionInfo;
import org.makagiga.commons.MApplication;
import org.makagiga.commons.MArrayList;
import org.makagiga.commons.MDataAction;
import org.makagiga.commons.MIcon;
import org.makagiga.commons.MLogger;
import org.makagiga.commons.OS;
import org.makagiga.commons.TK;
import org.makagiga.commons.UI;
import org.makagiga.commons.annotation.InvokedFromConstructor;
import org.makagiga.commons.preview.DefaultPreview;
import org.makagiga.commons.preview.PreviewPanel;
import org.makagiga.commons.security.MAccessController;

/**
 * A file chooser.
 * 
 * @since 4.0 (org.makagiga.commons.swing package)
 */
public class MFileChooser extends JFileChooser {

	// private

	private static boolean initDone;
	private boolean autoAddExtension;
	private static boolean recentURIRead;
	private Charset selectedCharset;
	private Component parent;
	private MButton charsetButton;
	private MCheckBox preview;
	private MCheckBox showHiddenFiles;
	private final Mnemonic mnemonic = new Mnemonic();
	private PreviewPanel previewPanel;
	private String configKey;

	// public

	/**
	 * Constructs a file chooser.
	 */
	public MFileChooser() {
		this(null, null);
	}

	/**
	 * Constructs a file chooser.
	 * @param parent A parent window
	 */
	public MFileChooser(final Window parent) {
		this(parent, null);
	}

	/**
	 * Constructs a file chooser.
	 * @param parent A parent window
	 * @param title A dialog title
	 */
	public MFileChooser(final Window parent, final String title) {
		this.parent = parent;
		setAccessory(createAccessory());
		setDragEnabled(Kiosk.actionDragDrop.get());
		setTitle(title);
		
		Config config = getConfigPrivileged();
		if (preview != null) {
			preview.setSelected(config.read("FileChooser.preview", true));
			setPreviewEnabled(preview.isSelected());
		}
		if (showHiddenFiles == null) {
			setFileHidingEnabled(false);
		}
		else {
			if (config.read("FileChooser.showHiddenFiles", false)) {
				showHiddenFiles.setSelected(true);
				setFileHidingEnabled(false);
			}
			else {
				showHiddenFiles.setSelected(false);
				setFileHidingEnabled(true);
			}
		}
		
		// setup text fields
		for (JTextField textField : ContainerIterator.findAll(this, JTextField.class)) {
			if (textField.isVisible())
				MText.commonSetup(textField);
		}
		
		updateMnemonic(false);
	}

	/**
	 * Adds a new filter to the filter combo box (<b>Files of Type</b>).
	 *
	 * @mg.example
	 * <pre class="brush: java">
	 * fileChooser.addFilter(i18n("Zip Archive"), "zip");
	 * FileFilter jpegFilter = fileChooser.addFilter(i18n("JPEG Image"), "jpg", "jpeg");
	 * fileChooser.setFileFilter(jpegFilter); // set default filter
	 * </pre>
	 *
	 * @param types A file types to display
	 * @param description A file types description
	 * 
	 * @return The file filter
	 * 
	 * @since 2.0
	 */
	public FileFilter addFilter(final String description, final String... types) {
		FileFilter filter = new FileNameExtensionFilter(description + " (" + TK.toString(types, ", ") + ")", types);
		addChoosableFileFilter(filter);
		
		return filter;
	}
	
	/**
	 * @since 2.0
	 */
	public FileFilter addImageIOReaderFilter() {
		return addFilter(i18n("Images"), ImageIO.getReaderFileSuffixes());
	}

	/**
	 * @since 2.0
	 */
	public FileFilter addImageIOWriterFilter() {
		return addFilter(i18n("Images"), ImageIO.getWriterFileSuffixes());
	}

	public static MFileChooser createDirectoryChooser(final Window parent, final String title) {
		MFileChooser result = new MFileChooser(parent, title);
		result.setFileSelectionMode(DIRECTORIES_ONLY);
		
		return result;
	}
	
	public static MFileChooser createFileChooser(final Window parent) {
		MFileChooser result = new MFileChooser(parent);
		result.setFileSelectionMode(FILES_ONLY);
		
		return result;
	}

	public static MFileChooser createFileChooser(final Window parent, final String title) {
		MFileChooser result = new MFileChooser(parent, title);
		result.setFileSelectionMode(FILES_ONLY);
		
		return result;
	}
	
	/**
	 * Removes all private data (e.g. directory history)
	 * from the default configuration file.
	 *
	 * @since 1.2
	 */
	public static void deletePrivateData() {
		Config config = Config.getDefault();
		config.removeAllValues(Pattern.compile("String\\.FileChooser\\.directory\\..+"));
		config.removeAllValues(Pattern.compile("String\\.FileChooser\\.recent\\..+"));
		config.removeValue("Integer.FileChooser.recent.count");
		config.sync();
	}
	
	/**
	 * @since 4.0
	 */
	public MPanel getAccessoryPanel() {
		return (MPanel)getAccessory();
	}

	/**
	 * @since 2.0
	 */
	public boolean getAutoAddExtension() { return autoAddExtension; }
	
	/**
	 * @since 2.0
	 */
	public void setAutoAddExtension(final boolean value) { autoAddExtension = value; }

	/**
	 * Returns the selected charset.
	 * 
	 * @since 3.0
	 */
	public Charset getSelectedCharset() {
		return TK.get(selectedCharset, StandardCharsets.UTF_8);
	}

	/**
	 * @since 3.0
	 */
	public void setSelectedCharset(final Charset value) {
		selectedCharset = value;
		
		if ((charsetButton != null) && (selectedCharset != null))
			charsetButton.setText(selectedCharset.displayName());
	}
	
	/**
	 * Initializes look and feel properties.
	 *
	 * @since 1.2
	 */
	public synchronized static void init() {
		if (initDone)
			return;

		if (UI.isMetal() || UI.isNimbus() || UI.isSubstance()) {
			UIManager.put("FileChooser.detailsViewIcon", MIcon.small("ui/detailedview"));
			UIManager.put("FileChooser.homeFolderIcon", MIcon.small("ui/home"));
			UIManager.put("FileChooser.listViewIcon", MIcon.small("ui/multicolumnview"));
			UIManager.put("FileChooser.newFolderIcon", MActionInfo.NEW_FOLDER.getSmallIcon());
			UIManager.put("FileChooser.upFolderIcon", MIcon.small("ui/up"));
			UIManager.put("FileView.directoryIcon", MIcon.small("ui/folder"));
			UIManager.put("FileView.fileIcon", MIcon.small("ui/file"));
		}
		
		initDone = true;
	}

	/**
	 * @since 3.0
	 */
	public boolean isCharsetVisible() {
		return (charsetButton != null) && charsetButton.isVisible();
	}

	/**
	 * @since 3.0
	 */
	public void setCharsetVisible(final boolean value, final int dialogType) {
		if (value != isCharsetVisible()) {
			if (charsetButton == null) {
				charsetButton = new MButton(getSelectedCharset().displayName());
				charsetButton.setPopupMenu(() -> {
					try {
						UI.setWaitCursor(charsetButton, true);

						return new MCharsetMenu(this);
					}
					finally {
						UI.setWaitCursor(charsetButton, false);
					}
				} );
				charsetButton.setToolTipText(i18n("Text Encoding"));

				if (selectedCharset == null) {
					Config config = getConfigPrivileged();
					String lastCharset = config.read(getCharsetKey(dialogType), null);
					if (lastCharset != null) {
						try {
							setSelectedCharset(Charset.forName(lastCharset));
						}
						catch (IllegalCharsetNameException | UnsupportedCharsetException exception) {
							MLogger.exception(exception);
						}
					}
				}

				getAccessory().add(charsetButton, 0);
				getAccessory().add(UI.createGap(0, 5), 1);
				getAccessory().revalidate();
			}
			charsetButton.setVisible(value);
		}
	}
	
	/**
	 * @since 4.4
	 */
	public boolean isPreviewVisible() {
		return (previewPanel != null) && previewPanel.isVisible();
	}

	/**
	 * @since 4.4
	 */
	public void setPreviewVisible(final boolean visible) {
		if (preview != null)
			preview.setVisible(visible);
		if (previewPanel != null)
			previewPanel.setPreviewEnabled(visible);
	}

	/**
	 * Shows the @b Open dialog,
	 * and returns @c true if dialog has been approved.
	 */
	public boolean openDialog() {
		updateMnemonic(false);
		
		if (showOpenDialog(parent) == APPROVE_OPTION) {
			previewPanel.cancel(true);
			saveConfig();
			
			return true;
		}
		previewPanel.cancel(true);

		return false;
	}

	/**
	 * Saves settings of this file chooser to the default configuration file.
	 */
	public void saveConfig() {
		readRecentURI();
	
		final boolean optionPreview = (preview != null) && preview.isSelected();
		final boolean optionShowHiddenFiles = !isFileHidingEnabled();

		final String optionCharset = (charsetButton != null) ? getSelectedCharset().name() : null;
		File optionCurrentDir = getCurrentDirectory();
		final String optionCurrentDirString = optionCurrentDir.getPath();

		File recentFile = getSelectedFile();
		final Globals.URIBookmark recentDirURI = new Globals.URIBookmark(optionCurrentDir, null, null);
		final Globals.URIBookmark recentFileURI =
			(recentFile != null)
			? new Globals.URIBookmark(recentFile, null, null)
			: null;

		AccessController.doPrivileged(new PrivilegedAction<Void>() {
			@Override
			public Void run() {
				Config config = Config.getDefault();

				Globals.addRecentURI(recentDirURI);

				if (recentFileURI != null)
					Globals.addRecentURI(recentFileURI);
				
				if (configKey != null)
					config.write(Config.getPlatformKey(configKey), optionCurrentDirString);
				if (optionCharset != null)
					config.write(getCharsetKey(getDialogType()), optionCharset);
				config.write("FileChooser.preview", optionPreview);
				config.write("FileChooser.showHiddenFiles", optionShowHiddenFiles);

				Config.ListWriter<Globals.URIBookmark> recentWriter = new Config.ListWriter<>(config, "FileChooser.recent");
				recentWriter.removeAll("path");
				recentWriter.write(Globals.getRecentURI(), bookmark -> {
					File file = bookmark.toFile();
					if (file != null) {
						config.write(recentWriter.itemKey("path"), file.getPath());
						
						return true;
					}
						
					return false;
				} );

				config.sync();
				
				return null;
			}
		} );
	}

	/**
	 * Shows the @b Save dialog,
	 * and returns @c true if dialog has been approved.
	 * Displays confirmation message if selected file already exists.
	 */
	public boolean saveDialog() {
		updateMnemonic(false);
		
		if (showSaveDialog(parent) == APPROVE_OPTION) {
			previewPanel.cancel(true);
			saveConfig();
			File file = getSelectedFile();
			
			FileFilter filter = getFileFilter();
			if (
				autoAddExtension &&
				(filter instanceof FileNameExtensionFilter) &&
				noExtension(file, (FileNameExtensionFilter)filter)
			) {
				FileNameExtensionFilter extFilter = (FileNameExtensionFilter)filter;
				file = new File(file.getPath() + "." + extFilter.getExtensions()[0]);
				setSelectedFile(file);
			}

			// selected file already exists?
			if (file.exists() && !MMessage.confirmFileOverwrite(UI.windowFor(null/* no dialog window after close */), null, file, null))
				return false;

			return true;
		}
		previewPanel.cancel(true);

		return false;
	}
	
	public void setApproveText(final String value) {
		setApproveButtonText(value);
		setApproveButtonMnemonic(0);
	}

	/**
	 * Sets configuration key to @p value.
	 * The key is used to read settings (e.g. last directory)
	 * from the default configuration file.
	 *
	 * @mg.example
	 * <pre class="brush: java">
	 * fileChooser.setConfigKey("backup"); // String.FileChooser.directory.backup=...
	 * </pre>
	 *
	 * @throws IllegalArgumentException If @p value is null or empty
	 */
	public void setConfigKey(final String value) {
		TK.checkNullOrEmpty(value, "value");

		configKey = "FileChooser.directory." + value;
		Config config = getConfigPrivileged();
		setCurrentDirectory(new File(config.read(Config.getPlatformKey(configKey), FS.getUserDir())));
	}

	@InvokedFromConstructor
	public void setPreviewEnabled(final boolean value) {
		previewPanel.setPreviewEnabled(value);
		previewPanel.update(getSelectedFile());
	}

	/**
	 * Sets dialog title to @p value.
	 */
	@InvokedFromConstructor
	public void setTitle(final String value) {
		setDialogTitle(value);
	}

	/**
	 * Setups the file system @p view.
	 */
	@Override
	public void setup(final FileSystemView view) {
		init();
		super.setup(view);
	}
	
	@Override
	public int showDialog(final Component parent, final String approveButtonText) {
		if (MApplication.getForceRTL())
			applyComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);

		return super.showDialog(parent, approveButtonText);
	}

	/**
	 * @since 4.10
	 */
	public void updateMnemonic(final boolean reset) {
		if (reset)
			mnemonic.reset();
		mnemonic.apply(this);
	}

	// protected
	
	/**
	 * Creates and returns an accessory panel.
	 */
	@InvokedFromConstructor
	protected MPanel createAccessory() {
		MPanel accessory = MPanel.createVBoxPanel();
		accessory.setMargin(0, 5, 0, 0);

// TODO: custom bookmarks
		MButton bookmarksButton = new MButton(i18n("Places")) {
			@Override
			protected MMenu onPopupMenu() {
				MMenu menu = new MMenu();

				for (Globals.URIBookmark i : MFileChooser.this.getGTKBookmarks())
					addAction(menu, i);
				
				menu.addSeparator(false);
				
				addAction(menu, new Globals.URIBookmark(FS.getConfigPath().toFile(), i18n("{0}: Settings and Data", MApplication.getFullName()), MApplication.getSmallIcon()));
				for (Globals.URIBookmark i : Globals.getURIBookmarks())
					addAction(menu, i);

				readRecentURI();

				List<Globals.URIBookmark> recent = Globals.getRecentURI();

				if (!recent.isEmpty()) {
					menu.addTitle(i18n("Recent Items"));
					for (Globals.URIBookmark i : recent)
						addAction(menu, i);
				}

				return menu;
			}
			private void addAction(final MMenu menu, final Globals.URIBookmark i) {
				File file = i.toFile();
				
				if (file == null)
					return;
				
				menu.addDefaultRadioButton(
					new BookmarkAction(MFileChooser.this, i, file),
					(
						file.isDirectory() &&
						file.equals(MFileChooser.this.getCurrentDirectory())
					) ||
					(
						file.isFile() &&
						file.equals(MFileChooser.this.getSelectedFile())
					)
				);
			}
		};
		bookmarksButton.setPopupMenuEnabled(true);
		accessory.add(bookmarksButton);
		accessory.addGap();

		showHiddenFiles = new MCheckBox(i18n("Show Hidden Files"));
		showHiddenFiles.addActionListener(e -> setFileHidingEnabled(!showHiddenFiles.isSelected()));
		accessory.add(showHiddenFiles);

		preview = new MCheckBox(i18n("Preview"));
		preview.addActionListener(e -> setPreviewEnabled(preview.isSelected()));
		accessory.add(preview);

		DefaultPreview defaultPreview;
		SecurityManager sm = System.getSecurityManager();
		if (sm == null)
			defaultPreview = DefaultPreview.getInstance();
		else
			defaultPreview = MAccessController.doPrivileged(DefaultPreview::getInstance);
		previewPanel = new PreviewPanel(defaultPreview, PreviewPanel.SMALL_WIDTH);
		accessory.add(previewPanel);
		
		addPropertyChangeListener(e -> {
			switch (e.getPropertyName()) {
				case DIRECTORY_CHANGED_PROPERTY:
					previewPanel.clear();
					break;
				case SELECTED_FILE_CHANGED_PROPERTY:
					File newFile = (File)e.getNewValue();
					previewPanel.update(newFile);
					break;
			}
		} );

		return accessory;
	}
	
	// private
	
	private List<Globals.URIBookmark> getGTKBookmarks() {
		if (!OS.isLinux())
			return Collections.emptyList();
		
		MArrayList<Globals.URIBookmark> list = new MArrayList<>();
/*
Line format:
URI<SPACE><Optional Description>
*/
		Path gtkBookmarks = FS.getUserPath().resolve(".gtk-bookmarks");
		if (Files.exists(gtkBookmarks)) {
			try {
				FS.lines(gtkBookmarks, stream ->
					stream
					.map(String::trim)
					.filter(line -> !line.isEmpty())
					.map(line -> TK.splitPair(line, ' ', TK.SPLIT_PAIR_TRIM))
					.forEach(pair -> {
						try {
							list.add(new Globals.URIBookmark(new URI(pair[0]), pair[1], null));
						}
						catch (URISyntaxException exception) {
							MLogger.exception(exception);
						}
					} )
				);
			}
			catch (IOException exception) {
				MLogger.exception(exception);
			}
		}
		
		return list;
	}

	private String getCharsetKey(final int dialogType) {
		String result = "FileChooser.charset.";
		if (dialogType == CUSTOM_DIALOG)
			result += "custom";
		else if (dialogType == OPEN_DIALOG)
			result += "open";
		else if (dialogType == SAVE_DIALOG)
			result += "save";
		else
			throw new IllegalArgumentException("dialogType must be CUSTOM_DIALOG, OPEN_DIALOG or SAVE_DIALOG");
		
		return result;
	}

	private Config getConfigPrivileged() {
		SecurityManager sm = System.getSecurityManager();

		if (sm == null)
			return Config.getDefault();

		return MAccessController.doPrivileged(Config::getDefault);
	}
	
	private boolean noExtension(final File file, final FileNameExtensionFilter filter) {
		String ext = FS.getFileExtension(file);
		
		if (ext.isEmpty())
			return true;
		
		return !filter.accept(file);
	}
	
	private void readRecentURI() {
		if (recentURIRead)
			return;
			
		recentURIRead = true;
	
		Config config = getConfigPrivileged();
		Config.ListReader<Globals.URIBookmark> recentReader = new Config.ListReader<>(config, "FileChooser.recent");
		for (Globals.URIBookmark i : recentReader.read(() -> {
			String path = config.read(recentReader.itemKey("path"), null);

			if (path != null)
				return new Globals.URIBookmark(new File(path), null, null);
				
			return null;
		} ))
			Globals.addRecentURI(i);
	}
	
	// private classes
	
	private static final class BookmarkAction extends MDataAction.Weak<MFileChooser> {
	
		// private
		
		private final File file;
	
		// public
		
		public BookmarkAction(final MFileChooser chooser, final Globals.URIBookmark bookmark, final File file) {
			super(chooser, TK.centerSqueeze(bookmark.toString()));
			this.file = file;
			
			Icon icon = bookmark.getSmallIcon();
			if (icon == null) {
				if (!file.exists())
					setIconName("ui/error");
				else if (file.isDirectory())
					setIconName("ui/folder");
				else
					setIconName("ui/file");
			}
			else {
				setSmallIcon(icon);
			}
		}
		
		@Override
		public void onAction() {
			MFileChooser chooser = get();
			File dir = file.isDirectory() ? file : file.getParentFile();
			if (dir != null)
				chooser.setCurrentDirectory(dir);
			if (file.isFile())
				chooser.setSelectedFile(file);
		}
	
	}

}
