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.Point;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EventListener;
import java.util.EventObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.EventListenerList;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.plaf.basic.BasicComboBoxEditor;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumnModel;

import charactermanaj.Main;
import charactermanaj.model.CustomLayerOrder;
import charactermanaj.model.CustomLayerOrderKey;
import charactermanaj.model.Layer;
import charactermanaj.model.ListChangeListener;
import charactermanaj.model.ObservableList;
import charactermanaj.model.PartsCategory;
import charactermanaj.ui.model.SimpleComboBoxModel;
import charactermanaj.ui.util.ScaleSupport;
import charactermanaj.util.LocalizedResourcePropertyLoader;

/**
 * レイヤーのカスタマイズを行う編集ダイアログ
 */
public class LayerOrderCustomizeDialog extends JDialog {

	private static final long serialVersionUID = 525988497443897372L;

	private static final String STRINGS_RESOURCE = "languages/layerordercustomizedialog";

	/**
	 * レイヤーの編集がされたときに通知されるリスナ
	 */
	public interface LayerOrderCustomizeListener extends EventListener {

		public static class Change extends EventObject {
			private static final long serialVersionUID = -6203020622537017109L;

			public Change(LayerOrderCustomizeDialog source) {
				super(source);
			}

			@Override
			public LayerOrderCustomizeDialog getSource() {
				return (LayerOrderCustomizeDialog) super.getSource();
			}
		}

		void onChange(Change e);
	}

	/**
	 * カテゴリのリスト
	 */
	private List<PartsCategory> categories;

	/**
	 * コンストラクタ
	 * @param window 親ウィンドウ
	 * @param categories カテゴリのリスト
	 */
	public LayerOrderCustomizeDialog(Window window, List<PartsCategory> categories) {
		super(window);
		try {
			setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
			addWindowListener(new WindowAdapter() {
				@Override
				public void windowClosing(WindowEvent e) {
					onClosing();
				}
			});

			this.categories = categories;
			initLayout();

		} catch (RuntimeException ex) {
			dispose();
			throw ex;
		}
	}

	/**
	 * イベントリスナ
	 */
	private final EventListenerList listeners = new EventListenerList();

	/**
	 * このダイアログのデータモデル
	 */
	private LayerOrderCustomizeDialogModel model;

	/**
	 * 変更通知リスナを登録する
	 * @param l リスナ
	 */
	public void addLayerOrderCustomizeListener(LayerOrderCustomizeListener l) {
		listeners.add(LayerOrderCustomizeListener.class, l);
	}

	/**
	 * 変更通知リスナを登録解除する
	 * @param l リスナ
	 */
	public void removeLayerOrderCustomizeListener(LayerOrderCustomizeListener l) {
		listeners.remove(LayerOrderCustomizeListener.class, l);
	}

	/**
	 * 全ての変更通知リスナに対して変更イベントを通知する。
	 * @param type 変更タイプ
	 * @param name 名前
	 */
	protected void fireEvent() {
		// Guaranteed to return a non-null array
		Object[] ll = listeners.getListenerList();
		// Process the listeners last to first, notifying
		// those that are interested in this event
		// ※ 逆順で通知するのがSwingの作法らしい。
		LayerOrderCustomizeListener.Change event = null;
		for (int i = ll.length - 2; i >= 0; i -= 2) {
			if (ll[i] == LayerOrderCustomizeListener.class) {
				// Lazily create the event:
				if (event == null) {
					event = new LayerOrderCustomizeListener.Change(this);
				}
				((LayerOrderCustomizeListener) ll[i + 1]).onChange(event);
			}
		}
	}

	public LayerOrderCustomizeDialogModel getModel() {
		return model;
	}

	/**
	 * LayerOrderCustomizeDialogModelの更新リスナ
	 */
	private final LayerOrderCustomizeDialogModel.ChangeListener modelChangeListener =
			new LayerOrderCustomizeDialogModel.ChangeListener() {
		@Override
		public void onChange(Change change) {
			CustomLayerOrderKey orderKey = change.getName();
			String name = orderKey.getDisplayName();
			if (name == null || name.isEmpty()) {
				// ※空文字の場合はcurrentプロパティの変更
				return;
			}

			switch (change.getChangeType()) {
			case ADD:
				// レイヤーパターンが追加された場合
				if (patternsModel.indexOf(orderKey) < 0) {
					patternsModel.add(orderKey);
					patternsModel.setSelectedItem(orderKey); // 登録された名前で選択しなおす為
				}
				break;

			case MODIFY:
				// レイヤーパターンが更新された場合
				break;

			case REMOVE:
				// レイヤーパターンが削除された場合
				if (name != null) {
					patternsModel.remove(orderKey);
					// 削除時は現在の選択もクリアする。(現在選択のものを削除しているので)
					patternsModel.setSelectedItem(null);
				}
				break;
			}
		}
	};

	public void setModel(LayerOrderCustomizeDialogModel model) {
		LayerOrderCustomizeDialogModel old = this.model;
		if (old != null) {
			old.removeListChangeListener(modelChangeListener);
		}

		this.model = model;
		loadModel();

		if (model != null) {
			model.addListChangeListener(modelChangeListener);
		}

		if (old == null ? model != null : !old.equals(model)) {
			firePropertyChange("model", old, model);
		}
	}

	private void loadModel() {
		patternsModel.clear();
		if (model == null) {
			return;
		}

		patternsModel.addAll(model.getPatternNames());
		patternsModel.setSelectedItem(null);

		dataModel.setList(model.getCurrentList());
		lastPatternName = null;
	}

	private JComboBox cmbPatternName = new JComboBox();

	private SimpleComboBoxModel<CustomLayerOrderKey> patternsModel = new SimpleComboBoxModel<CustomLayerOrderKey>();

	private final JTable tblLayerOrder = new JTable();

	private final LayerOrderTableModel dataModel = new LayerOrderTableModel();

	private CustomLayerOrderKey lastPatternName;

	private AbstractAction actRemove;

	private AbstractAction actSave;

	private void initLayout() {
		final Properties strings = LocalizedResourcePropertyLoader
				.getCachedInstance().getLocalizedProperties(STRINGS_RESOURCE);

		setTitle(strings.getProperty("title"));
		Container container = getContentPane();
		container.setLayout(new BorderLayout());

		JPanel selectorPanel = new JPanel(new BorderLayout(3, 3));
		selectorPanel.add(new JLabel(strings.getProperty("patternName")), BorderLayout.WEST);

		cmbPatternName.setEditable(true);
		selectorPanel.add(cmbPatternName, BorderLayout.CENTER);

		cmbPatternName.setModel(patternsModel);
		cmbPatternName.setSelectedItem(null);

		cmbPatternName.addActionListener(new AbstractAction() {
			private static final long serialVersionUID = 1L;

			@Override
			public void actionPerformed(ActionEvent e) {
				CustomLayerOrderKey item = findBy(cmbPatternName.getSelectedItem());
				if (item != null) {
					if (patternsModel.contains(item)) {
						onSelect(item);
					}
				}
			}
		});

		Box selectorBtns = Box.createHorizontalBox();

		actSave = new AbstractAction(strings.getProperty("btnSave")) {
			private static final long serialVersionUID = 1L;

			@Override
			public void actionPerformed(ActionEvent e) {
				Object inp = cmbPatternName.getSelectedItem();
				if (inp != null) {
					CustomLayerOrderKey name = findBy(inp);
					if (name == null) {
						// 既存の一覧にないので新規キーの作成
						String inpName = inp.toString();
						name = makeOrderKey(inpName);
					}
					lastPatternName = name; // discard changesの確認が出ないように先に保存済みパターン名にしておく
					onSave(name);

					// 現在の入力のパターン名で削除可能にするためUI更新を呼び出す
					onChangeComboText();
				}
			}
		};
		actRemove = new AbstractAction(strings.getProperty("btnRemove")) {
			private static final long serialVersionUID = 1L;

			@Override
			public void actionPerformed(ActionEvent e) {
				CustomLayerOrderKey name = findBy(cmbPatternName.getSelectedItem());
				if (name != null) {
					onRemove(name);
				}
			}
		};

		// コンボボックスのテキストが既存のパターン名と合致するかによって
		// ADD/REMOVEボタンを制御するためのリスナ
		final BasicComboBoxEditor cmbEditor = (BasicComboBoxEditor) cmbPatternName.getEditor();
		JTextField cmbEditorField = (JTextField) cmbEditor.getEditorComponent();
		cmbEditorField.getDocument().addDocumentListener(new DocumentListener() {
			@Override
			public void removeUpdate(DocumentEvent e) {
				changed(e);
			}

			@Override
			public void insertUpdate(DocumentEvent e) {
				changed(e);
			}

			@Override
			public void changedUpdate(DocumentEvent e) {
				changed(e);
			}

			protected void changed(DocumentEvent e) {
				onChangeComboText();
			}
		});

		selectorBtns.add(new JButton(actSave));
		selectorBtns.add(new JButton(actRemove));

		selectorPanel.add(selectorBtns, BorderLayout.EAST);
		selectorPanel.setBorder(BorderFactory.createTitledBorder(
				strings.getProperty("pattern.groupTitle")));

		container.add(selectorPanel, BorderLayout.NORTH);

		// セルの編集はフォーカスの移動でコミットする
		tblLayerOrder.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
		tblLayerOrder.setModel(dataModel);

		dataModel.addTableModelListener(new TableModelListener() {
			@Override
			public void tableChanged(TableModelEvent e) {
				fireEvent();
			}});

		JPanel tablePanel = new JPanel(new BorderLayout(3, 3));
		tablePanel.setBorder(BorderFactory.createTitledBorder(
				strings.getProperty("edittable.groupTitle")));
		JScrollPane scr = new JScrollPane(tblLayerOrder);
		scr.setPreferredSize(new Dimension(450, 250));
		tablePanel.add(scr);
		tblLayerOrder.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
		tblLayerOrder.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

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

		ScaleSupport scaleSupport = ScaleSupport.getInstance(this);
		int tableWidth = dataModel.adjustColumnModel(tblLayerOrder.getColumnModel(), scaleSupport.getManualScaleX());

		Dimension tablePrefSize = scr.getPreferredSize();
		tablePrefSize.width = tableWidth + 10;
		scr.setPreferredSize(tablePrefSize);

		AbstractAction actAddLayer = new AbstractAction(strings.getProperty("btnAdd")) {
			private static final long serialVersionUID = 1L;

			@Override
			public void actionPerformed(ActionEvent e) {
				onAddLayer();
			}
		};
		AbstractAction actDeleteLayer = new AbstractAction(strings.getProperty("btnDelete")) {
			private static final long serialVersionUID = 1L;

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

		JButton btnOK = new JButton(actAddLayer);
		JButton btnCancel = new JButton(actDeleteLayer);

		JPanel layerOpeBtns = new JPanel(new GridBagLayout());
		GridBagConstraints gbc = new GridBagConstraints();
		gbc.gridx = 0;
		gbc.gridy = 0;
		gbc.weightx = 1;
		gbc.fill = GridBagConstraints.BOTH;
		layerOpeBtns.add(btnOK, gbc);
		gbc.gridx = 0;
		gbc.gridy = 1;
		layerOpeBtns.add(btnCancel, gbc);

		gbc.gridx = 0;
		gbc.gridy = 2;
		gbc.weighty = 1;
		layerOpeBtns.add(Box.createGlue(), gbc);

		tablePanel.add(layerOpeBtns, BorderLayout.EAST);

		container.add(tablePanel, BorderLayout.CENTER);

		AbstractAction actClose = new AbstractAction(strings.getProperty("btnClose")) {
			private static final long serialVersionUID = 1L;

			@Override
			public void actionPerformed(ActionEvent e) {
				onClosing();
			}
		};
		JButton btnClose = new JButton(actClose);

		Box btns = Box.createHorizontalBox();
		btns.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 42));
		btns.add(Box.createHorizontalGlue());
		btns.add(btnClose);
		container.add(btns, BorderLayout.SOUTH);

		Toolkit tk = Toolkit.getDefaultToolkit();
		JRootPane rootPane = getRootPane();
		rootPane.setDefaultButton(btnOK);

		InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
		ActionMap am = rootPane.getActionMap();
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeLayerOrderCustomizeDialog");
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeLayerOrderCustomizeDialog");
		am.put("closeLayerOrderCustomizeDialog", actClose);

		setModel(new LayerOrderCustomizeDialogModel());

		// コンボボックスの状態からボタンのUI状態を更新する。
		onChangeComboText();

		pack();
	}

	/**
	 * パターン名コンボボックスのテキストが入力された場合に
	 * ADD/REMOVEボタンのUI状態を更新するためのハンドラ
	 */
	protected void onChangeComboText() {
		BasicComboBoxEditor cmbEditor = (BasicComboBoxEditor) cmbPatternName.getEditor();
		Object inp = cmbEditor.getItem();
		if (inp == null || (inp instanceof String && ((String) inp).trim().length() == 0)) {
			// 空の場合
			actRemove.setEnabled(false);
			actSave.setEnabled(false);
		} else {
			actRemove.setEnabled(patternsModel.contains(findBy(inp))); // 既存のパターン名と合致すれば削除可
			actSave.setEnabled(true); // 文字が入力されていれば登録・更新可
		}
	}

	/**
	 * 引数が文字列の場合、コンボボックスに登録されているパターンリストの表示名と一致するオブジェクトを取得する。
	 * なければnullを返す。
	 * 引数がCustomLayerOrderKey型の場合は、そのまま返す。
	 * nullの場合はnullを返す。
	 * @param inp 文字列またはCustomLayerOrderKey
	 * @return CustomLayerOrderKey、またはnull
	 */
	private CustomLayerOrderKey findBy(Object inp) {
		if (inp instanceof CustomLayerOrderKey) {
			return (CustomLayerOrderKey) inp;
		}
		if (inp instanceof String) {
			String inpName = (String) inp;
			int mx = patternsModel.size();
			for (int idx = 0; idx < mx; idx++) {
				CustomLayerOrderKey item = patternsModel.get(idx);
				if (item.toString().equals(inpName)) {
					return item;
				}
			}
		}
		return null;
	}

	/**
	 * キーを作成する
	 * @param displayName
	 * @return
	 */
	private CustomLayerOrderKey makeOrderKey(String displayName) {

		// 現在保持しているIDの一覧を取得する
		Set<String> usedIds = new HashSet<String>();
		int mx = patternsModel.size();
		for (int idx = 0; idx < mx; idx++) {
			CustomLayerOrderKey key = patternsModel.get(idx);
			usedIds.add(key.getId());
		}

		// IDとしてふさわしいように空白は除外しておく
		String canonicalName = displayName.replace(" ", "_");
		if (!canonicalName.matches("[A-Za-z0-9_]+")) {
			// アルファベット以外が含まれている場合はランダムなUUIDにする
			canonicalName = UUID.randomUUID().toString();
		}

		// 名前をもとにIDを衝突しないように振る
		String id = canonicalName;
		for (int idx = 2;; idx++) {
			if (!usedIds.contains(id)) {
				// 既存のIDではないのでOK.
				break;
			}
			id = canonicalName + idx;
		}

		// 現在のロケールでローカライズ文字列を構築しておく
		Map<String, String> langMap = new HashMap<String, String>();
		Locale locale = Locale.getDefault();
		String lang = locale.getLanguage();
		langMap.put(lang, displayName);
		langMap.put(CustomLayerOrderKey.DEFAULT_NAME_KEY, displayName);

		return new CustomLayerOrderKey(id, displayName, langMap);
	}

	/**
	 * カラム定義
	 */
	private enum ColumnDef {
		CATEGORY("column.category", String.class, 120) {
			@Override
			public Object getValue(CustomLayerOrder item) {
				return item.getCategory().getLocalizedCategoryName();
			}
		},
		LAYER("column.layer", String.class, 120) {
			@Override
			public Object getValue(CustomLayerOrder item) {
				return item.getLayer().getLocalizedName();
			}
		},
		DEFAULT_ORDER("column.defaultOrder", Integer.class, 80) {
			@Override
			public Object getValue(CustomLayerOrder item) {
				return item.getLayer().getOrder();
			}
		},
		CUSTOM_ORDER("column.order", Float.class, 80, true) {
			@Override
			public Object getValue(CustomLayerOrder item) {
				return item.getLayerOrder();
			}
		};

		private final String resourceKey;

		private final Class<?> dataType;

		private final int prefWidth;

		private final boolean editable;

		ColumnDef(String resourceKey, Class<?> dataType, int prefWidth) {
			this(resourceKey, dataType, prefWidth, false);
		}

		ColumnDef(String resourceKey, Class<?> dataType, int prefWidth, boolean editable) {
			this.resourceKey = resourceKey;
			this.dataType = dataType;
			this.prefWidth = prefWidth;
			this.editable = editable;
		}

		public String getResourceKey() {
			return resourceKey;
		}

		public int getPrefWidth() {
			return prefWidth;
		}

		public abstract Object getValue(CustomLayerOrder item);

		public Class<?> getDataType() {
			return dataType;
		}

		public boolean isEditable() {
			return editable;
		}
	}

	/**
	 * レイヤー編集テーブルのテーブルモデル
	 */
	protected static class LayerOrderTableModel extends AbstractTableModel {

		private static final long serialVersionUID = 1L;

		private final Properties strings = LocalizedResourcePropertyLoader
				.getCachedInstance().getLocalizedProperties(STRINGS_RESOURCE);

		/**
		 * カラム定義リスト
		 */
		private static final ColumnDef[] columns = ColumnDef.values();

		/**
		 * テーブルに設定されているレイヤーリスト。
		 * このリストでデータの追加更新削除を行い、変更リスナによってUI更新を行わせる。
		 */
		private ObservableList<CustomLayerOrder> layerOrderList;

		/**
		 * テーブルに設定されているレイヤーリストが変更された場合に
		 * テーブルモデルが更新されたことをUIに通知するための接続用リスナ。
		 */
		private final ListChangeListener<CustomLayerOrder> changeListener = new ListChangeListener<CustomLayerOrder>() {
			@Override
			public void onChanged(Change<? extends CustomLayerOrder> c) {
				int idx = c.getIndex();
				if (idx >= 0) {
					switch (c.getType()) {
					case ADD:
						fireTableRowsInserted(idx, idx);
						return;

					case MODIFIY:
						fireTableRowsUpdated(idx, idx);
						return;

					case REMOVE:
						fireTableRowsDeleted(idx, idx);
						return;

					default:
						break;
					}
				}
				fireTableDataChanged();
			}
		};

		/**
		 * コンストラクタ
		 */
		public LayerOrderTableModel() {
			setList(new ObservableList<CustomLayerOrder>());
		}

		/**
		 * カラム幅を設定する。
		 * @param columnModel
		 */
		public int adjustColumnModel(TableColumnModel columnModel, double scale) {
			int total = 0;
			for (int idx = 0; idx < columns.length; idx++) {
				int width = (int)(columns[idx].getPrefWidth() * scale);
				columnModel.getColumn(idx).setPreferredWidth(width);
				total += width;
			}
			return total;
		}

		/**
		 * 現在編集中のテーブル上のレイヤーリストを差し替える。
		 * レイヤーリストはcategory, layerの順序となるようにソートされる。
		 * @param layerOrderList
		 */
		public final void setList(ObservableList<CustomLayerOrder> layerOrderList) {
			if (this.layerOrderList != null) {
				this.layerOrderList.removeListChangeListener(changeListener);
			}

			// テーブルには既定の順序で並べる
			Collections.sort(layerOrderList, new Comparator<CustomLayerOrder>() {
				@Override
				public int compare(CustomLayerOrder o1, CustomLayerOrder o2) {
					int ret = o1.getCategory().compareTo(o2.getCategory());
					if (ret == 0) {
						ret = o1.getLayer().compareTo(o2.getLayer());
					}
					if (ret == 0) {
						ret = Float.compare(o1.getLayerOrder(), o2.getLayerOrder());
					}
					return ret;
				}
			});

			this.layerOrderList = layerOrderList;
			layerOrderList.addListChangeListener(changeListener);

			fireTableDataChanged();
		}

		/**
		 * 現在編集中のテーブル上のレイヤーリストを取得する。
		 * @return
		 */
		public final List<CustomLayerOrder> getList() {
			return layerOrderList;
		}

		/**
		 * レイヤーを追加する。
		 * レイヤーは既存のレイヤーのリストに対してcategory, layerの順序となるように挿入される。
		 * @param category
		 * @param layer
		 * @param layerOrder
		 */
		public void add(PartsCategory category, Layer layer, float layerOrder) {
			if (category == null || layer == null) {
				throw new NullPointerException("category, layerにnullは指定できません。");
			}

			CustomLayerOrder item = null;
			int idx = 0;
			for (; idx < layerOrderList.size(); idx++) {
				CustomLayerOrder selItem = layerOrderList.get(idx);

				PartsCategory selCategory = selItem.getCategory();
				Layer selLayer = selItem.getLayer();

				int ret = category.compareTo(selCategory);
				if (ret == 0) {
					ret = layer.compareTo(selLayer);
				}

				if (ret == 0) {
					// 一致するアイテムが発見された。
					item = selItem;
					break;

				} else if (ret < 0) {
					// 自分より大きなアイテムを発見
					break;
				}
			}

			if (item == null) {
				item = new CustomLayerOrder();
				item.setCategory(category);
				item.setLayer(layer);
				item.setLayerOrder(layerOrder);
				layerOrderList.add(idx, item);

			} else {
				item.setLayerOrder(layerOrder);
			}
		}

		public void remove(int rowIndex) {
			layerOrderList.remove(rowIndex);
		}

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

		@Override
		public int getColumnCount() {
			return columns.length;
		}

		@Override
		public String getColumnName(int column) {
			return strings.getProperty(columns[column].getResourceKey());
		}

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

		@Override
		public Object getValueAt(int rowIndex, int columnIndex) {
			CustomLayerOrder item = layerOrderList.get(rowIndex);
			return columns[columnIndex].getValue(item);
		}

		@Override
		public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
			if (columns[columnIndex].isEditable()) {
				CustomLayerOrder item = layerOrderList.get(rowIndex);
				if (aValue != null) {
					item.setLayerOrder(((Number) aValue).floatValue());
				}
			}
		}

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

	/**
	 * ダイアログを閉じる
	 */
	protected void onClosing() {
		if (canDiscardChanges()) {
			dispose();
		}
	}

	/**
	 * 現在編集中のカスタムレイヤーパターンを取得する。
	 * @return
	 */
	public List<CustomLayerOrder> getEdittingCustomLayerOrderList() {
		return dataModel.getList();
	}

	/**
	 * 現在の編集を破棄してよいか確認する。
	 * パターンをロードしている場合は変更があれば保存するか問い合わせる。
	 * パターンをロードしていない場合は破棄してよいか問い合わせる。
	 * @return 破棄して良い場合(もしくは保存した場合)
	 */
	private boolean canDiscardChanges() {
		final Properties strings = LocalizedResourcePropertyLoader
				.getCachedInstance().getLocalizedProperties(STRINGS_RESOURCE);

		if (lastPatternName == null) {
			// パターンをロードしていない場合
			ObservableList<CustomLayerOrder> oldLayerOrderList = model.getCurrentList();
			if (!oldLayerOrderList.equals(dataModel.getList())) {
				int ret = JOptionPane.showConfirmDialog(this,
						strings.getProperty("confirm.discardChanges.message"),
						strings.getProperty("confirm.discardChanges.title"),
						JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
				if (ret != JOptionPane.YES_OPTION) {
					// コンボボックスの選択を空にして戻す
					patternsModel.setSelectedItem(null);
					return false; // 破棄不可
				}
			}
		} else {
			// すでにパターンをロードしており、且つ、レイヤーリストを修正している場合
			// 新たに選択したパターンのロード前に、現在の編集を保存するか確認する。
			ObservableList<CustomLayerOrder> oldLayerOrderList = model.getCopy(lastPatternName);
			if (oldLayerOrderList == null) {
				oldLayerOrderList = model.createObservableList();
			}
			if (!oldLayerOrderList.equals(dataModel.getList())) {
				// パターンを編集中の場合、保存するか問い合わせる
				int ret = JOptionPane.showConfirmDialog(this,
						strings.getProperty("confirm.savechanges.message") + lastPatternName,
						strings.getProperty("confirm.savechanges.title"),
						JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
				if (ret == JOptionPane.YES_OPTION) {
					onSave(lastPatternName); // 現在の編集を保存する
				}
			}
		}
		return true; // 現在の編集は破棄して良い
	}

	/**
	 * レイヤーパターン名を選択した場合
	 * @param name
	 */
	protected void onSelect(CustomLayerOrderKey name) {
		if (canDiscardChanges()) {
			// 選択したパターンのロード
			ObservableList<CustomLayerOrder> layerOrderList = model.getCopy(name);
			if (layerOrderList == null) {
				layerOrderList = model.createObservableList();
			}

			dataModel.setList(layerOrderList);
			CustomLayerOrderKey old = this.lastPatternName;
			this.lastPatternName = name;

			firePropertyChange("lastPatternName", old, name);
		}
	}

	/**
	 * 最後に選択したパターン名を取得する
	 * @return
	 */
	public CustomLayerOrderKey getLastPatternName() {
		return lastPatternName;
	}

	/**
	 * 現在の編集をパターン名をつけて保存し、現在の選択パターン名とする。
	 * @param name
	 */
	protected void onSave(CustomLayerOrderKey name) {
		model.put(name, dataModel.getList());

		CustomLayerOrderKey old = this.lastPatternName;
		if (old == null ? name != null : !old.equals(name)) {
			this.lastPatternName = name;
			firePropertyChange("lastPatternName", old, name);
		}
	}

	/**
	 * 指定したパターン名を削除して、選択状態を解除する。
	 * @param name
	 */
	protected void onRemove(CustomLayerOrderKey name) {
		final Properties strings = LocalizedResourcePropertyLoader
				.getCachedInstance().getLocalizedProperties(STRINGS_RESOURCE);

		int ret = JOptionPane.showConfirmDialog(this,
				strings.getProperty("confirm.removepattern.message") + lastPatternName,
				strings.getProperty("confirm.removepattern.title"),
				JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
		if (ret != JOptionPane.YES_OPTION) {
			return;
		}

		model.setCurrentList(dataModel.getList()); // 無名の編集状態になるため、discard changesで判定されないように。
		model.remove(name);

		CustomLayerOrderKey old = this.lastPatternName;
		if (old != null) {
			this.lastPatternName = null;
			firePropertyChange("lastPatternName", old, null);
		}
	}

	/**
	 * レイヤーの追加ダイアログのデータモデル
	 */
	public static class AddLayerDialogModel {

		public static final String PARTS_CATEGORIES = "partsCategories";

		public static final String RESULT = "result";

		public static final String SELECTED_PARTS_CATEGORY = "partsCategory";

		public static final String SELECTED_LAYER = "layer";

		public static final String DEFAULT_ORDER = "defaultOrder";

		public static final String ORDER = "order";

		private final PropertyChangeSupport propChangeSupport = new PropertyChangeSupport(this);

		/**
		 * コンボボックスの候補とするカテゴリのリスト
		 */
		private List<PartsCategory> partsCategories = new ArrayList<PartsCategory>();

		/**
		 * ダイアログを閉じたときのOK/CANCEL状態
		 */
		private boolean result;

		/**
		 * 選択されているカテゴリ、なければnull
		 */
		private PartsCategory selectedPartsCategory;

		/**
		 * 選択されているレイヤー、なければnull
		 */
		private Layer selectedLayer;

		/**
		 * デフォルト順序
		 */
		private int defaultOrder;

		/**
		 * 順序
		 */
		private float order;

		/**
		 * 入力確定可能であるか？
		 * @return
		 */
		public boolean isValid() {
			return selectedPartsCategory != null && selectedLayer != null;
		}

		public List<PartsCategory> getPartsCategories() {
			return partsCategories;
		}

		public void setPartsCategories(List<PartsCategory> partsCategories) {
			List<PartsCategory> old = this.partsCategories;
			this.partsCategories = partsCategories;
			propChangeSupport.firePropertyChange(PARTS_CATEGORIES, old, partsCategories);
		}

		public void setResult(boolean result) {
			boolean old = this.result;
			this.result = result;
			propChangeSupport.firePropertyChange(RESULT, old, result);
		}

		public boolean isResult() {
			return result;
		}

		public void setSelectedPartsCategory(PartsCategory selectedPartsCategory) {
			PartsCategory old = this.selectedPartsCategory;
			if (!(old == null && selectedPartsCategory == null) || (old != null && !old.equals(selectedPartsCategory))) {
				this.selectedPartsCategory = selectedPartsCategory;
				propChangeSupport.firePropertyChange(SELECTED_PARTS_CATEGORY, old, selectedPartsCategory);
			}
		}

		public PartsCategory getSelectedPartsCategory() {
			return selectedPartsCategory;
		}

		public void setSelectedLayer(Layer selectedLayer) {
			Layer old = this.selectedLayer;
			if (!(old == null && selectedLayer == null) || (old != null && !old.equals(selectedLayer))) {
				this.selectedLayer = selectedLayer;
				propChangeSupport.firePropertyChange(SELECTED_LAYER, old, selectedLayer);
			}
		}

		public Layer getSelectedLayer() {
			return selectedLayer;
		}

		public void setOrder(float order) {
			float old = this.order;
			if (old != order) {
				this.order = order;
				propChangeSupport.firePropertyChange(ORDER, old, order);
			}
		}

		public float getOrder() {
			return order;
		}

		public void setDefaultOrder(int defaultOrder) {
			int old = this.defaultOrder;
			this.defaultOrder = defaultOrder;
			if (old != defaultOrder) {
				this.defaultOrder = defaultOrder;
				propChangeSupport.firePropertyChange(DEFAULT_ORDER, old, defaultOrder);
			}
		}

		public int getDefaultOrder() {
			return defaultOrder;
		}

		public void addPropertyChangeListener(PropertyChangeListener listener) {
			propChangeSupport.addPropertyChangeListener(listener);
		}

		public void removePropertyChangeListener(PropertyChangeListener listener) {
			propChangeSupport.removePropertyChangeListener(listener);
		}

		public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
			propChangeSupport.addPropertyChangeListener(propertyName, listener);
		}

		public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
			propChangeSupport.removePropertyChangeListener(propertyName, listener);
		}
	}

	/**
	 * レイヤーの追加ダイアログ
	 */
	public static class AddLayerDialog extends JDialog {
		private static final long serialVersionUID = 1L;

		public static final String MODEL = "model";

		public AddLayerDialog(JDialog parent, boolean modal) {
			super(parent, modal);
			try {
				initLayout();

			} catch (RuntimeException ex) {
				dispose();
				throw ex;
			}
		}

		private SimpleComboBoxModel<Layer> cmbLayerModel = new SimpleComboBoxModel<Layer>();

		private SimpleComboBoxModel<PartsCategory> cmbCategoryModel = new SimpleComboBoxModel<PartsCategory>();

		private JTextField txtDefaultOrder = new JTextField();

		private JTextField txtOrder = new JTextField();

		private AbstractAction actOK;

		private void initLayout() {
			final Properties strings = LocalizedResourcePropertyLoader
					.getCachedInstance().getLocalizedProperties(STRINGS_RESOURCE);

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

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

			JPanel editPanel = new JPanel(new GridBagLayout());
			GridBagConstraints gbc = new GridBagConstraints();
			gbc.insets = new Insets(3, 3, 3, 3);

			JComboBox cmbCategory = new JComboBox(cmbCategoryModel);

			JComboBox cmbLayer = new JComboBox(cmbLayerModel);
			cmbLayer.setRenderer(new ListCellRenderer() {

				private JLabel label = new JLabel();

				@Override
				public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
						boolean cellHasFocus) {
					Layer selectedLayer = (Layer) value;

					// 背景色透過制御
					label.setOpaque(isSelected && index >= 0);

					if (isSelected) {
						label.setBackground(list.getSelectionBackground());
						label.setForeground(list.getSelectionForeground());
					} else {
						label.setBackground(list.getBackground());
						label.setForeground(list.getForeground());
					}

					if (selectedLayer == null) {
						label.setText("");
					} else {
						label.setFont(list.getFont());
						label.setText(selectedLayer.getLocalizedName());
					}
					return label;
				}
			});

			gbc.gridx = 0;
			gbc.gridy = 0;
			editPanel.add(new JLabel(strings.getProperty("column.category")), gbc);

			gbc.gridx = 0;
			gbc.gridy = 1;
			editPanel.add(new JLabel(strings.getProperty("column.layer")), gbc);

			gbc.gridx = 0;
			gbc.gridy = 2;
			editPanel.add(new JLabel(strings.getProperty("column.defaultOrder")), gbc);

			gbc.gridx = 0;
			gbc.gridy = 3;
			editPanel.add(new JLabel(strings.getProperty("column.order")), gbc);

			gbc.gridx = 1;
			gbc.gridy = 0;
			gbc.weightx = 1;
			gbc.fill = GridBagConstraints.BOTH;
			editPanel.add(cmbCategory, gbc);

			gbc.gridx = 1;
			gbc.gridy = 1;
			editPanel.add(cmbLayer, gbc);

			gbc.gridx = 1;
			gbc.gridy = 2;
			editPanel.add(txtDefaultOrder, gbc);
			txtDefaultOrder.setEditable(false);

			gbc.gridx = 1;
			gbc.gridy = 3;
			editPanel.add(txtOrder, gbc);

			gbc.gridx = 0;
			gbc.gridy = 4;
			gbc.weighty = 1;
			editPanel.add(Box.createGlue(), gbc);

			container.add(editPanel, BorderLayout.CENTER);

			actOK = new AbstractAction(strings.getProperty("btnOK")) {
				private static final long serialVersionUID = 1L;

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

			AbstractAction actCancel = new AbstractAction(strings.getProperty("btnCancel")) {
				private static final long serialVersionUID = 1L;

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

			JButton btnOK = new JButton(actOK);
			JButton btnCancel = new JButton(actCancel);

			Box btnPanel = Box.createHorizontalBox();
			btnPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 42));
			btnPanel.add(Box.createHorizontalGlue());

			if (Main.isLinuxOrMacOSX()) {
				btnPanel.add(btnCancel);
				btnPanel.add(btnOK);
			} else {
				btnPanel.add(btnOK);
				btnPanel.add(btnCancel);
			}

			container.add(btnPanel, BorderLayout.SOUTH);

			// カタログの選択が変更された場合、初期化中のイベントでなければハンドラに渡す
			cmbCategory.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					if (updaingCmbCategoryModel.get() == 0) {
						onChangeSelectCategory((PartsCategory) cmbCategoryModel.getSelectedItem());
					}
				}
			});

			// レイヤーの選択が変更された場合、初期渦中のイベントでなければハンドラに渡す
			cmbLayer.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					if (updaingCmbLayerModel.get() == 0) {
						onChangeSelectLayer((Layer) cmbLayerModel.getSelectedItem());
					}
				}
			});

			// レイヤー順序テキストボックスの入力イベントが初期化中のイベントでなければ
			// UIモデルの更新のハンドラに引き渡す。
			txtOrder.getDocument().addDocumentListener(new DocumentListener() {
				@Override
				public void removeUpdate(DocumentEvent e) {
					onChange(e);
				}

				@Override
				public void insertUpdate(DocumentEvent e) {
					onChange(e);
				}

				@Override
				public void changedUpdate(DocumentEvent e) {
					onChange(e);
				}

				protected void onChange(DocumentEvent e) {
					if (updaingTxtOrderModel.get() == 0) {
						onChangeTxtOrder(txtOrder.getText());
					}
				}
			});

			// AddLayerDialogModelのプロパティ変更を監視し、対応するUIを更新するためのリスナ
			modelPropChangeListener = new PropertyChangeListener() {
				@Override
				public void propertyChange(PropertyChangeEvent evt) {
					String propName = evt.getPropertyName();
					if (AddLayerDialogModel.SELECTED_PARTS_CATEGORY.equals(propName)) {
						updateCmbCategory();
					} else if (AddLayerDialogModel.SELECTED_LAYER.equals(propName)) {
						updateCmbLayer();
					} else if (AddLayerDialogModel.DEFAULT_ORDER.equals(propName)) {
						updateTxtDefaultOrder();
					} else if (AddLayerDialogModel.ORDER.equals(propName)) {
						updateTxtOrder();
					}

					// モデルの状態をOKボタンの状態に反映する
					updateButtonState();
				}
			};

			// デフォルトキーの設定
			Toolkit tk = Toolkit.getDefaultToolkit();
			JRootPane rootPane = getRootPane();
			rootPane.setDefaultButton(btnOK);

			InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
			ActionMap am = rootPane.getActionMap();
			im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeAddLayerDialog");
			im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeAddLayerDialog");
			am.put("closeAddLayerDialog", actCancel);

			// コンボボックスの推奨幅を明示的に指定する。(言語によっては幅が小さくなりすぎるため)
			ScaleSupport scaleSupport = ScaleSupport.getInstance(this);
			Dimension prefSize = cmbCategory.getPreferredSize();
			prefSize.width = (int)(200 * scaleSupport.getManualScaleX());
			cmbCategory.setPreferredSize(prefSize);
			pack();

			// 初期化モデルの設定(dummy)
			setModel(new AddLayerDialogModel());
		}

		/**
		 * AddLayerDialogModelのプロパティ変更を監視するリスナ
		 */
		private PropertyChangeListener modelPropChangeListener;

		/**
		 * レイヤー追加ダイアログのモデル
		 */
		private AddLayerDialogModel model = new AddLayerDialogModel();

		public AddLayerDialogModel getModel() {
			return model;
		}

		public void setModel(AddLayerDialogModel model) {
			if (model == null) {
				throw new NullPointerException();
			}

			AddLayerDialogModel old = this.model;
			if (old.equals(model)) {
				// 同じなので何もしない
				return;
			}

			old.removePropertyChangeListener(modelPropChangeListener);
			this.model = model;
			model.addPropertyChangeListener(modelPropChangeListener);

			// モデルの内容でUIを更新する。
			// ※ UI更新中はイベントの発生を無視させる
			initCmbCategory();
			initCmbLayer();
			initDefaultOrder();
			initOrder();

			updateButtonState();

			firePropertyChange(MODEL, old, model);
		}

		/**
		 * cmbCategoryModelのUIの初期化中であることを表す。
		 * UIのイベントリスナで初期化中のイベントは別コンポーネントに連携させないようにする。
		 */
		private final AtomicInteger updaingCmbCategoryModel = new AtomicInteger();

		protected void initCmbCategory() {
			updaingCmbCategoryModel.incrementAndGet();
			try {
				List<PartsCategory> categories = model.getPartsCategories();
				cmbCategoryModel.setAll(categories);
				cmbCategoryModel.setSelectedItem(model.getSelectedPartsCategory());
			} finally {
				updaingCmbCategoryModel.decrementAndGet();
			}
		}

		protected void onChangeSelectCategory(PartsCategory selectedCategory) {
			model.setSelectedPartsCategory(selectedCategory);
			model.setSelectedLayer(null);
		}

		/**
		 * モデルのCategory現在値が変更されたので画面に反映する
		 */
		protected void updateCmbCategory() {
			PartsCategory selCategory = model.getSelectedPartsCategory();
			cmbCategoryModel.setSelectedItem(selCategory);
			initCmbLayer();
		}

		/**
		 * cmbLayerModelのUIの初期化中であることを表す。
		 * UIのイベントリスナで初期化中のイベントは別コンポーネントに連携させないようにする。
		 */
		private final AtomicInteger updaingCmbLayerModel = new AtomicInteger();

		protected void initCmbLayer() {
			updaingCmbLayerModel.incrementAndGet();
			try {
				PartsCategory selCategory = model.getSelectedPartsCategory();
				if (selCategory != null) {
					cmbLayerModel.setAll(selCategory.getLayers());
				} else {
					cmbLayerModel.setAll(null);
				}
				cmbLayerModel.setSelectedItem(model.getSelectedLayer());

			} finally {
				updaingCmbLayerModel.decrementAndGet();
			}
		}

		protected void onChangeSelectLayer(Layer selectedLayer) {
			model.setSelectedLayer(selectedLayer);

			// レイヤーの既定の順序を設定する
			int defaultOrder = 0;
			if (selectedLayer != null) {
				defaultOrder = selectedLayer.getOrder();
			}
			model.setDefaultOrder(defaultOrder);
			model.setOrder(defaultOrder);
		}

		/**
		 * モデルのLayer現在値が変更されたので画面に反映する
		 */
		protected void updateCmbLayer() {
			Layer selLayer = model.getSelectedLayer();
			cmbLayerModel.setSelectedItem(selLayer);
		}

		private void initDefaultOrder() {
			// txtDefaultOrderは編集不可のテキストボックスでイベントは監視していない。
			if (model.getSelectedLayer() != null) {
				txtDefaultOrder.setText(Integer.toString(model.getDefaultOrder()));
			} else {
				txtDefaultOrder.setText("");
			}
		}

		/**
		 * txtOrderのUIの初期化中であることを表す。
		 * UIのイベントリスナで初期化中のイベントは別コンポーネントに連携させないようにする。
		 */
		private final AtomicInteger updaingTxtOrderModel = new AtomicInteger();

		/**
		 * txtOrderを初期設定する
		 * この処理期間中は{@link #updaingCmbLayerModel}を0以上とすることで
		 * コンボボックスからのイベントが、この初期設定によるものであることを判別できる。
		 */
		private void initOrder() {
			updaingTxtOrderModel.incrementAndGet();
			try {
				txtOrder.setText(Float.toString(model.getOrder()));
				updateTxtOrderState(false);

			} finally {
				updaingTxtOrderModel.decrementAndGet();
			}
		}

		private void updateTxtOrderState(boolean error) {
			Color color;
			if (error) {
				color = Color.RED;
			} else {
				color = UIManager.getColor("TextField.background");
			}
			txtOrder.setBackground(color);
		}

		/**
		 * ユーザー操作によりテキストボックスが変更された場合
		 * @param text
		 */
		protected void onChangeTxtOrder(String text) {
			boolean error = true;
			try {
				if (text != null && text.length() > 0) {
					float order = Float.parseFloat(text);
					model.setOrder(order);
					error = false;
				}
			} catch (RuntimeException ex) {
				// なにもしない
			}

			updateTxtOrderState(error);
		}

		protected void updateTxtDefaultOrder() {
			// DefaultOrderテキストボックスは編集はしないのでinitと同じで良い
			initDefaultOrder();
		}

		/**
		 * モデルのorderが変更されたので画面に反映する
		 */
		protected void updateTxtOrder() {
			float order = model.getOrder();
			Float old;
			try {
				old = Float.parseFloat(txtOrder.getText());
			} catch (RuntimeException ex) {
				old = null;
			}
			if (old == null || old != order) {
				// 現在の入力している値と数値的に異なる場合のみ再設定する
				// (自分のテキストボックス変更イベントによりモデルが変更され、その結果、
				// テキストボックスへの値変更が呼び出された場合に状態エラーが発生する。)
				txtOrder.setText(Float.toString(order));
				updateTxtOrderState(false);
			}
		}

		protected void updateButtonState() {
			actOK.setEnabled(model.isValid());
		}

		protected void onOK() {
			if (!model.isValid()) {
				return;
			}
			model.setResult(true);
			dispose();
		}

		protected void onCancel() {
			model.setResult(false);
			dispose();
		}
	}

	/**
	 * レイヤーの追加ダイアログを開く
	 */
	protected void onAddLayer() {
		AddLayerDialogModel model = new AddLayerDialogModel();
		model.setPartsCategories(categories);

		final AddLayerDialog dlg = new AddLayerDialog(this, true);
		dlg.setModel(model);

		// 中央に配置
		Point loc = getLocationOnScreen();
		int centerX = loc.x + getWidth() / 2;
		int centerY = loc.y + getHeight() / 2;

		int x = centerX - dlg.getWidth() / 2;
		int y = centerY - dlg.getHeight() / 2;
		dlg.setLocation(x, y);
		dlg.setVisible(true);

		if (model.isResult()) {
			PartsCategory category = model.getSelectedPartsCategory();
			Layer layer = model.getSelectedLayer();
			float layerOrder = model.getOrder();
			dataModel.add(category, layer, layerOrder);
		}
	}

	/**
	 * 選択レイヤーを削除する
	 */
	protected void onDeleteLayer() {
		int selrow = tblLayerOrder.getSelectedRow();
		if (selrow >= 0) {
			dataModel.remove(selrow);
		}
	}
}

/**
 * レイヤーカスタマイズダイアログのデータモデル
 */
class LayerOrderCustomizeDialogModel {

	/**
	 * 保持しているレイヤーパターンの登録・変更・削除を通知されるリスナ
	 */
	public interface ChangeListener extends EventListener {

		/**
		 * 通知タイプ
		 */
		public enum ChangeType {
			ADD, MODIFY, REMOVE
		}

		/**
		 * 無名のキー名
		 */
		public static final CustomLayerOrderKey ANONYMOUS = new CustomLayerOrderKey("", "", null);

		/**
		 * イベント
		 */
		public class Change extends EventObject {

			private static final long serialVersionUID = -4578290841626577210L;

			/**
			 * レイヤーパターン名
			 */
			private CustomLayerOrderKey name;

			/**
			 * 変更タイプ
			 */
			private ChangeType changeType;

			public Change(LayerOrderCustomizeDialogModel source, CustomLayerOrderKey name, ChangeType changeType) {
				super(source);
				this.name = name;
				this.changeType = changeType;
			}

			public CustomLayerOrderKey getName() {
				return name;
			}

			public ChangeType getChangeType() {
				return changeType;
			}

			@Override
			public LayerOrderCustomizeDialogModel getSource() {
				return (LayerOrderCustomizeDialogModel) super.getSource();
			}

			@Override
			public String toString() {
				return "Change [name=" + name + ", changeType=" + changeType + "]";
			}
		}

		void onChange(Change change);
	}

	/**
	 * イベントリスナのリスト
	 */
	private final EventListenerList listeners = new EventListenerList();

	/**
	 * レイヤー名パターンを保持する。(patternsMapから導出され、以後、監視される。)
	 */
	private final ObservableList<CustomLayerOrderKey> patternNames = new ObservableList<CustomLayerOrderKey>();

	/**
	 * レイヤーパターン名をキーとして、レイヤーの構成リストを値とするマップ。
	 * キーはpatternNamesの監視リストと連携する。
	 */
	private final Map<CustomLayerOrderKey, List<CustomLayerOrder>> patternsMap =
			new HashMap<CustomLayerOrderKey, List<CustomLayerOrder>>();

	/**
	 * レイヤー編集画面の初期値
	 */
	private List<CustomLayerOrder> currentList = Collections.emptyList();

	/**
	 * コンストラクタ
	 */
	public LayerOrderCustomizeDialogModel() {
		patternNames.addListChangeListener(new ListChangeListener<CustomLayerOrderKey>() {
			@Override
			public void onChanged(Change<? extends CustomLayerOrderKey> c) {
				switch (c.getType()) {
				case ADD:
					// パターン名が追加された場合は、あわせて導出元のマップも追加する
					CustomLayerOrderKey newName = c.getNewValue();
					if (!patternsMap.containsKey(newName)) {
						// まだマップに存在しなければ空のリストを作成しておく
						patternsMap.put(newName, new ArrayList<CustomLayerOrder>());
						fireEvent(ChangeListener.ChangeType.ADD, newName);
					}
					break;

				case REMOVE:
					// パターン名が削除された場合は、導出元のマップも削除する。
					CustomLayerOrderKey oldName = c.getOldValue();
					patternsMap.remove(oldName);
					fireEvent(ChangeListener.ChangeType.REMOVE, oldName);
					break;
				}
			}
		});
	}

	/**
	 * 変更通知リスナを登録する
	 * @param l リスナ
	 */
	public void addListChangeListener(ChangeListener l) {
		listeners.add(ChangeListener.class, l);
	}

	/**
	 * 変更通知リスナを登録解除する
	 * @param l リスナ
	 */
	public void removeListChangeListener(ChangeListener l) {
		listeners.remove(ChangeListener.class, l);
	}

	/**
	 * 全ての変更通知リスナに対して変更イベントを通知する。
	 * @param type 変更タイプ
	 * @param name 名前
	 */
	protected void fireEvent(ChangeListener.ChangeType type, CustomLayerOrderKey name) {
		// Guaranteed to return a non-null array
		Object[] ll = listeners.getListenerList();
		// Process the listeners last to first, notifying
		// those that are interested in this event
		// ※ 逆順で通知するのがSwingの作法らしい。
		ChangeListener.Change event = null;
		for (int i = ll.length - 2; i >= 0; i -= 2) {
			if (ll[i] == ChangeListener.class) {
				// Lazily create the event:
				if (event == null) {
					event = new ChangeListener.Change(this, name, type);
				}
				((ChangeListener) ll[i + 1]).onChange(event);
			}
		}
	}

	public ObservableList<CustomLayerOrder> getCurrentList() {
		return copyWithoutListeners(currentList);
	}

	public void setCurrentList(List<CustomLayerOrder> currentList) {
		if (!currentList.equals(this.currentList)) {
			this.currentList = copyWithoutListeners(
					currentList != null ? currentList : new ArrayList<CustomLayerOrder>());
			fireEvent(ChangeListener.ChangeType.MODIFY, ChangeListener.ANONYMOUS);
		}
	}

	public ObservableList<CustomLayerOrderKey> getPatternNames() {
		return patternNames;
	}

	public ObservableList<CustomLayerOrder> getCopy(CustomLayerOrderKey name) {
		return copyWithoutListeners(patternsMap.get(name));
	}

	public void put(CustomLayerOrderKey name, List<CustomLayerOrder> layerOrderList) {
		boolean exist = patternsMap.containsKey(name);
		patternsMap.put(name, copyWithoutListeners(layerOrderList));
		if (!exist) {
			patternNames.add(name);
			fireEvent(ChangeListener.ChangeType.ADD, name);
		} else {
			fireEvent(ChangeListener.ChangeType.MODIFY, name);
		}
	}

	public void remove(CustomLayerOrderKey name) {
		if (patternNames.contains(name)) {
			// 名前の削除によりリスナーでマップも削除される
			patternNames.remove(name);
		}
	}

	/**
	 * 監視可能なリストをコピーして返す。ただし、既存のリスナーはコピーされない。
	 * 返される可能可能リストは要素の変更を監視してリストの変更通知として返すように設定される。
	 * @param src
	 * @return
	 */
	public ObservableList<CustomLayerOrder> copyWithoutListeners(List<CustomLayerOrder> src) {
		if (src == null) {
			return null;
		}
		ObservableList<CustomLayerOrder> list = createObservableList();
		for (CustomLayerOrder item : src) {
			list.add(item.copy());
		}
		return list;
	}

	/**
	 * 内包する要素の変更通知をリストの更新通知に接続する要素を含めた監視可能なリストを作成して返す。
	 * @return
	 */
	public ObservableList<CustomLayerOrder> createObservableList() {
		final List<CustomLayerOrder> rawList = new ArrayList<CustomLayerOrder>();
		final ObservableList<CustomLayerOrder> obsList = new ObservableList<CustomLayerOrder>(rawList);
		final PropertyChangeListener listener = new PropertyChangeListener() {
			@Override
			public void propertyChange(PropertyChangeEvent evt) {
				CustomLayerOrder source = (CustomLayerOrder) evt.getSource();
				int index = rawList.indexOf(source);
				obsList.fireEvent(ListChangeListener.ChangeType.MODIFIY, index, null, source);
			}
		};

		ObservableList.Hook<CustomLayerOrder> hook = new ObservableList.Hook<CustomLayerOrder>() {
			@Override
			public void add(CustomLayerOrder item) {
				item.addPropertyChangeListener(listener);
			}
			@Override
			public void remove(CustomLayerOrder item) {
				item.removePropertyChangeListener(listener);
			}
		};
		obsList.setHook(hook);

		return obsList;
	}
}
