package charactermanaj.ui;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Event;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
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.io.BufferedReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
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.JSlider;
import javax.swing.JSpinner;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;

import charactermanaj.Main;
import charactermanaj.graphics.colormodel.ColorModel;
import charactermanaj.graphics.colormodel.ColorModels;
import charactermanaj.graphics.filters.ColorConv;
import charactermanaj.graphics.filters.ColorConvertParameter;
import charactermanaj.model.AppConfig;
import charactermanaj.model.ColorGroup;
import charactermanaj.model.Layer;
import charactermanaj.model.PartsCategory;
import charactermanaj.model.PartsIdentifier;
import charactermanaj.ui.model.ColorChangeEvent;
import charactermanaj.ui.model.ColorChangeListener;
import charactermanaj.ui.util.ScaleSupport;
import charactermanaj.ui.util.SpinnerWheelSupportListener;
import charactermanaj.util.ErrorMessageHelper;
import charactermanaj.util.LocalizedResourcePropertyLoader;
import charactermanaj.util.UIHelper;


/**
 * カラーダイアログ.<br>
 * カラーダイアログはカテゴリ別に関連づけられており、カテゴリ内の各レイヤーに対応するタブを持つ.<br>
 *
 * @author seraphy
 */
public class ColorDialog extends JDialog {

	private static final long serialVersionUID = 1L;

	/**
	 * ロガー
	 */
	private static final Logger logger = Logger.getLogger(ColorDialog.class.getName());

	/**
	 * このカラーダイアログが対応するカテゴリ
	 */
	private final PartsCategory partsCategory;

	/**
	 * レイヤーごとのタブのマップ
	 */
	private HashMap<Layer, ColorDialogTabPanel> tabs = new HashMap<Layer, ColorDialogTabPanel>();

	/**
	 * タブペイン
	 */
	private JTabbedPane tabbedPane;

	/**
	 * レイヤーに対するタブインデックスのマップ
	 */
	private HashMap<Layer, Integer> tabbedPaneIndexMap = new HashMap<Layer, Integer>();

	/**
	 * 色変更イベントのリスナのコレクション
	 */
	private final EventListenerList listeners = new EventListenerList();

	/**
	 * キャプションのプレフィックス
	 */
	private String captionBase;

	/**
	 * 現在表示しているカラー情報の対象としているパーツ識別子
	 */
	private PartsIdentifier partsIdentifier;

	/**
	 * カテゴリ全体に適用するチェックボックス
	 */
	private JCheckBox chkApplyAll;

	/**
	 * リセットアクション
	 */
	private Action actReset;

	/**
	 * 詳細設定の表示切り替えチェックボックス
	 */
	private JCheckBox chkShowAdvancedSettings;


	/**
	 * コンストラクタ
	 *
	 * @param parent
	 *            親フレーム
	 * @param partsCategory
	 *            カテゴリ
	 * @param colorGroups
	 *            選択可能なカラーグループのコレクション
	 */
	public ColorDialog(JFrame parent, PartsCategory partsCategory, Collection<ColorGroup> colorGroups) {
		super(parent);
		this.partsCategory = partsCategory;

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

		String caption = strings.getProperty("colordialog.caption");
		String name = partsCategory.getLocalizedCategoryName();
		captionBase = caption + name;
		setTitle(captionBase);

		ScaleSupport scaleSupport = ScaleSupport.getInstance(this);

		// ダイアログを非表示にする.
		final AbstractAction actHide = new AbstractAction() {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				setVisible(false);
			}
		};

		// 非表示アクション
		addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				// ウィンドウの閉じるボタン押下により、ダイアログを「非表示」にする.
				actHide.actionPerformed(new ActionEvent(ColorDialog.this, 0, "closing"));
			}
		});

		Container container = getContentPane();

		this.tabbedPane = new JTabbedPane(JTabbedPane.TOP, JTabbedPane.WRAP_TAB_LAYOUT);
		for (final Layer layer : partsCategory.getLayers()) {

			final ColorDialogTabPanel tabContainer = new ColorDialogTabPanel(this, layer, colorGroups);
			final ColorChangeListener innerListener = new ColorChangeListener() {
				private Semaphore semaphore = new Semaphore(1); // イベントが循環発生することを防ぐ
				public void onColorChange(ColorChangeEvent event) {
					if (semaphore.tryAcquire()) {
						try {
							ColorDialog.this.fireColorChangeEvent(layer, false);
						} finally {
							semaphore.release();
						}
					}
				}
				public void onColorGroupChange(ColorChangeEvent event) {
					if (semaphore.tryAcquire()) {
						try {
							ColorDialog.this.fireColorGroupChangeEvent(layer);
							ColorDialog.this.fireColorChangeEvent(layer, false);
						} finally {
							semaphore.release();
						}
					}
				}
			};
			tabContainer.addColorChangeListener(innerListener);
			tabContainer.addComponentListener(new ComponentAdapter() {
				@Override
				public void componentShown(ComponentEvent e) {
					// レイヤータブが切り替えられるたびに、そのレイヤーの状況にあわせてリセットボタンの状態を更新する.
					updateResetButton(tabContainer);
				}
			});
			tabContainer.addPropertyChangeListener("colorConvertParameter", new PropertyChangeListener() {
				public void propertyChange(PropertyChangeEvent evt) {
							// レイヤーの情報が変るたびにリセットボタンの状態を更新する
					updateResetButton(tabContainer);
				}
			});

			tabbedPane.addTab(layer.getLocalizedName(), tabContainer);
			tabbedPaneIndexMap.put(layer, tabbedPane.getTabCount() - 1);
			tabs.put(layer, tabContainer);
		}

		// 適用アクション
		Action actApply = new AbstractAction(strings.getProperty("button.apply")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				apply();
			}
		};
		// リセットアクション
		actReset = new AbstractAction(strings.getProperty("button.reset")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				ColorDialogTabPanel tab = (ColorDialogTabPanel) tabbedPane
						.getSelectedComponent();
				if (tab != null) {
					resetColor(tab);
					apply();
				}
			}
		};

		JPanel btnPanel = new JPanel();
		btnPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
		GridBagLayout gbl = new GridBagLayout();
		btnPanel.setLayout(gbl);

		int colIdx = 0;

		GridBagConstraints gbc = new GridBagConstraints();
		gbc.gridx = colIdx++;
		gbc.gridy = 0;
		gbc.ipadx = 0;
		gbc.ipady = 0;
		gbc.anchor = GridBagConstraints.EAST;
		gbc.fill = GridBagConstraints.BOTH;
		gbc.gridheight = 1;
		gbc.gridwidth = 1;
		gbc.weightx = 0.;
		gbc.weighty = 0.;
		chkApplyAll = new JCheckBox(strings.getProperty("checkbox.applyAllItems"));
		chkApplyAll.setSelected(!partsCategory.isMultipleSelectable());
		chkApplyAll.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				// すべてに適用のチェックが変更された場合は全レイヤーの色の変更通知を出す.
				apply();
			}
		});
		btnPanel.add(chkApplyAll, gbc);

		gbc.gridx = colIdx++;
		gbc.gridy = 0;
		gbc.weightx = 1.;
		btnPanel.add(Box.createHorizontalGlue(), gbc);

		// 「適用」ボタン、アプリケーション設定により有無を選択できる.
		JButton btnApply = null;
		final AppConfig appConfig = AppConfig.getInstance();
		if ( !appConfig.isEnableAutoColorChange()) {
			gbc.gridx = colIdx++;
			gbc.gridy = 0;
			gbc.weightx = 0.;
			btnApply = new JButton(actApply);
			btnPanel.add(btnApply, gbc);
		}

		gbc.gridx = colIdx++;
		gbc.gridy = 0;
		gbc.weightx = 0.;
		JButton btnReset = new JButton(actReset);
		btnPanel.add(btnReset, gbc);

		// ダイアログ上部のボタンバー
		UIHelper uiUtl = UIHelper.getInstance();
		JButton copyBtn = uiUtl.createIconButton("icons/copy.png");
		JButton pasteBtn = uiUtl.createIconButton("icons/paste.png");

		copyBtn.setToolTipText(strings.getProperty("copy"));
		pasteBtn.setToolTipText(strings.getProperty("paste"));

		Action actCopy = new AbstractAction() {
			private static final long serialVersionUID = 1L;
			@Override
			public void actionPerformed(ActionEvent e) {
				boolean shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
				onCopy(shift);
			}
		};
		Action actPaste = new AbstractAction() {
			private static final long serialVersionUID = 1L;
			@Override
			public void actionPerformed(ActionEvent e) {
				onPaste();
			}
		};

		copyBtn.addActionListener(actCopy);
		pasteBtn.addActionListener(actPaste);

		final JToolBar toolBar = new JToolBar();
		toolBar.setFloatable(false);
		toolBar.add(copyBtn);
		toolBar.add(pasteBtn);

		AbstractAction actShowAdvancedSetting = new AbstractAction(
				strings.getProperty("chk.showAdvancedSettings")) {
			private static final long serialVersionUID = 1L;
			@Override
			public void actionPerformed(ActionEvent e) {
				onChangeShowAdvancedSettings();
			}
		};
		chkShowAdvancedSettings = new JCheckBox(actShowAdvancedSetting);
		chkShowAdvancedSettings.setSelected(appConfig.isEnableColorAdvancedSettings());
		onChangeShowAdvancedSettings();

		Box btns = Box.createHorizontalBox();
		btns.add(chkShowAdvancedSettings);
		btns.add(Box.createHorizontalGlue());
		btns.add(toolBar);

		container.setLayout(new BorderLayout());
		container.add(btns, BorderLayout.NORTH);
		container.add(tabbedPane, BorderLayout.CENTER);
		container.add(btnPanel, BorderLayout.SOUTH);


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

		// 「適用」ボタンがある場合は、デフォルトボタンに設定する.
		if (btnApply != null) {
			rootPane.setDefaultButton(btnApply);
		}

		// CTRL-Wでウィンドウを非表示にする. (ESCでは非表示にしない)
		InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
		ActionMap am = rootPane.getActionMap();

		int ctrlMask = tk.getMenuShortcutKeyMask();
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, ctrlMask), "hideColorDialog");
		am.put("hideColorDialog", actHide);

		// CTRL-SHIFT-C でパラメータのコピー
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, ctrlMask + Event.SHIFT_MASK), "copyColorParam");
		am.put("copyColorParam", actCopy);

		// CTRL-SHIFT-V でパラメーターの貼り付け
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, ctrlMask + Event.SHIFT_MASK), "pastColorParam");
		am.put("pastColorParam", actPaste);

		// CTRL-E で詳細設定の表示切り替え
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_E, ctrlMask), "showAdvancedSetting");
		am.put("showAdvancedSetting", new AbstractAction() {
			private static final long serialVersionUID = 1L;
			@Override
			public void actionPerformed(ActionEvent e) {
				// 詳細表示チェックボックスをトグル動作して画面を更新する
				chkShowAdvancedSettings.setSelected(!chkShowAdvancedSettings.isSelected());
				onChangeShowAdvancedSettings();
			}
		});

		pack();
	}

	/**
	 * 詳細設定の表示切り替え
	 */
	protected void onChangeShowAdvancedSettings() {
		boolean show = chkShowAdvancedSettings.isSelected();
		for (ColorDialogTabPanel tabContainer : tabs.values()) {
			tabContainer.setVisibleAdvancedSettings(show);
		}
		pack();
	}

	/**
	 * 設定値をクリップボードにコピーする
	 */
	protected void onCopy(boolean advanced) {
		Toolkit tk = Toolkit.getDefaultToolkit();

		ColorDialogTabPanel tabContainer = (ColorDialogTabPanel) tabbedPane.getSelectedComponent();
		if (tabContainer == null) {
			tk.beep();
			return;
		}

		ColorConvertParameter param = tabContainer.getColorConvertParameter();

		StringWriter sw = new StringWriter();
		PrintWriter pw = new PrintWriter(sw);

		// コメントヘッダ
		if (partsIdentifier != null) {
			pw.println("# " + partsIdentifier.getLocalizedPartsName());
		}
		pw.println("# " + partsCategory.getLocalizedCategoryName());
		pw.println("# " + tabContainer.getLayer().getLocalizedName());
		pw.println();

		// パラメータ
		pw.println("hue=" + param.getHue());
		pw.println("saturation=" + param.getSaturation());
		pw.println("brightness=" + param.getBrightness());
		pw.println("contrast=" + param.getContrast());

		if (advanced) {
			pw.println();
			pw.println("offsetR=" + param.getOffsetR());
			pw.println("offsetG=" + param.getOffsetG());
			pw.println("offsetB=" + param.getOffsetB());
			pw.println("offsetA=" + param.getOffsetA());

			pw.println("factorR=" + param.getFactorR());
			pw.println("factorG=" + param.getFactorG());
			pw.println("factorB=" + param.getFactorB());
			pw.println("factorA=" + param.getFactorA());

			pw.println("gammaR=" + param.getGammaR());
			pw.println("gammaG=" + param.getGammaG());
			pw.println("gammaB=" + param.getGammaB());
			pw.println("gammaA=" + param.getGammaA());

			pw.println("grayLevel=" + param.getGrayLevel());
			pw.println("replace=" + param.getColorReplace());
		}

		Clipboard cb = tk.getSystemClipboard();
		StringSelection selection = new StringSelection(sw.toString());
		cb.setContents(selection, null);
	}

	/**
	 * クリップボードから設定値を貼り付ける
	 */
	protected void onPaste() {
		Toolkit tk = Toolkit.getDefaultToolkit();

		ColorDialogTabPanel tabContainer = (ColorDialogTabPanel) tabbedPane.getSelectedComponent();
		if (tabContainer == null) {
			tk.beep();
			return;
		}

		Clipboard cb = tk.getSystemClipboard();
		Transferable trans = cb.getContents(null);
		if (trans == null) {
			tk.beep();
			return;
		}

		try {
			// 文字列を格納しているか？
		    if (!trans.isDataFlavorSupported(DataFlavor.stringFlavor)) {
				// テキストを持っていない
				tk.beep();
				return;
		    }

		    // 現在の設定を取得する
			ColorConvertParameter param = tabContainer.getColorConvertParameter();
			ColorConvertParameter org = param.clone(); // 変更有無の判定のため現在値をコピーする

			List<String> errorLines = new ArrayList<String>();
			BufferedReader rd = new BufferedReader(DataFlavor.stringFlavor.getReaderForText(trans));
			try {
				// テキストを読み込みつつ、対応するパラメータを設定する。
				for (;;) {
					String line = rd.readLine();
					if (line == null) {
						break;
					}

					// #以降はコメント
					int pt = line.indexOf('#');
					if (pt >= 0) {
						line = line.substring(0, pt);
					}
					line = line.trim();

					if (line.isEmpty()) {
						// 空行は無視
						continue;
					}

					String[] tokens = line.split("=");
					if (tokens.length == 2) {
						String name = tokens[0].trim().toLowerCase();
						String val = tokens[1].trim();

						try {
							if ("hue".equals(name)) {
								param.setHue(Float.parseFloat(val));
							} else if ("saturation".equals(name)) {
								param.setSaturation(Float.parseFloat(val));
							} else if ("brightness".equals(name)) {
								param.setBrightness(Float.parseFloat(val));
							} else if ("contrast".equals(name)) {
								param.setContrast(Float.parseFloat(val));

							} else if ("offsetr".equals(name)) {
								param.setOffsetR(Integer.parseInt(val));
							} else if ("offsetg".equals(name)) {
								param.setOffsetG(Integer.parseInt(val));
							} else if ("offsetb".equals(name)) {
								param.setOffsetB(Integer.parseInt(val));
							} else if ("offseta".equals(name)) {
								param.setOffsetA(Integer.parseInt(val));

							} else if ("factorr".equals(name)) {
								param.setFactorR(Float.parseFloat(val));
							} else if ("factorg".equals(name)) {
								param.setFactorG(Float.parseFloat(val));
							} else if ("factorb".equals(name)) {
								param.setFactorB(Float.parseFloat(val));
							} else if ("factora".equals(name)) {
								param.setFactorA(Float.parseFloat(val));

							} else if ("gammar".equals(name)) {
								param.setGammaR(Float.parseFloat(val));
							} else if ("gammag".equals(name)) {
								param.setGammaG(Float.parseFloat(val));
							} else if ("gammab".equals(name)) {
								param.setGammaB(Float.parseFloat(val));
							} else if ("gammaa".equals(name)) {
								param.setGammaA(Float.parseFloat(val));

							} else if ("graylevel".equals(name)) {
								param.setGrayLevel(Float.parseFloat(val));
							} else if ("replace".equals(name)) {
								param.setColorReplace(ColorConv.valueOf(val));
							} else {
								// 未知のパラメーター
								errorLines.add(line);
							}
						} catch (RuntimeException ex) {
							// パラメーターエラー
							logger.log(Level.WARNING, "failed to read the clipboard.", ex);
							errorLines.add(line);
							errorLines.add("* " + ex.toString());
						}
					} else {
						// フォーマットが不正
						errorLines.add(line);
					}

					if (errorLines.size() > 30) {
						// エラー行数が30を超えたら、
						// 操作ミス等による関係ないテキストを放り込んだ可能性が大なので打ち切る
						break;
					}
				}
			} finally {
				rd.close();
			}

			if (!param.equals(org)) {
				// クリップボートから取得されたパラメータを設定します。
				tabContainer.applyColorConvertParameter(param);
			}

			// パラメータの取り込みに失敗した行を表示する
			if (errorLines.size() > 0) {
				StringBuilder buf = new StringBuilder();
				String lineBreak = System.getProperty("line.separator");
				for (String line : errorLines) {
					buf.append(line).append(lineBreak);
				}
				JTextArea message = new JTextArea(buf.toString());
				message.setEditable(false);
				JScrollPane scr = new JScrollPane(message);
				scr.setPreferredSize(new Dimension(200, 200));

				final Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
						.getLocalizedProperties("languages/colordialog");
				String title = strings.getProperty("clipboard.paste.formatErrorTitle");
				JOptionPane.showMessageDialog(this, scr, title, JOptionPane.ERROR_MESSAGE);
			}

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

	/**
	 * 各レイヤーのカラー情報のタブが開かれた場合、もしくはカラーの設定値が変更されるたびに 呼び出されて、リセットボタンの状態を変更します.<br>
	 * 現在のタブが選択しているパネルと異なるパネルからの要求については無視されます.<br>
	 *
	 * @param panel
	 *            色情報が変更された、もしくは開かれた対象パネル
	 */
	protected void updateResetButton(ColorDialogTabPanel panel) {
		ColorDialogTabPanel currentPanel = (ColorDialogTabPanel) tabbedPane.getSelectedComponent();
		if (currentPanel != null && currentPanel.equals(panel)) {
			actReset.setEnabled(panel.isColorConvertParameterModified());
		}
	}

	/**
	 * このカラーダイアログが対応するパーツカテゴリを取得する.<br>
	 *
	 * @return パーツカテゴリ
	 */
	public PartsCategory getPartsCategory() {
		return partsCategory;
	}

	/**
	 * 指定したレイヤーのカラーグループが「連動」しているか?<br>
	 * カテゴリに属していないレイヤーを指定した場合は常にfalseを返す.<br>
	 *
	 * @param layer
	 *            レイヤー
	 * @return 連動している場合はtrue、そうでなければfalse
	 */
	public boolean isSyncColorGroup(Layer layer) {
		ColorDialogTabPanel tab = tabs.get(layer);
		if (tab == null) {
			return false;
		}
		return tab.isSyncColorGroup();
	}

	/**
	 * 指定したレイヤーのカラーグループの連動フラグを設定する.<br>
	 * カテゴリに属していないレイヤーを指定した場合は何もしない.<br>
	 *
	 * @param layer
	 *            レイヤー
	 * @param selected
	 *            連動フラグ
	 */
	public void setSyncColorGroup(Layer layer, boolean selected) {
		ColorDialogTabPanel tab = tabs.get(layer);
		if (tab != null) {
			tab.setSyncColorGroup(selected);
		}
	}

	/**
	 * レイヤーごとの色情報のマップを指定して、各レイヤーに色情報を設定する.<br>
	 * カテゴリに属していないレイヤーが含まれる場合は例外となる.<br>
	 *
	 * @param params
	 *            レイヤーと色情報のマップ
	 */
	public void setColorConvertParameters(Map<Layer, ColorConvertParameter> params) {
		if (params == null) {
			throw new IllegalArgumentException();
		}
		for (Layer layer : partsCategory.getLayers()) {
			ColorConvertParameter param = params.get(layer);
			if (param == null) {
				param = new ColorConvertParameter();
			}
			setColorConvertParameter(layer, param);
		}
	}

	/**
	 * 対象となるパーツ識別子を指定する。<br>
	 * カラーダイアログのキャプションにパーツ名を設定される.<br>
	 * nullを指定した場合はキャプションからパーツ名が除去される.<br>
	 *
	 * @param partsIdentifier
	 *            パーツ識別子、もしくはnull
	 */
	public void setPartsIdentifier(PartsIdentifier partsIdentifier) {
		this.partsIdentifier = partsIdentifier;
		if (partsIdentifier == null) {
			setTitle(captionBase);
		} else {
			setTitle(captionBase + "(" + partsIdentifier.getLocalizedPartsName() + ")");
		}
	}

	/**
	 * 対象となるパーツ識別子を取得する.<br>
	 * 設定されていなければnullが返される.<br>
	 *
	 * @return パーツ識別子、もしくはnull
	 */
	public PartsIdentifier getPartsIdentifier() {
		return partsIdentifier;
	}

	/**
	 * 各レイヤーのタブの有効・無効状態を設定します.<br>
	 * カテゴリに属さないレイヤーは無視されます.<br>
	 * nullを指定した場合は、すべてのレイヤーが「有効」となります.<br>
	 *
	 * @param layers
	 *            有効とするレイヤーのコレクション、もしくはnull
	 */
	public void setEnableLayers(Collection<Layer> layers) {
		for (Map.Entry<Layer, ColorDialogTabPanel> entry : tabs.entrySet()) {
			Layer layer = entry.getKey();
			boolean enabled = (layers == null) || layers.contains(layer);
			Integer tabIndex = tabbedPaneIndexMap.get(layer);
			if (tabIndex != null) {
				if (Main.isMacOSX()) {
					// OSXの場合、タブをディセーブルにしても表示が変化ないので
					// タブタイトルを変更することでディセーブルを示す.
					// (html3で表現しようとしたところ、かなりバギーだったため採用せず)
					tabbedPane.setTitleAt(tabIndex,
							enabled ? layer.getLocalizedName() : "-");
				}

				tabbedPane.setEnabledAt(tabIndex, enabled);

				if (logger.isLoggable(Level.FINEST)) {
					logger.log(Level.FINEST, "setEnableLayers(" + layer + ")=" + enabled);
				}
			}
		}
	}

	/**
	 * 「すべてに適用」フラグを取得する.<br>
	 *
	 * @return すべてに適用フラグ
	 */
	public boolean isApplyAll() {
		return chkApplyAll.isSelected();
	}

	/**
	 * 各レイヤーと、その色情報をマップとして取得する.<br>
	 *
	 * @return 各レイヤーと、その色情報のマップ
	 */
	public Map<Layer, ColorConvertParameter> getColorConvertParameters() {
		HashMap<Layer, ColorConvertParameter> params = new HashMap<Layer, ColorConvertParameter>();
		for (Layer layer : partsCategory.getLayers()) {
			ColorDialogTabPanel tab = tabs.get(layer);
			ColorConvertParameter param = tab.getColorConvertParameter();
			params.put(layer, param);
		}
		return params;
	}

	/**
	 * レイヤーを指定して、色情報を設定する.<br>
	 * カテゴリに属していないレイヤーを指定した場合は例外となる.<br>
	 *
	 * @param layer
	 *            レイヤー
	 * @param param
	 *            色情報
	 */
	public void setColorConvertParameter(Layer layer, ColorConvertParameter param) {
		if (layer == null || param == null) {
			throw new IllegalArgumentException();
		}
		ColorDialogTabPanel tab = tabs.get(layer);
		if (tab == null) {
			throw new IllegalArgumentException("layer not found. " + layer + "/tabs=" + tabs);
		}
		tab.setColorConvertParameter(param);
	}

	/**
	 * 指定したレイヤーの色情報を取得する.<br>
	 * カテゴリに属していないレイヤーを指定した場合は例外となる.<br>
	 *
	 * @param layer
	 *            レイヤー
	 * @return 色情報
	 */
	public ColorConvertParameter getColorConvertParameter(Layer layer) {
		if (layer == null) {
			throw new IllegalArgumentException();
		}
		ColorDialogTabPanel tab = tabs.get(layer);
		if (tab == null) {
			throw new IllegalArgumentException("layer not found. " + layer);
		}
		return tab.getColorConvertParameter();
	}

	/**
	 * 指定したレイヤーのカラーグループを取得する.<br>
	 * カテゴリに属さないレイヤーを指定した場合は例外となる.<br>
	 *
	 * @param layer
	 *            レイヤー
	 * @return カラーグループ
	 */
	public ColorGroup getColorGroup(Layer layer) {
		if (layer == null) {
			throw new IllegalArgumentException();
		}
		ColorDialogTabPanel tab = tabs.get(layer);
		if (tab == null) {
			throw new IllegalArgumentException("layer not found. " + layer);
		}
		return tab.getColorGroup();
	}

	/**
	 * 指定したレイヤーのカラーグループを設定する.<br>
	 * カテゴリに属さないレイヤーを指定した場合は例外となる.<br>
	 *
	 * @param layer
	 *            レイヤー
	 * @param colorGroup
	 *            カラーグループ
	 */
	public void setColorGroup(Layer layer, ColorGroup colorGroup) {
		if (layer == null || colorGroup == null) {
			throw new IllegalArgumentException();
		}
		ColorDialogTabPanel tab = tabs.get(layer);
		if (tab != null) {
			tab.setColorGroup(colorGroup);
		}
	}

	/**
	 * 色ダイアログが変更された場合に通知を受けるリスナーを登録する.<br>
	 *
	 * @param listener
	 *            リスナー
	 */
	public void addColorChangeListener(ColorChangeListener listener) {
		if (listener == null) {
			throw new IllegalArgumentException();
		}
		listeners.add(ColorChangeListener.class, listener);
	}

	/**
	 * 色ダイアログが変更された場合に通知を受けるリスナーを登録解除する.<br>
	 *
	 * @param listener
	 */
	public void removeColorChangeListener(ColorChangeListener listener) {
		listeners.remove(ColorChangeListener.class, listener);
	}

	/**
	 * 全レイヤーに対するカラー変更イベントを明示的に送信する.
	 */
	protected void apply() {
		for (Layer layer : getPartsCategory().getLayers()) {
			ColorDialog.this.fireColorChangeEvent(layer, true);
		}
	}

	/**
	 * カラーをリセットする.
	 */
	protected void resetColor(ColorDialogTabPanel tab) {
		tab.resetColor();
	}

	/**
	 * 指定したレイヤーに対するカラー変更イベントを通知する.<br>
	 * ただし、force引数がfalseである場合、アプリケーション設定で即時プレビューが指定されていない場合は通知しない.<br>
	 *
	 * @param layer
	 *            レイヤー
	 * @param force
	 *            アプリケーション設定に関わらず送信する場合はtrue
	 */
	protected void fireColorChangeEvent(Layer layer, boolean force) {
		if (layer == null) {
			throw new IllegalArgumentException();
		}
		if (!force) {
			AppConfig appConfig = AppConfig.getInstance();
			if (!appConfig.isEnableAutoColorChange()) {
				return;
			}
		}

		// 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の作法らしい。
		ColorChangeEvent event = null;
		for (int i = ll.length - 2; i >= 0; i -= 2) {
			if (ll[i] == ColorChangeListener.class) {
				// Lazily create the event:
				if (event == null) {
					event = new ColorChangeEvent(this, layer);
				}
				((ColorChangeListener) ll[i + 1]).onColorChange(event);
			}
		}
	}

	/**
	 * 色グループが変更されたことを通知する.<br>
	 *
	 * @param layer
	 *            レイヤー
	 */
	protected void fireColorGroupChangeEvent(Layer layer) {
		if (layer == null) {
			throw new IllegalArgumentException();
		}
		// 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の作法らしい。
		ColorChangeEvent event = null;
		for (int i = ll.length - 2; i >= 0; i -= 2) {
			if (ll[i] == ColorChangeListener.class) {
				// Lazily create the event:
				if (event == null) {
					event = new ColorChangeEvent(this, layer);
				}
				((ColorChangeListener) ll[i + 1]).onColorGroupChange(event);
			}
		}
	}

	@Override
	public String toString() {
		return "ColorDialog(partsCategory:" + partsCategory + ")";
	}
}


class ColorDialogTabPanel extends JPanel {
	private static final long serialVersionUID = 1L;

	private final ColorDialog parent;

	private Layer layer;

	private JComboBox cmbColorReplace;

	private JSpinner spinGray;

	private JSpinner spinOffsetR;

	private JSpinner spinOffsetG;

	private JSpinner spinOffsetB;

	private JSpinner spinOffsetA;

	private JSpinner spinFactorR;

	private JSpinner spinFactorG;

	private JSpinner spinFactorB;

	private JSpinner spinFactorA;

	private JSpinner spinHue;

	private JSpinner spinSaturation;

	private JSpinner spinBrightness;

	private JSpinner spinContrast;

	private JSpinner spinGammaR;

	private JSpinner spinGammaG;

	private JSpinner spinGammaB;

	private JSpinner spinGammaA;

	private JComboBox cmbColorGroup;

	private JCheckBox chkColorGroupSync;

	/**
	 * パラメータの明示的変更時に他のパラメータへの反映イベントを停止させるためのセマフォ
	 */
	private AtomicInteger changeEventDisableSemaphore = new AtomicInteger();

	/**
	 * 明示的に設定されたカラーパラメータを保存する.(リセットに使用するため)
	 */
	private ColorConvertParameter paramOrg = new ColorConvertParameter();

	/**
	 * 現在の画面項目からカラー情報を組み立てたキャッシュ。
	 * (画面項目の更新によりクリアされる。)
	 */
	private ColorConvertParameter chachedParam;

	private LinkedList<ColorChangeListener> listeners = new LinkedList<ColorChangeListener>();

	public void addColorChangeListener(ColorChangeListener listener) {
		if (listener == null) {
			throw new IllegalArgumentException();
		}
		listeners.add(listener);
	}

	public void removeColorChangeListener(ColorChangeListener listener) {
		listeners.remove(listener);
	}

	protected void fireColorChangeEvent() {
		if (layer == null) {
			throw new IllegalArgumentException();
		}
		chachedParam = null;
		if (changeEventDisableSemaphore.get() <= 0) {
			ColorChangeEvent event = new ColorChangeEvent(parent, layer);
			for (ColorChangeListener listener : listeners) {
				listener.onColorChange(event);
			}
		}
	}

	protected void fireColorGroupChangeEvent() {
		if (layer == null) {
			throw new IllegalArgumentException();
		}
		chachedParam = null;
		if (changeEventDisableSemaphore.get() <= 0) {
			ColorChangeEvent event = new ColorChangeEvent(parent, layer);
			for (ColorChangeListener listener : listeners) {
				listener.onColorGroupChange(event);
			}
		}
	}

	private JPanel colorReplacePanel;

	private JPanel colorLevelPanel;

	public ColorDialogTabPanel(final ColorDialog parent, final Layer layer, Collection<ColorGroup> colorGroups) {
		if (parent == null || layer == null || colorGroups == null) {
			throw new IllegalArgumentException();
		}
		this.parent = parent;
		this.layer = layer;

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

		setLayout(new BorderLayout());
		JPanel container = new JPanel();
		BoxLayout boxlayout = new BoxLayout(container, BoxLayout.PAGE_AXIS);
		container.setLayout(boxlayout);
		add(container, BorderLayout.NORTH);

		// 変更イベントハンドラ
		final ChangeListener changeEventHandler = new ChangeListener() {
			public void stateChanged(ChangeEvent e) {
				fireColorChangeEvent();
				firePropertyChange("colorConvertParameter", null, null);
			}
		};

		// 色置換パネル

		colorReplacePanel = new JPanel();
		colorReplacePanel.setBorder(BorderFactory.createCompoundBorder(
				BorderFactory.createEmptyBorder(3, 3, 3, 3),
				BorderFactory.createTitledBorder(strings.getProperty("group.replacergb.caption"))));
		GridBagLayout gbl = new GridBagLayout();
		colorReplacePanel.setLayout(gbl);
		GridBagConstraints gbc = new GridBagConstraints();

		JLabel lblColorReplace = new JLabel(strings.getProperty("replacergb"), JLabel.RIGHT);
		cmbColorReplace = new JComboBox(ColorConv.values());
		JLabel lblGray = new JLabel(strings.getProperty("bright"), JLabel.RIGHT);
		SpinnerNumberModel grayModel = new SpinnerNumberModel(1., 0., 1., 0.01);
		grayModel.addChangeListener(changeEventHandler);
		cmbColorReplace.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				fireColorChangeEvent();
				firePropertyChange("colorConvertParameter", null, null);
			}
		});
		spinGray = new JSpinner(grayModel);
		spinGray.addMouseWheelListener(new SpinnerWheelSupportListener(grayModel));

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

		gbc.gridx = 1;
		gbc.gridy = 0;
		gbc.gridwidth = 1;
		gbc.gridheight = 1;
		gbc.weightx = 1.;
		gbc.weighty = 0.;
		colorReplacePanel.add(cmbColorReplace, gbc);

		gbc.gridx = 2;
		gbc.gridy = 0;
		gbc.gridwidth = 1;
		gbc.gridheight = 1;
		gbc.weightx = 0.;
		gbc.weighty = 0.;
		colorReplacePanel.add(lblGray, gbc);

		gbc.gridx = 3;
		gbc.gridy = 0;
		gbc.weightx = 1.;
		gbc.weighty = 0.;
		gbc.gridwidth = 1;
		gbc.gridheight = 1;
		colorReplacePanel.add(spinGray, gbc);

		// RGB変更パネル

		colorLevelPanel = new JPanel();
		colorLevelPanel.setBorder(BorderFactory.createCompoundBorder(
				BorderFactory.createEmptyBorder(3, 3, 3, 3),
				BorderFactory.createTitledBorder(strings.getProperty("group.rgb.caption"))));
		GridLayout gl = new GridLayout(4, 5);
		gl.setHgap(2);
		gl.setVgap(2);
		colorLevelPanel.setLayout(gl);
		colorLevelPanel.add(Box.createGlue());
		colorLevelPanel.add(new JLabel(strings.getProperty("red"), JLabel.CENTER));
		colorLevelPanel.add(new JLabel(strings.getProperty("green"), JLabel.CENTER));
		colorLevelPanel.add(new JLabel(strings.getProperty("blue"), JLabel.CENTER));
		colorLevelPanel.add(new JLabel(strings.getProperty("alpha"), JLabel.CENTER));
		colorLevelPanel.add(new JLabel(strings.getProperty("offset"), JLabel.RIGHT));
		SpinnerNumberModel offsetModelR = new SpinnerNumberModel(0, -255, 255, 1);
		SpinnerNumberModel offsetModelG = new SpinnerNumberModel(0, -255, 255, 1);
		SpinnerNumberModel offsetModelB = new SpinnerNumberModel(0, -255, 255, 1);
		SpinnerNumberModel offsetModelA = new SpinnerNumberModel(0, -255, 255, 1);
		offsetModelR.addChangeListener(changeEventHandler);
		offsetModelG.addChangeListener(changeEventHandler);
		offsetModelB.addChangeListener(changeEventHandler);
		offsetModelA.addChangeListener(changeEventHandler);
		spinOffsetR = new JSpinner(offsetModelR);
		spinOffsetG = new JSpinner(offsetModelG);
		spinOffsetB = new JSpinner(offsetModelB);
		spinOffsetA = new JSpinner(offsetModelA);
		spinOffsetR.addMouseWheelListener(new SpinnerWheelSupportListener(offsetModelR));
		spinOffsetG.addMouseWheelListener(new SpinnerWheelSupportListener(offsetModelG));
		spinOffsetB.addMouseWheelListener(new SpinnerWheelSupportListener(offsetModelB));
		spinOffsetA.addMouseWheelListener(new SpinnerWheelSupportListener(offsetModelA));
		colorLevelPanel.add(spinOffsetR);
		colorLevelPanel.add(spinOffsetG);
		colorLevelPanel.add(spinOffsetB);
		colorLevelPanel.add(spinOffsetA);
		colorLevelPanel.add(new JLabel(strings.getProperty("factor"), JLabel.RIGHT));
		SpinnerNumberModel factorModelR = new SpinnerNumberModel(1., 0.01, 99, 0.01);
		SpinnerNumberModel factorModelG = new SpinnerNumberModel(1., 0.01, 99, 0.01);
		SpinnerNumberModel factorModelB = new SpinnerNumberModel(1., 0.01, 99, 0.01);
		SpinnerNumberModel factorModelA = new SpinnerNumberModel(1., 0.01, 99, 0.01);
		factorModelR.addChangeListener(changeEventHandler);
		factorModelG.addChangeListener(changeEventHandler);
		factorModelB.addChangeListener(changeEventHandler);
		factorModelA.addChangeListener(changeEventHandler);
		spinFactorR = new JSpinner(factorModelR);
		spinFactorG = new JSpinner(factorModelG);
		spinFactorB = new JSpinner(factorModelB);
		spinFactorA = new JSpinner(factorModelA);
		spinFactorR.addMouseWheelListener(new SpinnerWheelSupportListener(factorModelR));
		spinFactorG.addMouseWheelListener(new SpinnerWheelSupportListener(factorModelG));
		spinFactorB.addMouseWheelListener(new SpinnerWheelSupportListener(factorModelB));
		spinFactorA.addMouseWheelListener(new SpinnerWheelSupportListener(factorModelA));
		colorLevelPanel.add(spinFactorR);
		colorLevelPanel.add(spinFactorG);
		colorLevelPanel.add(spinFactorB);
		colorLevelPanel.add(spinFactorA);
		colorLevelPanel.add(new JLabel(strings.getProperty("gamma"), JLabel.RIGHT));
		SpinnerNumberModel gammaModelR = new SpinnerNumberModel(1., 0.01, 99, 0.01);
		SpinnerNumberModel gammaModelG = new SpinnerNumberModel(1., 0.01, 99, 0.01);
		SpinnerNumberModel gammaModelB = new SpinnerNumberModel(1., 0.01, 99, 0.01);
		SpinnerNumberModel gammaModelA = new SpinnerNumberModel(1., 0.01, 99, 0.01);
		gammaModelR.addChangeListener(changeEventHandler);
		gammaModelG.addChangeListener(changeEventHandler);
		gammaModelB.addChangeListener(changeEventHandler);
		gammaModelA.addChangeListener(changeEventHandler);
		spinGammaR = new JSpinner(gammaModelR);
		spinGammaG = new JSpinner(gammaModelG);
		spinGammaB = new JSpinner(gammaModelB);
		spinGammaA = new JSpinner(gammaModelA);
		spinGammaR.addMouseWheelListener(new SpinnerWheelSupportListener(gammaModelR));
		spinGammaG.addMouseWheelListener(new SpinnerWheelSupportListener(gammaModelG));
		spinGammaB.addMouseWheelListener(new SpinnerWheelSupportListener(gammaModelB));
		spinGammaA.addMouseWheelListener(new SpinnerWheelSupportListener(gammaModelA));
		colorLevelPanel.add(spinGammaR);
		colorLevelPanel.add(spinGammaG);
		colorLevelPanel.add(spinGammaB);
		colorLevelPanel.add(spinGammaA);

		// 色調パネル

		ColorModel colorModel = ColorModels.safeValueOf(layer.getColorModelName());

		JPanel colorTunePanel = new JPanel();
		colorTunePanel.setBorder(BorderFactory.createCompoundBorder(
				BorderFactory.createEmptyBorder(3, 3, 3, 3),
				BorderFactory.createTitledBorder(strings.getProperty(colorModel.getTitle()))));
		GridLayout gl2 = new GridLayout(3, 4);
		gl2.setHgap(3);
		gl2.setVgap(3);
		colorTunePanel.setLayout(gl2);

		colorTunePanel.add(new JLabel(strings.getProperty(colorModel
				.getItemTitle(0)), JLabel.CENTER)); // Hue 色相
		colorTunePanel.add(new JLabel(strings.getProperty(colorModel
				.getItemTitle(1)), JLabel.CENTER)); // Saturation 彩度
		colorTunePanel.add(new JLabel(strings.getProperty(colorModel
				.getItemTitle(2)), JLabel.CENTER)); // Brightness 明度
		colorTunePanel.add(new JLabel(strings.getProperty("contrast"),
				JLabel.CENTER)); // Contrast コントラスト

		SpinnerNumberModel hsbModelH = new SpinnerNumberModel(0., -1., 1., 0.001);
		SpinnerNumberModel hsbModelS = new SpinnerNumberModel(0., -1., 1., 0.001);
		SpinnerNumberModel hsbModelB = new SpinnerNumberModel(0., -1., 1., 0.001);
		SpinnerNumberModel hsbModelC = new SpinnerNumberModel(0., -1., 1., 0.001);

		hsbModelH.addChangeListener(changeEventHandler);
		hsbModelS.addChangeListener(changeEventHandler);
		hsbModelB.addChangeListener(changeEventHandler);
		hsbModelC.addChangeListener(changeEventHandler);

		spinHue = new JSpinner(hsbModelH);
		spinSaturation = new JSpinner(hsbModelS);
		spinBrightness = new JSpinner(hsbModelB);
		spinContrast = new JSpinner(hsbModelC);
		spinHue.addMouseWheelListener(new SpinnerWheelSupportListener(hsbModelH));
		spinSaturation.addMouseWheelListener(new SpinnerWheelSupportListener(hsbModelS));
		spinBrightness.addMouseWheelListener(new SpinnerWheelSupportListener(hsbModelB));
		spinContrast.addMouseWheelListener(new SpinnerWheelSupportListener(hsbModelC));

		colorTunePanel.add(spinHue);
		colorTunePanel.add(spinSaturation);
		colorTunePanel.add(spinBrightness);
		colorTunePanel.add(spinContrast);

		JSlider sliderHue = new JSlider();
		JSlider sliderSaturation = new JSlider();
		JSlider sliderBrightness = new JSlider();
		JSlider sliderContrast = new JSlider();

		sliderHue.setPreferredSize(spinHue.getPreferredSize());
		sliderSaturation.setPreferredSize(spinSaturation.getPreferredSize());
		sliderBrightness.setPreferredSize(spinBrightness.getPreferredSize());
		sliderContrast.setPreferredSize(spinContrast.getPreferredSize());

		colorTunePanel.add(sliderHue);
		colorTunePanel.add(sliderSaturation);
		colorTunePanel.add(sliderBrightness);
		colorTunePanel.add(sliderContrast);

		JSlider sliders[] = new JSlider[] {sliderHue, sliderSaturation, sliderBrightness, sliderContrast};
		JSpinner spinners[] = new JSpinner[] {spinHue, spinSaturation, spinBrightness, spinContrast};

		for (int idx = 0; idx < spinners.length; idx++) {
			final JSlider sl = sliders[idx];
			final JSpinner sp = spinners[idx];
			SpinnerNumberModel spModel = (SpinnerNumberModel) sp.getModel();
			sl.setMinimum((int)(((Number)spModel.getMinimum()).floatValue() * 100));
			sl.setMaximum((int)(((Number)spModel.getMaximum()).floatValue() * 100));
			sl.setValue((int)(((Number) sp.getValue()).doubleValue() * 100.));
			final Semaphore loopBlocker = new Semaphore(1); // イベントが循環発生することを防ぐ
			sl.addChangeListener(new ChangeListener() {
				public void stateChanged(ChangeEvent e) {
					if (loopBlocker.tryAcquire()) {
						try {
							double rate = sl.getValue() / 100.;
							sp.setValue(Double.valueOf(rate));
						} finally {
							loopBlocker.release();
						}
					}
				}
			});
			sp.addChangeListener(new ChangeListener() {
				public void stateChanged(ChangeEvent e) {
					if (loopBlocker.tryAcquire()) {
						try {
							int rate = (int)(((Number) sp.getValue()).doubleValue() * 100.);
							sl.setValue(rate);
						} finally {
							loopBlocker.release();
						}
					}
				}
			});
		}

		// カラーグループ
		ColorGroup colorGroup = layer.getColorGroup();
		JPanel colorGroupPanel = new JPanel();
		colorGroupPanel.setBorder(BorderFactory.createCompoundBorder(
				BorderFactory.createEmptyBorder(3, 3, 3, 3),
				BorderFactory.createTitledBorder(strings.getProperty("colorgroup"))));
		GridBagLayout gbl2 = new GridBagLayout();
		colorGroupPanel.setLayout(gbl2);
		GridBagConstraints gbc2 = new GridBagConstraints();

		JLabel lblColorGroup = new JLabel(strings.getProperty("group"), JLabel.RIGHT);
		cmbColorGroup = new JComboBox(colorGroups.toArray(new ColorGroup[colorGroups.size()]));
		cmbColorGroup.setSelectedItem(colorGroup);
		cmbColorGroup.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				ColorGroup selColorGroup = (ColorGroup) cmbColorGroup.getSelectedItem();
				chkColorGroupSync.setSelected(selColorGroup.isEnabled());
				fireColorGroupChangeEvent();
			}
		});
		chkColorGroupSync = new JCheckBox(strings.getProperty("synchronized"));
		chkColorGroupSync.setSelected(layer.isInitSync());

		gbc2.gridx = 0;
		gbc2.gridy = 0;
		gbc2.gridwidth = 1;
		gbc2.gridheight = 1;
		gbc2.weightx = 0.;
		gbc2.weighty = 0.;
		gbc2.anchor = GridBagConstraints.WEST;
		gbc2.fill = GridBagConstraints.BOTH;
		gbc2.ipadx = 0;
		gbc2.ipady = 0;
		gbc2.insets = new Insets(3, 3, 3, 3);
		colorGroupPanel.add(lblColorGroup, gbc2);

		gbc2.gridx = 1;
		gbc2.gridy = 0;
		gbc2.gridwidth = 1;
		gbc2.gridheight = 1;
		gbc2.weightx = 1.;
		gbc2.weighty = 0.;
		colorGroupPanel.add(cmbColorGroup, gbc2);

		gbc2.gridx = 2;
		gbc2.gridy = 0;
		gbc2.gridwidth = GridBagConstraints.REMAINDER;
		gbc2.gridheight = 1;
		gbc2.weightx = 0.;
		gbc2.weighty = 0.;
		colorGroupPanel.add(chkColorGroupSync, gbc2);

		if (colorGroupPanel != null) {
			container.add(colorGroupPanel);
		}
		container.add(colorLevelPanel);
		container.add(colorReplacePanel);
		container.add(colorTunePanel);
	}

	/**
	 * 詳細設定パネルの表示状態
	 * @return
	 */
	public boolean isVisibleAdvancedSettings() {
		return colorLevelPanel.isVisible();
	}

	/**
	 * 詳細設定パネルの表示制御
	 * @param show
	 */
	public void setVisibleAdvancedSettings(boolean show) {
		colorLevelPanel.setVisible(show);
		colorReplacePanel.setVisible(show);
		revalidate();
	}

	/**
	 * このパネルのレイヤーを取得する
	 * @return
	 */
	public Layer getLayer() {
		return layer;
	}

	/**
	 * このパネルで変更された色情報の状態をリセットする.<br>
	 * 最後に{@link #setColorConvertParameter(ColorConvertParameter)}された値で 設定し直す.<br>
	 */
	public void resetColor() {
		setColorConvertParameter(paramOrg);
	}

	/**
	 * 指定されたパラメータでダイアログを設定し、オリジナル値とする。
	 * @param param
	 */
	public void setColorConvertParameter(ColorConvertParameter param) {
		setColorConvertParameter(param, true);
	}

	/**
	 * 指定されたパラメータでダイアログを設定し、且つ、変更通知を送信する。
	 * @param param
	 */
	public void applyColorConvertParameter(ColorConvertParameter param) {
		setColorConvertParameter(param, false);
		fireColorChangeEvent();
	}

	/**
	 * 指定されたパラメータでダイアログを設定する。
	 * @param param 色パラメーター
	 * @param setOriginal オリジナル値に設定するか？
	 */
	private void setColorConvertParameter(ColorConvertParameter param, boolean setOriginal) {
		if (param == null) {
			throw new IllegalArgumentException();
		}

		if (setOriginal) {
			// オリジナル値として設定する
			paramOrg = param.clone();
		}

		ColorConv colorReplace = param.getColorReplace();
		if (colorReplace == null) {
			colorReplace = ColorConv.NONE;
		}
		changeEventDisableSemaphore.incrementAndGet();
		try {
			cmbColorReplace.setSelectedItem(colorReplace);
			spinGray.setValue(Double.valueOf(param.getGrayLevel()));
			spinOffsetR.setValue(Integer.valueOf(param.getOffsetR()));
			spinOffsetG.setValue(Integer.valueOf(param.getOffsetG()));
			spinOffsetB.setValue(Integer.valueOf(param.getOffsetB()));
			spinOffsetA.setValue(Integer.valueOf(param.getOffsetA()));
			spinFactorR.setValue(Double.valueOf(param.getFactorR()));
			spinFactorG.setValue(Double.valueOf(param.getFactorG()));
			spinFactorB.setValue(Double.valueOf(param.getFactorB()));
			spinFactorA.setValue(Double.valueOf(param.getFactorA()));
			spinGammaR.setValue(Double.valueOf(param.getGammaR()));
			spinGammaG.setValue(Double.valueOf(param.getGammaG()));
			spinGammaB.setValue(Double.valueOf(param.getGammaB()));
			spinGammaA.setValue(Double.valueOf(param.getGammaA()));
			spinHue.setValue(Double.valueOf(param.getHue()));
			spinSaturation.setValue(Double.valueOf(param.getSaturation()));
			spinBrightness.setValue(Double.valueOf(param.getBrightness()));
			spinContrast.setValue(Double.valueOf(param.getContrast()));

		} finally {
			changeEventDisableSemaphore.decrementAndGet();
		}

		chachedParam = param;

		firePropertyChange("colorConvertParameter", null, param);
	}

	public ColorConvertParameter getColorConvertParameter() {
		if (chachedParam != null) {
			return chachedParam;
		}
		ColorConvertParameter param = new ColorConvertParameter();
		param.setColorReplace((ColorConv) cmbColorReplace.getSelectedItem());
		param.setGrayLevel(((Number) spinGray.getValue()).floatValue());
		param.setOffsetR(((Number) spinOffsetR.getValue()).intValue());
		param.setOffsetG(((Number) spinOffsetG.getValue()).intValue());
		param.setOffsetB(((Number) spinOffsetB.getValue()).intValue());
		param.setOffsetA(((Number) spinOffsetA.getValue()).intValue());
		param.setFactorR(((Number) spinFactorR.getValue()).floatValue());
		param.setFactorG(((Number) spinFactorG.getValue()).floatValue());
		param.setFactorB(((Number) spinFactorB.getValue()).floatValue());
		param.setFactorA(((Number) spinFactorA.getValue()).floatValue());
		param.setGammaR(((Number) spinGammaR.getValue()).floatValue());
		param.setGammaG(((Number) spinGammaG.getValue()).floatValue());
		param.setGammaB(((Number) spinGammaB.getValue()).floatValue());
		param.setGammaA(((Number) spinGammaA.getValue()).floatValue());
		param.setHue(((Number) spinHue.getValue()).floatValue());
		param.setSaturation(((Number) spinSaturation.getValue()).floatValue());
		param.setBrightness(((Number) spinBrightness.getValue()).floatValue());
		param.setContrast(((Number) spinContrast.getValue()).floatValue());
		chachedParam = param;
		return param;
	}

	/**
	 * カラー設定が変更されているか?
	 *
	 * @return 変更されている場合はtrue、そうでなければfalse
	 */
	public boolean isColorConvertParameterModified() {
		return !paramOrg.equals(getColorConvertParameter());
	}

	public ColorGroup getColorGroup() {
		return (ColorGroup) cmbColorGroup.getSelectedItem();
	}

	public void setColorGroup(ColorGroup colorGroup) {
		if (colorGroup == null) {
			colorGroup = ColorGroup.NA;
		}
		changeEventDisableSemaphore.incrementAndGet();
		try {
			cmbColorGroup.setSelectedItem(colorGroup);
		} finally {
			changeEventDisableSemaphore.decrementAndGet();
		}
	}

	public boolean isSyncColorGroup() {
		return chkColorGroupSync == null ? false : chkColorGroupSync.isSelected();
	}

	public void setSyncColorGroup(boolean selected) {
		if (chkColorGroupSync != null) {
			changeEventDisableSemaphore.incrementAndGet();
			try {
				chkColorGroupSync.setSelected(selected);
			} finally {
				changeEventDisableSemaphore.decrementAndGet();
			}
		}
	}
}
