#include <iostream>
#include <string>
#include <memory>
#include <stdlib.h>

#define TINYOBJLOADER_IMPLEMENTATION
#include "tiny_obj_loader.h"

#include "Image.h"
#include "Triangle.h"

// This allows you to skip the `std::` in front of C++ standard library
// functions. You can also say `using std::cout` to be more selective.
// You should never do this in a header file.
using namespace std;

int main(int argc, char **argv)
{
	if(argc < 6) {
		cout << "Usage: A1 meshfile image width height colormode" << endl;
		return 0;
	}

  srand(743298);
	string meshName(argv[1]);
  string filename(argv[2]);
  int width = atoi(argv[3]);
  int height = atoi(argv[4]);
  int colormode = atoi(argv[5]);

	// Load geometry
	vector<float> posBuf; // list of vertex positions
	
  tinyobj::attrib_t attrib;
	std::vector<tinyobj::shape_t> shapes;
	std::vector<tinyobj::material_t> materials;
	string errStr;
	
  bool rc = tinyobj::LoadObj(&attrib, &shapes, &materials, &errStr, meshName.c_str());
	
  if(!rc) {
		cerr << errStr << endl;
	} else {
		// Some OBJ files have different indices for vertex positions, normals,
		// and texture coordinates. For example, a cube corner vertex may have
		// three different normals. Here, we are going to duplicate all such
		// vertices.
		
    // Loop over shapes
		for(size_t s = 0; s < shapes.size(); s++) {
			
      // Loop over faces (polygons)
			size_t index_offset = 0;
			for(size_t f = 0; f < shapes[s].mesh.num_face_vertices.size(); f++) {
				size_t fv = shapes[s].mesh.num_face_vertices[f];
				
        // Loop over vertices in the face.
				for(size_t v = 0; v < fv; v++) {
					
          // access to vertex
					tinyobj::index_t idx = shapes[s].mesh.indices[index_offset + v];
					posBuf.push_back(attrib.vertices[3*idx.vertex_index+0]);
					posBuf.push_back(attrib.vertices[3*idx.vertex_index+1]);
					posBuf.push_back(attrib.vertices[3*idx.vertex_index+2]);
				
        }
				index_offset += fv;
				
        // per-face material (IGNORE)
				shapes[s].mesh.material_ids[f];
			}
		}
	}
	cout << "Number of vertices: " << posBuf.size()/3 << endl;

  vector<vector<float>> zbuf;

  std::vector<Tri> tris;

  // Split posBuf into triangles.
  for(int i = 0; i < (int)posBuf.size()/9; i++)
  {
          vertex v1;
          v1.x = posBuf[i*9];
          v1.y = posBuf[i*9 + 1];
          v1.z = posBuf[i*9 + 2];
          vertex v2;
          v2.x = posBuf[i*9 + 3];
          v2.y = posBuf[i*9 + 4];
          v2.z = posBuf[i*9 + 5];
          vertex v3;
          v3.x = posBuf[i*9 + 6];
          v3.y = posBuf[i*9 + 7];
          v3.z = posBuf[i*9 + 8];
          Tri t(v1, v2, v3);
          tris.push_back(t);
  }

  // Initialize min/max variables.
  float xmin = posBuf[0];
  float xmax = xmin;
  float ymin = posBuf[1];
  float ymax = ymin;
  float zmin = tris[0].v[0].z;
  float zmax = zmin;

  // First time finding max and min.
  // Also, initializing r,g,b per triangle.
  for(int i = 0; i < (int)tris.size(); i++)
  { 
          for(int j = 0; j < (int)tris[i].v.size(); j++)
          {

                  if(xmin > tris[i].v[j].x)
                          xmin = tris[i].v[j].x;
                  if(xmax < tris[i].v[j].x)
                          xmax = tris[i].v[j].x;
                  if(ymin > tris[i].v[j].y)
                          ymin = tris[i].v[j].y;
                  if(ymax < tris[i].v[j].y)
                          ymax = tris[i].v[j].y;
                  if(zmin > tris[i].v[j].z)
                          zmin = tris[i].v[j].z;
                  if(zmax < tris[i].v[j].z)
                          zmax = tris[i].v[j].z;
                  
                  int temp1 = rand()%255;
                  int temp2 = rand()%255;
                  int temp3 = rand()%255;
                  tris[i].v[j].r = temp1;
                  tris[i].v[j].g = temp2;
                  tris[i].v[j].b = temp3;
          }
  }

  unsigned char r = rand()%255;
  unsigned char b = rand()%255;
  unsigned char g = rand()%255;


	// Create the image. We're using a `shared_ptr`, a C++11 feature.
	auto image = make_shared<Image>(width, height);

  // Find scaling ratio.
  float xratio = (1.0*width/(xmax - xmin));
  float yratio = (1.0*height/(ymax - ymin));
  float zratio = (255.0/(zmax-zmin));

  float aratio = 1.0;

  // Determine and assign smaller ratio.
  if(xratio < yratio)
  {
          aratio = xratio;
  }
  else
  {
          aratio = yratio;
  }


  for(int i = 0; i < (int)tris.size(); i++)
  {
          for(int j = 0; j < (int)tris[i].v.size(); j++)
          {
                  tris[i].v[j].x *= aratio;
                  tris[i].v[j].y *= aratio;
                  tris[i].v[j].z *= zratio;
          }
  }
  
  xmin = tris[0].v[0].x;
  xmax = xmin;
  ymin = tris[0].v[0].y;
  ymax = ymin;
  zmin = tris[0].v[0].z;
  zmax = zmin;

  for(int i = 0; i < (int)tris.size(); i++)
  { 

          for(int j = 0; j < (int)tris[i].v.size(); j++)
          {

                  if(xmin > tris[i].v[j].x)
                          xmin = tris[i].v[j].x;
                  if(xmax < tris[i].v[j].x)
                          xmax = tris[i].v[j].x;
                  if(ymin > tris[i].v[j].y)
                          ymin = tris[i].v[j].y;
                  if(ymax < tris[i].v[j].y)
                          ymax = tris[i].v[j].y;
                  if(zmin > tris[i].v[j].z)
                          zmin = tris[i].v[j].z;
                  if(zmax < tris[i].v[j].z)
                          zmax = tris[i].v[j].z;
          }
  }
  
  float xoffset = ((width)/2 - (xmax + xmin)/2);
  float yoffset = ((height)/2 - (ymax + ymin)/2);
  float zoffset = zmin;

  for(int i = 0; i < (int)tris.size(); i++)
  {
          for(int j = 0; j < (int)tris[i].v.size();j++)
          {
                  tris[i].v[j].x += xoffset;
                  tris[i].v[j].y += yoffset;
                  tris[i].v[j].z -= zoffset;
          }
  }
  
  xmin = tris[0].v[0].x;
  xmax = xmin;
  ymin = tris[0].v[0].y;
  ymax = ymin;
  zmin = tris[0].v[0].z;
  zmax = zmin;

  for(int i = 0; i < (int)tris.size(); i++)
  { 
          tris[i].xmin = tris[i].v[0].x;
          tris[i].xmax = tris[i].v[0].x;
          tris[i].ymin = tris[i].v[0].y;
          tris[i].ymax = tris[i].v[0].y;

          for(int j = 0; j < (int)tris[i].v.size(); j++)
          {

                  if(xmin > tris[i].v[j].x)
                          xmin = tris[i].v[j].x;
                  if(xmax < tris[i].v[j].x)
                          xmax = tris[i].v[j].x;
                  if(ymin > tris[i].v[j].y)
                          ymin = tris[i].v[j].y;
                  if(ymax < tris[i].v[j].y)
                          ymax = tris[i].v[j].y;
                  if(zmin > tris[i].v[j].z)
                          zmin = tris[i].v[j].z;
                  if(zmax < tris[i].v[j].z)
                          zmax = tris[i].v[j].z;

                  if(tris[i].xmin > tris[i].v[j].x)
                          tris[i].xmin = tris[i].v[j].x;
                  if(tris[i].xmax < tris[i].v[j].x)
                          tris[i].xmax = tris[i].v[j].x;
                  if(tris[i].ymin > tris[i].v[j].y)
                          tris[i].ymin = tris[i].v[j].y;
                  if(tris[i].ymax < tris[i].v[j].y)
                          tris[i].ymax = tris[i].v[j].y;
          }
  }
  

  for(int r = 0; r < width; r++)
  {
          vector<float> temp;
          zbuf.push_back(temp);
          for(int c = 0; c < height; c++)
          {
                  zbuf.at(r).push_back(zmin);
          }
  }

  // Initialize variables for the three
  // Barycentric areas.
  float a1 = 0.0;
  float a2 = 0.0;
  float a3 = 0.0;

  // Initialize variables for the three
  // Barycentric coordinates.
  float l1 = 0.0;
  float l2 = 0.0;
  float l3 = 0.0;

  // Variable to hold current z coordinate.
  float z = 0.0;

  // I had each triangle hold a variable which was its area,
  // Here, I make sure they all have their areas set correctly.
  for(int i = 0; i < (int)tris.size(); i++)
  {
        tris[i].area = findArea(tris[i].v[0], tris[i].v[1], tris[i].v[2]);
  }
  for(int i = 0; i < (int)tris.size(); i++)
  {
          for(int x = tris[i].xmin; x < tris[i].xmax; x++)
          {
                  for(int y = tris[i].ymin; y < tris[i].ymax; y++)
                  {
                          // Temporary vertex so we can utilize findArea.
                          vertex tempv;
                          tempv.x = x;
                          tempv.y = y;

                          a3 = findArea(tris[i].v[0], tris[i].v[1], tempv);
                          a2 = findArea(tris[i].v[0], tempv, tris[i].v[2]);
                          a1 = findArea(tempv, tris[i].v[1], tris[i].v[2]);
                          
                          l1 = a1/tris[i].area;
                          l2 = a2/tris[i].area;
                          l3 = a3/tris[i].area;

		                    	r = (a1*tris[i].v[0].r + a2*tris[i].v[1].r + a3*tris[i].v[2].r)/tris[i].area;
	                    		g = (a1*tris[i].v[0].g + a2*tris[i].v[1].g + a3*tris[i].v[2].g)/tris[i].area;
                    			b = (a1*tris[i].v[0].b + a2*tris[i].v[1].b + a3*tris[i].v[2].b)/tris[i].area;
                          
                          if(colormode == 0)
                          {
                                  if(l1 + l2 + l3 <= 1.01 && zbuf.at(x).at(y) <= z)
                                  {
                                          image->setPixel(x, y, r, g ,b);
                                          zbuf.at(x).at(y) = z;
                                  }
                          }

                          float z1 = tris[i].v[0].z;
                          float z2 = tris[i].v[1].z;
                          float z3 = tris[i].v[2].z;

                          z = (a1*z1 + a2*z2 + a3*z3)/tris[i].area;
                          if(colormode == 1)
                          {
                                  if(l1 + l2 + l3 <= 1.01 && zbuf.at(x).at(y) <= z)
                                  {
                                          image->setPixel(x, y, z, 0, 0);
                                          zbuf.at(x).at(y) = z;
                                  }
                          }
                          else if(colormode == 2)
                          {
                                  if(l1 + l2 + l3 <= 1.01 && zbuf.at(x).at(y) <= z)
                                  {
                                          image->setPixel(x, y, 0, (255/(ymax - ymin)) - (y - ymin)*(255/(ymax - ymin)), (y - ymin)*(255/(ymax - ymin)));
                                          zbuf.at(x).at(y) = z;
                                  }
                          }
                  }
          }
  }

	// Write image to file
	image->writeToFile(filename);
	return 0;
}
