package charactermanaj.ui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
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.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.AbstractAction;
import javax.swing.AbstractCellEditor;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JColorChooser;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.SwingConstants;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumnModel;

import charactermanaj.Main;
import charactermanaj.model.AppConfig;
import charactermanaj.ui.util.ScaleSupport;
import charactermanaj.util.BeanPropertiesUtilities;
import charactermanaj.util.BeanPropertiesUtilities.PropertyAccessor;
import charactermanaj.util.BeanPropertiesUtilities.PropertyAccessorMap;
import charactermanaj.util.ConfigurationDirUtilities;
import charactermanaj.util.DesktopUtilities;
import charactermanaj.util.ErrorMessageHelper;
import charactermanaj.util.LocalizedResourcePropertyLoader;
import charactermanaj.util.SetupLocalization;


/**
 * アプリケーション設定ダイアログ
 *
 * @author seraphy
 */
public class AppConfigDialog extends JDialog {

	private static final long serialVersionUID = 1L;

	private static final Logger logger = Logger.getLogger(AppConfigDialog.class.getName());

	private AppConfigTableModel appConfigTableModel;

	private JTable appConfigTable;

	private JCheckBox chkResetDoNotAskAgain;

	private RecentCharactersDir recentCharactersDir;

	private AbstractAction actApply;

	private boolean orgDoNotAskAgain;

	public enum ColumnDef {
		NAME("column.key", String.class, false) {
			@Override
			public Object getValue(AppConfigRow row) {
				return row.getDisplayName();
			}
		},
		VALUE("column.value", String.class, true) {
			@Override
			public Object getValue(AppConfigRow row) {
				return row.getValue();
			}
			@Override
			public void setValue(AppConfigRow row, Object value) {
				row.setValue(value);
			}
		};

		private final String reskey;

		private final Class<?> type;

		private final boolean editable;

		ColumnDef(String reskey, Class<?> type, boolean editable) {
			this.reskey = reskey;
			this.type = type;
			this.editable = editable;
		}

		public boolean isEditable() {
			return editable;
		}

		public String getResourceKey() {
			return reskey;
		}

		public Class<?> getType() {
			return type;
		}

		public abstract Object getValue(AppConfigRow row);

		public void setValue(AppConfigRow row, Object value) {
			throw new UnsupportedOperationException(name());
		}
	}

	private static class AppConfigRow {

		private final String name;

		private final PropertyAccessor accessor;

		private String order = "";

		private String displayName;

		private Object orgValue;

		private Object value;

		private boolean rejected;

		public AppConfigRow(String name, PropertyAccessor accessor, Object value) {
			this.name = name;
			this.accessor = accessor;
			this.value = value;
			this.orgValue = value;
		}

		public String getName() {
			return name;
		}

		public Class<?> getPropertyType() {
			Class<?> dataType = accessor.getPropertyType();
			// JTableのセルレンダラーではプリミティブ型の編集は対応していないので
			// ラッパー型に置き換える
			if (dataType.isPrimitive()) {
				if (dataType.equals(int.class)) {
					dataType = Integer.class;
				} else if (dataType.equals(long.class)) {
					dataType = Long.class;
				} else if (dataType.equals(float.class)) {
					dataType = Float.class;
				} else if (dataType.equals(double.class)) {
					dataType = Double.class;
				} else if (dataType.equals(boolean.class)) {
					dataType = Boolean.class;
				}
			}
			return dataType;
		}

		public String getOrder() {
			return order;
		}

		public void setOrder(String order) {
			if (order == null) {
				order = "";
			}
			this.order = order;
		}

		public String getDisplayName() {
			return (displayName == null || displayName.length() == 0) ? name : displayName;
		}

		public void setDisplayName(String displayName) {
			this.displayName = displayName;
		}

		public Object getValue() {
			return value;
		}

		public void setValue(Object value) {
			this.value = value;
		}

		public boolean isRejected() {
			return rejected;
		}

		public void setRejected(boolean rejected) {
			this.rejected = rejected;
		}

		public boolean isModified() {
			return orgValue == null ? value != null : !orgValue.equals(value);
		}
	}

	private static class AppConfigTableModel extends AbstractTableModel {

		private static final long serialVersionUID = 1L;

		protected static final ColumnDef[] COLUMNS = ColumnDef.values();

		private List<AppConfigRow> items = Collections.emptyList();

		public List<AppConfigRow> getItems() {
			return items;
		}

		public void setItems(List<AppConfigRow> items) {
			if (items == null) {
				items = Collections.emptyList();
			}
			this.items = items;
			fireTableDataChanged();
		}

		public void setRejectNames(Set<String> rejectNames) {
			if (rejectNames == null) {
				rejectNames = Collections.emptySet();
			}
			for (AppConfigRow item : items) {
				String key = item.getName();
				boolean rejected = rejectNames.contains(key);
				item.setRejected(rejected);
			}
			fireTableDataChanged();
		}

		/**
		 * 編集されているか?
		 *
		 * @return 編集されていればtrue、そうでなければfalse
		 */
		public boolean isModified() {
			for (AppConfigRow rowItem : items) {
				if (rowItem.isModified()) {
					return true;
				}
			}
			return false;
		}


		@Override
		public int getRowCount() {
			return items.size();
		}

		public int getColumnCount() {
			return COLUMNS.length;
		}

		@Override
		public Class<?> getColumnClass(int columnIndex) {
			return COLUMNS[columnIndex].getType();
		}

		@Override
		public String getColumnName(int column) {
			Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
					.getLocalizedProperties("languages/appconfigdialog");
			String reskey = COLUMNS[column].getResourceKey();
			return strings.getProperty(reskey, reskey);
		}

		@Override
		public boolean isCellEditable(int rowIndex, int columnIndex) {
			return COLUMNS[columnIndex].isEditable();
		}

		public Object getValueAt(int rowIndex, int columnIndex) {
			AppConfigRow row = items.get(rowIndex);
			return COLUMNS[columnIndex].getValue(row);
		}

		@Override
		public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
			AppConfigRow row = items.get(rowIndex);
			COLUMNS[columnIndex].setValue(row, aValue);
			fireTableRowsUpdated(rowIndex, rowIndex);
		}

		public void adjustColumnModel(TableColumnModel columnModel, double scale) {
			Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
					.getLocalizedProperties("languages/appconfigdialog");
			int mx = columnModel.getColumnCount();
			for (int idx = 0; idx < mx; idx++) {
				String reskey = COLUMNS[idx].getResourceKey() + ".width";
				int width = Integer.parseInt(strings.getProperty(reskey));
				columnModel.getColumn(idx).setPreferredWidth((int)(width * scale));
			}
		}
	}

	public AppConfigDialog(JFrame parent) {
		super(parent, true);
		try {
			setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
			addWindowListener(new WindowAdapter() {
				@Override
				public void windowClosing(WindowEvent e) {
					onClose();
				}
			});

			initComponent();

			loadData();

		} catch (RuntimeException ex) {
			logger.log(Level.SEVERE, "appConfig construct failed.", ex);
			dispose();
			throw ex;
		}
	}

	private void initComponent() {

		Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
				.getLocalizedProperties("languages/appconfigdialog");

		setTitle(strings.getProperty("title"));

		Container contentPane = getContentPane();
		contentPane.setLayout(new BorderLayout());

		// buttons
		JPanel btnPanel = new JPanel();
		btnPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 45));
		GridBagLayout btnPanelLayout = new GridBagLayout();
		btnPanel.setLayout(btnPanelLayout);

		GridBagConstraints gbc = new GridBagConstraints();

		actApply = new AbstractAction(strings.getProperty("btn.apply")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				onUpdate();
			}
		};
		Action actCancel = new AbstractAction(strings.getProperty("btn.cancel")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				onClose();
			}
		};
		Action actLocalization = new AbstractAction(strings.getProperty("btn.setupLocalization")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				onSetupLocalization();
			}
		};
		Action actResetSettings = new AbstractAction(strings.getProperty("btn.resetSettingsToDefault")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				onResetSettings();
			}
		};

		Box pnlButtons = Box.createHorizontalBox();
		pnlButtons.add(new JButton(actLocalization));
		pnlButtons.add(new JButton(actResetSettings));

		chkResetDoNotAskAgain = new JCheckBox(strings.getProperty("chk.askForCharactersDir"));
		chkResetDoNotAskAgain.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				// 保存ボタンの状態更新のため
				updateUIState();
			}
		});

		gbc.gridx = 0;
		gbc.gridy = 0;
		gbc.gridheight = 1;
		gbc.gridwidth = 3;
		gbc.anchor = GridBagConstraints.WEST;
		gbc.fill = GridBagConstraints.NONE;
		gbc.insets = new Insets(0, 0, 0, 0);
		gbc.ipadx = 0;
		gbc.ipady = 0;
		gbc.weightx = 1.;
		gbc.weighty = 0.;
		btnPanel.add(chkResetDoNotAskAgain, gbc);

		gbc.gridx = 0;
		gbc.gridy = 1;
		gbc.gridheight = 1;
		gbc.gridwidth = 3;
		gbc.anchor = GridBagConstraints.WEST;
		gbc.fill = GridBagConstraints.NONE;
		gbc.insets = new Insets(3, 3, 3, 3);
		gbc.ipadx = 0;
		gbc.ipady = 0;
		gbc.weightx = 1.;
		gbc.weighty = 0.;
		btnPanel.add(pnlButtons, gbc);

		gbc.gridx = 0;
		gbc.gridy = 2;
		gbc.gridheight = 1;
		gbc.gridwidth = 1;
		gbc.fill = GridBagConstraints.BOTH;
		gbc.weightx = 1.;
		gbc.weighty = 0.;
		btnPanel.add(Box.createHorizontalGlue(), gbc);

		gbc.gridx = Main.isLinuxOrMacOSX() ? 2 : 1;
		gbc.weightx = 0.;
		JButton btnApply = new JButton(actApply);
		btnPanel.add(btnApply, gbc);

		gbc.gridx = Main.isLinuxOrMacOSX() ? 1 : 2;
		gbc.weightx = 0.;
		JButton btnCancel = new JButton(actCancel);
		btnPanel.add(btnCancel, gbc);

		add(btnPanel, BorderLayout.SOUTH);

		Dimension dim = new Dimension(600, 400);
		ScaleSupport scaleSupport = ScaleSupport.getInstance(this);
		if (scaleSupport != null) {
			// HiDpi環境でのスケールを考慮したウィンドウサイズに補正する
			dim = scaleSupport.manualScaled(dim);
		}
		setSize(dim);
		setLocationRelativeTo(getParent());

		// Notes
		JLabel lblCaution = new JLabel(strings.getProperty("caution"), JLabel.CENTER);
		lblCaution.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
		lblCaution.setForeground(Color.red);
		contentPane.add(lblCaution, BorderLayout.NORTH);

		// Model
		appConfigTableModel = new AppConfigTableModel();

		// JTable
		AppConfig appConfig = AppConfig.getInstance();
		final Color invalidBgColor = appConfig.getInvalidBgColor();

		appConfigTable = new JTable(appConfigTableModel) {
			private static final long serialVersionUID = 1L;

			@Override
			public Component prepareRenderer(TableCellRenderer renderer,
					int row, int column) {
				Component comp = super.prepareRenderer(renderer, row, column);
				AppConfigRow configRow = appConfigTableModel.getItems().get(row);
				if (configRow.isRejected()) {
					// 差し戻された項目は警告色とする
					comp.setBackground(invalidBgColor);
				} else {
					// そうでなければ標準色に戻す
					if (isCellSelected(row, column)) {
						comp.setBackground(getSelectionBackground());
					} else {
						comp.setBackground(getBackground());
					}
				}

				if (configRow.isModified() && !configRow.isRejected()) {
					// 変更行の色を警告色にする
					// (ただし、rejectのものは背景色を変えているので何もしない)
					comp.setForeground(invalidBgColor);
				} else {
					// そうでなければ標準色に戻す
					comp.setForeground(getForeground());
				}
				return comp;
			}

			@Override
			public String getToolTipText(MouseEvent event) {
				int row = rowAtPoint(event.getPoint());
				int col = columnAtPoint(event.getPoint());
				if (AppConfigTableModel.COLUMNS[col] == ColumnDef.NAME) {
					// 最初の列の表示をツールチップとして表示させる
					int modelRow = convertRowIndexToModel(row);
					return appConfigTableModel.getItems().get(modelRow).getDisplayName();
				}
				return super.getToolTipText(event);
			}

			// 1つの列で複数のデータタイプの編集を可能にする
			// Jtable with different types of cells depending on data type
			// https://stackoverflow.com/questions/16970824/jtable-with-different-types-of-cells-depending-on-data-type

			private Class<?> editingClass;

			@Override
			public TableCellRenderer getCellRenderer(int row, int column) {
				editingClass = null;
				int modelColumn = convertColumnIndexToModel(column);
				if (AppConfigTableModel.COLUMNS[modelColumn] == ColumnDef.VALUE) {
					// VALUE列の場合
					int modelRow = convertRowIndexToModel(row);
					AppConfigRow rowData = appConfigTableModel.getItems().get(modelRow);

					// 行のデータ型に対応するレンダラーを取得する
					Class<?> dataType = rowData.getPropertyType();
					TableCellRenderer renderer = getDefaultRenderer(dataType);
					if (renderer != null) {
						return renderer;
					}
				}
				// VALUE列以外は、標準のまま (もしくはレンダラーがみつからない場合)
				return super.getCellRenderer(row, column);
			}

			@Override
			public TableCellEditor getCellEditor(int row, int column) {
				editingClass = null;
				int modelColumn = convertColumnIndexToModel(column);
				if (AppConfigTableModel.COLUMNS[modelColumn] == ColumnDef.VALUE) {
					// VALUE列の場合
					int modelRow = convertRowIndexToModel(row);
					AppConfigRow rowData = appConfigTableModel.getItems().get(modelRow);

					// 行のデータ型に対応するレンダラーを取得する
					editingClass = rowData.getPropertyType();
					return getDefaultEditor(editingClass);

				} else {
					// VALUE列以外は、標準のまま
					return super.getCellEditor(row, column);
				}
			}

			//  This method is also invoked by the editor when the value in the editor
			//  component is saved in the TableModel. The class was saved when the
			//  editor was invoked so the proper class can be created.

			@Override
			public Class<?> getColumnClass(int column) {
				return editingClass != null ? editingClass : super.getColumnClass(column);
			}
		};

		appConfigTable.setShowGrid(true);
		appConfigTable.setGridColor(appConfig.getGridColor());
		appConfigTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
		appConfigTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
		appConfigTable.setCellSelectionEnabled(true);

		// 行の高さをフォントの高さにする
		appConfigTable.setRowHeight((int)(appConfigTable.getFont().getSize() * 1.2));

		// データタイプがColorの場合のセルレンダラーとエディタを設定する
		appConfigTable.setDefaultRenderer(Color.class, new ColorCellRender());
		appConfigTable.setDefaultEditor(Color.class, new ColorCellEditor());

		appConfigTableModel.adjustColumnModel(appConfigTable.getColumnModel(), scaleSupport.getManualScaleX());

		appConfigTableModel.addTableModelListener(new TableModelListener() {
			@Override
			public void tableChanged(TableModelEvent e) {
				// テーブルが変更された場合、保存ボタンの活性やReject状態の変更のため
				updateUIState();
			}
		});

		JScrollPane appConfigTableSP = new JScrollPane(appConfigTable);
		appConfigTableSP.setBorder(BorderFactory.createCompoundBorder(
				BorderFactory.createEmptyBorder(0, 3, 0, 3),
				BorderFactory.createTitledBorder(strings.getProperty("table.caption")))
				);
		appConfigTableSP.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
		contentPane.add(appConfigTableSP, BorderLayout.CENTER);

		// RootPane
		Toolkit tk = Toolkit.getDefaultToolkit();
		JRootPane rootPane = getRootPane();
		InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
		ActionMap am = rootPane.getActionMap();
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeAppConfigDialog");
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeAppConfigDialog");
		am.put("closeAppConfigDialog", actCancel);

		// 保存ボタンの活性制御
		updateUIState();
	}

	/**
	 * 保存ボタンの活性制御とテーブルの編集状態の表示の更新
	 * (テーブルが編集された場合に更新される)
	 */
	protected void updateUIState() {
		boolean hasModified = false;

		// テーブルを走査して変更を確認する
		for (AppConfigRow itemRow : appConfigTableModel.getItems()) {
			if (itemRow.isModified()) {
				// 変更あり
				hasModified = true;
			} else if (itemRow.isRejected()) {
				// 変更されていない状態であれば、差し戻し状態を解除する
				itemRow.setRejected(false);
			}
		}

		// キャラクターデータディレクトリの問い合わせ状態が変わっているか？
		if (orgDoNotAskAgain != chkResetDoNotAskAgain.isSelected()) {
			hasModified = true;
		}

		// 保存先が無効であれば適用ボタンを有効にしない.
		AppConfig appConfig = AppConfig.getInstance();
		boolean enableSave = !appConfig.getPrioritySaveFileList().isEmpty();

		// 保存が有効であり、且つ、変更された行があれば保存ボタンを有効とする
		actApply.setEnabled(enableSave && hasModified);
	}

	private void loadData() {

		Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
				.getLocalizedProperties("languages/appconfigdialog");

		// AppConfigへのアクセッサを取得する
		PropertyAccessorMap<AppConfig> accessorMap = BeanPropertiesUtilities.getPropertyAccessorMap(AppConfig.class);

		AppConfig appConfig = AppConfig.getInstance();
		accessorMap.setBean(appConfig);

		List<AppConfigRow> items = new ArrayList<AppConfigRow>();
		int fallbackOrder = 1000;
		for (Map.Entry<String, PropertyAccessor> accessorEntry : accessorMap.entrySet()) {
			// プロパティ名と現在値を取得する
			String name = accessorEntry.getKey();
			PropertyAccessor accessor = accessorEntry.getValue();
			Object value = accessor.getValue();

			// リソースからプロパティ名に対応する表示名を取得する(なければプロパティ名のまま)
			String displayName = strings.getProperty(name, name);
			int pt = displayName.indexOf(";");
			String order = Integer.toString(fallbackOrder++);
			if (pt > 0) {
				order = displayName.substring(0, pt);
				displayName = displayName.substring(pt + 1);
			}

			// 行オブジェクト作成
			AppConfigRow rowItem = new AppConfigRow(name, accessor, value);
			rowItem.setDisplayName(displayName);
			rowItem.setOrder(order);

			items.add(rowItem);
		}

		// 表示順に並べる
		Collections.sort(items, new Comparator<AppConfigRow>() {
			@Override
			public int compare(AppConfigRow o1, AppConfigRow o2) {
				int ret = o1.getOrder().compareTo(o2.getOrder());
				if (ret == 0) {
					ret = o1.getDisplayName().compareTo(o2.getDisplayName());
				}
				if (ret == 0) {
					ret = o1.getName().compareTo(o2.getName());
				}
				return ret;
			}
		});

		appConfigTableModel.setItems(items);

		// 最後に使ったキャラクターデータディレクトリの自動選択設定
		try {
			recentCharactersDir = RecentCharactersDir.load();

			if (recentCharactersDir != null) {
				File lastUseCharactersDir = recentCharactersDir.getLastUseCharacterDir();
				boolean enableLastUseCharacterDir = lastUseCharactersDir != null && lastUseCharactersDir.isDirectory();
				boolean doNotAskAgain = enableLastUseCharacterDir && recentCharactersDir.isDoNotAskAgain();
				chkResetDoNotAskAgain.setEnabled(enableLastUseCharacterDir);
				chkResetDoNotAskAgain.setSelected(!doNotAskAgain);
			}

		} catch (Exception ex) {
			recentCharactersDir = null;
			logger.log(Level.WARNING, "RecentCharactersDir load failed.", ex);
		}

		// 初期状態の保存
		this.orgDoNotAskAgain = chkResetDoNotAskAgain.isSelected();
	}

	/**
	 * 設定値を初期化する
	 */
	protected void onResetSettings() {
		Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
				.getLocalizedProperties("languages/appconfigdialog");
		if (JOptionPane.showConfirmDialog(this, strings.getProperty("confirm.resetSettingsToDefault"),
				strings.getProperty("confirm.close.caption"), JOptionPane.YES_NO_OPTION,
				JOptionPane.QUESTION_MESSAGE) != JOptionPane.YES_OPTION) {
			return;
		}

		Map<String, Object> defMap = AppConfig.getDefaultProperties();
		for (AppConfigRow rowItem : appConfigTableModel.getItems()) {
			String name = rowItem.getName();
			if (defMap.containsKey(name)) {
				Object value = defMap.get(name);
				rowItem.setValue(value);
			}
		}
		appConfigTableModel.fireTableDataChanged();
	}

	/**
	 * ローカライズリソースをユーザディレクトリ上に展開する.
	 */
	protected void onSetupLocalization() {
		Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
			.getLocalizedProperties("languages/appconfigdialog");
		if (JOptionPane.showConfirmDialog(this,
				strings.getProperty("setupLocalization"),
				strings.getProperty("confirm.setupLocalization.caption"),
				JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.OK_OPTION) {
			return;
		}

		try {
			File baseDir = ConfigurationDirUtilities.getUserDataDir();
			SetupLocalization setup = new SetupLocalization(baseDir);
			setup.setupToLocal(
					EnumSet.allOf(SetupLocalization.Resources.class), true);

			File resourceDir = setup.getResourceDir();
			DesktopUtilities.open(resourceDir);

		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}

	protected void onClose() {
		if (appConfigTableModel.isModified()) {
			Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
					.getLocalizedProperties("languages/appconfigdialog");
			if (JOptionPane.showConfirmDialog(this, strings.getProperty("confirm.close"),
					strings.getProperty("confirm.close.caption"), JOptionPane.YES_NO_OPTION,
					JOptionPane.QUESTION_MESSAGE) != JOptionPane.YES_OPTION) {
				return;
			}
		}
		dispose();
	}

	/**
	 * AppConfigと、キャラクターデータディレクトリの起動時の選択有無の設定値を保存する。
	 */
	protected void onUpdate() {

		if (appConfigTable.isEditing()) {
			// 編集中ならば許可しない.
			Toolkit tk = Toolkit.getDefaultToolkit();
			tk.beep();
			return;
		}

		Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
				.getLocalizedProperties("languages/appconfigdialog");

		// 編集されたAppConfigの設定値を取得する. (変更のあるもののみ)
		Map<String, Object> modifiedValues = new HashMap<String, Object>();
		for (AppConfigRow rowItem : appConfigTableModel.getItems()) {
			if (rowItem.isModified()) {
				String name = rowItem.getName();
				Object value = rowItem.getValue();
				modifiedValues.put(name, value);
			}
		}

		// キャラクターデータディレクトリの起動時の選択状態の変更状態
		boolean updateRecentCharactersDir = (orgDoNotAskAgain != chkResetDoNotAskAgain.isSelected());

		if (!updateRecentCharactersDir && modifiedValues.isEmpty()) {
			// 変更点がないので何もしない
			return;
		}

		// AppConfigの保存
		if (!modifiedValues.isEmpty()) {
			// 編集されたプロパティが適用可能か検証する.
			Set<String> rejectNames = AppConfig.checkProperties(modifiedValues);
			if (!rejectNames.isEmpty()) {
				// エラーがある場合
				appConfigTableModel.setRejectNames(rejectNames);

				JOptionPane.showMessageDialog(this, strings.getProperty("error.message"),
						strings.getProperty("error.caption"), JOptionPane.ERROR_MESSAGE);
				return;
			}

			try {
				// アプリケーション設定を更新し、保存する.
				AppConfig appConfig = AppConfig.getInstance();
				appConfig.update(modifiedValues);
				appConfig.saveConfig();

			} catch (Exception ex) {
				ErrorMessageHelper.showErrorDialog(this, ex);
				return;
			}
		}

		// キャラクターデータディレクトリの起動時の選択の保存
		if (updateRecentCharactersDir) {
			try {
				if (chkResetDoNotAskAgain.isEnabled()) {
					boolean doNotAskAgain = !chkResetDoNotAskAgain.isSelected();
					if (doNotAskAgain != recentCharactersDir.isDoNotAskAgain()) {
						recentCharactersDir.setDoNotAskAgain(doNotAskAgain);
						recentCharactersDir.saveRecents();
					}
				}
			} catch (Exception ex) {
				ErrorMessageHelper.showErrorDialog(this, ex);
				return;
			}
		}

		// アプリケーションの再起動が必要なことを示すダイアログを表示する.
		String message = strings.getProperty("caution");
		JOptionPane.showMessageDialog(this, message);

		dispose();
	}
}

/**
 * カラーセル
 */
class ColorCell extends JPanel {
	private static final long serialVersionUID = 1L;

	private String title = "Color";

	private JPanel box;

	private JLabel label;

	private JButton button;

	private ActionListener actionListener;

	public ColorCell() {
		this(null);
	}

	/**
	 * ボタンのアクションリスナを指定して構築する
	 * @param actionListener
	 */
	public ColorCell(ActionListener actionListener) {
		super(new BorderLayout());
		this.actionListener = actionListener;

		box = new JPanel(new BorderLayout());

		label = new JLabel();
		label.setHorizontalAlignment(SwingConstants.CENTER);
		box.add(label, BorderLayout.CENTER);

		AbstractAction actColorChoose = new AbstractAction() {
			private static final long serialVersionUID = 1L;

			@Override
			public void actionPerformed(ActionEvent e) {
				onClick(e);
			}
		};

		button = new JButton(actColorChoose);
		Dimension dim = button.getPreferredSize();
		dim.width = 24;
		button.setPreferredSize(dim);

		add(box, BorderLayout.CENTER);
		add(button, BorderLayout.EAST);
		setSelectedColor(Color.BLACK);

		// ボックスのダブルクリックで、ボタンクリックと同じ動きとする
		box.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				if (e.getClickCount() == 2) {
					onClick(null);
					e.consume();
				}
			}
		});

		// このセルのスペースキー押下で、ボタンクリックと同じ動きとする
		InputMap im = getInputMap(WHEN_FOCUSED);
		im.put(KeyStroke.getKeyStroke(' '), "ON_CLICK_COLOR_CHOOSER");
		ActionMap am = getActionMap();
		am.put("ON_CLICK_COLOR_CHOOSER", actColorChoose);

		setMinimumSize(new Dimension(50, 30));
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		String old = this.title;
		if (old == null ? title != null : !old.equals(title)) {
			this.title = title;
			firePropertyChange("title", old, title);
		}
	}

	public Border getBoxBorder() {
		return box.getBorder();
	}

	public void setBoxBorder(Border border) {
		box.setBorder(border);
	}

	public void onClick(ActionEvent e) {
		// ※ カラー選択ダイアログは、Java7以降でないとアルファ値の設定はできない。
		// Java6で実行するとアルファチャネルが消されたものになる。
		// (設定ファイルとしては手作業では設定可能なので、とりあえず、このまま。)
		Color selColor = JColorChooser.showDialog(ColorCell.this, title, selectedColor);
		if (selColor != null) {
			setSelectedColor(selColor);
			if (actionListener != null) {
				actionListener.actionPerformed(e);
			}
		}
	}

	private Color bgColor = Color.WHITE;

	private Color selectedColor;

	public Color getSelectedColor() {
		return selectedColor;
	}

	public void setSelectedColor(Color color) {
		Color old = this.selectedColor;
		if (old == null ? color != null : !old.equals(color)) {
			this.selectedColor = color;

			// nullの場合は黒と見なして処理をつづける
			// (プロパティにはnullを格納したまま)
			boolean dummyColor = false;
			if (color == null) {
				color = Color.BLACK;
				dummyColor = true;
			}

			// テキスト色は塗りつぶし色と反転色にする (同系で重なりにくくするため)
			Color colorForeground = new Color(color.getRGB() ^ 0xffffff).brighter();

			int alpha = color.getAlpha();

			// JPanelの背景色としてアルファの透過色をそのまま使うと
			// 親コンポーネントの背景色と混じり、色のカタログとして用をなさないので
			// 固定された背景色(たとえば白)と予め合成済みに補正しておく
			// (アルファが255の場合は合成する必要はない)
			Color premultipliedColor;
			if (alpha == 255) {
				premultipliedColor = color;
			} else {
				float[] rgb = color.getRGBColorComponents(null);
				float[] bgRgb = bgColor.getRGBColorComponents(null); // 背景色
				// アルファを合成済みにする
				float a = ((float) alpha) / 255f;
				rgb[0] = rgb[0] * a + bgRgb[0] * (1 - a);
				rgb[1] = rgb[1] * a + bgRgb[1] * (1 - a);
				rgb[2] = rgb[2] * a + bgRgb[2] * (1 - a);
				premultipliedColor = new Color(rgb[0], rgb[1], rgb[2]);
			}

			box.setBackground(premultipliedColor);
			label.setForeground(colorForeground);

			String msg;
			if (dummyColor) {
				// NULLの場合はテキストは表示しない
				msg = "";
			} else if (alpha != 255) {
				// アルファが255以外の場合はアルファ値も含めてARGBで表示する
				msg = String.format("#%08X", ((long) color.getRGB()) & 0xffffffffL);
			} else {
				// アルファが255の場合はRGBのみ表示する。
				msg = String.format("#%06X", ((long) color.getRGB()) & 0xffffffL);
			}
			label.setText(msg);

			firePropertyChange("selectedColor", old, color);
		}
	}
}

/**
 * カラーセルのレンダラー
 * 参考: https://github.com/haifengl/smile/blob/master/plot/src/main/java/smile/swing/table/ButtonCellRenderer.java
 */
class ColorCellRender extends DefaultTableCellRenderer {

	private static final long serialVersionUID = 1L;

	private ColorCell colorCell = new ColorCell();

	@Override
	public Component getTableCellRendererComponent(JTable table, Object value,
			boolean isSelected, boolean hasFocus, int row, int column) {
		Color color = (Color) value;
		colorCell.setSelectedColor(color);

		LineBorder focusedBorder = null;
		if (hasFocus) {
			// フォーカスがある場合はボーダーをつける
			Color colorBorder = Color.CYAN;
			focusedBorder = new LineBorder(colorBorder, 2);
		}

		colorCell.setBoxBorder(focusedBorder);

		return colorCell;
	}
}

/**
 * カラーセルを編集モードにした場合のエディタ
 */
class ColorCellEditor extends AbstractCellEditor implements TableCellEditor {

	private static final long serialVersionUID = 1L;

	private Border focusedBorder = BorderFactory.createLineBorder(Color.WHITE, 2);

	private ColorCell colorCell = new ColorCell(new ActionListener() {
		@Override
		public void actionPerformed(ActionEvent e) {
			fireEditingStopped();
		}
	});

	public Component getTableCellEditorComponent(final JTable table, final Object value,
			final boolean isSelected, final int row, final int column) {
		colorCell.setSelectedColor((Color) value);
		colorCell.setBoxBorder(focusedBorder); // 編集中はボーターをつける
		return colorCell;
	}

	public Object getCellEditorValue() {
		return colorCell.getSelectedColor();
	}
}
