package com.xith.client.landscape;

import java.awt.geom.*;
import javax.vecmath.*;
import javax.media.j3d.*;
import com.xith.java3d.geometry.terrain.heightmap.*;
import com.xith.client.world.*;
import java.util.*;
import com.xith.java3d.transients.*;
import com.navtools.util.ThreadLocalInstance;
import com.xith.utility.general.*;

/**
 * Class for definining paths and then generating geometry for them.
 *
 * Copyright:  Copyright (c) 2000,2001
 * Company:    Teseract Software, LLP
 * @author David Yazel
 *
 */

import org.apache.log4j.Category;
public class Paths {
    public static final Category LOG =
        Category.getInstance(Paths.class.getName());

   static final boolean debug = false;

   float points[];
   float width;
   float density;

   /**
    * Pass in an array of points X,Z representing nodes along the path.
    */
   public Paths(float points[], float width, float density) {
      this.points = points;
      this.width = width;
      this.density = density;

   }


   /**
    * Inner class which can solve for two optimal control points for a bezier curve segment
    * <p> </p>
    * <p> </p>
    * <p>Copyright: Copyright (c) 2000-2002</p>
    * <p>Company: Teseract Software, LLP</p>
    * @author David Yazel
    *
    */

    private static ThreadLocalInstance tlBezierSolver =
        new ThreadLocalInstance() {
              public Object make() {
            return new BezierSolver();
         }
      };

   public static float[] reverse( float p[] ) {

      float pp[] = new float[p.length];
      for (int i=0;i<p.length;i+=2) {
         pp[p.length-2-i]=p[i];
         pp[p.length-1-i]=p[i+1];
      }
      return pp;
   }

   /**
    * Turns the points into a series of Bezier curve segments and returns
    * the GeneralPath.  This algorithm guarentees that the curve will
    * go through every point.
    */

   public static GeneralPath getSmoothedPath(float ctrlpts[], float smoothness, boolean closed) {

      GeneralPath gp = new GeneralPath(GeneralPath.WIND_NON_ZERO);
      gp.moveTo(ctrlpts[0], ctrlpts[1]);
      BezierSolver bs = (BezierSolver)tlBezierSolver.get();
      bs.smoothness = smoothness;

      for (int i = 2; i < ctrlpts.length; i += 2) {

         bs.p2.x = ctrlpts[i-2];
         bs.p2.y = ctrlpts[i-1];

         bs.p3.x = ctrlpts[i];
         bs.p3.y = ctrlpts[i+1];

         // if this is the first point we are calculating then we need
         // we need a prior point, so get it from the end

         if (i==2) {

            if (closed) {
               bs.p1.x = ctrlpts[ctrlpts.length-2];
               bs.p1.y = ctrlpts[ctrlpts.length-1];
            } else {
               bs.p1.set(bs.p2);
            }
         } else {
            bs.p1.x = ctrlpts[i-4];
            bs.p1.y = ctrlpts[i-3];
         }

         // if this is the last point we are calculating then we need
         // to get the first point as the next

         if (i==ctrlpts.length-2) {

            if (closed) {
               bs.p4.x = ctrlpts[0];
               bs.p4.y = ctrlpts[1];
            } else {
               bs.p4.set(bs.p3);
            }
         } else {
            bs.p4.x = ctrlpts[i+2];
            bs.p4.y = ctrlpts[i+3];
         }

         bs.solve();

         gp.curveTo(bs.c1.x, bs.c1.y, bs.c2.x, bs.c2.y, bs.p3.x, bs.p3.y);

      }
      return gp;

   }

   /**
    * Turns the points into a series of Bezier curve segments and returns
    * the GeneralPath.
    */

   public static GeneralPath getGeneralPath(float ctrlpts[]) {

      int len = ctrlpts.length;

      float prevx = ctrlpts[0];
      float prevy = ctrlpts[1];
      float curx = ctrlpts[2];
      float cury = ctrlpts[3];
      float midx = (curx + prevx) / 2.0f;
      float midy = (cury + prevy) / 2.0f;
      GeneralPath gp = new GeneralPath(GeneralPath.WIND_NON_ZERO);
      gp.moveTo(midx, midy);

      for (int i = 4; i < ctrlpts.length; i += 2) {

          float x1 = (midx + curx) / 2.0f;
          float y1 = (midy + cury) / 2.0f;

          prevx = curx;
          prevy = cury;

          curx = ctrlpts[i + 0];
          cury = ctrlpts[i + 1];

          midx = (curx + prevx) / 2.0f;
          midy = (cury + prevy) / 2.0f;
          float x2 = (prevx + midx) / 2.0f;
          float y2 = (prevy + midy) / 2.0f;
          gp.curveTo(x1, y1, x2, y2, midx, midy);
//          gp.curveTo(x1, y1, x2, y2, curx, cury);

      }
      return gp;

   }


   public static GeneralPath getStraightPath(float ctrlpts[]) {

      int len = ctrlpts.length;

      GeneralPath gp = new GeneralPath(GeneralPath.WIND_NON_ZERO);
      gp.moveTo(ctrlpts[0], ctrlpts[1]);

      for (int i = 2; i < ctrlpts.length; i += 2) {

          float x1 = ctrlpts[i];
          float y1 = ctrlpts[i+1];
          gp.lineTo(x1,y1);

      }
      return gp;

   }

   public static float[] getSpecificPath(GeneralPath p, float density) {

      // calculate the length of the path, so we know what the size of the
      // array needs to be

      double flatness = 0.00001;
      int limit = 4;
      int count = 0;
      float point[] = new float[2];
      float length = 0;
      Point2f curPoint;
      Point2f prevPoint;

      // used to calculate some statistics on the path

      float minLength = 100;
      float maxLength = 0;
      FlatteningPathIterator flat;


      // get the first point

      flat = new FlatteningPathIterator(p.getPathIterator(new AffineTransform()),flatness,limit);

      // Count the number of points we will get from the flattened path

      while (!flat.isDone()) {
         count++;
         flat.next();
      }

      // now allocate enough points and pull them out to an array

      float parray[] = new float[count*2];

      flat = new FlatteningPathIterator(p.getPathIterator(new AffineTransform()),flatness,limit);

      // move through all the points and build the array of points

      count = 0;
      while (!flat.isDone()) {

         flat.currentSegment(point);
         parray[count*2] = point[0];
         parray[count*2+1] = point[1];
         flat.next();
         count++;

      }

      // calculate the length of the path

      prevPoint = new Point2f(parray[0],parray[1]);
      for (int i=1;i<count;i++) {

         curPoint = new Point2f(parray[i*2],parray[i*2+1]);
         float l = curPoint.distance(prevPoint);
         length += l;
         if (l<minLength) minLength = l;
         if (l>maxLength) maxLength = l;
         prevPoint = curPoint;
      }

      if (debug) {
         System.out.println("  Flattened path to "+count+" segments with length "+length);
         System.out.println("  Max segment = "+maxLength+", Min segment = "+minLength);
      }

      // now we need to step through the array and get the new points in increments
      // asked for in the density.

      int spathSize = (int)(length / density);
      float spath[] = new float[spathSize*2];
      if (debug) System.out.println("  Specific path should have "+spathSize+" nodes all "+density+" long");

      // to translate the curved nodes into a series of straight lines we need to
      // interpolate along the vectors from one point to another.  Sometimes we will
      // consume several curved points to make one new point, sometimes one curved segment
      // will yield multiple specific points.

      spath[0] = parray[0];
      spath[1] = parray[1];

      // set the current pointers into the arrays

      int cs = 2;
      int cp = 2;

      // define the starting point conditions

      prevPoint = new Point2f(spath[0],spath[1]);
      Point2f curPPoint = new Point2f(parray[2],parray[3]);
      curPoint = new Point2f();

      // now iterate until we have all the points

      while (cs<spathSize*2) {

         while (prevPoint.distance(curPPoint)<density) {

            cp += 2;
            if (cp>=count*2) break;
            curPPoint.x = parray[cp];
            curPPoint.y = parray[cp+1];

         }
         if (prevPoint.distance(curPPoint)<density) break;

         // ok now we have a a point long the path that is at least as long
         // from the last point as we want in density, now interpolate

         float alpha = density / prevPoint.distance(curPPoint);
         curPoint.interpolate(prevPoint,curPPoint,alpha);
         spath[cs++] = curPoint.x;
         spath[cs++] = curPoint.y;

         // move the current point to the previous point

         prevPoint.x = curPoint.x;
         prevPoint.y = curPoint.y;

      }

      // validate the sizes of all the segments are density

      prevPoint.x = spath[0];
      prevPoint.y = spath[1];
      minLength = 100;
      maxLength = 0;
      length = 0;
      for (int i=2;i<cs;i+=2) {

         curPoint.x = spath[i];
         curPoint.y = spath[i+1];
         float l = curPoint.distance(prevPoint);
         if (l<minLength) minLength = l;
         if (l>maxLength) maxLength = l;
         length += l;
         prevPoint.x = curPoint.x;
         prevPoint.y = curPoint.y;

      }
      if (debug) {
         System.out.println("  Specific path found "+cs/2+" interpolated nodes");
         System.out.println("  Max segment = "+maxLength+", Min segment = "+minLength+", length ="+length);
      }

      return spath;

   }

   public static float[] getSpecificHeightPath(GeneralPath p, float density) {

      // calculate the length of the path, so we know what the size of the
      // array needs to be

      double flatness = 0.00001;
      int limit = 8;
      int count = 0;
      float point[] = new float[2];
      float length = 0;
      Point2f curPoint;
      Point2f prevPoint;

      // used to calculate some statistics on the path

      float minLength = 100;
      float maxLength = 0;
      FlatteningPathIterator flat;


      // get the first point

      flat = new FlatteningPathIterator(p.getPathIterator(new AffineTransform()),flatness,limit);

      // Count the number of points we will get from the flattened path

      while (!flat.isDone()) {
         count++;
         flat.next();
      }

      // now allocate enough points and pull them out to an array

      float parray[] = new float[count*2];

      flat = new FlatteningPathIterator(p.getPathIterator(new AffineTransform()),flatness,limit);

      // move through all the points and build the array of points

      count = 0;
      while (!flat.isDone()) {

         flat.currentSegment(point);
         parray[count*2] = point[0];
         parray[count*2+1] = point[1];
         flat.next();
         count++;

      }

      // calculate the length of the path

      prevPoint = new Point2f(parray[0],parray[1]);
      for (int i=1;i<count;i++) {

         curPoint = new Point2f(parray[i*2],parray[i*2+1]);
         curPoint.y = 0;
         float l = curPoint.distance(prevPoint);
         length += l;
         if (l<minLength) minLength = l;
         if (l>maxLength) maxLength = l;
         prevPoint = curPoint;
      }

      if (debug) {
         System.out.println("  Flattened path to "+count+" segments with length "+length);
         System.out.println("  Max segment = "+maxLength+", Min segment = "+minLength);
      }

      // now we need to step through the array and get the new points in increments
      // asked for in the density.

      int spathSize = (int)(length / density);
      float spath[] = new float[spathSize*2];
      if (debug) System.out.println("  Specific path should have "+spathSize+" nodes all "+density+" long");

      // to translate the curved nodes into a series of straight lines we need to
      // interpolate along the vectors from one point to another.  Sometimes we will
      // consume several curved points to make one new point, sometimes one curved segment
      // will yield multiple specific points.

      spath[0] = parray[0];
      spath[1] = parray[1];

      // set the current pointers into the arrays

      int cs = 0;
      int cp = 2;

      // define the starting point conditions

      prevPoint = new Point2f(spath[0],spath[1]);
      Point2f curPPoint = new Point2f(parray[2],parray[3]);
      curPoint = new Point2f();

      // now iterate until we have all the points
      float curY = 0;
      float prevY = prevPoint.y;
      prevPoint.y = 0;
      float delta = 0;
      float distance =0;

      while (cs<spathSize*2) {

         do{

            cp += 2;
            if (cp>=count*2) break;
            curPPoint.x = parray[cp];
            curPPoint.y = 0;
            curY = parray[cp+1];
            distance = prevPoint.distance(curPPoint);

         } while (distance<density);
         if (distance <density) break;

         // ok now we have a a point long the path that is at least as long
         // from the last point as we want in density, now interpolate

         int n = (int)(distance / density);

         curPPoint.y = curY;
         prevPoint.y = prevY;

         for (int i=0;i<n;i++) {
            float alpha = (i+1)/((float)n);
            curPoint.interpolate(prevPoint,curPPoint,alpha);
            if (cs>=spath.length) break;
            spath[cs] = (float)cs/2*density;
            spath[cs+1] = curPoint.y;
            cs += 2;
         }


         // move the current point to the previous point

         prevY = curPoint.y;
         prevPoint.x = (cs-2)/2*density;
         prevPoint.y = 0;


      }

      // validate the sizes of all the segments are density

      prevPoint.x = spath[0];
      prevPoint.y = 0;
      minLength = 100;
      maxLength = 0;
      length = 0;
      for (int i=2;i<cs;i+=2) {

         curPoint.x = spath[i];
         curPoint.y = 0;
         float l = curPoint.distance(prevPoint);
         if (l<minLength) minLength = l;
         if (l>maxLength) maxLength = l;
         length += l;
         prevPoint.x = curPoint.x;
         prevPoint.y = curPoint.y;

      }
      if (debug) {
         System.out.println("  Specific path found "+cs/2+" interpolated nodes");
         System.out.println("  Max segment = "+maxLength+", Min segment = "+minLength+", length ="+length);
      }

      return spath;

   }

   /**
    * Generates a polygon for the path bounds.  This can generate a lot of points
    * if there is a low density.
    */

   public float[] getPolygon( float[] sPoints ) {

      Point3d curPoint = new Point3d();
      Point3d nextPoint = new Point3d();
      Vector3d vec = new Vector3d();
      Point3f p1 = new Point3f();
      Point3f p2 = new Point3f();
      Vector3f straightUp = new Vector3f(0,1,0);
      int vertex = 0;
      TexCoord2f tex = new TexCoord2f();
      float length = 0;
      Random r = new Random(183483);

      float left[] = new float[sPoints.length+2];
      float right[] = new float[sPoints.length+2];

      int n=0;
      // some transforms we are going to use

      Transform3D rot = new Transform3D();

      // step through all the vertices and write them out.  For each point in the sPoints
      // we will create 2 points in the geometry

      for (int i=0;i<sPoints.length/2-1;i++) {

         // create the points then calculate the vector from the current
         // point to the next point

         curPoint.x = sPoints[i*2];
         curPoint.z = sPoints[i*2+1];

         nextPoint.x = sPoints[(i+1)*2];
         nextPoint.z = sPoints[(i+1)*2+1];

         float l = (float)(curPoint.distance(nextPoint));

         // build a vector from the current point to the next
         // point of length equal to 1/2 the path width

         vec.set(curPoint);
         vec.sub(nextPoint);
         vec.normalize();
         vec.scale(width/2);

         // rotate the vector right 90 degrees

         rot.rotY(90*(3.14/180));
         rot.transform(vec);
         p2.set(vec);
         p2.x += curPoint.x;
         p2.z += curPoint.z;

         right[n] = p2.x;
         right[n+1] = p2.z;

         // now reverse it to get the other point

         rot.rotY(-180*(3.14/180));
         rot.transform(vec);
         p1.set(vec);
         p1.x += curPoint.x;
         p1.z += curPoint.z;

         // fill the left hand side in reverse so later we can easily add it to
         // the final array backwards

         left[n] = p1.z;
         left[n+1] = p1.x;

         n += 2;

         length += l;
      }

      // now make a float array big enough to hold all the points

      float ret[] = new float[n*2];

      // fill in the right hand side in order

      for (int i=0;i<n;i++) ret[i] = right[i];
      for (int i=0;i<n;i++) ret[n+i] = left[n-i-1];

      return ret;

   }

   public PolygonArea getPolygonArea() {

      GeneralPath gp = getGeneralPath(points);
      float[] sPoints =  getSpecificPath(gp,density);
      float[] aPoints = getPolygon(sPoints);
      PolygonArea g = new PolygonArea(aPoints);
      return g;
   }

   /**
    * Create the path over the terrain map, along the path nodes specified
    * in the constructor.
    */

   public GeometryArray getGeometry(HeightMapInterface map, Brush brush) {

      GeneralPath gp = getGeneralPath(points);
      float[] sPoints =  getSpecificPath(gp,density);

      // create a triangle strip, which should have

      TriangleStripArray geom;

      int vertexCounts[] = new int[1];
      vertexCounts[0] = sPoints.length;

      if (brush.dtexEnable) {
         int texMapping[] = {0,1};
         geom = new TriangleStripArray(sPoints.length,GeometryArray.COORDINATES |
            GeometryArray.COLOR_4 |
            GeometryArray.TEXTURE_COORDINATE_2 | GeometryArray.NORMALS,2,texMapping,vertexCounts);
      } else {
         geom = new TriangleStripArray(sPoints.length, GeometryArray.COORDINATES |
            GeometryArray.COLOR_4 |
            GeometryArray.TEXTURE_COORDINATE_2 | GeometryArray.NORMALS,vertexCounts);
      }

      Point3d curPoint = new Point3d();
      Point3d nextPoint = new Point3d();
      Vector3d vec = new Vector3d();
      Point3f p1 = new Point3f();
      Point3f p2 = new Point3f();
      Vector3f straightUp = new Vector3f(0,1,0);
      int vertex = 0;
      TexCoord2f tex = new TexCoord2f();
      float length = 0;
//      Color4f white = new Color4f(1f,1f,1f,1f);
      Color4f white = new Color4f(130f/255f,78f/255f,10f/255f,1f);
//      Color4f white = new Color4f(169f/255f,139f/255f,87f/255f,1f);
      Color4f invisible = new Color4f(white.x,white.y,white.z,0f);
      Random r = new Random(183483);

      // some transforms we are going to use

      Transform3D rot = new Transform3D();

      // step through all the vertices and write them out.  For each point in the sPoints
      // we will create 2 points in the geometry

      for (int i=0;i<sPoints.length/2-1;i++) {

         // create the points then calculate the vector from the current
         // point to the next point

         curPoint.x = sPoints[i*2];
         curPoint.z = sPoints[i*2+1];

         nextPoint.x = sPoints[(i+1)*2];
         nextPoint.z = sPoints[(i+1)*2+1];

         float l = (float)(curPoint.distance(nextPoint));

         // build a vector from the current point to the next
         // point of length equal to 1/2 the path width

         vec.set(curPoint);
         vec.sub(nextPoint);
         vec.normalize();
         vec.scale(width/2);

         // rotate the vector right 90 degrees

         rot.rotY(90*(3.14/180));
         rot.transform(vec);
         p2.set(vec);
         p2.x += curPoint.x;
         p2.z += curPoint.z;

         // now reverse it to get the other point

         rot.rotY(-180*(3.14/180));
         rot.transform(vec);
         p1.set(vec);
         p1.x += curPoint.x;
         p1.z += curPoint.z;

         // now we have 2 points straddling the path we need to determine their
         // height

         p1.y = map.getY(p1.x,p1.z)+0.3f;
         p2.y = map.getY(p2.x,p2.z)+0.3f;

         // determine if this is alpha blended

         Color4f color = white;
         if (brush.hasAlpha) {
            if ((i==0) || (i>=sPoints.length/2-2))
               color = invisible;
         }

//         System.out.println("p1:"+p1+",  p2:"+p2);

         geom.setCoordinate(vertex,p1);
         geom.setNormal(vertex,straightUp);
         geom.setColor(vertex,color);
         tex.x = 0;
         tex.y = (length/width)/8;
         geom.setTextureCoordinate(0,vertex,tex);
         vertex++;

         geom.setCoordinate(vertex,p2);
         geom.setNormal(vertex,straightUp);
         geom.setColor(vertex,color);
         tex.x = 1;
         geom.setTextureCoordinate(0,vertex,tex);
         vertex++;

         length += l;
      }

      return geom;
   }

   public GeometryArray getGeometry() {

      GeneralPath gp = getGeneralPath(points);
      float[] sPoints =  getSpecificPath(gp,0.5f);
      return null;

   }

   public static float[] getTestPoints() {

      float p[] = {1154.9f, 1186.58f,  1161.29f, 1181.5f,  1185.32f, 1161.60f, 1210.49f, 1147.1f,
                   1226.76f, 1152.47f, 1248.82f, 1130.76f, 1247.87f, 1095.08f,
                   1313.37f, 1060.54f, 1365.95f, 1010.14f, 1404.28f, 992.45f,
                   1448.51f, 969.45f,  1473.21f, 934.94f,  1498.24f, 887.29f,
                   1505.09f, 823.08f,  1505.47f, 769.98f,  1513.42f, 720.47f,
                   1518.56f, 659.98f,  1527.80f, 604.45f,  1521.39f, 570.15f,
                   1496.02f, 539.33f};
      return p;
   }

   static PolygonArea road = null;

   public static PolygonArea getRoad() {
      if (road!=null) return road;
      float p[] = {

         1489,542,
         1524,707,
         1560,770,
         1482,877,
         1424,915,
         1359,924,
         1311,916,
         1275,923,
         1199,943,
         61,47,
         57,48,
         52,47,
         51,44,
         50,42,
         47,41,
         43,41,
         894,825,
         845,819,
         861,819,
         832,812,
         824,826,
         767,820};

      for (int i=0;i<p.length;i++) if (p[i]<100) p[i] *= 20;

      Paths path = new Paths(p,15,40);
      road = path.getPolygonArea();
      return road;

   }


   public static float townAreaPoints[] = {
         63,50,
         57,42,
         54,42,
         50,36,
         46,35,
         45,32,
         43,32,
         40,34,
         36,36,
         30,37,
         31,44,
         23,48,
         41,49,
         45,51,
         51,51,
         57,51,
         1175/20,1037/20,
         1203/20,1063/20,
         1242/20,1084/20,
         1312/20,1094/20
      };

   static PolygonArea townArea = null;
   public static PolygonArea getTownArea() {
      if (townArea!=null) return townArea;

      float p[] = new float[townAreaPoints.length];
      for (int i=0;i<p.length;i++) p[i] = townAreaPoints[i]*20;

      GeneralPath gp = getSmoothedPath(p,0.75f,true);
      float[] sPoints =  getSpecificPath(gp,20);
      townArea = new PolygonArea(sPoints);
      return townArea;

   }


   static public void main(String[] args) {

      Paths p = new Paths(getTestPoints(),3f,1f);
      p.getGeometry(null,Brushes.getStoneWall());

   }


}
