/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *
 */

package org.apache.polygene.envisage.graph;

import java.awt.Color;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Iterator;
import org.apache.polygene.envisage.event.LinkEvent;
import prefuse.Constants;
import prefuse.Visualization;
import prefuse.action.Action;
import prefuse.action.ActionList;
import prefuse.action.ItemAction;
import prefuse.action.RepaintAction;
import prefuse.action.animate.ColorAnimator;
import prefuse.action.animate.LocationAnimator;
import prefuse.action.animate.QualityControlAnimator;
import prefuse.action.animate.VisibilityAnimator;
import prefuse.action.assignment.ColorAction;
import prefuse.action.assignment.FontAction;
import prefuse.action.filter.FisheyeTreeFilter;
import prefuse.action.layout.CollapsedSubtreeLayout;
import prefuse.action.layout.graph.NodeLinkTreeLayout;
import prefuse.activity.SlowInSlowOutPacer;
import prefuse.controls.ControlAdapter;
import prefuse.controls.FocusControl;
import prefuse.controls.PanControl;
import prefuse.controls.WheelZoomControl;
import prefuse.controls.ZoomControl;
import prefuse.controls.ZoomToFitControl;
import prefuse.data.Graph;
import prefuse.data.expression.Predicate;
import prefuse.data.expression.parser.ExpressionParser;
import prefuse.data.tuple.TupleSet;
import prefuse.render.AbstractShapeRenderer;
import prefuse.render.DefaultRendererFactory;
import prefuse.render.EdgeRenderer;
import prefuse.render.LabelRenderer;
import prefuse.util.ColorLib;
import prefuse.util.FontLib;
import prefuse.util.GraphicsLib;
import prefuse.util.PrefuseLib;
import prefuse.util.display.DisplayLib;
import prefuse.visual.VisualItem;
import prefuse.visual.sort.TreeDepthItemSorter;

public final class TreeGraphDisplay
    extends GraphDisplay
{

    static final String FILTER_ACTION = "filter";
    static final String REPAINT_ACTION = "repaint";
    static final String FULL_PAINT_ACTION = "fullPaint";
    static final String ANIMATE_PAINT_ACTION = "animatePaint";
    static final String ANIMATE_ACTION = "animate";
    static final String LAYOUT_ACTION = "layout";
    static final String SUB_LAYOUT_ACTION = "subLayout";
    static final String AUTO_ZOOM_ACTION = "autoZoom";

    private final LabelRenderer nodeRenderer;
    private final EdgeRenderer edgeRenderer;
    private final EdgeRenderer usesRenderer;

    private final int orientation = Constants.ORIENT_LEFT_RIGHT;

    public TreeGraphDisplay()
    {
        super( new Visualization() );

        Color BACKGROUND = Color.WHITE;
        Color FOREGROUND = Color.BLACK;

        setBackground( BACKGROUND );
        setForeground( FOREGROUND );

        nodeRenderer = new LabelRenderer( NAME_LABEL );
        nodeRenderer.setRenderType( AbstractShapeRenderer.RENDER_TYPE_FILL );
        nodeRenderer.setHorizontalAlignment( Constants.LEFT );
        nodeRenderer.setRoundedCorner( 8, 8 );
        edgeRenderer = new EdgeRenderer( Constants.EDGE_TYPE_CURVE );
        usesRenderer = new EdgeRenderer( Constants.EDGE_TYPE_CURVE, Constants.EDGE_ARROW_FORWARD );

        Predicate edgesPredicate = (Predicate) ExpressionParser.parse( "ingroup('graph.edges') AND [" + USES_EDGES + "]==false", true );
        Predicate usesPredicate = (Predicate) ExpressionParser.parse( "ingroup('graph.edges') AND [" + USES_EDGES + "]==true", true );

        DefaultRendererFactory rf = new DefaultRendererFactory( nodeRenderer );
        rf.add( edgesPredicate, edgeRenderer );
        rf.add( usesPredicate, usesRenderer );
        m_vis.setRendererFactory( rf );

        // colors
        ItemAction nodeColor = new NodeColorAction( GRAPH_NODES );
        ItemAction textColor = new ColorAction( GRAPH_NODES, VisualItem.TEXTCOLOR, ColorLib.rgb( 0, 0, 0 ) );
        m_vis.putAction( "textColor", textColor );

        ItemAction edgeColor = new ColorAction( GRAPH_EDGES, edgesPredicate, VisualItem.STROKECOLOR, ColorLib.rgb( 200, 200, 200 ) );
        ItemAction usesColor = new ColorAction( GRAPH_EDGES, usesPredicate, VisualItem.STROKECOLOR, ColorLib.rgb( 255, 100, 100 ) );
        ItemAction usesArrow = new ColorAction( GRAPH_EDGES, usesPredicate, VisualItem.FILLCOLOR, ColorLib.rgb( 255, 100, 100 ) );

        // quick repaint
        ActionList repaint = new ActionList();
        repaint.add( nodeColor );
        repaint.add( new RepaintAction() );
        m_vis.putAction( REPAINT_ACTION, repaint );

        // full paint
        ActionList fullPaint = new ActionList();
        fullPaint.add( nodeColor );
        m_vis.putAction( FULL_PAINT_ACTION, fullPaint );

        // animate paint change
        //ActionList animatePaint = new ActionList( 0 );
        ActionList animatePaint = new ActionList();
        animatePaint.add( new ColorAnimator( GRAPH_NODES ) );
        animatePaint.add( new RepaintAction() );
        m_vis.putAction( ANIMATE_PAINT_ACTION, animatePaint );

        // create the GRAPH layout action
        NodeLinkTreeLayout treeLayout = new NodeLinkTreeLayout( GRAPH, orientation, 40, 0, 8 );
        treeLayout.setLayoutAnchor( new Point2D.Double( 25, 300 ) );
        m_vis.putAction( LAYOUT_ACTION, treeLayout );

        CollapsedSubtreeLayout subLayout = new CollapsedSubtreeLayout( GRAPH, orientation );
        m_vis.putAction( SUB_LAYOUT_ACTION, subLayout );

        // create the filtering and layout
        ActionList filter = new ActionList();
        filter.add( new ExtendedFisheyeTreeFilter( GRAPH, 2 ) );
        filter.add( new FontAction( GRAPH_NODES, FontLib.getFont( "Tahoma", 14 ) ) );
        filter.add( treeLayout );
        filter.add( subLayout );
        filter.add( textColor );
        filter.add( nodeColor );
        filter.add( edgeColor );
        filter.add( usesColor );
        filter.add( usesArrow );
        m_vis.putAction( FILTER_ACTION, filter );

        // animated transition
        AutoPanAction autoPan = new AutoPanAction();
        ActionList animate = new ActionList( 750 );
        animate.setPacingFunction( new SlowInSlowOutPacer() );
        animate.add( autoPan );
        animate.add( new QualityControlAnimator() );
        animate.add( new VisibilityAnimator( GRAPH ) );
        animate.add( new LocationAnimator( GRAPH_NODES ) );
        animate.add( new ColorAnimator( GRAPH_NODES ) );
        animate.add( new RepaintAction() );
        m_vis.putAction( ANIMATE_ACTION, animate );
        m_vis.alwaysRunAfter( FILTER_ACTION, ANIMATE_ACTION );

        m_vis.putAction( AUTO_ZOOM_ACTION, new AutoZoomAction() );

        // initialize the display
        setItemSorter( new TreeDepthItemSorter() );
        addControlListener( new ZoomToFitControl() );
        addControlListener( new ZoomControl() );
        addControlListener( new WheelZoomControl() );
        addControlListener( new PanControl() );
        addControlListener( new FocusControl( 1, FILTER_ACTION ) );
        addControlListener( new ItemSelectionControl() );

        // set orientation
        nodeRenderer.setHorizontalAlignment( Constants.LEFT );
        edgeRenderer.setHorizontalAlignment1( Constants.RIGHT );
        edgeRenderer.setHorizontalAlignment2( Constants.LEFT );
        edgeRenderer.setVerticalAlignment1( Constants.CENTER );
        edgeRenderer.setVerticalAlignment2( Constants.CENTER );
        usesRenderer.setHorizontalAlignment1( Constants.CENTER );
        usesRenderer.setHorizontalAlignment2( Constants.CENTER );
        usesRenderer.setVerticalAlignment1( Constants.BOTTOM );
        usesRenderer.setVerticalAlignment2( Constants.CENTER );
        NodeLinkTreeLayout rtl = (NodeLinkTreeLayout) m_vis.getAction( LAYOUT_ACTION );
        CollapsedSubtreeLayout stl = (CollapsedSubtreeLayout) m_vis.getAction( SUB_LAYOUT_ACTION );
        rtl.setOrientation( orientation );
        stl.setOrientation( orientation );
    }

    @Override
    public void run( Graph graph )
    {
        m_vis.add( GRAPH, graph );
        run();
        m_vis.run( AUTO_ZOOM_ACTION );

        // disable edges interactive
        m_vis.setInteractive( GRAPH_EDGES, null, false );
    }

    @Override
    public void run()
    {
        m_vis.run( FILTER_ACTION );
    }

    /**
     * select the specified object
     *
     * @param object the object to select eg: Descriptor
     */
    @Override
    public void setSelectedValue( Object object )
    {
        if( object == null )
        {
            return;
        }

        VisualItem item = null;

        Iterator iter = m_vis.items( GRAPH_NODES );
        while( iter.hasNext() )
        {
            VisualItem tItem = (VisualItem) iter.next();
            Object tObj = tItem.get( USER_OBJECT );
            if( tObj.equals( object ) )
            {
                item = tItem;
                break;
            }
        }

        if( item != null )
        {
            TupleSet ts = m_vis.getFocusGroup( Visualization.FOCUS_ITEMS );
            ts.setTuple( item );
            m_vis.run( FILTER_ACTION );
        }
    }

    private class AutoZoomAction
        extends Action
    {
        @Override
        public void run( double frac )
        {
            int duration = 20;
            int margin = 50;
            Visualization vis = getVisualization();
            Rectangle2D bounds = vis.getBounds( Visualization.ALL_ITEMS );
            GraphicsLib.expand( bounds, margin + (int) ( 1 / getScale() ) );
            DisplayLib.fitViewToBounds( TreeGraphDisplay.this, bounds, duration );
        }
    }

    private class AutoPanAction
        extends Action
    {
        private final Point2D m_start = new Point2D.Double();
        private final Point2D m_end = new Point2D.Double();
        private final Point2D m_cur = new Point2D.Double();
        private final int m_bias = 150;

        @Override
        public void run( double frac )
        {
            TupleSet ts = m_vis.getFocusGroup( Visualization.FOCUS_ITEMS );
            if( ts.getTupleCount() == 0 )
            {
                return;
            }

            if( frac == 0.0 )
            {
                int xbias, ybias = 0;

                xbias = m_bias;
                switch( orientation )
                {
                    case Constants.ORIENT_LEFT_RIGHT:

                        break;
                    case Constants.ORIENT_RIGHT_LEFT:
                        xbias = -m_bias;
                        break;
                    case Constants.ORIENT_TOP_BOTTOM:
                        ybias = m_bias;
                        break;
                    case Constants.ORIENT_BOTTOM_TOP:
                        ybias = -m_bias;
                        break;
                }

                VisualItem vi = (VisualItem) ts.tuples().next();
                m_cur.setLocation( getWidth() / 2, getHeight() / 2 );
                getAbsoluteCoordinate( m_cur, m_start );
                m_end.setLocation( vi.getX() + xbias, vi.getY() + ybias );
            }
            else
            {
                m_cur.setLocation( m_start.getX() + frac * ( m_end.getX() - m_start.getX() ),
                                   m_start.getY() + frac * ( m_end.getY() - m_start.getY() ) );
                panToAbs( m_cur );
            }
        }
    }

    private static class NodeColorAction
        extends ColorAction
    {

        private NodeColorAction( String group )
        {
            super( group, VisualItem.FILLCOLOR );
        }

        @Override
        public int getColor( VisualItem item )
        {
            if( m_vis.isInGroup( item, Visualization.SEARCH_ITEMS ) )
            {
                return ColorLib.rgb( 255, 190, 190 );
            }
            else if( m_vis.isInGroup( item, Visualization.FOCUS_ITEMS ) )
            {
                return ColorLib.rgb( 198, 229, 229 );
            }
            else if( item.getDOI() > -1 )
            {
                return ColorLib.rgb( 164, 193, 193 );
            }
            else
            {
                return ColorLib.rgba( 255, 255, 255, 0 );
            }
        }
    }

    private class ItemSelectionControl
        extends ControlAdapter
    {
        @Override
        public final void itemClicked( VisualItem anItem, MouseEvent anEvent )
        {
            if( !anItem.canGet( USER_OBJECT, Object.class ) )
            {
                return;
            }
            Object object = anItem.get( USER_OBJECT );
            LinkEvent evt = new LinkEvent( TreeGraphDisplay.this, object );
            fireLinkActivated( evt );
        }
    }

    private static class ExtendedFisheyeTreeFilter
        extends FisheyeTreeFilter
    {
        private ExtendedFisheyeTreeFilter( String group, int distance )
        {
            super( group, distance );
        }

        @Override
        public void run( double frac )
        {
            super.run( frac );

            // set uses_edges always visible
            Iterator items = m_vis.items( GRAPH_EDGES );
            while( items.hasNext() )
            {
                VisualItem item = (VisualItem) items.next();
                if( item.getBoolean( USES_EDGES ) )
                {
                    PrefuseLib.updateVisible( item, true );
                }
            }
        }
    }

}
