// based on GEF ShapesEditor example
/*******************************************************************************
 * Copyright (c) 2004, 2005 Elias Volanakis and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Elias Volanakis - initial API and implementation
 *******************************************************************************/
package jp.hasc.hasctool.ui.bdeditor2;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.EventObject;

import jp.hasc.hasctool.core.blockdiagram.model.AbstractBlock;
import jp.hasc.hasctool.core.blockdiagram.model.BeanBlock;
import jp.hasc.hasctool.core.blockdiagram.model.BlockDiagram;
import jp.hasc.hasctool.core.blockdiagram.model.Shape;
import jp.hasc.hasctool.core.runtime.RuntimeContext;
import jp.hasc.hasctool.ui.bdeditor2.model.BlockDiagramElement;
import jp.hasc.hasctool.ui.bdeditor2.model.ModelElement;
import jp.hasc.hasctool.ui.bdeditor2.model.commands.BlockDiagramPasteCommand;
import jp.hasc.hasctool.ui.bdeditor2.parts.DiagramEditPartFactory;
import jp.hasc.hasctool.ui.bdeditor2.parts.DiagramTreeEditPartFactory;
import jp.hasc.hasctool.ui.util.UIUtil;
import jp.hasc.hasctool.ui.views.beanslist.IBeansListTarget;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.gef.ContextMenuProvider;
import org.eclipse.gef.DefaultEditDomain;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.EditPartViewer;
import org.eclipse.gef.GraphicalViewer;
import org.eclipse.gef.dnd.TemplateTransferDragSourceListener;
import org.eclipse.gef.dnd.TemplateTransferDropTargetListener;
import org.eclipse.gef.editparts.ScalableFreeformRootEditPart;
import org.eclipse.gef.palette.PaletteRoot;
import org.eclipse.gef.requests.CreationFactory;
import org.eclipse.gef.requests.SimpleFactory;
import org.eclipse.gef.ui.actions.ActionRegistry;
import org.eclipse.gef.ui.palette.PaletteViewer;
import org.eclipse.gef.ui.palette.PaletteViewerProvider;
import org.eclipse.gef.ui.parts.ContentOutlinePage;
import org.eclipse.gef.ui.parts.GraphicalEditorWithPalette;
import org.eclipse.gef.ui.parts.GraphicalViewerKeyHandler;
import org.eclipse.gef.ui.parts.TreeViewer;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.util.TransferDropTargetListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.actions.WorkspaceModifyOperation;
import org.eclipse.ui.dialogs.SaveAsDialog;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.part.IPageSite;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.StreamException;
import com.thoughtworks.xstream.io.xml.DomDriver;

/**
 * hascxbdを編集するためのブロック図エディタです
 * @author iwasaki
 */
@SuppressWarnings("rawtypes")
public class BlockDiagramEditor extends GraphicalEditorWithPalette implements IBeansListTarget {

	/** This is the root of the editor's model. */
	private BlockDiagramElement diagram;
	/** Palette component, holding the tools and shapes. */
	private static PaletteRoot PALETTE_MODEL;

	/** Create a new ShapesEditor instance. This is called by the Workspace. */
	public BlockDiagramEditor() {
		setEditDomain(new DefaultEditDomain(this));
	}

	/**
	 * Configure the graphical viewer before it receives contents.
	 * <p>
	 * This is the place to choose an appropriate RootEditPart and
	 * EditPartFactory for your editor. The RootEditPart determines the behavior
	 * of the editor's "work-area". For example, GEF includes zoomable and
	 * scrollable root edit parts. The EditPartFactory maps model elements to
	 * edit parts (controllers).
	 * </p>
	 * 
	 * @see org.eclipse.gef.ui.parts.GraphicalEditor#configureGraphicalViewer()
	 */
	protected void configureGraphicalViewer() {
		super.configureGraphicalViewer();

		GraphicalViewer viewer = getGraphicalViewer();
		viewer.setEditPartFactory(new DiagramEditPartFactory());
		viewer.setRootEditPart(new ScalableFreeformRootEditPart());
		viewer.setKeyHandler(new GraphicalViewerKeyHandler(viewer));

		// configure the context menu provider
		ContextMenuProvider cmProvider = new BlockDiagramEditorContextMenuProvider(
				viewer, getActionRegistry());
		viewer.setContextMenu(cmProvider);
		getSite().registerContextMenu(cmProvider, viewer);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.gef.ui.parts.GraphicalEditor#commandStackChanged(java.util
	 * .EventObject)
	 */
	public void commandStackChanged(EventObject event) {
		firePropertyChange(IEditorPart.PROP_DIRTY);
		super.commandStackChanged(event);
	}
	
	private void createOutputStream(OutputStream os) throws IOException {
		XStream xs = newXStream();
		BlockDiagram bd = getModel().getBlockDiagram();
		xs.toXML(bd, new OutputStreamWriter(os, RuntimeContext.DEFAULT_CHARSET));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.gef.ui.parts.GraphicalEditorWithFlyoutPalette#
	 * createPaletteViewerProvider()
	 */
	protected PaletteViewerProvider createPaletteViewerProvider() {
		return new PaletteViewerProvider(getEditDomain()) {
			protected void configurePaletteViewer(PaletteViewer viewer) {
				super.configurePaletteViewer(viewer);
				// create a drag source listener for this palette viewer
				// together with an appropriate transfer drop target listener,
				// this will enable
				// model element creation by dragging a
				// CombinatedTemplateCreationEntries
				// from the palette into the editor
				// @see ShapesEditor#createTransferDropTargetListener()
				viewer.addDragSourceListener(new TemplateTransferDragSourceListener(
						viewer));
			}
		};
	}

	/**
	 * Create a transfer drop target listener. When using a
	 * CombinedTemplateCreationEntry tool in the palette, this will enable model
	 * element creation by dragging from the palette.
	 * 
	 * @see #createPaletteViewerProvider()
	 */
	private TransferDropTargetListener createTransferDropTargetListener() {
		return new TemplateTransferDropTargetListener(getGraphicalViewer()) {
			protected CreationFactory getFactory(Object template) {
				return new SimpleFactory((Class) template);
			}
		};
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.ISaveablePart#doSave(org.eclipse.core.runtime.IProgressMonitor)
	 */
	public void doSave(IProgressMonitor monitor) {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		try {
			createOutputStream(out);
			IFile file = ((IFileEditorInput) getEditorInput()).getFile();
			file.setContents(
				new ByteArrayInputStream(out.toByteArray()), 
				true,  // keep saving, even if IFile is out of sync with the Workspace
				false, // dont keep history
				monitor); // progress monitor
			getCommandStack().markSaveLocation();
		} catch (CoreException ce) { 
			ce.printStackTrace();
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.ISaveablePart#doSaveAs()
	 */
	public void doSaveAs() {
		// Show a SaveAs dialog
		Shell shell = getSite().getWorkbenchWindow().getShell();
		SaveAsDialog dialog = new SaveAsDialog(shell);
		dialog.setOriginalFile(((IFileEditorInput) getEditorInput()).getFile());
		dialog.open();
		
		IPath path = dialog.getResult();	
		if (path != null) {
			// try to save the editor's contents under a different file name
			final IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(path);
			try {
				new ProgressMonitorDialog(shell).run(
						false, // don't fork
						false, // not cancelable
						new WorkspaceModifyOperation() { // run this operation
							public void execute(final IProgressMonitor monitor) {
								try {
									ByteArrayOutputStream out = new ByteArrayOutputStream();
									createOutputStream(out);
									file.create(
										new ByteArrayInputStream(out.toByteArray()), // contents
										true, // keep saving, even if IFile is out of sync with the Workspace
										monitor); // progress monitor
								} catch (CoreException ce) {
									ce.printStackTrace();
								} catch (IOException ioe) {
									ioe.printStackTrace();
								} 
							}
						});
				// set input to the new file
				//setInput(new FileEditorInput(file)); // diagramが入れ替わってしまうのでダメ
				super.setInput(new FileEditorInput(file));
				setPartName(file.getName());
				
				getCommandStack().markSaveLocation();
			} catch (InterruptedException ie) {
	  			// should not happen, since the monitor dialog is not cancelable
				ie.printStackTrace(); 
			} catch (InvocationTargetException ite) { 
				ite.printStackTrace(); 
			}
		}
	}

	public Object getAdapter(Class type) {
		if (type == IContentOutlinePage.class)
			return new ShapesOutlinePage(new TreeViewer());
		return super.getAdapter(type);
	}

	public BlockDiagramElement getModel() {
		return diagram;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.gef.ui.parts.GraphicalEditorWithFlyoutPalette#getPaletteRoot
	 * ()
	 */
	protected PaletteRoot getPaletteRoot() {
		if (PALETTE_MODEL == null)
			PALETTE_MODEL = BlockDiagramEditorPaletteFactory.createPalette();
		return PALETTE_MODEL;
	}

	private void handleLoadException(Exception e) {
		String message = "Error:\n"+e.toString(); //$NON-NLS-1$
		if (e instanceof StreamException) {
			String emsg = e.getMessage();
			if (emsg.contains("Invalid byte") && emsg.contains("UTF-8 sequence.")) { //$NON-NLS-1$ //$NON-NLS-2$
				message = Messages.BlockDiagramEditor_FileIsNotUTF8 + "\n" + message; //$NON-NLS-1$
			}
		}
		
		UIUtil.showMessageDialog(getSite().getShell(), message);
		e.printStackTrace();
		throw new RuntimeException(e);
		//diagram = new BlockDiagramElement();
	}

	/**
	 * Set up the editor's inital content (after creation).
	 * 
	 * @see org.eclipse.gef.ui.parts.GraphicalEditorWithFlyoutPalette#initializeGraphicalViewer()
	 */
	protected void initializeGraphicalViewer() {
		// super.initializeGraphicalViewer();
		GraphicalViewer viewer = getGraphicalViewer();
		viewer.setContents(getModel()); // set the contents of this editor

		// listen for dropped parts
		viewer.addDropTargetListener(createTransferDropTargetListener());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ui.ISaveablePart#isSaveAsAllowed()
	 */
	public boolean isSaveAsAllowed() {
		return true;
	}
	
	// got from BlockDiagramExecutor
	public static XStream newXStream() {
		XStream xs = new XStream(new DomDriver());
		xs.autodetectAnnotations(true);
		xs.aliasPackage("model", BlockDiagram.class.getPackage().getName()); //$NON-NLS-1$
		return xs;
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ui.part.EditorPart#setInput(org.eclipse.ui.IEditorInput)
	 */
	protected void setInput(IEditorInput input) {
		try {
			super.setInput(input);
			IFile file = ((IFileEditorInput) input).getFile();
			XStream xs = newXStream();
			BlockDiagram bd = (BlockDiagram)xs.fromXML(file.getContents());
			diagram = new BlockDiagramElement(bd);
			//
			setPartName(file.getName());
		} catch (Exception e) {
			handleLoadException(e);
		}
	}

	/**
	 * Creates an outline pagebook for this editor.
	 */
	public class ShapesOutlinePage extends ContentOutlinePage {
		/**
		 * Create a new outline page for the shapes editor.
		 * 
		 * @param viewer
		 *            a viewer (TreeViewer instance) used for this outline page
		 * @throws IllegalArgumentException
		 *             if editor is null
		 */
		public ShapesOutlinePage(EditPartViewer viewer) {
			super(viewer);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * org.eclipse.ui.part.IPage#createControl(org.eclipse.swt.widgets.Composite
		 * )
		 */
		public void createControl(Composite parent) {
			// create outline viewer page
			getViewer().createControl(parent);
			// configure outline viewer
			getViewer().setEditDomain(getEditDomain());
			getViewer().setEditPartFactory(new DiagramTreeEditPartFactory());
			// configure & add context menu to viewer
			ContextMenuProvider cmProvider = new BlockDiagramEditorContextMenuProvider(
					getViewer(), getActionRegistry());
			getViewer().setContextMenu(cmProvider);
			getSite().registerContextMenu(
					"jp.hasc.hasctool.ui.bdeditor2.outline.contextmenu", //$NON-NLS-1$
					cmProvider, getSite().getSelectionProvider());
			// hook outline viewer
			getSelectionSynchronizer().addViewer(getViewer());
			// initialize outline viewer with model
			getViewer().setContents(getModel());
			// show outline viewer
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.ui.part.IPage#dispose()
		 */
		public void dispose() {
			// unhook outline viewer
			getSelectionSynchronizer().removeViewer(getViewer());
			// dispose
			super.dispose();
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.ui.part.IPage#getControl()
		 */
		public Control getControl() {
			return getViewer().getControl();
		}

		/**
		 * @see org.eclipse.ui.part.IPageBookViewPage#init(org.eclipse.ui.part.IPageSite)
		 */
		public void init(IPageSite pageSite) {
			super.init(pageSite);
			ActionRegistry registry = getActionRegistry();
			IActionBars bars = pageSite.getActionBars();
			String id = ActionFactory.UNDO.getId();
			bars.setGlobalActionHandler(id, registry.getAction(id));
			id = ActionFactory.REDO.getId();
			bars.setGlobalActionHandler(id, registry.getAction(id));
			id = ActionFactory.DELETE.getId();
			bars.setGlobalActionHandler(id, registry.getAction(id));
		}
	}

	@Override
	public void addBeanBlock(String clsName) {
		Shell shell = getSite().getShell();
		Class<?> cls;
		try{
			cls=Class.forName(clsName);
		}catch(ClassNotFoundException ex) {
			UIUtil.showMessageDialog(shell, ex.toString());
			return;
		}
		
		// paste
		BeanBlock bb = new BeanBlock();
		bb.setRuntimeClassName(cls.getName());
		bb.setName(diagram.getUniqueNameForClass(cls));
		BlockDiagram bd=new BlockDiagram();
		bd.addBlock(bb);
		BlockDiagramPasteCommand cmd = new BlockDiagramPasteCommand(this,bd);
		getCommandStack().execute(cmd);
		
		/*
		// position
		Rectangle bounds = getContentsBounds(diagram.getBlockDiagram());
		Point loc = new Point(BLOCK_HALF_WIDTH, bounds.bottom() + BLOCK_HALF_HEIGHT);
		
		BlockElement newShape=new BlockElement();
		BeanBlock bb = new BeanBlock();
		bb.setRuntimeClassName(cls.getName());
		bb.setName(diagram.getUniqueNameForClass(cls));
		newShape.init(diagram, bb);
		newShape.setLocation(loc,true);
		diagram.addBlock(newShape);
		
		// select
		GraphicalViewer gv = getGraphicalViewer();
		gv.flush();
		Object ep = gv.getEditPartRegistry().get(newShape);
		if (ep instanceof EditPart) {
			gv.select((EditPart) ep);
			gv.reveal((EditPart) ep);
		}
		*/
	}
	
	public static final int BLOCK_HALF_WIDTH = 80;
	public static final int BLOCK_HALF_HEIGHT = 16;
	
	public static final jp.hasc.hasctool.core.blockdiagram.model.Point ZERO_POINT =
		new jp.hasc.hasctool.core.blockdiagram.model.Point(0,0);
	
	public static Rectangle getContentsBounds(BlockDiagram bd) {
		int x0=Integer.MAX_VALUE, y0=Integer.MAX_VALUE, x1=Integer.MIN_VALUE, y1=Integer.MIN_VALUE;
		
		for(AbstractBlock b:bd.getBlocks()) {
			jp.hasc.hasctool.core.blockdiagram.model.Point p = b.getViewPosition();
			if (p==null) p=ZERO_POINT;
			x0=Math.min(x0, p.getX()-BLOCK_HALF_WIDTH);
			y0=Math.min(y0, p.getY()-BLOCK_HALF_HEIGHT);
			x1=Math.max(x1, p.getX()+BLOCK_HALF_WIDTH);
			y1=Math.max(y1, p.getY()+BLOCK_HALF_HEIGHT);
		}
		
		for(Shape b:bd.getShapes()) {
			jp.hasc.hasctool.core.blockdiagram.model.Point p = b.getViewPosition();
			if (p==null) p=ZERO_POINT;
			jp.hasc.hasctool.core.blockdiagram.model.Point s = b.getViewSize();
			if (s==null) s=ZERO_POINT;
			x0=Math.min(x0, p.getX());
			y0=Math.min(y0, p.getY());
			x1=Math.max(x1, p.getX()+s.getX());
			y1=Math.max(y1, p.getY()+s.getY());
		}
		
		if (x0==Integer.MAX_VALUE) return new Rectangle(0,0,0,0); else return new Rectangle(x0,y0,x1-x0,y1-y0);
	}
	
	@SuppressWarnings("unchecked")
	@Override
	protected void createActions() {
		super.createActions();
		ActionRegistry registry = getActionRegistry();
		IAction action;
		
		action = new BlockDiagramEditorCopyAction(this);
		registry.registerAction(action);
		getSelectionActions().add(action.getId());
		
		action = new BlockDiagramEditorPasteAction(this);
		registry.registerAction(action);
		getSelectionActions().add(action.getId());
	}

	@SuppressWarnings("unchecked")
	public static <T> T cloneByXStream(T destbd) {
		XStream xs = newXStream();
		return (T)xs.fromXML(xs.toXML(destbd));
	}

	public void selectModelElements(Iterable<ModelElement> models) {
		GraphicalViewer gv = getGraphicalViewer();
		gv.flush();
		boolean first=true;
		for(ModelElement e:models) {
			Object ep = gv.getEditPartRegistry().get(e);
			if (ep instanceof EditPart) {
				EditPart cep = (EditPart)ep;
				if (first) gv.select(cep);
				else gv.appendSelection(cep);
				gv.reveal(cep);
				first=false;
			}
		}
		/*
		if (boundsOrNull!=null) {
			FigureCanvas fc=(FigureCanvas) gv.getControl();
			fc.scrollSmoothTo(Math.max(0, boundsOrNull.right()-fc.getSize().x), Math.max(0, boundsOrNull.bottom()-fc.getSize().y));
		}
		*/
	}
}