// Supakorn "Jamie" Rassameemasuang <jamievlin@outlook.com> and
// John C. Bowman

import three;
import v3dtypes;
import v3dheadertypes;

struct triangleGroup
{
  triple[] positions;
  triple[] normals;
  pen[] colors;

  int[][] positionIndices;
  int[][] normalIndices;
  int[][] colorIndices;
}

struct pixel
{
  triple point;
  real width;
}

struct CameraInformation
{
  int canvasWidth;
  int canvasHeight;
  bool absolute;

  triple b;
  triple B;
  bool orthographic;
  real angle;
  real Zoom0;
  pair viewportshift;
  pair viewportmargin;

  light light;

  void setCameraInfo()
  {
    size(canvasWidth,canvasHeight);
    triple center=0.5*(b.z+B.z)*Z;

    if(orthographic)
      currentprojection=orthographic(Z,target=center,Zoom0,
                                     viewportshift=viewportshift);
    else
      currentprojection=perspective(Z,Y,target=center,Zoom0,
                                    degrees(2.0*atan(tan(0.5*angle)/Zoom0)),
                                    viewportshift=viewportshift,
                                    autoadjust=false);
    light.specular=light.diffuse;
    currentlight=light;
  }
}

transform3 Align(real polar, real azimuth)
{
  return align(dir(degrees(polar),degrees(azimuth)));
}

struct v3dfile
{
  file xdrfile;
  int version;
  bool hasCameraInfo=false;
  CameraInformation info;
  bool singleprecision=false;

  triple[] centers;
  material[] materials;

  surface[][][] surfaces;
  path3[][][] paths3;
  triangleGroup[][][] triangles;
  pixel[][] pixels;

  void initSurface(int center, int material) {
    if(!surfaces.initialized(center))
      surfaces[center]=new surface[][];
    if(!surfaces[center].initialized(material))
      surfaces[center][material]=new surface[] {new surface};
  }

  void initPath3(int center, int material) {
    if(!paths3.initialized(center))
      paths3[center]=new path3[][];
    if(!paths3[center].initialized(material))
      paths3[center][material]=new path3[];
  }

  void initTriangleGroup(int center, int material) {
    if (!triangles.initialized(center))
      triangles[center]=new triangleGroup[][];
    if(!triangles[center].initialized(material))
      triangles[center][material]=new triangleGroup[];
  }

  void initPixel(int material) {
    if(!pixels.initialized(material))
      pixels[material]=new pixel[];
  }

  void surface(int center, int material, patch p) {
    initSurface(center,material);
    surfaces[center][material][0].s.push(p);
  }

  void primitive(int center, int material, surface s) {
    initSurface(center,material);
    surfaces[center][material].push(s);
  }

  void path3(int center, int material, path3 p) {
    initPath3(center,material);
    paths3[center][material].push(p);
  }

  void triangleGroup(int center, int material, triangleGroup g) {
    initTriangleGroup(center,material);
    triangles[center][material].push(g);
  }

  void pixel(int material, pixel P) {
    initPixel(material);
    pixels[material].push(P);
  }

  void operator init(string name)
  {
    xdrfile=input(name, mode="xdrgz");
    version=xdrfile;

    int doubleprecision=xdrfile;
    singleprecision=doubleprecision == 0;
    xdrfile.singlereal(singleprecision);
  }

  int getType()
  {
    return xdrfile;
  }

  void setCameraInfo()
  {
    if(hasCameraInfo)
      info.setCameraInfo();
  }

  pen[] readColorData(int size=4, bool alpha=true)
  {
    xdrfile.singlereal(true);

    xdrfile.dimension(alpha ? 4 : 3);
    pen[] newPen=new pen[size];
    for (int i=0; i < size; ++i)
      {
        newPen[i]=alpha ? rgba(xdrfile) : rgb((real[]) xdrfile);
      }

    xdrfile.singlereal(singleprecision);

    return newPen;
  }

  CameraInformation processHeader()
  {
    CameraInformation ci;

    int entryCount=xdrfile;
    for (int i=0;i<entryCount;++i)
      {
        int headerKey=xdrfile;
        int headerSz=xdrfile;

        if (headerKey == v3dheadertypes.canvasWidth)
          {
            ci.canvasWidth=xdrfile;
          }
        else if (headerKey == v3dheadertypes.canvasHeight)
          {
            ci.canvasHeight=xdrfile;
          }
        else if (headerKey == v3dheadertypes.absolute)
          {
            int val=xdrfile;
            ci.absolute=(val != 0);
          }
        else if (headerKey == v3dheadertypes.minBound)
          {
            ci.b=xdrfile;
          }
        else if (headerKey == v3dheadertypes.maxBound)
          {
            ci.B=xdrfile;
          }
        else if (headerKey == v3dheadertypes.orthographic)
          {
            int val=xdrfile;
            ci.orthographic=(val != 0);
          }
        else if (headerKey == v3dheadertypes.angleOfView)
          {
            ci.angle=xdrfile;
          }
        else if (headerKey == v3dheadertypes.initialZoom)
          {
            ci.Zoom0=xdrfile;
          }
        else if (headerKey==v3dheadertypes.viewportShift)
          {
            ci.viewportshift=xdrfile;
          }
        else if (headerKey==v3dheadertypes.viewportMargin)
          {
            ci.viewportmargin=xdrfile;
          }
        else if (headerKey==v3dheadertypes.background)
          {
            ci.light.background=readColorData(1)[0];
          }
        else if (headerKey==v3dheadertypes.light)
          {
            triple position=xdrfile;
            ci.light.position.push(position);
            ci.light.diffuse.push(rgba(readColorData(1,false)[0]));
          }
        else
          {
            xdrfile.dimension(headerSz);
            int[] _dmp=xdrfile;
          }
      }
    return ci;
  }

  material readMaterial() {
    xdrfile.dimension(4);
    xdrfile.singlereal(true);

    pen diffusePen=rgba(xdrfile);
    pen emissivePen=rgba(xdrfile);
    pen specularPen=rgba(xdrfile);

    xdrfile.dimension(3);
    real[] params=xdrfile;
    real shininess=params[0];
    real metallic=params[1];
    real F0=params[2];

    xdrfile.singlereal(singleprecision);

    return material(diffusePen,emissivePen,specularPen,1,shininess,
                    metallic,F0);
  }

  triple[][] readRawPatchData() {
    triple[][] val;
    xdrfile.dimension(4,4);
    val=xdrfile;
    return val;
  }

  triple[][] readRawTriangleData() {
    triple[][] val;

    for(int i=0; i < 4; ++i) {
      xdrfile.dimension(i+1);
      triple[] v=xdrfile;
      val.push(v);
    }
    return val;
  }

  void readBezierPatch() {
    triple[][] val=readRawPatchData();
    int center=xdrfile;
    int material=xdrfile;
    surface(center,material,patch(val));
  }

  void readBezierTriangle() {
    triple[][] val=readRawTriangleData();
    int center=xdrfile;
    int material=xdrfile;
    surface(center,material,patch(val,triangular=true));
  }

  triple[] readCenters() {
    int centerCount=xdrfile;
    xdrfile.dimension(centerCount);
    triple[] centersFetched;
    if (centerCount>0)
      centersFetched=xdrfile;
    return centersFetched;
  }

  void readBezierPatchColor() {
    triple[][] val=readRawPatchData();
    int center=xdrfile;
    int material=xdrfile;
    pen[] colors=readColorData(4);
    surface(center,material,patch(val,colors=colors));
  }

  void readBezierTriangleColor() {
    triple[][] val=readRawTriangleData();
    int center=xdrfile;
    int material=xdrfile;
    pen[] colors=readColorData(3);
    surface(center,material,patch(val,triangular=true,colors=colors));
  }

  void readSphere() {
    triple origin=xdrfile;
    real radius=xdrfile;

    int center=xdrfile;
    int material=xdrfile;

    surface s=shift(origin)*scale3(radius)*unitsphere;
    s.draw=unitsphere.draw;
    primitive(center,material,s);
  }

  void readHemisphere() {
    triple origin=xdrfile;
    real radius=xdrfile;

    int center=xdrfile;
    int material=xdrfile;

    real polar=xdrfile;
    real azimuth=xdrfile;

    surface s=shift(origin)*Align(polar,azimuth)*scale3(radius)*unithemisphere;
    s.draw=unithemisphere.draw;
    primitive(center,material,s);
  }

  void readDisk() {
    triple origin=xdrfile;
    real radius=xdrfile;

    int center=xdrfile;
    int material=xdrfile;

    real polar=xdrfile;
    real azimuth=xdrfile;

    surface s=shift(origin)*Align(polar,azimuth)*scale3(radius)*unitdisk;
    s.draw=unitdisk.draw;
    primitive(center,material,s);
  }

  void readPolygon(int n)
  {
    xdrfile.dimension(n);
    triple[] val=xdrfile;

    int center=xdrfile;
    int material=xdrfile;
    surface(center,material,patch(val));
  }

  void readPolygonColor(int n)
  {
    xdrfile.dimension(n);
    triple[] val=xdrfile;

    int center=xdrfile;
    int material=xdrfile;

    pen[] colors=readColorData(n);
    surface(center,material,patch(val,colors=colors));
  }

  void readCylinder() {
    triple origin=xdrfile;
    real radius=xdrfile;
    real height=xdrfile;

    int center=xdrfile;
    int material=xdrfile;

    real polar=xdrfile;
    real azimuth=xdrfile;

    int core=xdrfile;

    transform3 T=shift(origin)*Align(polar,azimuth)*
      scale(radius,radius,height);
    if(core != 0)
      path3(center,material,T*(O--Z));

    surface s=T*unitcylinder;
    s.draw=unitcylinder.draw;
    primitive(center,material,s);
  }

  void readTube() {
    xdrfile.dimension(4);
    triple[] g=xdrfile;

    real width=xdrfile;
    int center=xdrfile;
    int material=xdrfile;

    int core=xdrfile;

    if(core != 0)
      path3(center,material,g[0]..controls g[1] and g[2]..g[3]);

    surface s=tube(g[0],g[1],g[2],g[3],width);
    s.draw=drawTube(g,width,info.b,info.B);
    primitive(center,material,s);
  }

  void readCurve() {
    xdrfile.dimension(4);
    triple[] points=xdrfile;

    int center=xdrfile;
    int material=xdrfile;

    path3(center,material,
          points[0]..controls points[1] and points[2]..points[3]);
  }

  void readLine() {
    xdrfile.dimension(2);
    triple[] points=xdrfile;

    int center=xdrfile;
    int material=xdrfile;

    path3(center,material,points[0]--points[1]);
  }

  void readTriangles() {
    triangleGroup g;

    int nI=xdrfile;

    int nP=xdrfile;
    xdrfile.dimension(nP);
    g.positions=xdrfile;

    int nN=xdrfile;
    xdrfile.dimension(nN);
    g.normals=xdrfile;

    int explicitNI=xdrfile;

    int nC=xdrfile;
    int explicitCI;
    if (nC > 0) {
      g.colors=readColorData(nC);
      explicitCI=xdrfile;
    }

    g.positionIndices=new int[nI][3];
    g.normalIndices=new int[nI][3];
    int[][] colorIndices;
    if (nC > 0)
      g.colorIndices=new int[nI][3];

    for (int i=0; i < nI; ++i) {
      xdrfile.dimension(3);
      g.positionIndices[i]=xdrfile;
      g.normalIndices[i]=explicitNI != 0 ? xdrfile :
        g.positionIndices[i];
      g.colorIndices[i]=nC > 0 && explicitCI != 0 ? xdrfile :
        g.positionIndices[i];
    }
    int center=xdrfile;
    int material=xdrfile;
    triangleGroup(center,material,g);
  }

  void readPixel() {
    pixel P;
    P.point=xdrfile;
    P.width=xdrfile;
    int material=xdrfile;
    pixel(material,P);
  }

  void process()
  {
    static bool processed;
    if(processed) return;

    while (!eof(xdrfile))
      {
        int ty=getType();
        if(ty == v3dtypes.header)
          {
            hasCameraInfo=true;
            info=processHeader();
          }
        else if(ty == v3dtypes.material)
          {
            materials.push(readMaterial());
          }
        else if(ty == v3dtypes.bezierPatch)
          {
            readBezierPatch();
          }
        else if(ty == v3dtypes.bezierTriangle)
          {
            readBezierTriangle();
          }
        else if(ty == v3dtypes.bezierPatchColor)
          {
            readBezierPatchColor();
          }
        else if(ty == v3dtypes.bezierTriangleColor)
          {
            readBezierTriangleColor();
          }
        else if(ty == v3dtypes.quad)
          {
            readPolygon(4);
          }
        else if(ty == v3dtypes.quadColor)
          {
            readPolygonColor(4);
          }
        else if(ty == v3dtypes.triangle)
          {
            readPolygon(3);
          }
        else if(ty == v3dtypes.triangleColor)
          {
            readPolygonColor(3);
          }
        else if(ty == v3dtypes.sphere)
          {
            readSphere();
          }
        else if(ty == v3dtypes.halfSphere)
          {
            readHemisphere();
          }
        else if(ty == v3dtypes.cylinder)
          {
            readCylinder();
          }
        else if(ty == v3dtypes.tube)
          {
            readTube();
          }
        else if(ty == v3dtypes.disk)
          {
            readDisk();
          }
        else if(ty == v3dtypes.curve)
          {
            readCurve();
          }
        else if(ty == v3dtypes.line)
          {
            readLine();
          }
        else if(ty == v3dtypes.triangles)
          {
            readTriangles();
          }
        else if(ty == v3dtypes.centers)
          {
            centers=readCenters();
          }
        else if(ty == v3dtypes.pixel)
          {
            readPixel();
          }
        else
          {
            //  abort("Unknown type:"+string(ty));
          }
      }
    processed=true;
  }
}

void importv3d(string name)
{
  if(name == stripextension(name)) name += ".v3d";
  v3dfile xf=v3dfile(name);
  xf.process();
  xf.setCameraInfo();

  for(int c=0; c < xf.surfaces.length; ++c) {
    triple center=c > 0 ? xf.centers[c-1] : O;
    render r=render(interaction(c == 0 ? Embedded : Billboard,center=center));
    surface[][] S=xf.surfaces[c];
    for(int m=0; m < S.length; ++m)
      if(S.initialized(m))
        draw(S[m],xf.materials[m],r);
  }

  for(int c=0; c < xf.paths3.length; ++c) {
    triple center=c > 0 ? xf.centers[c-1] : O;
    render r=render(interaction(c == 0 ? Embedded : Billboard,center=center));
    path3[][] G=xf.paths3[c];
    for(int m=0; m < G.length; ++m)
      if(G.initialized(m)) {
        material material=material(xf.materials[m]);
        material.p[0] += thin();
        draw(G[m],material,currentlight,r);
      }
  }

  for(int c=0;c < xf.triangles.length; ++c) {
    triple center=c > 0 ? xf.centers[c-1] : O;
    render r=render(interaction(c == 0 ? Embedded : Billboard,center=center));
    triangleGroup[][] groups=xf.triangles[c];
    for(int m=0; m < groups.length; ++m) {
      if(groups.initialized(m)) {
        material material=xf.materials[m];
        triangleGroup[] triangleGroups=groups[m];
        for(triangleGroup g : triangleGroups) {
          if(g.colors.length > 0)
            draw(g.positions,g.positionIndices,g.normals,g.normalIndices,
                 g.colors,g.colorIndices,r);
          else
            draw(g.positions,g.positionIndices,g.normals,g.normalIndices,
                 material,r);
        }
      }
    }
  }

  for(int m=0; m < xf.pixels.length; ++m) {
    if(xf.pixels.initialized(m)) {
      material material=xf.materials[m];
      pixel[] pixels=xf.pixels[m];
      for(pixel P : pixels)
        pixel(P.point,material.p[0],P.width);
    }
  }
}
