package y.layout.hierarchic;

import y.base.DataProvider;
import y.base.Node;
import y.base.NodeCursor;
import y.base.NodeList;
import y.layout.LayoutGraph;
import y.layout.NodeLayout;
import y.util.DataProviderAdapter;

/**
 * This class can be used to simply wrap another Drawer implementation.
 * It will rearrange nodes within each layer so that they are aligned with
 * respect to a given alignment point, that is provided on a per node basis,
 * which is provided by a registered
 * DataProvider instance that may provide doubles that 
 * are interpreted as relative
 * coordinates to the center of the node.
 */
public class AlignmentDrawer implements Drawer
{
  
  /** The DataProvider key used for the DataProvider instance that provides for 
   * each node a double depicting the center anchored offset to the 
   * alignment point.
   */
  public static final Object NODE_ALIGNMENT_POINT_DPKEY = "y.layout.hierarchic.AlignmentDrawer.NODE_ALIGNMENT_POINT_DPKEY";
  
  /** the Drawer instance the actual drawing (x-coordinates) is calculated by */
  private Drawer inner;
  
  /** Creates a new AlignmentDrawer using the given drawer as the actual drawer */
  public AlignmentDrawer(Drawer inner)
  {
    if (inner == null) throw new NullPointerException();
    this.inner = inner;
  }
  
  /**
   * Modifies the given drawing by modifying the y-coordinates only
   */
  protected void alignNodes(LayoutGraph graph, NodeList[] lists)
  {
    // accumulated shifts over all layers
    double accuShift = 0;

    for(int i =  0; i < lists.length; i++)
    {
      final NodeCursor layer = lists[i].nodes();
      
      // calulate old and new layer height, taking alignment into account
      double topExtension = 0;
      double bottomExtension = 0;

      double oldMin = Double.MAX_VALUE;
      double oldMax = -Double.MAX_VALUE;
      
      for(layer.toFirst(); layer.ok(); layer.next()) {
        final Node node = layer.node();
        final NodeLayout nl = graph.getNodeLayout(node);
        oldMin = Math.min(oldMin, nl.getY());
        oldMax = Math.max(oldMax, nl.getY() + nl.getHeight());
        final double alignment = getAlignment(node);
        topExtension = Math.max(topExtension, nl.getHeight() * 0.5d + alignment);
        bottomExtension = Math.max(bottomExtension, nl.getHeight() * 0.5d - alignment);
      }
      
      final double height = topExtension + bottomExtension;
      
      //calculate the y-coordinate of the alignment line..
      //move this layer's nodes according to the new alignment
      final double alignmentLine = oldMin + accuShift + topExtension;
      for(layer.toFirst(); layer.ok(); layer.next())
      {
        final Node node = layer.node();
        final double alignment = getAlignment(node);
        graph.setCenter(node, graph.getCenterX(node), alignmentLine - alignment);
      }
      accuShift += height - (oldMax - oldMin);
    }
  }  
  
  /** Helper method that returns a non-null relative alignment point for each
   * given node.
   */
  private static final double getAlignment(Node node){
    final DataProvider alignmentPointProvider = node.getGraph().getDataProvider(NODE_ALIGNMENT_POINT_DPKEY);
    if (alignmentPointProvider == null){
      return 0.0d;
    } else {
      return alignmentPointProvider.getDouble(node);
    }
  }
  
  public void assignCoordinates(LayoutGraph g, y.base.NodeList[] layerLists, DataProvider layerID)
  {
    inner.assignCoordinates(g, layerLists, layerID);
    alignNodes(g, layerLists);
  }
  
  public void setDummyMap(y.base.NodeMap dummy)
  {
    inner.setDummyMap(dummy);
  }
  
  public void setMinimalEdgeDistance(double d)
  {
    inner.setMinimalEdgeDistance(d);
  }
  
  public void setMinimalLayerDistance(double d)
  {
    inner.setMinimalLayerDistance(d);
  }
  
  public void setMinimalMultiEdgeDistance(double d)
  {
    inner.setMinimalMultiEdgeDistance(d);
  }
  
  public void setMinimalNodeDistance(double d)
  {
    inner.setMinimalNodeDistance(d);
  }
  
  /**
   * Simple utility class that can be registered with the graph to be laid out.
   * Instances of this class will make all nodes top-aligned (for top to bottom layouts).
   */
  public static final class TopAlignmentDataProvider extends DataProviderAdapter {
    
    public double getDouble(Object dataHolder)
    {
      final Node node = (Node) dataHolder;
      LayoutGraph graph = (LayoutGraph) node.getGraph();
      final double height = graph.getHeight(node);
      return -0.5d * height;
    }
  }

  /**
   * Simple utility class that can be registered with the graph to be laid out.
   * Instances of this class will make all nodes left-aligned (for left to right layouts).
   */
  public static final class LeftAlignmentDataProvider extends DataProviderAdapter {
    
    public double getDouble(Object dataHolder)
    {
      final Node node = (Node) dataHolder;
      LayoutGraph graph = (LayoutGraph) node.getGraph();
      final double width = graph.getWidth(node);
      return -0.5d * width;
    }
  }

  /**
   * Simple utility class that can be registered with the graph to be laid out.
   * Instances of this class will make all nodes right-aligned (for left to right layouts).
   */
  public static final class RightAlignmentDataProvider extends DataProviderAdapter {
    
    public double getDouble(Object dataHolder)
    {
      final Node node = (Node) dataHolder;
      LayoutGraph graph = (LayoutGraph) node.getGraph();
      final double width = graph.getWidth(node);
      return -0.5d * width;
    }
  }

  /**
   * Simple utility class that can be registered with the graph to be laid out.
   * Instances of this class will make all nodes bottom-aligned  (for top to bottom layouts).
   */
  public static final class BottomAlignmentDataProvider extends DataProviderAdapter {
    
    public double getDouble(Object dataHolder)
    {
      final Node node = (Node) dataHolder;
      LayoutGraph graph = (LayoutGraph) node.getGraph();
      final double height = graph.getHeight(node);
      return 0.5d * height;
    }
  }
}
