package charactermanaj.ui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.net.URI;
import java.net.URL;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Semaphore;
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.InputMap;
import javax.swing.JButton;
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.JPopupMenu;
import javax.swing.JRootPane;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableColumnModel;

import charactermanaj.Main;
import charactermanaj.model.AppConfig;
import charactermanaj.model.CharacterData;
import charactermanaj.model.PartsAuthorInfo;
import charactermanaj.model.PartsCategory;
import charactermanaj.model.PartsIdentifier;
import charactermanaj.model.PartsManageData;
import charactermanaj.model.PartsSpec;
import charactermanaj.model.io.CharacterDataPersistent;
import charactermanaj.ui.model.AbstractTableModelWithComboBoxModel;
import charactermanaj.util.ApplicationLogger;
import charactermanaj.util.DesktopUtilities;
import charactermanaj.util.ErrorMessageHelper;
import charactermanaj.util.LocalizedResourcePropertyLoader;

public class PartsManageDialog extends JDialog {

	private static final long serialVersionUID = 1L;
	
	protected static final String STRINGS_RESOURCE = "strings/partsmanagedialog";
	
	
	private static final Logger logger = ApplicationLogger.getLogger();

	private CharacterData characterData;
	
	private PartsManageTableModel partsManageTableModel;
	
	private JTable partsManageTable;
	
	private JTextField txtHomepage;
	
	private JTextField txtAuthor;
	
	private boolean updated;

	
	public PartsManageDialog(JFrame parent, CharacterData characterData) {
		super(parent, true);
		
		if (characterData == null) {
			throw new IllegalArgumentException();
		}
		this.characterData = characterData;
		
		setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
		addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				onClose();
			}
		});

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

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

		Container contentPane = getContentPane();
		
		// パーツリストテーブル
		JPanel partsListPanel = new JPanel();
		partsListPanel.setBorder(BorderFactory.createCompoundBorder(
				BorderFactory.createEmptyBorder(5, 5, 5, 5), BorderFactory
						.createTitledBorder(strings.getProperty("partslist"))));

		GridBagLayout partsListPanelLayout = new GridBagLayout();
		partsListPanel.setLayout(partsListPanelLayout);
		
		partsManageTableModel = new PartsManageTableModel();
		partsManageTable = new JTable(partsManageTableModel);
		partsManageTable.setShowGrid(true);
		partsManageTable.setGridColor(AppConfig.getInstance().getGridColor());
		partsManageTableModel.adjustColumnModel(partsManageTable.getColumnModel());
		partsManageTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
		partsManageTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);

		JScrollPane partsManageTableSP = new JScrollPane(partsManageTable);
		
		partsManageTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
			public void valueChanged(ListSelectionEvent e) {
				if (!e.getValueIsAdjusting()) {
					onChangeSelection();
				}
			}
		});
		
		partsManageTableModel.addTableModelListener(new TableModelListener() {
			public void tableChanged(TableModelEvent e) {
				onTableDataChange(e.getFirstRow(), e.getLastRow());
			}
		});
		
		GridBagConstraints gbc = new GridBagConstraints();
		gbc.gridx = 0;
		gbc.gridy = 0;
		gbc.gridheight = 1;
		gbc.gridwidth = 4;
		gbc.anchor = GridBagConstraints.EAST;
		gbc.fill = GridBagConstraints.BOTH;
		gbc.insets = new Insets(3, 3, 3, 3);
		gbc.ipadx = 0;
		gbc.ipady = 0;
		gbc.weightx = 1.;
		gbc.weighty = 1.;
		partsListPanel.add(partsManageTableSP, gbc);
		
		Action actSortByName = new AbstractAction(strings.getProperty("sortByName")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				onSortByName();
			}
		};
		Action actSortByAuthor = new AbstractAction(strings.getProperty("sortByAuthor")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				onSortByAuthor();
			}
		};
		Action actSortByTimestamp = new AbstractAction(strings.getProperty("sortByTimestamp")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				onSortByTimestamp();
			}
		};
		
		gbc.gridx = 0;
		gbc.gridy = 1;
		gbc.gridheight = 1;
		gbc.gridwidth = 1;
		gbc.weightx = 0.;
		gbc.weighty = 0.;
		partsListPanel.add(new JButton(actSortByName), gbc);

		gbc.gridx = 1;
		gbc.gridy = 1;
		gbc.weightx = 0.;
		gbc.weighty = 0.;
		partsListPanel.add(new JButton(actSortByAuthor), gbc);

		gbc.gridx = 2;
		gbc.gridy = 1;
		gbc.weightx = 0.;
		gbc.weighty = 0.;
		partsListPanel.add(new JButton(actSortByTimestamp), gbc);

		gbc.gridx = 3;
		gbc.gridy = 1;
		gbc.weightx = 1.;
		gbc.weighty = 0.;
		partsListPanel.add(Box.createHorizontalGlue(), gbc);

		contentPane.add(partsListPanel, BorderLayout.CENTER);

		// テーブルのコンテキストメニュー
		final JPopupMenu popupMenu = new JPopupMenu();
		Action actApplyAllDownloadURL = new AbstractAction(strings.getProperty("applyAllDownloadURL")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				onApplyAllDownloadURL();
			}
		};
		Action actApplyAllVersion = new AbstractAction(strings.getProperty("applyAllVersion")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				onApplyAllVersion();
			}
		};
		popupMenu.add(actApplyAllVersion);
		popupMenu.add(actApplyAllDownloadURL);
		
		partsManageTable.addMouseListener(new MouseAdapter() {
			@Override
			public void mousePressed(MouseEvent e) {
				evaluatePopup(e);
			}
			@Override
			public void mouseReleased(MouseEvent e) {
				evaluatePopup(e);
			}
			private void evaluatePopup(MouseEvent e) {
				if (e.isPopupTrigger()) {
					popupMenu.show(partsManageTable, e.getX(), e.getY());
				}
			}
		});
		
		// 作者情報パネル
		JPanel authorPanel = new JPanel();
		authorPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory
				.createEmptyBorder(5, 5, 5, 5), BorderFactory
				.createTitledBorder(strings.getProperty("author.info"))));
		GridBagLayout authorPanelLayout = new GridBagLayout();
		authorPanel.setLayout(authorPanelLayout);
		
		gbc.gridx = 0;
		gbc.gridy = 0;
		gbc.gridheight = 1;
		gbc.gridwidth = 1;
		gbc.anchor = GridBagConstraints.EAST;
		gbc.fill = GridBagConstraints.BOTH;
		gbc.insets = new Insets(3, 3, 3, 3);
		gbc.ipadx = 0;
		gbc.ipady = 0;
		gbc.weightx = 0.;
		gbc.weighty = 0.;
		authorPanel.add(new JLabel(strings.getProperty("author"), JLabel.RIGHT), gbc);

		gbc.gridx = 1;
		gbc.gridy = 0;
		gbc.gridwidth = 2;
		gbc.weightx = 1.;
		txtAuthor = new JTextField();
		authorPanel.add(txtAuthor, gbc);

		gbc.gridx = 0;
		gbc.gridy = 1;
		gbc.gridwidth = 1;
		gbc.weightx = 0.;
		authorPanel.add(new JLabel(strings.getProperty("homepage"), JLabel.RIGHT), gbc);

		gbc.gridx = 1;
		gbc.gridy = 1;
		gbc.gridwidth = 1;
		gbc.weightx = 1.;
		txtHomepage = new JTextField();
		authorPanel.add(txtHomepage, gbc);

		gbc.gridx = 2;
		gbc.gridy = 1;
		gbc.gridwidth = 1;
		gbc.weightx = 0.;
		Action actBrowseHomepage = new AbstractAction(strings.getProperty("open")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				onBrosweHomepage();
			}
		};
		authorPanel.add(new JButton(actBrowseHomepage), gbc);

		if (!DesktopUtilities.isSupported()) {
			actBrowseHomepage.setEnabled(false);
		}
		
		txtAuthor.getDocument().addDocumentListener(new DocumentListener() {
			public void removeUpdate(DocumentEvent e) {
				onEditAuthor();
			}
			public void insertUpdate(DocumentEvent e) {
				onEditAuthor();
			}
			public void changedUpdate(DocumentEvent e) {
				onEditAuthor();
			}
		});
		txtHomepage.getDocument().addDocumentListener(new DocumentListener() {
			public void removeUpdate(DocumentEvent e) {
				onEditHomepage();
			}
			public void insertUpdate(DocumentEvent e) {
				onEditHomepage();
			}
			public void changedUpdate(DocumentEvent e) {
				onEditHomepage();
			}
		});
		
		
		// ボタンパネル
		JPanel btnPanel = new JPanel();
		btnPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 45));
		GridBagLayout btnPanelLayout = new GridBagLayout();
		btnPanel.setLayout(btnPanelLayout);
		
		Action actClose = new AbstractAction(strings.getProperty("cancel")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				onClose();
			}
		};
		Action actOK = new AbstractAction(strings.getProperty("update")) {
			private static final long serialVersionUID = 1L;
			public void actionPerformed(ActionEvent e) {
				onOK();
			}
		};
		
		gbc.gridx = 0;
		gbc.gridy = 0;
		gbc.weightx = 1.;
		btnPanel.add(Box.createHorizontalGlue(), gbc);
		
		gbc.gridx = Main.isMacOSX() ? 2 : 1;
		gbc.gridy = 0;
		gbc.weightx = 0.;
		btnPanel.add(new JButton(actOK), gbc);

		gbc.gridx = Main.isMacOSX() ? 1 : 2;
		gbc.gridy = 0;
		gbc.weightx = 0.;
		btnPanel.add(new JButton(actClose), gbc);
		
		// ダイアログ下部
		JPanel southPanel = new JPanel(new BorderLayout());
		southPanel.add(authorPanel, BorderLayout.NORTH);
		southPanel.add(btnPanel, BorderLayout.SOUTH);

		contentPane.add(southPanel, BorderLayout.SOUTH);
		
		// キーボード
		
		JRootPane rootPane = getRootPane();
		InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
		ActionMap am = rootPane.getActionMap();
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closePartsManageDialog");
		am.put("closePartsManageDialog", actClose);

		// モデル構築
		partsManageTableModel.initModel(characterData);
		
		// ウィンドウ配置
		setSize(500, 400);
		setLocationRelativeTo(parent);
	}

	private Semaphore authorEditSemaphore = new Semaphore(1);
	
	protected void onChangeSelection() {
		try {
			authorEditSemaphore.acquire();
			try {
				int [] selRows = partsManageTable.getSelectedRows();
				HashSet<String> authors = new HashSet<String>();
				for (int selRow : selRows) {
					PartsManageTableRow row = partsManageTableModel.getRow(selRow);
					authors.add(row.getAuthor() == null ? "" : row.getAuthor());
				}
				if (authors.size() > 1) {
					AppConfig appConfig = AppConfig.getInstance();
					txtAuthor.setBackground(appConfig.getAuthorEditConflictBgColor());
					txtHomepage.setBackground(appConfig.getAuthorEditConflictBgColor());
				} else {
					Color bgColor = UIManager.getColor("TextField.background");
					if (bgColor == null) {
						bgColor = Color.white;
					}
					txtAuthor.setBackground(bgColor);
					txtHomepage.setBackground(bgColor);
				}
				if (authors.isEmpty()) {
					// 選択されているauthorがない場合は全部編集不可
					txtAuthor.setEditable(false);
					txtAuthor.setText("");
					txtHomepage.setEditable(false);
					txtHomepage.setText("");
				} else {
					// 選択されているAuthorが1つ以上あればAuthorは編集可
					txtAuthor.setEditable(true);
					txtHomepage.setEditable(true);
					if (authors.size() == 1) {
						// 選択されているAuthorが一個であれば、それを表示
						String author = authors.iterator().next();
						String homepage = partsManageTableModel.getHomepage(author);
						txtAuthor.setText(author);
						txtHomepage.setText(homepage);
					} else {
						// 選択されているAuthorが二個以上あれば編集可能だがテキストには表示しない.
						txtAuthor.setText("");
						txtHomepage.setText("");
					}
				}
			} finally {
				authorEditSemaphore.release();
			}
		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}
	
	protected void onTableDataChange(int firstRow, int lastRow) {
		onChangeSelection();
	}
	
	protected void onApplyAllDownloadURL() {
		int[] selRows = partsManageTable.getSelectedRows();
		if (selRows.length == 0) {
			Toolkit tk = Toolkit.getDefaultToolkit();
			tk.beep();
			return;
		}
		Arrays.sort(selRows);

		HashSet<String> authors = new HashSet<String>();
		for (int selRow : selRows) {
			PartsManageTableRow row = partsManageTableModel.getRow(selRow);
			authors.add(row.getAuthor() == null ? "" : row.getAuthor());
		}

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

		if (authors.size() > 1) {
			if (JOptionPane.showConfirmDialog(this,
					strings.getProperty("confirm.authorConflict"),
					strings.getProperty("confirm"),
					JOptionPane.OK_CANCEL_OPTION) != JOptionPane.OK_OPTION) {
				return;
			}
		}
		
		PartsManageTableRow firstRow = partsManageTableModel.getRow(selRows[0]);
		String downloadURL = firstRow.getDownloadURL();
		if (downloadURL == null) {
			downloadURL = "";
		}
		String downloadURL_new = JOptionPane.showInputDialog(this, strings.getProperty("input.downloadURL"), downloadURL);
		if (downloadURL_new == null || downloadURL.equals(downloadURL_new)) {
			// キャンセルされたか、内容に変化ない場合は何もしない
			return;
		}
		
		for (int selRow : selRows) {
			PartsManageTableRow row = partsManageTableModel.getRow(selRow);
			row.setDownloadURL(downloadURL_new);
		}
		partsManageTableModel.fireTableRowsUpdated(selRows[0], selRows[selRows.length - 1]);
	}
	
	protected void onApplyAllVersion() {
		Toolkit tk = Toolkit.getDefaultToolkit();
		int[] selRows = partsManageTable.getSelectedRows();
		if (selRows.length == 0) {
			tk.beep();
			return;
		}
		Arrays.sort(selRows);

		HashSet<String> authors = new HashSet<String>();
		for (int selRow : selRows) {
			PartsManageTableRow row = partsManageTableModel.getRow(selRow);
			authors.add(row.getAuthor() == null ? "" : row.getAuthor());
		}

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

		if (authors.size() > 1) {
			if (JOptionPane.showConfirmDialog(this,
					strings.getProperty("confirm.authorConflict"),
					strings.getProperty("confirm"),
					JOptionPane.OK_CANCEL_OPTION) != JOptionPane.OK_OPTION) {
				return;
			}
		}

		PartsManageTableRow firstRow = partsManageTableModel.getRow(selRows[0]);
		double version = firstRow.getVersion();
		String strVersion = (version < 0) ? "" : Double.toString(version);
		String strVersion_new = JOptionPane.showInputDialog(this, strings.getProperty("input.homepageURL"), strVersion);
		if (strVersion_new == null || strVersion.equals(strVersion_new)) {
			// キャンセルされたか、内容に変化ない場合は何もしない
			return;
		}
		double version_new;
		try {
			version_new = Double.parseDouble(strVersion_new);
		} catch (Exception ex) {
			// 数値として不正であれば何もしない.
			tk.beep();
			return;
		}
		
		for (int selRow : selRows) {
			PartsManageTableRow row = partsManageTableModel.getRow(selRow);
			row.setVersion(version_new);
		}
		partsManageTableModel.fireTableRowsUpdated(selRows[0], selRows[selRows.length - 1]);
	}
	
	protected void onEditHomepage() {
		try {
			if (!authorEditSemaphore.tryAcquire()) {
				return;
			}
			try {
				String author = txtAuthor.getText();
				String homepage = txtHomepage.getText();
				partsManageTableModel.setHomepage(author, homepage);
			} finally {
				authorEditSemaphore.release();
			}
		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}

	protected void onEditAuthor() {
		try {
			if (!authorEditSemaphore.tryAcquire()) {
				return;
			}
			try {
				String author = txtAuthor.getText();
				int[] selRows = partsManageTable.getSelectedRows();
				int firstRow = -1;
				int lastRow = Integer.MAX_VALUE;
				for (int selRow : selRows) {
					PartsManageTableRow row = partsManageTableModel.getRow(selRow);
					String oldValue = row.getAuthor();
					if (oldValue == null || !oldValue.equals(author)) {
						row.setAuthor(author);
						firstRow = Math.max(firstRow, selRow);
						lastRow = Math.min(lastRow, selRow);
					}
				}
				
				String homepage = partsManageTableModel.getHomepage(author);
				if (homepage == null) {
					homepage = "";
				}
				txtHomepage.setText(homepage);
				
				if (firstRow >= 0) {
					partsManageTable.repaint();
				}
			} finally {
				authorEditSemaphore.release();				
			}
		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}
	
	protected void onClose() {
		Properties strings = LocalizedResourcePropertyLoader.getInstance()
				.getLocalizedProperties(STRINGS_RESOURCE);
		if (JOptionPane.showConfirmDialog(this,
				strings.getProperty("confirm.cancel"),
				strings.getProperty("confirm"),
				JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
			return;
		}
		updated = false;
		dispose();
	}
	
	protected void onBrosweHomepage() {
		Toolkit tk = Toolkit.getDefaultToolkit();
		String homepage = txtHomepage.getText();
		if (homepage == null || homepage.trim().length() == 0) {
			tk.beep();
			return;
		}
		try {
			URI uri = new URL(homepage).toURI();
			DesktopUtilities.browse(uri);

		} catch (Exception ex) {
			tk.beep();
			logger.log(Level.INFO, "browse failed. : " + homepage, ex);
		}
	}
	
	protected void onSortByAuthor() {
		partsManageTableModel.sortByAuthor();
	}
	
	protected void onSortByName() {
		partsManageTableModel.sortByName();
	}
	
	protected void onSortByTimestamp() {
		partsManageTableModel.sortByTimestamp();
	}
	
	protected void onOK() {
		if (partsManageTable.isEditing()) {
			Toolkit tk = Toolkit.getDefaultToolkit();
			tk.beep();
			return;
		}
		
		int mx = partsManageTableModel.getRowCount();

		// 作者ごとのホームページ情報の取得
		// (同一作者につきホームページは一つ)
		HashMap<String, PartsAuthorInfo> authorInfoMap = new HashMap<String, PartsAuthorInfo>();
		for (int idx = 0; idx < mx; idx++) {
			PartsManageTableRow row = partsManageTableModel.getRow(idx);
			String author = row.getAuthor();
			String homepage = row.getHomepage();
			if (author != null && author.trim().length() > 0) {
				PartsAuthorInfo authorInfo = authorInfoMap.get(author.trim());
				if (authorInfo == null) {
					authorInfo = new PartsAuthorInfo();
					authorInfo.setAuthor(author.trim());
					authorInfoMap.put(authorInfo.getAuthor(), authorInfo);
				}
				authorInfo.setHomePage(homepage);
			}
		}

		PartsManageData partsManageData = new PartsManageData();
		
		// パーツごとの作者とバージョン、ダウンロード先の取得
		for (int idx = 0; idx < mx; idx++) {
			PartsManageTableRow row = partsManageTableModel.getRow(idx);
			
			String author = row.getAuthor();
			PartsAuthorInfo partsAuthorInfo = null;
			if (author != null && author.trim().length() > 0) {
				partsAuthorInfo = authorInfoMap.get(author.trim());
			}
			
			double version = row.getVersion();
			String downloadURL = row.getDownloadURL();
			String localizedName = row.getLocalizedName();
			
			PartsIdentifier partsIdentifier = row.getPartsIdentifier();
			
			PartsManageData.PartsKey partsKey = new PartsManageData.PartsKey(partsIdentifier);
			partsManageData.putPartsInfo(partsKey,
					localizedName,
					partsAuthorInfo,
					new PartsManageData.PartsVersionInfo(version, downloadURL));
		}
		
		// パーツ管理情報を書き込む.
		CharacterDataPersistent persist = CharacterDataPersistent.getInstance();
		try {
			persist.savePartsManageData(characterData.getDocBase(), partsManageData);

		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
			return;
		}
		
		updated = true;
		dispose();
	}

	/**
	 * パーツ管理情報が更新されたか?
	 * @return 更新された場合はtrue、そうでなければfalse
	 */
	public boolean isUpdated() {
		return updated;
	}
}

class PartsManageTableRow {
	
	private PartsIdentifier partsIdentifier;
	
	private Timestamp timestamp;
	
	private String localizedName;
	
	private String author;
	
	private String homepage;
	
	private String downloadURL;
	
	private double version;
	
	public PartsManageTableRow(PartsIdentifier partsIdentifier, PartsSpec partsSpec) {
		if (partsIdentifier == null || partsSpec == null) {
			throw new IllegalArgumentException();
		}
		this.partsIdentifier = partsIdentifier;
		this.localizedName = partsIdentifier.getLocalizedPartsName();
		this.timestamp = new Timestamp(partsSpec.getPartsFiles().lastModified());
		
		PartsAuthorInfo autherInfo = partsSpec.getAuthorInfo();
		if (autherInfo != null) {
			author = autherInfo.getAuthor();
			homepage = autherInfo.getHomePage();
		}
		if (author == null) {
			author = "";
		}
		if (homepage == null) {
			homepage = "";
		}
		downloadURL = partsSpec.getDownloadURL();
		version = partsSpec.getVersion();
	}
	
	public PartsIdentifier getPartsIdentifier() {
		return partsIdentifier;
	}
	
	public Timestamp getTimestamp() {
		return timestamp;
	}

	public String getLocalizedName() {
		return localizedName;
	}
	
	public void setLocalizedName(String localizedName) {
		if (localizedName == null || localizedName.trim().length() == 0) {
			throw new IllegalArgumentException();
		}
		this.localizedName = localizedName;
	}
	
	public String getAuthor() {
		return author;
	}
	
	public String getDownloadURL() {
		return downloadURL;
	}
	
	public double getVersion() {
		return version;
	}
	
	public void setAuthor(String author) {
		this.author = author;
	}
	
	public void setDownloadURL(String downloadURL) {
		this.downloadURL = downloadURL;
	}
	
	public void setVersion(double version) {
		this.version = version;
	}
	
	public String getHomepage() {
		return homepage;
	}
	
	public void setHomepage(String homepage) {
		this.homepage = homepage;
	}
	
}

class PartsManageTableModel extends AbstractTableModelWithComboBoxModel<PartsManageTableRow> {

	private static final long serialVersionUID = 1L;

	private static final String[] COLUMN_NAMES;
	
	private static final int[] COLUMN_WIDTHS;

	static {
		Properties strings = LocalizedResourcePropertyLoader.getInstance()
				.getLocalizedProperties(PartsManageDialog.STRINGS_RESOURCE);

		COLUMN_NAMES = new String[] {
				strings.getProperty("column.partsid"),
				strings.getProperty("column.lastmodified"),
				strings.getProperty("column.category"),
				strings.getProperty("column.partsname"),
				strings.getProperty("column.author"),
				strings.getProperty("column.version"),
				strings.getProperty("column.downloadURL"),
		};
		
		COLUMN_WIDTHS = new int[] {
				Integer.parseInt(strings.getProperty("column.partsid.width")),
				Integer.parseInt(strings.getProperty("column.lastmodified.width")),
				Integer.parseInt(strings.getProperty("column.category.width")),
				Integer.parseInt(strings.getProperty("column.partsname.width")),
				Integer.parseInt(strings.getProperty("column.author.width")),
				Integer.parseInt(strings.getProperty("column.version.width")),
				Integer.parseInt(strings.getProperty("column.downloadURL.width")),
		};
		
	}
	
	public int getColumnCount() {
		return COLUMN_NAMES.length;
	}
	
	@Override
	public String getColumnName(int column) {
		return COLUMN_NAMES[column];
	}
	
	public void adjustColumnModel(TableColumnModel columnModel) {
		for (int idx = 0; idx < COLUMN_WIDTHS.length; idx++) {
			columnModel.getColumn(idx).setPreferredWidth(COLUMN_WIDTHS[idx]);
		}
	}

	public Object getValueAt(int rowIndex, int columnIndex) {
		PartsManageTableRow row = getRow(rowIndex);
		switch (columnIndex) {
		case 0:
			return row.getPartsIdentifier().getPartsName();
		case 1:
			return row.getTimestamp().toString();
		case 2:
			return row.getPartsIdentifier().getPartsCategory().getLocalizedCategoryName();
		case 3:
			return row.getLocalizedName();
		case 4:
			return row.getAuthor();
		case 5:
			return row.getVersion() > 0 ? row.getVersion() : null;
		case 6:
			return row.getDownloadURL();
		}
		return "";
	}
	
	@Override
	public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
		PartsManageTableRow row = getRow(rowIndex);
		switch (columnIndex) {
		case 0:
			return;
		case 1:
			return;
		case 2:
			return;
		case 3:
			{
				String localizedName = (String) aValue;
				if (localizedName != null && localizedName.trim().length() > 0) {
					String oldValue = row.getLocalizedName();
					if (oldValue != null && oldValue.equals(localizedName)) {
						return; // 変化なし
					}
					row.setLocalizedName(localizedName);
				}
			}
			break;
		case 4:
			{
				String author = (String) aValue;
				if (author == null) {
					author = "";
				}
				String oldValue = row.getAuthor();
				if (oldValue != null && oldValue.equals(author)) {
					return; // 変化なし
				}
				row.setAuthor(author);
			}
			break;
		case 5:
			{
				Double version = (Double) aValue;
				if (version == null || version.doubleValue() <= 0) {
					version = Double.valueOf(0.);
				}
				Double oldValue = row.getVersion();
				if (oldValue != null && oldValue.equals(version)) {
					return; // 変化なし
				}
				row.setVersion(version);
			}
			break;
		case 6:
			{
				String downloadURL = (String) aValue;
				if (downloadURL == null) {
					downloadURL = "";
				}
				String oldValue = row.getDownloadURL();
				if (oldValue != null && oldValue.equals(downloadURL)) {
					return; // 変化なし
				}
				row.setDownloadURL(downloadURL);
			}
			break;
		default:
			return;
		}
		// 変更通知
		fireTableRowsUpdated(rowIndex, columnIndex);
	}
	
	@Override
	public Class<?> getColumnClass(int columnIndex) {
		switch (columnIndex) {
		case 0:
			return String.class;
		case 1:
			return String.class;
		case 2:
			return String.class;
		case 3:
			return String.class;
		case 4:
			return String.class;
		case 5:
			return Double.class;
		case 6:
			return String.class;
		}
		return String.class;
	}
	
	@Override
	public boolean isCellEditable(int rowIndex, int columnIndex) {
		if (columnIndex == 0 || columnIndex == 1 || columnIndex == 2) {
			// PARTS ID/Timetsmap/CATEGORYは変更不可
			return false;
		}
		return true;
	}
	
	public void initModel(CharacterData characterData) {
		if (characterData == null) {
			throw new IllegalArgumentException();
		}
		clear();

		for (PartsCategory partsCategory : characterData.getPartsCategories()) {
			for (Map.Entry<PartsIdentifier, PartsSpec> entry : characterData
					.getPartsSpecMap(partsCategory).entrySet()) {
				PartsIdentifier partsIdentifier = entry.getKey();
				PartsSpec partsSpec = entry.getValue();
				
				PartsManageTableRow row = new PartsManageTableRow(partsIdentifier, partsSpec);
				addRow(row);
			}
		}

		sortByAuthor();
	}

	/**
	 * ホームページを設定する.<br>
	 * ホームページはAuthorに対して1つであるが、Authorが自由編集可能であるため便宜的にRowに持たせている.<br>
	 * 結果として同じAuthorに対して同じ値を設定する必要がある.<br>
	 * ホームページはテーブルに表示されないのでリスナーへの通知は行わない.<br>
	 * @param author 作者、空またはnullは何もしない.
	 * @param homepage ホームページ
	 */
	public void setHomepage(String author, String homepage) {
		if (author == null || author.length() == 0) {
			return;
		}
		for (PartsManageTableRow row : elements) {
			String targetAuthor = row.getAuthor();
			if (targetAuthor == null) {
				targetAuthor = "";
			}
			if (targetAuthor.equals(author)) {
				row.setHomepage(homepage);
			}
		}
	}

	/**
	 * ホームページを取得する.<br>
	 * 該当する作者がないか、作者がnullまたは空の場合は常にnullを返す.<br>
	 * @param author 作者
	 * @return ホームページ、またはnull
	 */
	public String getHomepage(String author) {
		if (author == null || author.length() == 0) {
			return null;
		}
		for (PartsManageTableRow row : elements) {
			String targetAuthor = row.getAuthor();
			if (targetAuthor == null) {
				targetAuthor = "";
			}
			if (targetAuthor.equals(author)) {
				return row.getHomepage();
			}
		}
		return null;
	}
	
	protected static final Comparator<PartsManageTableRow> NAMED_SORTER
		= new Comparator<PartsManageTableRow>() {
		public int compare(PartsManageTableRow o1, PartsManageTableRow o2) {
			// カテゴリで順序づけ
			int ret = o1.getPartsIdentifier().getPartsCategory().compareTo(
					o2.getPartsIdentifier().getPartsCategory());
			if (ret == 0) {
				// 表示名で順序づけ
				String lnm1 = o1.getLocalizedName();
				String lnm2 = o2.getLocalizedName();
				if (lnm1 == null) {
					lnm1 = "";
				}
				if (lnm2 == null) {
					lnm2 = "";
				}
				ret = lnm1.compareTo(lnm2);
			}
			if (ret == 0) {
				// それでも判定できなければ元の識別子で判定する.
				ret = o1.getPartsIdentifier().compareTo(o2.getPartsIdentifier());
			}
			return ret;
		}
	};
	
	public void sortByName() {
		Collections.sort(elements, NAMED_SORTER);
		fireTableDataChanged();
	}
	
	public void sortByTimestamp() {
		Collections.sort(elements, new Comparator<PartsManageTableRow>() {
			public int compare(PartsManageTableRow o1, PartsManageTableRow o2) {
				// 更新日で順序づけ (新しいもの順)
				int ret = o1.getTimestamp().compareTo(o2.getTimestamp()) * -1;
				if (ret == 0) {
					// それでも判定できなければ名前順と同じ
					ret = NAMED_SORTER.compare(o1, o2);
				}
				return ret;
			}
		});
		fireTableDataChanged();
	}

	public void sortByAuthor() {
		Collections.sort(elements, new Comparator<PartsManageTableRow>() {
			public int compare(PartsManageTableRow o1, PartsManageTableRow o2) {
				// 作者で順序づけ
				String author1 = o1.getAuthor();
				String author2 = o2.getAuthor();
				if (author1 == null) {
					author1 = "";
				}
				if (author2 == null) {
					author2 = "";
				}
				int ret = author1.compareTo(author2);
				if (ret == 0) {
					// それでも判定できなければ名前順と同じ
					ret = NAMED_SORTER.compare(o1, o2);
				}
				return ret;
			}
		});
		fireTableDataChanged();
	}
}
