/*
 * (C) 2004 - Geotechnical Software Services
 * 
 * This code is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public 
 * License as published by the Free Software Foundation; either 
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This code 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public 
 * License along with this program; if not, write to the Free 
 * Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, 
 * MA  02111-1307, USA.
 */
package no.geosoft.cc.graphics;



import java.util.Collection;
import java.util.Iterator;

import no.geosoft.cc.geometry.Geometry;
import no.geosoft.cc.geometry.Region;
import no.geosoft.cc.geometry.Rect;



/**
 * Layout manager for positionals. Positionals are object attached
 * to graphic elements by means of rendering hints.
 * 
 * @author <a href="mailto:info@geosoft.no">GeoSoft</a>
 */   
class GAnnotator
{
  private static final int     MAX_ATTEMTS = 300;
  private static final double  DLENGTH     = 8.0;
    
  private final GScene  scene_;   
  private final Region  region_;  // Annotation region

  // Precomputed viewport numbers for speed
  private int     vx0_, vy0_;
  private int     vx1_, vy1_;  
  private int     vx2_, vy2_;
  private int     vx3_, vy3_;  

  // Working variable during annotation
  private double  distance_;

  

  /**
   * Create annotator for the specified scene. The annotator use the
   * scene viewport as its canvas, and position all positional elements
   * of the scene inside this region.
   * 
   * @param scene  Scene of this annotator.
   */
  GAnnotator (GScene scene)
  {
    region_  = new Region();
    scene_ = scene;
  }


  
  /**
   * Reset this annotator. The annotator must be reset when the scene
   * viewport is changed.
   */
  void reset()
  {
    region_.set (scene_.getRegion());

    // Precompute some viewport numbers
    GViewport viewport = scene_.getViewport();
    int p0[] = viewport.getP0();  // x,y
    int p1[] = viewport.getP1();  // x,y
    int p2[] = viewport.getP2();  // x,y
    
    vx0_ = p0[0];
    vy0_ = p0[1];
    
    vx1_ = p1[0];
    vy1_ = p1[1];
    
    vx2_ = p2[0];
    vy2_ = p2[1];
    
    vx3_ = vx2_ + vx1_ - vx0_;
    vy3_ = vy1_ + vy2_ - vy0_;     
  }



  /**
   * Compute the x and y position of the specified positional elements
   * and set isVisible flag in case they are outside the screen.
   * After this computation is done on all nodes, the positionals can be
   * rendered directly by the GCanvas object.
   * 
   * @param positionals  Elements to position (of GPositional).
   */
  void computePositions (Collection positionals)
  {
    // Return here if nothing to position
    if (positionals == null) return;

    int objectNo = 0;
    for (Iterator i = positionals.iterator(); i.hasNext(); ) {
      GPositional positional = (GPositional) i.next();
      GSegment segment = positional.getSegment();
      int positionHint = positional.getPositionHint();
      
      // Save a copy of the old rectangle
      Rect oldRectangle = new Rect (positional.getRectangle());
      boolean wasVisible = positional.isVisible();
      
      if (segment.isVisible()) {
        int x[] = segment.getX();
        int y[] = segment.getY();
        int x0  = segment.getCenterX();
        int y0  = segment.getCenterY();
        
        // Compute positional size
        positional.computeSize();
        if (positional.getRectangle().isEmpty()) {
          positional.setVisible (false);
          continue;
        }
        
        // Find the preferred annotation according to line position hints
        int pointNo = findPreferredPosition (positional, x, y, x0, y0,
                                             objectNo);

        // If the current point is outside the viewport move it inside 
        if ((positionHint & GPosition.DYNAMIC) != 0 &&
            (positionHint & GPosition.MIDDLE) == 0) {
          positional.setVisible (true);

          distance_ = 0.0;
          if (!scene_.getRegion().isInside (positional.getRectangle().x,
                                            positional.getRectangle().y))
            pointNo = moveInsideWindow (positional, x, y, pointNo);
        }
        else {
          positional.setVisible (scene_.getRegion().
                                 isInside (positional.getRectangle().x,
                                           positional.getRectangle().y));
        }
        
        if (positional.isVisible()) {
          
          // Tune the positional according to point position hint
          adjustPosition (positional);

          // For a static positional we're now done
          if ((positionHint & GPosition.STATIC) != 0 |
              (positionHint & GPosition.MIDDLE) != 0)
            positional.setVisible (region_.isIntersecting (positional.
                                                           getRectangle()));
          else { // GPosition.DYNAMIC and don't allow overlapping
            if (!positional.isAllowingOverlaps() &&
                !region_.isInside (positional.getRectangle()))
              findNonOverlappingPosition (positional, x, y, pointNo);
          }
        }
      }

      // If positional was moved update damage
      if (!oldRectangle.equals (positional.getRectangle())) {
        if (wasVisible)
          scene_.getWindow().updateDamageArea (oldRectangle);
        if (positional.isVisible())
          scene_.getWindow().updateDamageArea (positional.getRectangle());

        if (wasVisible || positional.isVisible())
          segment.getOwner().flagRegionValid (false);
      }

      // If a valid position was found update the annotation region
      if (positional.isVisible() &&
          (positional.getPositionHint() & GPosition.STATIC) == 0 &&
          !positional.isAllowingOverlaps())
        region_.subtract (positional.getRectangle());

      if (!positional.isLinePositional()) objectNo++;
    }
  }


  
  /**
   * Find the preferred position of a positional element. This is the
   * position as dictaed by its rendering hint and before any adjustments
   * are done.
   * 
   * @param positional  Positional element to position.
   * @param x           X values of polyline to position according to.
   * @param y           Y values of polyline to position according to.   
   * @param x0          X coordinate of center of gravity of polyline
   *                    (used with rendering hint MIDDLE).
   * @param y0          Y coordinate of center of gravity of polyline
   *                    (used with rendering hint MIDDLE).
   * @param pointNo     Index number of this positional on this polyline
   *                    (used when no line rendering hints are given).
   * @return            Index in x,y of point to attach this positional to.
   */
  private int findPreferredPosition (GPositional positional,
                                     int x[], int y[], int x0, int y0,
                                     int pointNo)
  {
    int positionHint = positional.getPositionHint();
    int nPoints  = x.length;

    if (positional.isLinePositional()) {
      
      // Find the point on the segment that the positional should attach to
      if ((positionHint & GPosition.LAST) != 0) {
        pointNo = nPoints - 1;
      }
      else if ((positionHint & GPosition.MIDDLE) != 0) {
        positional.getRectangle().x = x0;
        positional.getRectangle().y = y0;
        return -1;
      }
      else if ((positionHint & GPosition.TOP) != 0) {
        int yMin = y[0];
        for (int i = 0; i < nPoints; i++) {
          if (y[i] < yMin) {
            yMin = y[i];
            pointNo = i;
          }
        }
      }
      else if ((positionHint & GPosition.BOTTOM) != 0) {
        int yMax = y[0];
        for (int i = 0; i < nPoints; i++) {
          if (y[i] > yMax) {
            yMax = y[i];
            pointNo = i;
          }
        }
      }
      else if ((positionHint & GPosition.LEFT) != 0) {
        int xMin = x[0];
        for (int i = 0; i < nPoints; i++) {
          if (x[i] < xMin) {
            xMin = x[i];
            pointNo = i;
          }
        }
      }
      else if ((positionHint & GPosition.RIGHT) != 0) {
        int xMax = x[0];
        for (int i = 0; i < nPoints; i++) {
          if (x[i] > xMax) {
            xMax = x[i];
            pointNo = i;
          }
        }
      }
    }
    
    positional.getRectangle().x = x[pointNo];
    positional.getRectangle().y = y[pointNo];

    return pointNo;
  }

  

  /**
   * Adjsut the position of the specified positional according to
   * point position hint.
   * 
   * @param positional  Positionla to adjust.
   */
  private void adjustPosition (GPositional positional)
  {
    int positionHint = positional.getPositionHint();
    int x            = positional.getRectangle().x;
    int y            = positional.getRectangle().y;
    int width        = positional.getRectangle().width;
    int height       = positional.getRectangle().height;

    int halfWidth  = (int) Math.ceil ((double) width  / 2.0);
    int halfHeight = (int) Math.ceil ((double) height / 2.0);    
                    

    if ((positionHint & GPosition.NORTHWEST) != 0) {
      x -= width  + positional.getMargin();
      y -= height + positional.getMargin();
    }
    else if ((positionHint & GPosition.NORTHEAST) != 0) {
      x += positional.getMargin();
      y -= height + positional.getMargin();
    }
    else if ((positionHint & GPosition.SOUTHWEST) != 0) {
      x -= width  + positional.getMargin();
      y += positional.getMargin();
    }
    else if ((positionHint & GPosition.SOUTHEAST) != 0) {
      x += positional.getMargin();
      y += positional.getMargin();
    }
    else if ((positionHint & GPosition.NORTH) != 0) {
      x -= halfWidth;
      y -= height + positional.getMargin();
    }
    else if ((positionHint & GPosition.WEST) != 0) {
      x -= width  + positional.getMargin();
      y -= halfHeight;
    }
    else if ((positionHint & GPosition.EAST) != 0) {
      x += positional.getMargin();
      y -= halfHeight;
    }
    else if ((positionHint & GPosition.SOUTH) != 0) {
      x -= halfWidth;
      y += positional.getMargin();
    }
    else { // GPosition.CENTER
      x -= halfWidth;
      y -= halfHeight;
    }

    positional.getRectangle().x = x;
    positional.getRectangle().y = y;
  }

  

  /**
   * Adjust position of the specified positional so that it doesn't overlap
   * any other positionals. The algorithm is to start on polyline at current
   * location, move back and forth on the line further and further away from
   * the origin until a free location is found.
   * 
   * @param positional  Positional to adjust.
   * @param x           X values of polyline to position along.
   * @param y           Y values of polyline to position along.   
   * @param pointNo     Index in polyline where positional is initially
   *                    attached.
   */
  private void findNonOverlappingPosition (GPositional positional,
                                           int x[], int y[], int pointNo)
  {
    // Find current position along polyline
    int x0, y0;
    
    if (distance_ == 0.0) {
      x0 = x[pointNo];
      y0 = y[pointNo];
    }
    else {
      double legLength = Geometry.length (x[pointNo], y[pointNo],
                                          x[pointNo+1], y[pointNo+1]);
      double fraction = (double) distance_ / (double) legLength;
      
      x0 = (int) Math.round (x[pointNo] +
                             fraction * (x[pointNo+1] - x[pointNo]));
      y0 = (int) Math.round (y[pointNo] +
                             fraction * (y[pointNo+1] - y[pointNo]));
    }
    
    // Find total length into the polyline
    double totalLength = 0.0;
    for (int i = 0; i < pointNo; i++)
      totalLength += Geometry.length (x[i], y[i], x[i+1], y[i+1]);
    totalLength += distance_;

    // Then loop alternating back and forth from x0,y0 and check position
    boolean isDoneLeft  = false;
    boolean isDoneRight = false;
    int     nAttempts = 0; // We cannot try for ever
    boolean isDone = false;

    double left  = totalLength -= DLENGTH;
    double right = totalLength += DLENGTH;

    int leftPosition[]  = {x0, y0};
    int rightPosition[] = {x0, y0};
    int position[]      = new int[2];        

    Rect  leftRectangle  = new Rect (positional.getRectangle());
    Rect  rightRectangle = new Rect (positional.getRectangle());
    
    int dx, dy;
    
    while (!isDone) {
      // Check to the left
      if (!isDoneLeft) {
        isDoneLeft = !Geometry.findPolygonPosition (x, y, left, position);
        if (!isDoneLeft) {
          dx = leftPosition[0] - position[0];
          dy = leftPosition[1] - position[1];

          leftRectangle.x -= dx;
          leftRectangle.y -= dy;

          if (region_.isInside (leftRectangle)) {
            positional.getRectangle().copy (leftRectangle);
            return;
          }
        }

        leftPosition[0] = position[0];
        leftPosition[1] = position[1];        
        
        nAttempts++;
      }
      
      // Check to the right
      if (!isDoneRight) {
        isDoneRight = !Geometry.findPolygonPosition (x, y, right, position);
        if (!isDoneRight) {
          dx = rightPosition[0] - position[0];
          dy = rightPosition[1] - position[1];

          rightRectangle.x -= dx;
          rightRectangle.y -= dy;

          if (region_.isInside (rightRectangle)) {
            positional.getRectangle().copy (rightRectangle);            
            return;            
          }
        }

        rightPosition[0] = position[0];
        rightPosition[1] = position[1];        

        nAttempts++;        
      }

      left  -= DLENGTH;
      right += DLENGTH;

      isDone = (isDoneLeft && isDoneRight) || nAttempts > MAX_ATTEMTS;
    }

    positional.setVisible (false);
  }



  /**
   * Given a polyline (x[], y[]) and a position (pointNo) on this
   * polyline for the positional. This function is called because we
   * know that pointNo is outside the visible. We find the closest
   * point (along line) that is inside. If none is found the positional
   * is set invisible and no further positioning is required.
   * If a point is found the positional rectangle is set to the
   * viewport intersection point, the polyline point is returned and
   * the distance_ variable is set as the distance from the positional
   * to the returned point.
   *
   * @param positional  Positional to move inside window.
   * @param x           X values of polyline.
   * @param y           Y values of polyline.
   * @param pointNo     Index of polyline where positional is initially
   *                    attached.
   * @return            New index to attach positional to.
   */
  private int moveInsideWindow (GPositional positional,
                                int x[], int y[], int pointNo)
  {
    int intersection[] = null;
    int iPoint;
    
    // Check to the "left" of current point
    for (iPoint= pointNo - 1; iPoint >= 0; iPoint--) {
      intersection = findViewportIntersection (x[iPoint+1], y[iPoint+1],
                                               x[iPoint], y[iPoint]);
      if (intersection != null) break;
    }

    // Check the "right" of the current point
    if (intersection == null) {
      for (iPoint = pointNo; iPoint < x.length - 1; iPoint++) {
        intersection = findViewportIntersection (x[iPoint], y[iPoint],
                                                 x[iPoint+1], y[iPoint+1]);
        if (intersection != null) break;
      }
    }

    if (intersection == null)
      positional.setVisible (false);
    else {
      positional.setVisible (true);      
      positional.getRectangle().x = intersection[0];
      positional.getRectangle().y = intersection[1];      
      distance_ = Geometry.length (intersection[0], intersection[1],
                                   x[iPoint], y[iPoint]);
    }
    
    return iPoint;
  }


  
  /**
   * Find the intersection point of the specified line segment and the
   * current viewport.
   * 
   * @param x0  X coordinate of first line segment endpoint.
   * @param y0  Y coordinate of first line segment endpoint.
   * @param x1  X coordinate of second line segment endpoint.
   * @param y1  Y coordinate of second line segment endpoint.
   * @return    Coordinate of intersection (or null if the specified
   *            line segment doesn't intersect the viewport).
   */
  private int[] findViewportIntersection (int x0, int y0, int x1, int y1)
  {
    double  length[]       = new double[2];
    int     x[]            = new int[2];
    int     y[]            = new int[2];
    double  intersection[] = new double[2];
    int     result[]       = new int[2];
    int     n = 0;
    int     type;

    
    // Top viewport edge
    type = Geometry.findLineSegmentIntersection (x0, y0, x1, y1,
                                                 vx0_, vy0_, vx1_, vy1_,
                                                 intersection);
    if (type == 1l) {
      x[n] = (int) Math.round (intersection[0]);
      y[n] = (int) Math.round (intersection[1]);
      length[n] = Geometry.length (x0, y0, x[n], y[n]);
      n++;
    }
    
    // Left viewport edge
    type = Geometry.findLineSegmentIntersection (x0, y0, x1, y1,
                                                 vx0_, vy0_, vx2_, vy2_,
                                                 intersection);
    if (type == 1) {
      x[n] = (int) Math.round (intersection[0]);
      y[n] = (int) Math.round (intersection[1]);
      length[n] = Geometry.length (x0, y0, x[n], y[n]);
      n++;
    }

    // Right viewport edge
    if (n < 2) {
      type = Geometry.findLineSegmentIntersection (x0, y0, x1, y1,
                                                   vx1_, vy1_, vx3_, vy3_,
                                                   intersection);
      if (type == 1) {
        x[n] = (int) Math.round (intersection[0]);
        y[n] = (int) Math.round (intersection[1]);
        length[n] = Geometry.length (x0, y0, x[n], y[n]);
        n++;
      }
    }

    // Bottom viewport edge
    if (n < 2) {    
      type = Geometry.findLineSegmentIntersection (x0, y0, x1, y1,
                                                   vx2_, vy2_, vx3_, vy3_,
                                                   intersection);
      if (type == 1) {
        x[n] = (int) Math.round (intersection[0]);
        y[n] = (int) Math.round (intersection[1]);
        length[n] = Geometry.length (x0, y0, x[n], y[n]);
        n++;
      }
    }

    if (n == 0) return null;

    if (n == 1) {
      result[0] = x[0];
      result[1] = y[0];
    }

    // Intersects the viewport twice. Pick the closest
    else {
      if (length[0] < length[1]) {
        result[0] = x[0];
        result[1] = y[0];
      }
      else {
        result[0] = x[1];
        result[1] = y[1];
      }
    }
    
    return result;
  }


  
  /**
   * This method is used to position a positional on every vertex along
   * a polyline. The positioanl rectangle x and y values are computed
   * as delta measure, so that the positionals can later be positioned
   * by adding this delta to each vertex.
   * 
   * @param positional  Positional to compute position of.
   */
  void computeVertexPositions (GPositional positional)
  {
    positional.computeSize();

    int width  = positional.getRectangle().width;
    int height = positional.getRectangle().height;

    int x, y;

    int positionHint = positional.getPositionHint();
    
    if      ((positionHint & GPosition.NORTH)   != 0) {
      x = - (int) Math.floor (width / 2.0);
      y = - height;
    }
    else if ((positionHint & GPosition.SOUTH)     != 0) {
      x = - (int) Math.floor (width / 2.0);
      y = 0;
    }
    else if ((positionHint & GPosition.WEST)      != 0) {
      x = - width;
      y = - (int) Math.floor (height / 2.0);
    }
    else if ((positionHint & GPosition.EAST)      != 0) {
      y = - (int) Math.floor (height / 2.0);
      x = 0;
    }
    else if ((positionHint & GPosition.NORTHWEST) != 0) {
      x = - width;
      y = - height;
    }
    else if ((positionHint & GPosition.NORTHEAST) != 0) {
      x = 0;
      y = - height;      
    }
    else if ((positionHint & GPosition.SOUTHWEST) != 0) {
      x = - width;      
      y = 0;
    }
    else if ((positionHint & GPosition.SOUTHEAST) != 0) {
      x = 0;
      y = 0;
    }
    else { // CENTER (or unset implying center)
      x = - (int) Math.floor (width  / 2.0);
      y = - (int) Math.floor (height / 2.0);      
    }

    positional.getRectangle().x = x;
    positional.getRectangle().y = y;    
  }
}