package charactermanaj.ui;

import static java.lang.Math.max;
import static java.lang.Math.min;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.BoxLayout;
import javax.swing.JCheckBox;
import javax.swing.JColorChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JSplitPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;

import charactermanaj.Main;
import charactermanaj.clipboardSupport.ClipboardUtil;
import charactermanaj.graphics.AsyncImageBuilder;
import charactermanaj.graphics.ColorConvertedImageCachedLoader;
import charactermanaj.graphics.ImageBuildJobAbstractAdaptor;
import charactermanaj.graphics.ImageBuilder.ImageOutput;
import charactermanaj.graphics.io.ImageSaveHelper;
import charactermanaj.model.AppConfig;
import charactermanaj.model.CharacterData;
import charactermanaj.model.ColorGroup;
import charactermanaj.model.PartsCategory;
import charactermanaj.model.PartsColorInfo;
import charactermanaj.model.PartsColorManager;
import charactermanaj.model.PartsIdentifier;
import charactermanaj.model.PartsSet;
import charactermanaj.model.WorkingSet;
import charactermanaj.model.io.CharacterDataPersistent;
import charactermanaj.model.io.PartsImageDirectoryWatchAgent;
import charactermanaj.model.io.PartsImageDirectoryWatchEvent;
import charactermanaj.model.io.PartsImageDirectoryWatchListener;
import charactermanaj.model.io.RecentDataPersistent;
import charactermanaj.ui.ImageSelectPanel.ImageSelectPanelEvent;
import charactermanaj.ui.ImageSelectPanel.ImageSelectPanelListener;
import charactermanaj.ui.PreviewPanel.PreviewPanelEvent;
import charactermanaj.ui.PreviewPanel.PreviewPanelListener;
import charactermanaj.ui.model.ColorChangeEvent;
import charactermanaj.ui.model.ColorChangeListener;
import charactermanaj.ui.model.ColorGroupCoordinator;
import charactermanaj.ui.model.PartsColorCoordinator;
import charactermanaj.ui.model.PartsSelectionManager;
import charactermanaj.util.DesktopUtilities;
import charactermanaj.util.ErrorMessageHelper;
import charactermanaj.util.LocalizedResourcePropertyLoader;
import charactermanaj.util.ApplicationLogger;
import charactermanaj.util.UIHelper;
import charactermanaj.util.UserData;
import charactermanaj.util.UserDataFactory;


public class MainFrame extends JFrame {

	private static final long serialVersionUID = 1L;
	
	private static final Logger logger = ApplicationLogger.getLogger();
	
	protected static final String STRINGS_RESOURCE = "strings/mainframe";
	
	protected static final String MENU_STRINGS_RESOURCE = "menu/menu";
	
	private PreviewPanel previewPane;
	
	protected final BufferedImage icon;
	
	protected final CharacterData characterData;
	
	protected final PartsSelectionManager partsSelectionManager;
	
	protected final ImageSelectPanelList imageSelectPanels;
	
	protected final ColorGroupCoordinator colorGroupCoordinator;
	
	protected final PartsColorCoordinator partsColorCoordinator;
	
	protected static MainFrame activedMainFrame;
	
	private ColorConvertedImageCachedLoader imageLoader;
	
	private AsyncImageBuilder imageBuilder;
	
	private ImageSaveHelper imageSaveHelper = new ImageSaveHelper();
	
	private PartsImageDirectoryWatchAgent watchAgent;
	
	private String characterDataName;
	
	/**
	 * 使用中のキャラクターデータのコレクション.
	 */
	private static final HashMap<URL, Integer> activeCharacterDatas = new HashMap<URL, Integer>(); 
	
	/**
	 * キャラクターデータが使用中であるか?<br>
	 * キャラクターデータのDocBaseをもとに判断する.<br>
	 * nullを指定した場合は常にfalseを返す.<br>
	 * @param characterData キャラクターデータ、またはnull
	 * @return 使用中であればtrue
	 */
	public static boolean isUsingCharacterData(CharacterData characterData) {
		if (characterData == null) {
			return false;
		}
		URL characterDocBase = characterData.getDocBase();
		if (characterDocBase == null) {
			return false;
		}
		synchronized (activeCharacterDatas) {
			Integer cnt = activeCharacterDatas.get(characterDocBase);
			if (cnt != null && cnt.intValue() > 0) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * メインフレームを構築する.
	 * @param characterData キャラクターデータ
	 */
	public MainFrame(CharacterData characterData) {
		if (characterData == null) {
			throw new IllegalArgumentException();
		}
		
		this.characterData = characterData;

		// 使用中のキャラクターデータであることを登録する.
		synchronized (activeCharacterDatas) {
			URL characterDocBase = characterData.getDocBase();
			if (characterDocBase != null) {
				Integer cnt = activeCharacterDatas.get(characterDocBase);
				if (cnt == null) {
					cnt = Integer.valueOf(0);
				}
				cnt = Integer.valueOf(cnt.intValue() + 1);
				activeCharacterDatas.put(characterDocBase, cnt);
			}
		}
		
		AppConfig appConfig = AppConfig.getInstance();
		
		PartsColorManager partsColorManager = characterData.getPartsColorManager();

		imageLoader = new ColorConvertedImageCachedLoader();
		imageBuilder = new AsyncImageBuilder(imageLoader);
		partsSelectionManager = new PartsSelectionManager(partsColorManager);
		colorGroupCoordinator = new ColorGroupCoordinator(partsSelectionManager, partsColorManager);
		partsColorCoordinator = new PartsColorCoordinator(partsColorManager, colorGroupCoordinator);
		watchAgent = new PartsImageDirectoryWatchAgent(characterData);

		Properties strings = LocalizedResourcePropertyLoader.getInstance()
				.getLocalizedProperties(STRINGS_RESOURCE);

		String title;
		if (Main.isMacOSX()) {
			// Mac OS Xの場合はウィンドウにタイトルはつけない。
			title = "";
		} else {
			title = strings.getProperty("title");
		}
		characterDataName = characterData.getName();
		setTitle(title + characterDataName);
		
		setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				onCloseProfile();
			}
			@Override
			public void windowClosed(WindowEvent e) {
				imageBuilder.stop();
				watchAgent.stop();
			}
			@Override
			public void windowActivated(WindowEvent e) {
				activedMainFrame = MainFrame.this;
			}
			@Override
			public void windowOpened(WindowEvent e) {
				if (!imageBuilder.isAlive()) {
					imageBuilder.start();
				}
				scrollToSelectedParts();
			}
		});

		icon = UIHelper.getInstance().getImage("icons/icon.png");
		setIconImage(icon);
		
		JMenuBar menuBar = createMenuBar();
		setJMenuBar(menuBar);
		
		Container contentPane = getContentPane();

		previewPane = new PreviewPanel();
		previewPane.setTitle(characterDataName);
		previewPane.addPreviewPanelListener(new PreviewPanelListener() {
			public void addFavorite(PreviewPanelEvent e) {
				onRegisterFavorite();
			}
			public void changeBackgroundColor(PreviewPanelEvent e) {
				onChangeBgColor();
			}
			public void copyPicture(PreviewPanelEvent e) {
				onCopy();
			}
			public void savePicture(PreviewPanelEvent e) {
				onSavePicture();
			}
			public void showInformation(PreviewPanelEvent e) {
				onInformation();
			}
			public void flipHorizontal(PreviewPanelEvent e) {
				onFlipHolizontal();
			}
		});
		
		imageSelectPanels = new ImageSelectPanelList();
		
		JPanel imgSelectPanelsPanel = new JPanel();
		BoxLayout bl = new BoxLayout(imgSelectPanelsPanel, BoxLayout.PAGE_AXIS);
		imgSelectPanelsPanel.setLayout(bl);
		for (PartsCategory category : characterData.getPartsCategories()) {
			final ImageSelectPanel imageSelectPanel = new ImageSelectPanel(category, characterData);
			imgSelectPanelsPanel.add(imageSelectPanel);
			imageSelectPanels.add(imageSelectPanel);
			partsSelectionManager.register(imageSelectPanel);
		}
		
		JScrollPane imgSelectPanelsPanelSp = new JScrollPane(imgSelectPanelsPanel) {
			private static final long serialVersionUID = 1L;
			@Override
			public JScrollBar createVerticalScrollBar() {
				JScrollBar sb = super.createVerticalScrollBar();
				sb.setUnitIncrement(12);
				return sb;
			}
		};
		imgSelectPanelsPanelSp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);

		JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, imgSelectPanelsPanelSp, previewPane);
		contentPane.add(splitPane, BorderLayout.CENTER);

		Dimension imageSize = characterData.getImageSize();
		// 画像サイズ300x400を基準サイズとして、それ以下にはならない.
		// 画像サイズ800x600が最大サイズとして、それ以上の場合はウィンドウサイズは固定してスクロールバーに任せる
		int imageWidth = min(800, max(300, imageSize != null ? imageSize.width : 0));
		int imageHeight = min(600, max(400, imageSize != null ? imageSize.height : 0));
		// 300x400の画像の場合にメインフレームが600x550だとちょうどいい感じ.
		// それ以上大きい画像の場合は増えた分だけフレームを大きくしておく.
		setSize(imageWidth - 300 + 600, imageHeight - 400 + 550);

		//setLocationRelativeTo(null);
		setLocationByPlatform(true);
		
		imgSelectPanelsPanelSp.requestFocus();
		
		ArrayList<ColorGroup> colorGroups = new ArrayList<ColorGroup>();
		colorGroups.addAll(characterData.getColorGroups());
		
		final ColorChangeListener colorChangeListener = new ColorChangeListener() {
			public void onColorGroupChange(ColorChangeEvent event) {
				// do nothing.
			}
			public void onColorChange(ColorChangeEvent event) {
				MainFrame.this.requestPreview();
			}
		};
		colorGroupCoordinator.addColorChangeListener(colorChangeListener);
		
		for (int idx = 0; idx < imageSelectPanels.size(); idx++) {
			ImageSelectPanel imageSelectPanel = imageSelectPanels.get(idx);
			final PartsCategory partsCategory = imageSelectPanel.getPartsCategory();
			final ColorDialog colorDialog = new ColorDialog(this, partsCategory, colorGroups);
			colorGroupCoordinator.registerColorDialog(colorDialog);
			partsColorCoordinator.register(imageSelectPanel, colorDialog);
			final int curidx = idx;
			imageSelectPanel.addImageSelectListener(new ImageSelectPanelListener() {
				public void onChangeColor(ImageSelectPanelEvent event) {
					colorDialog.adjustLocation(curidx);
					colorDialog.setVisible(!colorDialog.isVisible());
				}
				public void onPreferences(ImageSelectPanelEvent event) {
					// do nothing. (not supported)
				}
				public void onChange(ImageSelectPanelEvent event) {
					MainFrame.this.requestPreview();
				}
				public void onSelectChange(ImageSelectPanelEvent event) {
					// do nothing.
				}
			});
		}
		
		partsSelectionManager.loadParts();
		
		if (!loadWorkingSet()) {
			String defaultPresetId = characterData.getDefaultPartsSetId();
			PartsSet presetParts = null;
			if (defaultPresetId != null) {
				presetParts = characterData.getPartsSets().get(defaultPresetId);
			}
			if (presetParts == null) {
				partsColorCoordinator.initColorDialog();
				requestPreview();
			} else {
				selectPresetParts(presetParts);
			}
		}
		
		// ディレクトリを監視し変更があった場合にパーツをリロードするリスナ
		watchAgent.addPartsImageDirectoryWatchListener(new PartsImageDirectoryWatchListener() {
			public void detectPartsImageChange(PartsImageDirectoryWatchEvent e) {
				Runnable refreshJob = new Runnable() {
					public void run() {
						onDetectPartsImageChange();
					}
				};
				if (SwingUtilities.isEventDispatchThread()) {
					refreshJob.run();
				} else {
					SwingUtilities.invokeLater(refreshJob);
				}
			}
		});

		// 監視が有効であれば、ディレクトリの監視をスタートする
		if (appConfig.isEnableDirWatch() && characterData.isWatchDirectory()) {
			watchAgent.start();
		}
	}

	/**
	 * パーツが変更されたことを検知した場合.<br>
	 * パーツデータをリロードし、各カテゴリのパーツ一覧を再表示させ、プレビューを更新する.<br>
	 */
	protected void onDetectPartsImageChange() {
		if (characterData.reloadPartsData()) {
			partsSelectionManager.loadParts();
			requestPreview();
		}
	}

	/**
	 * すべてのカテゴリのリストで選択中のアイテムが見えるようにスクロールする.
	 */
	protected void scrollToSelectedParts() {
		partsSelectionManager.scrollToSelectedParts();
	}

	/**
	 * プリセットを適用しキャラクターイメージを再構築します.
	 * @param presetParts
	 */
	protected void selectPresetParts(PartsSet presetParts) {
		// プリセットパーツで選択を変える
		partsSelectionManager.selectPartsSet(presetParts);
		// カラーパネルを選択されているアイテムをもとに再設定する
		partsColorCoordinator.initColorDialog();
		// 再表示
		requestPreview();
	}
	
	/**
	 * お気に入りメニューが開いたとき
	 * @param menu
	 */
	protected void onSelectedFavoriteMenu(JMenu menu) {
		int mx = menu.getMenuComponentCount();
		int separatorIdx = -1;
		for (int idx = 0; idx < mx; idx++) {
			Component item = menu.getMenuComponent(idx);
			if (item instanceof JSeparator) {
				separatorIdx = idx;
				break;
			}
		}
		// 既存メニューの削除
		if (separatorIdx > 0) {
			while (menu.getMenuComponentCount() > separatorIdx + 1) {
				menu.remove(separatorIdx + 1);
			}
		}
		
		// 表示順にソート
		ArrayList<PartsSet> partssets = new ArrayList<PartsSet>();
		partssets.addAll(characterData.getPartsSets().values());
		Collections.sort(partssets, new Comparator<PartsSet>() {
			public int compare(PartsSet o1, PartsSet o2) {
				int ret = o1.getLocalizedName().compareTo(o2.getLocalizedName());
				if (ret == 0) {
					ret = o1.getPartsSetId().compareTo(o2.getPartsSetId());
				}
				if (ret == 0) {
					ret = o1.hashCode() - o2.hashCode();
				}
				return ret;
			}
		});
		
		// メニューの再構築
		for (final PartsSet presetParts : partssets) {
			JMenuItem favoriteMenu = new JMenuItem();
			favoriteMenu.setName(presetParts.getPartsSetId());
			favoriteMenu.setText(presetParts.getLocalizedName());
			if (presetParts.isPresetParts()) {
				Font font = favoriteMenu.getFont();
				favoriteMenu.setFont(font.deriveFont(Font.BOLD));
			}
			favoriteMenu.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					selectPresetParts(presetParts);
				}
			});
			menu.add(favoriteMenu);
		}
	}
	
	/**
	 * プレビューの更新を要求する.
	 * 更新は非同期に行われる.
	 */
	protected void requestPreview() {
		if (!characterData.isValid()) {
			return;
		}
		
		// 選択されているパーツの各イメージを取得しレイヤー順に並び替えて合成する.
		// 合成は別スレッドにて非同期に行われる.
		// リクエストは随時受け付けて、最新のリクエストだけが処理される.
		// (処理がはじまる前に新しいリクエストで上書きされた場合、前のリクエストは単に捨てられる.)
		imageBuilder.requestJob(new ImageBuildJobAbstractAdaptor(characterData) {
			private long ticket;
			@Override
			public void onQueueing(long ticket) {
				this.ticket = ticket;
				previewPane.setLoadingRequest(ticket);
			}
			@Override
			public void buildImage(ImageOutput output) {
				// 合成結果のイメージを引数としてイメージビルダから呼び出される.
				final BufferedImage img = output.getImageOutput();
				final Color bgColor = output.getImageBgColor();
				Runnable refreshJob = new Runnable() {
					public void run() {
						previewPane.setImageBgColor(bgColor);
						previewPane.setPreviewImage(img);
						previewPane.setLoadingComplete(ticket);
					}
				};
				if (SwingUtilities.isEventDispatchThread()) {
					refreshJob.run();
				} else {
					try {
						SwingUtilities.invokeAndWait(refreshJob);
					} catch (Exception ex) {
						logger.log(Level.WARNING, "build image failed.", ex);
					}
				}
			}
			@Override
			public void handleException(final Exception ex) {
				// 合成中に例外が発生した場合、イメージビルダから呼び出される.
				Runnable showExceptionJob = new Runnable() {
					public void run() {
						ErrorMessageHelper.showErrorDialog(MainFrame.this, ex);
					}
				};
				if (SwingUtilities.isEventDispatchThread()) {
					showExceptionJob.run();
				} else {
					SwingUtilities.invokeLater(showExceptionJob);
				}
			}
			@Override
			protected PartsSet getPartsSet() {
				// 合成できる状態になった時点でイメージビルダから呼び出される.
				final PartsSet[] result = new PartsSet[1];
				Runnable collectPartsSetJob = new Runnable() {
					public void run() {
						PartsSet partsSet = partsSelectionManager.createPartsSet();
						result[0] = partsSet;
					}
				};
				if (SwingUtilities.isEventDispatchThread()) {
					collectPartsSetJob.run();
				} else {
					try {
						// スレッドによるSwingのイベントディスパッチスレッド以外からの呼び出しの場合、
						// Swingディスパッチスレッドでパーツの選択状態を取得する.
						SwingUtilities.invokeAndWait(collectPartsSetJob);
						
					} catch (InvocationTargetException e) {
						throw new RuntimeException(e.getMessage(), e);
					} catch (InterruptedException e) {
						throw new RuntimeException("interrupted:" + e, e);
					}
				}
				return result[0];
			}
		});
	}
	
	/**
	 * プロファイルを開く
	 */
	protected void onOpenProfile() {
		try {
			MainFrame main2 = ProfileSelectorDialog.openProfile(this);
			if (main2 != null) {
				Point pt = getLocation();
				pt.x += 100;
				main2.setLocation(pt);
				main2.setVisible(true);
			}
				
		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}

	/**
	 * 背景色を変更する.
	 */
	protected void onChangeBgColor() {
		getJMenuBar().setEnabled(false);
		try {
			Properties strings = LocalizedResourcePropertyLoader.getInstance()
					.getLocalizedProperties(STRINGS_RESOURCE);

			Color color = partsSelectionManager.getImageBgColor();
			color = JColorChooser.showDialog(this, strings.getProperty("chooseBgColor"), color);
			if (color != null) {
				partsSelectionManager.setImageBgColor(color);
				requestPreview();
			}
		} finally {
			getJMenuBar().setEnabled(true);
		}
	}
	
	/**
	 * プリビューしている画像をファイルに保存する。
	 * サポートしているのはPNG/JPEGのみ。
	 */
	protected void onSavePicture() {
		Toolkit tk = Toolkit.getDefaultToolkit();
		BufferedImage img = previewPane.getPreviewImage();
		Color imgBgColor = partsSelectionManager.getImageBgColor();
		if (img == null) {
			tk.beep();
			return;
		}
		
		try {
			File outFile = imageSaveHelper.showSaveFileDialog(this);
			if (outFile == null) {
				return;
			}
			
			StringBuilder warnings = new StringBuilder();

			setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
			try {
				imageSaveHelper.savePicture(img, imgBgColor, outFile, warnings);
			} finally {
				setCursor(Cursor.getDefaultCursor());
			}
			if (warnings.length() > 0) {
				JOptionPane.showMessageDialog(this, warnings.toString(), "WARNINGS", JOptionPane.WARNING_MESSAGE);
			}

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

	protected void onManageParts() {
		if (!characterData.isValid()) {
			return;
		}
		
		PartsManageDialog mrgDlg = new PartsManageDialog(this, characterData);
		mrgDlg.setVisible(true);
		
		if (mrgDlg.isUpdated()) {
			// パーツ管理情報が更新された場合、
			// パーツデータをリロードする.
			if (characterData.reloadPartsData()) {
				partsSelectionManager.loadParts();
				requestPreview();
			}
		}
	}
	
	protected void onSearch() {
		if (!characterData.isValid()) {
			return;
		}
		
		SearchPartsDialog searchPartsDlg = new SearchPartsDialog(this, characterData, partsSelectionManager);
		searchPartsDlg.setVisible(true);
	}
	
	protected void onCopy() {
		try {
			BufferedImage img = previewPane.getPreviewImage();
			Color imgBgColor = partsSelectionManager.getImageBgColor();
			if (img == null) {
				Toolkit tk = Toolkit.getDefaultToolkit();
				tk.beep();
				return;
			}
			
			ClipboardUtil.setImage(img, imgBgColor);

		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}
	
	protected void onPreferences() {
		AppConfigDialog appConfigDlg = new AppConfigDialog(this);
		appConfigDlg.setVisible(true);
	}

	/**
	 * インポートウィザードを実行する.<br>
	 * インポートが実行された場合は、パーツをリロードする.<br>
	 * インポートウィザード表示中は監視スレッドは停止される.<br>
	 */
	protected void onImport() {
		// 監視スレッドを停止してからインポート処理を始める
		boolean stopped = watchAgent.stop();
		try {
			// インポートウィザードの実行
			ImportWizardDialog importWizDialog = new ImportWizardDialog(this, characterData);
			importWizDialog.setVisible(true);

			if (importWizDialog.getExitCode() == ImportWizardDialog.EXIT_PROFILE_UPDATED) {
				try {
					setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
					try {
						// インポートが実行された場合、パーツデータをリロードする.
						if (characterData.reloadPartsData()) {
							partsSelectionManager.loadParts();

							// パーツデータが変更された可能性があるので、
							// 監視状態をリセットしてから再開するようにする.
							watchAgent.reset();
						}
						
						// お気に入りをリロードする.
						CharacterDataPersistent persiste = CharacterDataPersistent.getInstance();
						persiste.loadFavorites(characterData);
					
					} finally {
						setCursor(Cursor.getDefaultCursor());
					}

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

				// 再表示
				requestPreview();
			}
			
		} finally {
			// 監視スレッドを再開する.
			if (stopped) {
				watchAgent.start();
			}
		}
	}
	
	protected void onExport() {
		ExportWizardDialog exportWizDlg = new ExportWizardDialog(this, characterData, previewPane.getPreviewImage());
		exportWizDlg.setVisible(true);
	}

	protected void onResetColor() {
		if (!characterData.isValid()) {
			Toolkit tk = Toolkit.getDefaultToolkit();
			tk.beep();
			return;
		}
		
		Properties strings = LocalizedResourcePropertyLoader.getInstance()
				.getLocalizedProperties(STRINGS_RESOURCE);
		
		if (JOptionPane.showConfirmDialog(this, strings.get("confirm.resetcolors"), strings.getProperty("confirm"),
				JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) != JOptionPane.YES_OPTION) {
			return;
		}
		characterData.getPartsColorManager().resetPartsColorInfo();
		partsColorCoordinator.initColorDialog();
		requestPreview();
	}
	
	protected void onCloseProfile() {
		saveWorkingSet();
		URL characterDocBase = characterData.getDocBase();
		if (characterDocBase != null) {
			// 使用中のキャラクターデータとしてのカウントを減らす
			synchronized (activeCharacterDatas) {
				int cnt = activeCharacterDatas.get(characterDocBase);
				cnt = cnt - 1;
				activeCharacterDatas.put(characterDocBase, Integer.valueOf(cnt));
			}

			// 最後に使用したキャラクターデータとして記憶する.
			RecentDataPersistent recentPersist = RecentDataPersistent.getInstance();
			try {
				recentPersist.saveRecent(characterData);
			} catch (Exception ex) {
				logger.log(Level.WARNING, "recent data saving failed.", ex);
				// recent情報の記録に失敗しても致命的ではないので、これは無視する.
			}
		}
		
		// スレッドを停止する.
		imageBuilder.stop();
		watchAgent.stop();

		// フレームウィンドウを破棄する.
		dispose();
	}
	
	/**
	 * 開いている、すべてのプロファイルを閉じる
	 */
	public static void closeAllProfiles() {
		// ウィンドウが閉じられることでアクティブなフレームが切り替わる場合を想定し、
		// 現在のアクティブなウィンドウをあらかじめ記憶しておく
		MainFrame mainFrame = activedMainFrame;

		// gcをかけてファイナライズを促進させる
		System.gc();
		
		// ファイナライズされていないFrameのうち、ネイティブリソースと関連づけられている
		// フレームについて、それがMainFrameのインスタンスであれば閉じる.
		// ただし、現在アクティブなものは除く
		for (Frame frame : JFrame.getFrames()) {
			try {
				if (frame.isDisplayable()) {
					// ネイティブリソースと関連づけられているフレーム
					if (frame instanceof MainFrame && frame != mainFrame) {
						// MainFrameのインスタンスであるので閉じる処理が可能.
						((MainFrame) frame).onCloseProfile();
					}
				}
			
			} catch (Exception ex) {
				logger.log(Level.SEVERE, "mainframe closing failed.", ex);
				// フレームを閉じるときに失敗した場合、通常、致命的問題だが
				// クローズ処理は継続しなければならない.
			}
		}
		
		// 現在アクティブなフレームを閉じる.
		// 最後に閉じることで「最後に使ったプロファイル」として記憶させる.
		if (activedMainFrame != null && activedMainFrame.isDisplayable()) {
			try {
				activedMainFrame.onCloseProfile();

			} catch (Exception ex) {
				logger.log(Level.SEVERE, "mainframe closing failed.", ex);
				// フレームを閉じるときに失敗した場合、通常、致命的問題だが
				// クローズ処理は継続しなければならない.
			}
		}
	}
	
	/**
	 * 画面の作業状態を保存するユーザーデータを取得する.
	 * @return ユーザーデータ
	 */
	protected UserData getWorkingSetUserData() {
		return getWorkingSetUserData(characterData);
	}
	
	/**
	 * 画面の作業状態を保存するユーザーデータを取得する.<br>
	 * キャラクターデータがnullまたは有効でない場合はnullを返す.<br>
	 * @param cd キャラクターデータ
	 * @return 作業状態を保存するユーザーデータ、もしくはnull
	 */
	public static UserData getWorkingSetUserData(CharacterData cd) {
		if (cd == null || !cd.isValid()) {
			return null;
		}
		String dataname = cd.getId() + "-workingset.ser";
		UserDataFactory userDataFactory = UserDataFactory.getInstance();
		UserData workingSetStore = userDataFactory
				.getMangledNamedUserData(cd.getDocBase(), dataname);
		return workingSetStore;
	}
	
	/**
	 * 画面の作業状態を保存する.
	 */
	protected void saveWorkingSet() {
		if (!characterData.isValid()) {
			return;
		}
		try {
			WorkingSet workingSet = new WorkingSet();
			workingSet.setCharacterDocBase(characterData.getDocBase());
			workingSet.setCharacterDataRev(characterData.getRev());
			PartsSet partsSet = partsSelectionManager.createPartsSet();
			workingSet.setPartsSet(partsSet);
			workingSet.setPartsColorInfoMap(characterData.getPartsColorManager().getPartsColorInfoMap());
			workingSet.setLastUsedSaveDir(imageSaveHelper.getLastUsedSaveDir());
			workingSet.setLastUsedExportDir(ExportWizardDialog.getLastUsedDir());

			UserData workingSetStore = getWorkingSetUserData();
			workingSetStore.save(workingSet);
			
		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}
	
	/**
	 * 画面の作業状態を復元する.
	 * @return
	 */
	protected boolean loadWorkingSet() {
		if (!characterData.isValid()) {
			return false;
		}
		try {
			UserData workingSetStore = getWorkingSetUserData();
			if (workingSetStore != null && workingSetStore.exists()) {
				WorkingSet workingSet = (WorkingSet) workingSetStore.load();
				if (!characterData.getDocBase().equals(workingSet.getCharacterDocBase())) {
					// ワーキングセットのキャラクターデータとキャラクターデータを定義しているDocBaseが異なる場合はワーキングセットを無視する
					logger.info("workingset data mismatch:" + characterData);
					return false;
				}

				String docRev = characterData.getRev();
				String workRev = workingSet.getCharacterDataRev();
				if (docRev == null || workRev == null || !docRev.equals(workRev)) {
					// ワーキングセットが保存されてからキャラクターデータの定義が変更されている場合はワーキングセットは無視する.
					logger.info("workingset revision mismatch: actual=" + characterData + "/workingSet=" + workingSet);
					return false;
				}
				
				Map<PartsIdentifier, PartsColorInfo> partsColorInfoMap = characterData.getPartsColorManager().getPartsColorInfoMap();
				for (Map.Entry<PartsIdentifier, PartsColorInfo> entry : workingSet.getPartsColorInfoMap().entrySet()) {
					PartsIdentifier partsIdentifier = entry.getKey();
					PartsColorInfo partsColorInfo = entry.getValue();
					partsColorInfoMap.put(partsIdentifier, partsColorInfo);
				}
				
				PartsSet partsSet = workingSet.getPartsSet();
				selectPresetParts(partsSet.createCompatible(characterData));
				
				imageSaveHelper.setLastUseSaveDir(workingSet.getLastUsedSaveDir());
				ExportWizardDialog.setLastUsedDir(workingSet.getLastUsedExportDir());
				
				return true;
			}
			
		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
		return false;
	}


	protected void onAbout() {
		try {
			AboutBox aboutBox = new AboutBox(this);
			aboutBox.showAboutBox();
			
		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}

	protected void onHelp() {
		try {
			Properties strings = LocalizedResourcePropertyLoader.getInstance()
					.getLocalizedProperties(STRINGS_RESOURCE);
			String helpURL = strings.getProperty("help.url");

			URI helpURI = new URI(helpURL);

			if (!DesktopUtilities.browse(helpURI) ){
				// JDK5で実行中の場合

				JPanel panel = new JPanel();
				BoxLayout layout = new BoxLayout(panel, BoxLayout.Y_AXIS);
				panel.setLayout(layout);
				panel.add(new JLabel(strings.getProperty("help.show")));
				JTextField txtURL = new JTextField(helpURL);
				panel.add(txtURL);
				JOptionPane.showMessageDialog(this, panel);
			}

		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}
	
	protected void onFlipHolizontal() {
		if (!characterData.isValid()) {
			Toolkit tk = Toolkit.getDefaultToolkit();
			tk.beep();
			return;
		}

		double[] affineTransformParameter = partsSelectionManager.getAffineTransformParameter();
		if (affineTransformParameter == null) {
			// 左右フリップするアフィン変換パラメータを構築する.
			Dimension siz = characterData.getImageSize();
			if (siz != null) {
				affineTransformParameter = new double[] {-1., 0, 0, 1., siz.width, 0};
			}
		} else {
			// アフィン変換パラメータをクリアする.
			affineTransformParameter = null;
		}
		partsSelectionManager.setAffineTransformParameter(affineTransformParameter);
		requestPreview();
	}
	
	protected void onInformation() {
		if (!characterData.isValid()) {
			Toolkit tk = Toolkit.getDefaultToolkit();
			tk.beep();
			return;
		}

		PartsSet partsSet = partsSelectionManager.createPartsSet();
		InformationDialog infoDlg = new InformationDialog(this, characterData, partsSet);
		infoDlg.setVisible(true);
	}
	
	protected void onManageFavorites() {
		if (!characterData.isValid()) {
			Toolkit tk = Toolkit.getDefaultToolkit();
			tk.beep();
			return;
		}

		ManageFavoriteDialog dlg = new ManageFavoriteDialog(this, characterData);
		dlg.setVisible(true);
		if (!dlg.isModified()) {
			return;
		}

		try {
			setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
			try {
				CharacterDataPersistent persiste = CharacterDataPersistent.getInstance();
				persiste.saveFavorites(characterData);
			} finally {
				setCursor(Cursor.getDefaultCursor());
			}

		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}
	
	protected void onRegisterFavorite() {
		if (!characterData.isValid()) {
			Toolkit tk = Toolkit.getDefaultToolkit();
			tk.beep();
			return;
		}
		try {
			// パーツセットを生成
			PartsSet partsSet = partsSelectionManager.createPartsSet();
			if (partsSet.isEmpty()) {
				// 空のパーツセットは登録しない.
				return;
			}

			// カラー情報の有無と名称を入力する.
			Properties strings = LocalizedResourcePropertyLoader.getInstance()
					.getLocalizedProperties(STRINGS_RESOURCE);
			
			JCheckBox chkColorInfo = new JCheckBox(strings.getProperty("input.favoritesColorInfo"));
			chkColorInfo.setSelected(true);
			
			String name = JOptionPane.showInputDialog(this, chkColorInfo, strings.getProperty("input.favorites"), JOptionPane.QUESTION_MESSAGE);
			if (name == null || name.trim().length() == 0) {
				return;
			}

			boolean includeColorInfo = chkColorInfo.isSelected();
			if (!includeColorInfo) {
				// カラー情報を除去する.
				partsSet.removeColorInfo();
			}
			
			// IDを設定する.
			String partsSetId = "ps" + UUID.randomUUID().toString();
			partsSet.setPartsSetId(partsSetId);

			// 名前を設定する.
			partsSet.setLocalizedName(name);

			// お気に入りコレクションに登録
			characterData.addPartsSet(partsSet);

			// ファイルに保存
			setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
			try {
				CharacterDataPersistent persiste = CharacterDataPersistent.getInstance();
				persiste.saveFavorites(characterData);
			} finally {
				setCursor(Cursor.getDefaultCursor());
			}
			
		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}

	protected JMenuBar createMenuBar() {
		JMenuBar menuBar = new JMenuBar() {
			private static final long serialVersionUID = 1L;
			@Override
			public void paint(Graphics g) {
				((Graphics2D) g).setRenderingHint(
						RenderingHints.KEY_TEXT_ANTIALIASING,
						RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
				super.paint(g);
			}
		};

		Properties menuProps = LocalizedResourcePropertyLoader.getInstance()
				.getLocalizedProperties(MENU_STRINGS_RESOURCE);
		
		MenuDataFactory[] menus = new MenuDataFactory[] {
				new MenuDataFactory("menu.file", new MenuDataFactory[] {
						new MenuDataFactory("file.openProfile", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onOpenProfile();
							}
						}),
						new MenuDataFactory("file.savePicture", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onSavePicture();
							}
						}),
						new MenuDataFactory("file.manageParts", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onManageParts();
							}
						}),
						new MenuDataFactory("file.import", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onImport();
							};
						}),
						new MenuDataFactory("file.export", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onExport();
							};
						}),
						new MenuDataFactory("file.preferences", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onPreferences();
							};
						}),
						null,
						new MenuDataFactory("file.closeProfile", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onCloseProfile();
							}
						}),
				}),
				new MenuDataFactory("menu.edit", new MenuDataFactory[] {
						new MenuDataFactory("edit.search", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onSearch();
							}
						}),
						new MenuDataFactory("edit.copy", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onCopy();
							}
						}),
						new MenuDataFactory("edit.flipHorizontal", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onFlipHolizontal();
							}
						}),
						new MenuDataFactory("edit.resetcolor", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onResetColor();
							}
						}),
						null,
						new MenuDataFactory("edit.information", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onInformation();
							}
						}),
				}),
				new MenuDataFactory("menu.favorite", new MenuDataFactory[] {
						new MenuDataFactory("favorite.register", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onRegisterFavorite();
							}
						}),
						new MenuDataFactory("favorite.manage", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onManageFavorites();
							}
						}),
						null,
				}),
				new MenuDataFactory("menu.help", new MenuDataFactory[] {
						new MenuDataFactory("help.help", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onHelp();
							}
						}),
						new MenuDataFactory("help.about", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onAbout();
							}
						}),
				}), };
		
		final HashMap<String, JMenu> menuMap = new HashMap<String, JMenu>();
		final HashMap<String, JMenuItem> menuItemMap = new HashMap<String, JMenuItem>();

		for (MenuDataFactory menuDataFactory : menus) {
			MenuData menuData = menuDataFactory.createMenuData(menuProps);
			JMenu menu = new JMenu() {
				private static final long serialVersionUID = 1L;
				@Override
				public void paint(Graphics g) {
					((Graphics2D) g).setRenderingHint(
							RenderingHints.KEY_TEXT_ANTIALIASING,
							RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
					super.paint(g);
				}
			};
			if (menuData.makeMenu(menu)) {
				menuBar.add(menu);
				
				menuMap.put(menuDataFactory.getName(), menu);
				
				for (MenuData child : menuData) {
					if (child == null) {
						menu.add(new JSeparator());
					} else {
						JMenuItem menuItem = new JMenuItem() {
							private static final long serialVersionUID = 1L;
							@Override
							public void paint(Graphics g) {
								((Graphics2D) g).setRenderingHint(
										RenderingHints.KEY_TEXT_ANTIALIASING,
										RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
								super.paint(g);
							}
						};
						if (child.makeMenu(menuItem)) {
							menu.add(menuItem);
							menuItemMap.put(child.getName(), menuItem);
						}
					}
				}
			}
		}
		
		menuMap.get("menu.edit").addMenuListener(new MenuListener() {
			public void menuCanceled(MenuEvent e) {
				// do nothing.
			}
			public void menuDeselected(MenuEvent e) {
				// do nothing.
			}
			public void menuSelected(MenuEvent e) {
				menuItemMap.get("edit.copy").setEnabled(previewPane.getPreviewImage() != null);
			}
		});
		menuMap.get("menu.favorite").addMenuListener(new MenuListener() {
			public void menuCanceled(MenuEvent e) {
				// do nothing.
			}
			public void menuDeselected(MenuEvent e) {
				// do nothing.
			}
			public void menuSelected(MenuEvent e) {
				JMenu menu = menuMap.get("menu.favorite");
				onSelectedFavoriteMenu(menu);
			}
		});
		
		return menuBar;
	}
	
}




