// Contour routines written by Radoslav Marinov and John Bowman.

import graph_settings;

real eps=10000*realEpsilon;

//                         1
//             6 +-------------------+ 5
//               | \               / |
//               |   \          /    |
//               |     \       /     |
//               |       \   /       |
//             2 |         X         | 0
//               |       /   \       |
//               |     /       \     |
//               |   /           \   |
//               | /               \ |
//             7 +-------------------+ 4 or 8
//                         3

private struct segment
{
  bool active;
  pair a,b;        // Endpoints; a is always an edge point if one exists.
  int c;           // Contour value.
  int edge;        // -1: interior, 0 to 3: edge,
                   // 4-8: single-vertex edge, 9: double-vertex edge.
}

// Case 1: line passes through two vertices of a triangle
private segment case1(pair p0, pair p1, int edge)
{
  // Will cause a duplicate guide; luckily case1 is rare
  segment rtrn;
  rtrn.active=true;
  rtrn.a=p0;
  rtrn.b=p1;
  rtrn.edge=edge;
  return rtrn;
}

// Case 2: line passes through a vertex and a side of a triangle
// (the first vertex passed and the side between the other two)
private segment case2(pair p0, pair p1, pair p2,
                      real v0, real v1, real v2, int edge)
{
  segment rtrn;
  pair val=interp(p1,p2,abs(v1/(v2-v1)));
  rtrn.active=true;
  if(edge < 4) {
    rtrn.a=val;
    rtrn.b=p0;
  } else {
    rtrn.a=p0;
    rtrn.b=val;
  }
  rtrn.edge=edge;
  return rtrn;
}

// Case 3: line passes through two sides of a triangle
// (through the sides formed by the first & second, and second & third
// vertices)
private segment case3(pair p0, pair p1, pair p2,
                      real v0, real v1, real v2, int edge=-1)
{
  segment rtrn;
  rtrn.active=true;
  rtrn.a=interp(p1,p0,abs(v1/(v0-v1)));
  rtrn.b=interp(p1,p2,abs(v1/(v2-v1)));
  rtrn.edge=edge;
  return rtrn;
}

// Check if a line passes through a triangle, and draw the required line.
private segment checktriangle(pair p0, pair p1, pair p2,
                              real v0, real v1, real v2, int edge=-1)
{
  // default null return
  static segment dflt;

  real eps=eps*max(abs(v0),abs(v1),abs(v2));

  if(v0 < -eps) {
    if(v1 < -eps) {
      if(v2 < -eps) return dflt; // nothing to do
      else if(v2 <= eps) return dflt; // nothing to do
      else return case3(p0,p2,p1,v0,v2,v1);
    } else if(v1 <= eps) {
      if(v2 < -eps) return dflt; // nothing to do
      else if(v2 <= eps) return case1(p1,p2,5+edge);
      else return case2(p1,p0,p2,v1,v0,v2,5+edge);
    } else {
      if(v2 < -eps) return case3(p0,p1,p2,v0,v1,v2,edge);
      else if(v2 <= eps)
        return case2(p2,p0,p1,v2,v0,v1,edge);
      else return case3(p1,p0,p2,v1,v0,v2,edge);
    }
  } else if(v0 <= eps) {
    if(v1 < -eps) {
      if(v2 < -eps) return dflt; // nothing to do
      else if(v2 <= eps) return case1(p0,p2,4+edge);
      else return case2(p0,p1,p2,v0,v1,v2,4+edge);
    } else if(v1 <= eps) {
      if(v2 < -eps) return case1(p0,p1,9);
      else if(v2 <= eps) return dflt; // use finer partitioning.
      else return case1(p0,p1,9);
    } else {
      if(v2 < -eps) return case2(p0,p1,p2,v0,v1,v2,4+edge);
      else if(v2 <= eps) return case1(p0,p2,4+edge);
      else return dflt; // nothing to do
    }
  } else {
    if(v1 < -eps) {
      if(v2 < -eps) return case3(p1,p0,p2,v1,v0,v2,edge);
      else if(v2 <= eps)
        return case2(p2,p0,p1,v2,v0,v1,edge);
      else return case3(p0,p1,p2,v0,v1,v2,edge);
    } else if(v1 <= eps) {
      if(v2 < -eps) return case2(p1,p0,p2,v1,v0,v2,5+edge);
      else if(v2 <= eps) return case1(p1,p2,5+edge);
      else return dflt; // nothing to do
    } else {
      if(v2 < -eps) return case3(p0,p2,p1,v0,v2,v1);
      else if(v2 <= eps) return dflt; // nothing to do
      else return dflt; // nothing to do
    }
  }
}

// Collect connecting path segments.
private void collect(pair[][][] points, real[] c)
{
  // use to reverse an array, omitting the first point
  int[] reverseF(int n) {return sequence(new int(int x){return n-1-x;},n-1);}
  // use to reverse an array, omitting the last point
  int[] reverseL(int n) {return sequence(new int(int x){return n-2-x;},n-1);}

  for(int cnt=0; cnt < c.length; ++cnt) {
    pair[][] gdscnt=points[cnt];
    for(int i=0; i < gdscnt.length; ++i) {
      pair[] gig=gdscnt[i];
      int Li=gig.length;
      for(int j=i+1; j < gdscnt.length; ++j) {
        pair[] gjg=gdscnt[j];
        int Lj=gjg.length;
        if(abs(gig[0]-gjg[0]) < eps) {
          gdscnt[j]=gjg[reverseF(Lj)];
          gdscnt[j].append(gig);
          gdscnt.delete(i);
          --i;
          break;
        } else if(abs(gig[0]-gjg[Lj-1]) < eps) {
          gig.delete(0);
          gdscnt[j].append(gig);
          gdscnt.delete(i);
          --i;
          break;
        } else if(abs(gig[Li-1]-gjg[0]) < eps) {
          gjg.delete(0);
          gig.append(gjg);
          gdscnt[j]=gig;
          gdscnt.delete(i);
          --i;
          break;
        } else if(abs(gig[Li-1]-gjg[Lj-1]) < eps) {
          gig.append(gjg[reverseL(Lj)]);
          gdscnt[j]=gig;
          gdscnt.delete(i);
          --i;
          break;
        }
      }
    }
  }
}

// Join path segments.
private guide[][] connect(picture pic, pair[][][] points, real[] c,
                          interpolate join)
{
  // set up return value
  guide[][] result=new guide[c.length][];
  for(int cnt=0; cnt < c.length; ++cnt) {
    pair[][] pointscnt=points[cnt];
    guide[] resultcnt=result[cnt]=new guide[pointscnt.length];
    for(int i=0; i < pointscnt.length; ++i) {
      pair[] pts=pointscnt[i];
      guide gd;
      if(pts.length > 0) {
        if(pts.length > 1 && abs(pts[0]-pts[pts.length-1]) < eps) {
          guide[] g=sequence(new guide(int i) {
              return (pic.scale.x.T(pts[i].x), pic.scale.y.T(pts[i].y));
            },pts.length-1);
          g.push(cycle);
          gd=join(...g);
        } else
          gd=join(...sequence(new guide(int i) {
                return (pic.scale.x.T(pts[i].x), pic.scale.y.T(pts[i].y));
              },pts.length));
      }
      resultcnt[i]=gd;
    }
  }
  return result;
}


// Return contour guides for a 2D data array.
// z:         two-dimensional array of nonoverlapping mesh points
// f:         two-dimensional array of corresponding f(z) data values
// midpoint:  optional array containing values of f at cell midpoints
// c:         array of contour values
// join:      interpolation operator (e.g. operator -- or operator ..)
guide[][] contour(picture pic=currentpicture, pair[][] z, real[][] f,
                  real[][] midpoint=new real[][], real[] c,
                  interpolate join=operator --)
{
  int nx=z.length-1;
  if(nx == 0)
    abort("array z must have length >= 2");
  int ny=z[0].length-1;
  if(ny == 0)
    abort("array z[0] must have length >= 2");

  c=sort(c);
  bool midpoints=midpoint.length > 0;

  segment segments[][][]=new segment[nx][ny][];

  // go over region a rectangle at a time
  for(int i=0; i < nx; ++i) {
    pair[] zi=z[i];
    pair[] zp=z[i+1];
    real[] fi=f[i];
    real[] fp=f[i+1];
    real[] midpointi;
    if(midpoints) midpointi=midpoint[i];
    segment[][] segmentsi=segments[i];
    for(int j=0; j < ny; ++j) {
      segment[] segmentsij=segmentsi[j];

      // define points
      pair bleft=zi[j];
      pair bright=zp[j];
      pair tleft=zi[j+1];
      pair tright=zp[j+1];
      pair middle=0.25*(bleft+bright+tleft+tright);

      real f00=fi[j];
      real f01=fi[j+1];
      real f10=fp[j];
      real f11=fp[j+1];
      real fmm=midpoints ? midpoint[i][j] : 0.25*(f00+f01+f10+f11);

      // optimization: we make sure we don't work with empty rectangles
      int checkcell(int cnt) {
        real C=c[cnt];
        real vertdat0=f00-C;  // bottom-left vertex
        real vertdat1=f10-C;  // bottom-right vertex
        real vertdat2=f01-C;  // top-left vertex
        real vertdat3=f11-C;  // top-right vertex

        // optimization: we make sure we don't work with empty rectangles
        int countm=0;
        int countz=0;
        int countp=0;

        void check(real vertdat) {
          if(vertdat < -eps) ++countm;
          else {
            if(vertdat <= eps) ++countz;
            else ++countp;
          }
        }

        check(vertdat0);
        check(vertdat1);
        check(vertdat2);
        check(vertdat3);

        if(countm == 4) return 1;  // nothing to do
        if(countp == 4) return -1; // nothing to do
        if((countm == 3 || countp == 3) && countz == 1) return 0;

        // go through the triangles

        void addseg(segment seg) {
          if(seg.active) {
            seg.c=cnt;
            segmentsij.push(seg);
          }
        }
        real vertdat4=fmm-C;
        addseg(checktriangle(bright,tright,middle,
                             vertdat1,vertdat3,vertdat4,0));
        addseg(checktriangle(tright,tleft,middle,
                             vertdat3,vertdat2,vertdat4,1));
        addseg(checktriangle(tleft,bleft,middle,
                             vertdat2,vertdat0,vertdat4,2));
        addseg(checktriangle(bleft,bright,middle,
                             vertdat0,vertdat1,vertdat4,3));
        return 0;
      }

      void process(int l, int u) {
        if(l >= u) return;
        int i=quotient(l+u,2);
        int sign=checkcell(i);
        if(sign == -1) process(i+1,u);
        else if(sign == 1) process(l,i);
        else {
          process(l,i);
          process(i+1,u);
        }
      }

      process(0,c.length);
    }
  }

  // set up return value
  pair[][][] points=new pair[c.length][][];

  for(int i=0; i < nx; ++i) {
    segment[][] segmentsi=segments[i];
    for(int j=0; j < ny; ++j) {
      segment[] segmentsij=segmentsi[j];
      for(int k=0; k < segmentsij.length; ++k) {
        segment C=segmentsij[k];

        if(!C.active) continue;

        pair[] g=new pair[] {C.a,C.b};
        segmentsij[k].active=false;

        int forward(int I, int J, bool first=true) {
          if(I >= 0 && I < nx && J >= 0 && J < ny) {
            segment[] segmentsIJ=segments[I][J];
            for(int l=0; l < segmentsIJ.length; ++l) {
              segment D=segmentsIJ[l];
              if(!D.active) continue;
              if(abs(D.a-g[g.length-1]) < eps) {
                g.push(D.b);
                segmentsIJ[l].active=false;
                if(D.edge >= 0 && !first) return D.edge;
                first=false;
                l=-1;
              } else if(abs(D.b-g[g.length-1]) < eps) {
                g.push(D.a);
                segmentsIJ[l].active=false;
                if(D.edge >= 0 && !first) return D.edge;
                first=false;
                l=-1;
              }
            }
          }
          return -1;
        }

        int backward(int I, int J, bool first=true) {
          if(I >= 0 && I < nx && J >= 0 && J < ny) {
            segment[] segmentsIJ=segments[I][J];
            for(int l=0; l < segmentsIJ.length; ++l) {
              segment D=segmentsIJ[l];
              if(!D.active) continue;
              if(abs(D.a-g[0]) < eps) {
                g.insert(0,D.b);
                segmentsIJ[l].active=false;
                if(D.edge >= 0 && !first) return D.edge;
                first=false;
                l=-1;
              } else if(abs(D.b-g[0]) < eps) {
                g.insert(0,D.a);
                segmentsIJ[l].active=false;
                if(D.edge >= 0 && !first) return D.edge;
                first=false;
                l=-1;
              }
            }
          }
          return -1;
        }

        void follow(int f(int, int, bool first=true), int edge) {
          int I=i;
          int J=j;
          while(true) {
            static int ix[]={1,0,-1,0};
            static int iy[]={0,1,0,-1};
            if(edge >= 0 && edge < 4) {
              I += ix[edge];
              J += iy[edge];
              edge=f(I,J);
            } else {
              if(edge == -1) break;
              if(edge < 9) {
                int edge0=(edge-5) % 4;
                int edge1=(edge-4) % 4;
                int ix0=ix[edge0];
                int iy0=iy[edge0];
                I += ix0;
                J += iy0;
                // Search all 3 corner cells
                if((edge=f(I,J)) == -1) {
                  I += ix[edge1];
                  J += iy[edge1];
                  if((edge=f(I,J)) == -1) {
                    I -= ix0;
                    J -= iy0;
                    edge=f(I,J);
                  }
                }
              } else {
                // Double-vertex edge: search all 8 surrounding cells
                void search() {
                  for(int i=-1; i <= 1; ++i) {
                    for(int j=-1; j <= 1; ++j) {
                      if((edge=f(I+i,J+j,false)) >= 0) {
                        I += i;
                        J += j;
                        return;
                      }
                    }
                  }
                }
                search();
              }
            }
          }
        }

        // Follow contour in cell
        int edge=forward(i,j,first=false);

        // Follow contour forward outside of cell
        follow(forward,edge);

        // Follow contour backward outside of cell
        follow(backward,C.edge);

        points[C.c].push(g);
      }
    }
  }

  collect(points,c); // Required to join remaining case1 cycles.

  return connect(pic,points,c,join);
}

// Return contour guides for a 2D data array on a uniform lattice
// f:         two-dimensional array of real data values
// midpoint:  optional array containing data values at cell midpoints
// a,b:       diagonally opposite vertices of rectangular domain
// c:         array of contour values
// join:      interpolation operator (e.g. operator -- or operator ..)
guide[][] contour(picture pic=currentpicture, real[][] f,
                  real[][] midpoint=new real[][], pair a, pair b, real[] c,
                  interpolate join=operator --)
{
  int nx=f.length-1;
  if(nx == 0)
    abort("array f must have length >= 2");
  int ny=f[0].length-1;
  if(ny == 0)
    abort("array f[0] must have length >= 2");

  pair[][] z=new pair[nx+1][ny+1];
  for(int i=0; i <= nx; ++i) {
    pair[] zi=z[i];
    real xi=interp(a.x,b.x,i/nx);
    for(int j=0; j <= ny; ++j) {
      zi[j]=(xi,interp(a.y,b.y,j/ny));
    }
  }
  return contour(pic,z,f,midpoint,c,join);
}

// return contour guides for a real-valued function
// f:        real-valued function of two real variables
// a,b:      diagonally opposite vertices of rectangular domain
// c:        array of contour values
// nx,ny:    number of subdivisions in x and y directions (determines accuracy)
// join:     interpolation operator (e.g. operator -- or operator ..)
guide[][] contour(picture pic=currentpicture, real f(real, real), pair a,
                  pair b, real[] c, int nx=ngraph, int ny=nx,
                  interpolate join=operator --)
{
  // evaluate function at points and midpoints
  real[][] dat=new real[nx+1][ny+1];
  real[][] midpoint=new real[nx+1][ny+1];

  for(int i=0; i <= nx; ++i) {
    real x=interp(a.x,b.x,i/nx);
    real x2=interp(a.x,b.x,(i+0.5)/nx);
    real[] dati=dat[i];
    real[] midpointi=midpoint[i];
    for(int j=0; j <= ny; ++j) {
      dati[j]=f(x,interp(a.y,b.y,j/ny));
      midpointi[j]=f(x2,interp(a.y,b.y,(j+0.5)/ny));
    }
  }

  return contour(pic,dat,midpoint,a,b,c,join);
}

void draw(picture pic=currentpicture, Label[] L=new Label[],
          guide[][] g, pen[] p)
{
  begingroup(pic);
  for(int cnt=0; cnt < g.length; ++cnt) {
    guide[] gcnt=g[cnt];
    pen pcnt=p[cnt];
    for(int i=0; i < gcnt.length; ++i)
      draw(pic,gcnt[i],pcnt);
    if(L.length > 0) {
      Label Lcnt=L[cnt];
      for(int i=0; i < gcnt.length; ++i) {
        if(Lcnt.s != "" && size(gcnt[i]) > 1)
          label(pic,Lcnt,gcnt[i],pcnt);
      }
    }
  }
  endgroup(pic);
}

void draw(picture pic=currentpicture, Label[] L=new Label[],
          guide[][] g, pen p=currentpen)
{
  draw(pic,L,g,sequence(new pen(int) {return p;},g.length));
}

// Extend palette by the colors below and above at each end.
pen[] extend(pen[] palette, pen below, pen above) {
  pen[] p=copy(palette);
  p.insert(0,below);
  p.push(above);
  return p;
}

// Compute the interior palette for a sequence of cyclic contours
// corresponding to palette.
pen[][] interior(picture pic=currentpicture, guide[][] g, pen[] palette)
{
  if(palette.length != g.length+1)
    abort("Palette array must have length one more than guide array");
  pen[][] fillpalette=new pen[g.length][];
  for(int i=0; i < g.length; ++i) {
    guide[] gi=g[i];
    guide[] gp;
    if(i+1 < g.length) gp=g[i+1];
    guide[] gm;
    if(i > 0) gm=g[i-1];

    pen[] fillpalettei=new pen[gi.length];
    for(int j=0; j < gi.length; ++j) {
      path P=gi[j];
      if(cyclic(P)) {
        int index=i+1;
        bool nextinside;
        for(int k=0; k < gp.length; ++k) {
          path next=gp[k];
          if(cyclic(next)) {
            if(inside(P,point(next,0)))
              nextinside=true;
            else if(inside(next,point(P,0)))
              index=i;
          }
        }
        if(!nextinside) {
          // Check to see if previous contour is inside
          for(int k=0; k < gm.length; ++k) {
            path prev=gm[k];
            if(cyclic(prev)) {
              if(inside(P,point(prev,0)))
                index=i;
            }
          }
        }
        fillpalettei[j]=palette[index];
      }
      fillpalette[i]=fillpalettei;
    }
  }
  return fillpalette;
}

// Fill the interior of cyclic contours with palette
void fill(picture pic=currentpicture, guide[][] g, pen[][] palette)
{
  for(int i=0; i < g.length; ++i) {
    guide[] gi=g[i];
    guide[] gp;
    if(i+1 < g.length) gp=g[i+1];
    guide[] gm;
    if(i > 0) gm=g[i-1];

    for(int j=0; j < gi.length; ++j) {
      path P=gi[j];
      path[] S=P;
      if(cyclic(P)) {
        for(int k=0; k < gp.length; ++k) {
          path next=gp[k];
          if(cyclic(next) && inside(P,point(next,0)))
            S=S^^next;
        }
        for(int k=0; k < gm.length; ++k) {
          path next=gm[k];
          if(cyclic(next) && inside(P,point(next,0)))
            S=S^^next;
        }
        fill(pic,S,palette[i][j]+evenodd);
      }
    }
  }
}

// routines for irregularly spaced points:

// check existing guides and adds new segment to them if possible,
// or otherwise store segment as a new guide
private void addseg(pair[][] gds, segment seg)
{
  if(!seg.active) return;
  // search for a path to extend
  for(int i=0; i < gds.length; ++i) {
    pair[] gd=gds[i];
    if(abs(gd[0]-seg.b) < eps) {
      gd.insert(0,seg.a);
      return;
    } else if(abs(gd[gd.length-1]-seg.b) < eps) {
      gd.push(seg.a);
      return;
    } else if(abs(gd[0]-seg.a) < eps) {
      gd.insert(0,seg.b);
      return;
    } else if(abs(gd[gd.length-1]-seg.a) < eps) {
      gd.push(seg.b);
      return;
    }
  }

  // in case nothing is found
  pair[] segm;
  segm=new pair[] {seg.a,seg.b};
  gds.push(segm);

  return;
}

guide[][] contour(picture pic=currentpicture, real f(pair), pair a, pair b,
                  real[] c, int nx=ngraph, int ny=nx,
                  interpolate join=operator --)
{
  return contour(pic,new real(real x, real y) {return f((x,y));},a,b,c,nx,ny,join);
}

guide[][] contour(picture pic=currentpicture, pair[] z, real[] f, real[] c, interpolate join=operator --)
{
  if(z.length != f.length)
    abort("z and f arrays have different lengths");

  int[][] trn=triangulate(z);

  // array to store guides found so far
  pair[][][] points=new pair[c.length][][];

  for(int cnt=0; cnt < c.length; ++cnt) {
    pair[][] pointscnt=points[cnt];
    real C=c[cnt];
    for(int i=0; i < trn.length; ++i) {
      int[] trni=trn[i];
      int i0=trni[0], i1=trni[1], i2=trni[2];
      addseg(pointscnt,checktriangle(z[i0],z[i1],z[i2],
                                     f[i0]-C,f[i1]-C,f[i2]-C));
    }
  }

  collect(points,c);

  return connect(pic,points,c,join);
}
