package charactermanaj.ui;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
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.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JTabbedPane;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

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.util.LocalizedResourcePropertyLoader;


/**
 * カラーダイアログ.<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 LinkedList<ColorChangeListener> listeners = new LinkedList<ColorChangeListener>();

	/**
	 * キャプションのプレフィックス
	 */
	private String captionBase;
	
	/**
	 * 現在表示しているカラー情報の対象としているパーツ識別子
	 */
	private PartsIdentifier partsIdentifier;
	
	/**
	 * カテゴリ全体に適用するチェックボックス
	 */
	private JCheckBox chkApplyAll;
	
	/**
	 * コンストラクタ
	 * @param parent 親フレーム
	 * @param partsCategory カテゴリ
	 * @param colorGroups 選択可能なカラーグループのコレクション
	 */
	public ColorDialog(JFrame parent, PartsCategory partsCategory, Collection<ColorGroup> colorGroups) {
		super(parent);
		this.partsCategory = partsCategory;

		final Properties strings = LocalizedResourcePropertyLoader.getInstance().getLocalizedProperties("strings/colordialog");
		
		String caption = strings.getProperty("colordialog.caption");
		String name = partsCategory.getLocalizedCategoryName();
		captionBase = caption + name;
		setTitle(captionBase);
		
		addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				setVisible(false);
			}
		});
		
		Container container = getContentPane();
		
		this.tabbedPane = new JTabbedPane(JTabbedPane.TOP, JTabbedPane.WRAP_TAB_LAYOUT);
		for (final Layer layer : partsCategory.getLayers()) {
			
			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);

			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();
			}
		};
		Action 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());
		btnPanel.add(chkApplyAll, gbc);
		
		gbc.gridx = colIdx++;
		gbc.gridy = 0;
		gbc.weightx = 1.;
		btnPanel.add(Box.createHorizontalGlue(), gbc);

		JButton btnApply = null;
		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);

		container.setLayout(new BorderLayout());
		container.add(tabbedPane, BorderLayout.CENTER);
		container.add(btnPanel, BorderLayout.SOUTH);
		
		if (btnApply != null) {
			getRootPane().setDefaultButton(btnApply);
		}
		
		pack();
	}
	
	/**
	 * ダイアログの表示位置を調整する.<br>
	 * 横位置Xはメインフレームの右側とし、縦位置Yはメインフレームの上位置からのoffset_yを加えた位置とする.<br>
	 * @param offset_y オフセットY
	 */
	public void adjustLocation(int offset_y) {
		Point pt = getParent().getLocation();
		Insets insets = getParent().getInsets();
		pt.x += getParent().getWidth();
		pt.y += (offset_y * insets.top);
		setLocation(pt);
	}

	/**
	 * このカラーダイアログが対応するパーツカテゴリを取得する.<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) {
				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(listener);
	}

	/**
	 * 色ダイアログが変更された場合に通知を受けるリスナーを登録解除する.<br>
	 * @param listener
	 */
	public void removeColorChangeListener(ColorChangeListener listener) {
		listeners.remove(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;
			}
		}
		ColorChangeEvent event = new ColorChangeEvent(this, layer);
		for (ColorChangeListener listener : listeners) {
			listener.onColorChange(event);
		}
	}

	/**
	 * 色グループが変更されたことを通知する.<br>
	 * @param layer レイヤー
	 */
	protected void fireColorGroupChangeEvent(Layer layer) {
		if (layer == null) {
			throw new IllegalArgumentException();
		}
		ColorChangeEvent event = new ColorChangeEvent(this, layer);
		for (ColorChangeListener listener : listeners) {
			listener.onColorGroupChange(event);
		}
	}
	
	@Override
	public String toString() {
		return "ColorDialog(partsCategory:" + partsCategory + ")";
	}
}


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

	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 spinGammaR;

	private JSpinner spinGammaG;
	
	private JSpinner spinGammaB;
	
	private JSpinner spinGammaA;
	
	private JComboBox cmbColorGroup;
	
	private JCheckBox chkColorGroupSync;

	private final ColorDialog parent;
	
	/**
	 * パラメータの明示的変更時に他のパラメータへの反映イベントを停止させるためのセマフォ
	 */
	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(Layer layer) {
		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(Layer layer) {
		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);
			}
		}
	}

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

		final Properties strings = LocalizedResourcePropertyLoader.getInstance().getLocalizedProperties("strings/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(layer);
			}
		};
		
		// 色置換パネル
		
		JPanel 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.05);
		grayModel.addChangeListener(changeEventHandler);
		cmbColorReplace.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				fireColorChangeEvent(layer);
			}
		});
		spinGray = new JSpinner(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変更パネル
		
		JPanel 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);
		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);
		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);
		colorLevelPanel.add(spinGammaR);
		colorLevelPanel.add(spinGammaG);
		colorLevelPanel.add(spinGammaB);
		colorLevelPanel.add(spinGammaA);
		
		// 色調パネル
		
		JPanel colorTunePanel = new JPanel();
		colorTunePanel.setBorder(BorderFactory.createCompoundBorder(
				BorderFactory.createEmptyBorder(3, 3, 3, 3),
				BorderFactory.createTitledBorder(strings.getProperty("group.hsb.caption"))));
		GridLayout gl2 = new GridLayout(3, 3);
		gl2.setHgap(3);
		gl2.setVgap(3);
		colorTunePanel.setLayout(gl2);
		colorTunePanel.add(new JLabel(strings.getProperty("hue"), JLabel.CENTER)); // Hue 色相
		colorTunePanel.add(new JLabel(strings.getProperty("saturation"), JLabel.CENTER)); // Saturation 彩度
		colorTunePanel.add(new JLabel(strings.getProperty("brightness"), JLabel.CENTER)); // Brightness 明度

		SpinnerNumberModel hsbModelH = new SpinnerNumberModel(0., -1., 1., 0.01);
		SpinnerNumberModel hsbModelS = new SpinnerNumberModel(0., -1., 1., 0.01);
		SpinnerNumberModel hsbModelB = new SpinnerNumberModel(0., -1., 1., 0.01);
		
		hsbModelH.addChangeListener(changeEventHandler);
		hsbModelS.addChangeListener(changeEventHandler);
		hsbModelB.addChangeListener(changeEventHandler);
		
		spinHue = new JSpinner(hsbModelH);
		spinSaturation = new JSpinner(hsbModelS);
		spinBrightness = new JSpinner(hsbModelB);
		
		colorTunePanel.add(spinHue);
		colorTunePanel.add(spinSaturation);
		colorTunePanel.add(spinBrightness);
		
		JSlider sliderHue = new JSlider();
		JSlider sliderSaturation = new JSlider();
		JSlider sliderBrightness = new JSlider();
		sliderHue.setPreferredSize(spinHue.getPreferredSize());
		sliderSaturation.setPreferredSize(spinSaturation.getPreferredSize());
		sliderBrightness.setPreferredSize(spinBrightness.getPreferredSize());
		colorTunePanel.add(sliderHue);
		colorTunePanel.add(sliderSaturation);
		colorTunePanel.add(sliderBrightness);

		JSlider sliders[] = new JSlider[] {sliderHue, sliderSaturation, sliderBrightness};
		JSpinner spinners[] = new JSpinner[] {spinHue, spinSaturation, spinBrightness};
		
		for (int idx = 0; idx < spinners.length; idx++) {
			final JSlider sl = sliders[idx];
			final JSpinner sp = spinners[idx];
			sl.setMinimum(-100);
			sl.setMaximum(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(layer);
			}
		});
		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);
	}
	
	public void resetColor() {
		setColorConvertParameter(paramOrg);
	}
	
	public void setColorConvertParameter(ColorConvertParameter param) {
		if (param == null) {
			throw new IllegalArgumentException();
		}
		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()));
		} finally {
			changeEventDisableSemaphore.decrementAndGet();
		}
		chachedParam = 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());
		chachedParam = param;
		return param;
	}
	
	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();
			}
		}
	}
}
