/* $Id: ElementList.java,v 1.1 2007/10/19 08:19:34 nito Exp $
 * 
 * Copyright (c)ARGO 21, Corporation. 2005, 2006.  All rights reserved.
 * 
 * This file is part of Nautica Workflow.
 * 
 *  Nautica Workflow is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 * 
 *  Nautica Workflow is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 *  You should have received a copy of the GNU General Public License
 *  along with Nautica Workflow; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *  
 */
package jp.co.argo21.nautica.tool.wfd;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Shape;
import java.awt.SystemColor;
import java.awt.Toolkit;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.List;

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;

import jp.co.argo21.nautica.commons.swing.DialogUtilities;
import jp.co.argo21.nautica.commons.util.ResourceManager;
import jp.co.argo21.nautica.commons.util.StringUtils;
import jp.co.argo21.nautica.tool.wfd.dnd.*;
import jp.co.argo21.nautica.tool.wfd.element.Element;
import jp.co.argo21.nautica.tool.wfd.element.PackageElement;
import jp.co.argo21.nautica.tool.wfd.element.ProcessElement;
import jp.co.argo21.nautica.tool.wfd.inspector.ChangeElementListener;


/**
 * 要素リストを提供する。
 * 
 * @author  Norihiro Itoh(ARGO 21 Corp.)
 * @version $Revision: 1.1 $
 * @since   Nautica Workflow 1.0
 */
public class ElementList extends JScrollPane implements ChangeElementListener
{
	/** シリアルバージョンID */
	private static final long serialVersionUID = 1L;

	/** 単位幅 */
	private static final int GRID_W = 80;
	/** 単位高さ */
	private static final int GRID_H = 70;
	/** 最小ビューサイズ */
	private static final Dimension MINIMUM_VIEW_SIZE = new Dimension(GRID_W, GRID_H);
	/** ビューのフォント */
	private static final Font VIEW_FONT = new Font("MonoSpaced", Font.PLAIN, 10);
	/** ドラッグ時のマウスカーソルホットポイント位置 */
	private static final Point CURSOR_POINT = new Point(16, 16);

	/** ポップアップメニュー */
	private JPopupMenu popup;
	/** 新規作成メニュー */
	private JMenuItem popNew;
	/** 編集メニュー */
	private JMenuItem popInspect;
	/** 削除メニュー */
	private JMenuItem popDelete;
	/** アイコン表示用ビュー */
	private IconView view;

	/** 要素の型 */
	private String elementType;
	/** 要素一覧 */
	private List<Element> elements;

	/** 選択されているインデクス */
	private int selectedIndex;

	/**
	 * 要素を生成する。
	 * 
	 * @param elems	要素一覧
	 * @param type	要素の型
	 */
	public ElementList(List<Element> elems, String type)
	{
		elementType = type;
		elements = elems;
		this.selectedIndex = -1;

		InspectorDialog.addChangeElementListener(elementType, this);

		buildPopupMenu();

		view = new IconView();
		Dimension size = new Dimension(MINIMUM_VIEW_SIZE);
		view.setPreferredSize(size);
		view.setSize(size);
		
		setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
		setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
		setViewportView(view);
	}

	/**
	 * ポップアップメニューの生成
	 */
	private void buildPopupMenu()
	{
		ResourceManager rm = WorkflowDesignerManager.getResourceManager();

		popup = new JPopupMenu();

		popNew = new JMenuItem(rm.getResource("ElementList.menu.new"));
		popInspect = new JMenuItem(rm.getResource("ElementList.menu.inspect"));
		popDelete = new JMenuItem(rm.getResource("ElementList.menu.delete"));

		popNew.addActionListener(
			new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					executeNewElement();
				}
			}
		);

		popInspect.addActionListener(
			new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					executeInspectElement();
				}
			}
		);

		popDelete.addActionListener(
			new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					executeDeleteElement();
				}
			}
		);

		popup.add(popNew);
		popup.add(popInspect);
		popup.add(popDelete);
	}

	/**
	 * アイコン表示エリアのサイズを再計算する。
	 */
	public void recalc()
	{
		Dimension size = getSize();
		int count = 1;
		if (elements != null) count += elements.size();

		size.width = count * GRID_W;
		size.height = GRID_H;
		view.setPreferredSize(size);
		view.setSize(size);
		view.repaint();
	}

	/**
	 * 要素の新規作成を行う。
	 */
	private void executeNewElement()
	{
		//新規エレメントを作成
		ElementFactory factory = ElementFactory.getInstance();
		Element newElem = factory.createElement(elementType);
		if (newElem == null) return;
		
		elements.add(newElem);
		selectedIndex = elements.size() - 1;
		recalc();
	}

	/**
	 * 要素の編集を行う。
	 */
	private void executeInspectElement()
	{
		Element elem = getSelectedElement();
		if (elem == null) return;
		
		if (elementType == WorkflowDesignerConstants.EL_PROCESS) {
			//フロー表示
			DesktopPane desktop = WorkflowDesignerManager.getMainFrame().getDesktop();
			ProcessDesignFrame frame = desktop.getEditingProcessDesignFrame(elem);
			if (frame == null) {
				frame = desktop.createProcessDesignFrame(elem);
			}
			WorkflowDesignerManager.getMainFrame().getToolbox().setSelectedIndex(0);
		} else {
			//インスペクタ表示
			InspectorDialog.showDialog(this, elem);
		}
	}

	/**
	 * 要素の削除を行う。
	 */
	private void executeDeleteElement()
	{
		Element elem = getSelectedElement();
		if (elem == null) return;

		PackageElement pkg = WorkflowDesignerManager.getMainFrame().getPackage();
		if (pkg.isUsed(elem)) {
			ResourceManager rm = WorkflowDesignerManager.getResourceManager();
			DialogUtilities.showWarning(rm.getResource("message.warn.0104"));
			return;
		}
		if (elementType == WorkflowDesignerConstants.EL_PROCESS) {
			if (((ProcessElement)elem).getAccessLevel() == ProcessElement.PUBLIC) {
				//ルートプロセスのときは削除せずエラーにして抜ける。
				ResourceManager rm = WorkflowDesignerManager.getResourceManager();
				DialogUtilities.showWarning(rm.getResource("message.warn.0001"));
				return;
			} else {
				DesktopPane desktop = WorkflowDesignerManager.getMainFrame().getDesktop();
				//既にそのプロセスが開かれているかを確認する。
				ProcessDesignFrame frame = desktop.getEditingProcessDesignFrame(elem);
				if (frame != null) {
					frame.setVisible(false);
					desktop.remove(frame);
					frame.dispose();
				}
			}
		}
		elements.remove(elem);
		
		//desktopの更新
		DesktopPane desktop = WorkflowDesignerManager.getMainFrame().getDesktop();
		desktop.updateAllFrame();
		recalc();
	}

	/**
	 * 要素一覧を設定する。
	 * 
	 * @param list	要素一覧
	 */
	@SuppressWarnings("unchecked")
	public void setElements(List list)
	{
		elements = list;
		selectedIndex = -1;
		recalc();
	}

	/**
	 * 指定された位置の要素を返す。
	 * 
	 * @param p	マウス座標
	 * @return	要素
	 */
	private Element getElement(Point p)
	{
		if (elements == null) return  null;
		if (p.x < 0) return  null;
		else if (p.y < 0 || p.y >= GRID_H) return  null;

		int ix = p.x / GRID_W;
		int count = elements.size();
		if (ix >= count) return null;
		return (Element)elements.get(ix);
	}

	/**
	 * 選択された要素を返す。
	 * 
	 * @return	要素
	 */
	private Element getSelectedElement()
	{
		if (elements == null) return  null;
		if (selectedIndex < 0) return  null;
		
		int count = elements.size();
		if (selectedIndex >= count) return null;
		return (Element)elements.get(selectedIndex);
	}

	/**
	 * 要素の変更を反映する。
	 * 
	 * @param elem	変更された要素
	 * @see jp.co.argo21.nautica.tool.wfd.inspector.ChangeElementListener#elementChanged(jp.co.argo21.nautica.tool.wfd.element.Element)
	 */
	public void elementChanged(Element elem)
	{
		repaint();
	}

	/**
	 * 要素のドロップを反映する。未使用。
	 * 
	 * @param elem	ドロップされた要素
	 * @see jp.co.argo21.nautica.tool.wfd.dnd.DropElementListener#elementDropped(jp.co.argo21.nautica.tool.wfd.element.Element)
	 */
	public void elementDropped(Element elem)
	{
		//NOP
	}
	
	/**
	 * 要素のアイコンを実際に表示するビューである。
	 */
	private class IconView
	extends JComponent
	implements MouseListener, MouseMotionListener, DragGestureListener
	{
		/** シリアルバージョンID */
		private static final long serialVersionUID = 1L;

		/**
		 * アイコンビューを生成する。
		 */
		public IconView()
		{
			//Dimension size = new Dimension(MINIMUM_VIEW_SIZE);
			setPreferredSize(MINIMUM_VIEW_SIZE);
			setMinimumSize(MINIMUM_VIEW_SIZE);
			setSize(MINIMUM_VIEW_SIZE);

			addMouseListener(this);
			addMouseMotionListener(this);
			DragSource dragSource = new DragSource();
			dragSource.createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_COPY, this);
		}

		/**
		 * アイコン表示エリアを描画する。
		 *
		 * @param g 描画対象グラフィックス
		 */
		public void paintComponent(Graphics g)
		{
			g.setFont(VIEW_FONT);
			Dimension size = this.getSize();
			g.setColor(Color.white);
			g.fillRect(0, 0, size.width, size.height);

			this.paintElements(g);

			g.dispose();
		}

		/**
		 * 表示要素を描画する。
		 *
		 * @param g 描画対象グラフィックス
		 */
		private void paintElements(Graphics g)
		{
			if (elements == null) return;

			int count = elements.size();
			int sindex = -1;
			Element selement = null;
			for (int i = 0; i < count; i++) {
				Element child = (Element)elements.get(i);

				if (i != selectedIndex) {
					paintElement(g, child, i, 0);
				} else {
					sindex = i;
					selement = child;
				}
			}
			if (selement != null) paintSelectedElement(g, selement, sindex, 0);
		}

		/**
		 * 表示要素を描画する。
		 *
		 * @param g     描画対象グラフィックス
		 * @param elem  表示要素
		 * @param px    X座標
		 * @param py    Y座標
		 */
		private void paintElement(Graphics g, Element elem, int px, int py)
		{
			ResourceManager rm = WorkflowDesignerManager.getResourceManager();

			Graphics2D g2 = (Graphics2D)g;
			Shape shape = g2.getClip();
		
			int x = GRID_W * px;
			int y = GRID_H * py;
			int w = GRID_W;
			//int h = GRID_H;

			Icon icon = IconManager.getIcon(elementType);
			int iconW = icon.getIconWidth();
			int iconH = icon.getIconHeight();
			int iconX = x + ((w - iconW) / 2);
			int iconY = y + 2;
			icon.paintIcon(this, g2, iconX, iconY);

			FontMetrics metrics = g.getFontMetrics();
			int fontH = metrics.getHeight();
			int descent = metrics.getDescent();

			g.setColor(Color.black);
			String n = elem.getName();
			if (StringUtils.checkNull(n)) n = rm.getResource("ElementList.noname");
			int lineW = metrics.stringWidth(n);
			if (lineW <= (w - 4)) {
				int dx = x + ((w - lineW) / 2);
				int dy = y + 2 + iconH + fontH - descent;
				g2.drawString(n, dx, dy);
			} else {
				int dotW = metrics.stringWidth("...");
				int dx = x + 2;
				int cy = y + 2 + iconH;
				int dw = w - (dotW + 4);
				g2.clipRect(dx, cy, dw, fontH);
				int dy = cy + fontH - descent;
				g2.drawString(n, dx, dy);
				g2.setClip(shape);
				dx = x + w - (dotW + 1);
				g2.clipRect(dx, cy, dotW, fontH);
				g2.drawString("...", dx, dy);
			}
			g2.setClip(shape);
		}

		/**
		 * 表示要素を描画する。
		 *
		 * @param g     描画対象グラフィックス
		 * @param elem   表示要素
		 * @param px    X座標
		 * @param py    Y座標
		 */
		private void paintSelectedElement(Graphics g, Element elem, int px, int py)
		{
			ResourceManager rm = WorkflowDesignerManager.getResourceManager();

			if (selectedIndex < 0) return;
			//Dimension size = getSize();
			int x = GRID_W * px;
			int y = GRID_H * py;
			int w = GRID_W;
			//int h = GRID_H;

			Icon selectedIcon = IconManager.getIcon(elementType + "-s");
			int iconW = selectedIcon.getIconWidth();
			int iconH = selectedIcon.getIconHeight();
			int iconX = x + ((w - iconW) / 2);
			int iconY = y + 2;
			selectedIcon.paintIcon(this, g, iconX, iconY);

			FontMetrics metrics = g.getFontMetrics();
			int fontH = metrics.getHeight();
			int descent = metrics.getDescent();
			String n = elem.getName();
			if (StringUtils.checkNull(n)) n = rm.getResource("ElementList.noname");
			int lineW = metrics.stringWidth(n);
			int dx = x + ((w - lineW) / 2);
			int dy = y + 2 + iconH;

			if (dx < 0) dx = 0;
			g.setColor(SystemColor.textHighlight);
			g.fillRect(dx, dy, lineW, fontH);
			
			dy = dy + fontH - descent;
			g.setColor(SystemColor.textHighlightText);
			g.drawString(n, dx, dy);
		}

		/**
		 * 選択状態を再計算する。
		 * 
		 * @param p	マウス位置
		 */		
		private void recalcSelection(Point p)
		{
			if (p.x < 0) selectedIndex = -1;
			else if (p.y < 0 || p.y >= GRID_H) selectedIndex = -1;
			else selectedIndex = p.x / GRID_W;
			repaint();
		}


		/*
		 * インタフェースDragGestureListener実装
		 */
		/**
		 * 要素のドラッグ開始を判定する。
		 * 
		 * @param e	イベント
		 * @see java.awt.dnd.DragGestureListener#dragGestureRecognized(java.awt.dnd.DragGestureEvent)
		 */
		public void dragGestureRecognized(DragGestureEvent e)
		{
			//ドラッグ操作によって選択された場合の処理
			recalcSelection(e.getDragOrigin());

			Element elem = ElementList.this.getSelectedElement();
			if (elem == null) return;

			Transferable t = new ElementContext(elem, elementType);

			String iconName = elementType + "-cur";
			ImageIcon icon = (ImageIcon)IconManager.getIcon(iconName);
			Cursor cursor = Toolkit.getDefaultToolkit()
				.createCustomCursor(icon.getImage(), CURSOR_POINT, elementType);

			e.startDrag(cursor, t);
		}

		/**
		 * インタフェースMouseListener実装
		 */
		/**
		 * マウスクリックを判定する。
		 *
		 * @param e マウスイベント
		 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
		 */
		public void mouseClicked(MouseEvent e)
		{
			recalcSelection(e.getPoint());

			if (selectedIndex < 0) return;

			if (e.getClickCount() >= 2) {
				executeInspectElement();
			}
		}

		/**
		 * マウス押下を判定する。
		 *
		 * @param e マウスイベント
		 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
		 */
		public void mousePressed(MouseEvent e)
		{
		}

		/**
		 * マウスの押下解除を判定する。
		 *
		 * @param e マウスイベント
		 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
		 */
		public void mouseReleased(MouseEvent e)
		{
			if (e.isPopupTrigger()
			|| e.getModifiers() == MouseEvent.BUTTON2_MASK
			|| e.getModifiers() == MouseEvent.BUTTON3_MASK) {
				//ポップアップを出す前に選択状態を判定する。
				recalcSelection(e.getPoint());

				Element elem = getSelectedElement();
				if (elem == null) {
					popInspect.setEnabled(false);
					popDelete.setEnabled(false);
				} else {
					popInspect.setEnabled(true);
					popDelete.setEnabled(true);
				}
				popup.show(e.getComponent(), e.getX(), e.getY());
			}
		}

		/**
		 * マウスの侵入を判定する。
		 *
		 * @param e マウスイベント
		 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
		 */
		public void mouseEntered(MouseEvent e)
		{
		}

		/**
		 * マウスの脱出を判定する。
		 *
		 * @param e マウスイベント
		 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
		 */
		public void mouseExited(MouseEvent e)
		{
		}

		/**
		 * マウスドラッグを判定する。
		 *
		 * @param	e		マウスイベント
		 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
		 */
		public void mouseDragged(MouseEvent e)
		{
		}

		/**
		 * マウス移動を判定する。
		 *
		 * @param	e		マウスイベント
		 * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
		 */
		public void mouseMoved(MouseEvent e)
		{
			ResourceManager rm = WorkflowDesignerManager.getResourceManager();

			Element elem = getElement(e.getPoint());
			if (elem == null) setToolTipText(null);
			else setToolTipText(elem.getTooltipText(rm));
		}
	}
}
