#include <amscudafractallevelset/amscudafractallevelset.cuh>

namespace amscuda
{
namespace fractallevelset
{
    marchingtet_pars::marchingtet_pars()
    {

    }

    __device__ __host__ void mtgetcube(const marchingtet_pars &mtpars, int64_t I, mtcube &cubeout)
    {
        int Ix,Iy,Iz;
        float dx,dy,dz;
        Ix = (int)(I%mtpars.Nx);
        Iy = (int)((I/mtpars.Nx)%mtpars.Ny);
        Iz = (int)((I/(mtpars.Nx*mtpars.Ny)) % mtpars.Nz);

        dx = (mtpars.xyz_max[0]-mtpars.xyz_min[0])/((float)mtpars.Nx);
        dy = (mtpars.xyz_max[1]-mtpars.xyz_min[1])/((float)mtpars.Ny);
        dz = (mtpars.xyz_max[2]-mtpars.xyz_min[2])/((float)mtpars.Nz);
        
        cubeout.xyz_min = cuvec3f(mtpars.xyz_min[0] + Ix*dx, mtpars.xyz_min[1] + Iy*dy, mtpars.xyz_min[2] + Iz*dz);
        cubeout.xyz_max = cuvec3f(mtpars.xyz_min[0] + (Ix+1)*dx, mtpars.xyz_min[1] + (Iy+1)*dy, mtpars.xyz_min[2] + (Iz+1)*dz);
    }


    __global__ void marchingtets_gpu_tricount(marchingtet_pars mtpars, int *countbuffer)
    {
        int64_t psize = (int64_t)mtpars.Nx*(int64_t)mtpars.Ny*(int64_t)mtpars.Nz;
        int64_t nt = blockDim.x*gridDim.x;
        int64_t tn = threadIdx.x + blockIdx.x*blockDim.x;
        int64_t I;


        mtcube cube;

        countbuffer[tn] = 0;

        for(I=tn;I<psize;I+=nt)
        {
            mtgetcube(mtpars,I,cube);
            cube.eval_function(mtpars.pars);
            countbuffer[tn] += cube.count_triangles();
        }
    }

    __global__ void marchingtets_gpu_debug(marchingtet_pars mtpars, float *debugbuffer)
    {
        int64_t psize = (int64_t)mtpars.Nx*(int64_t)mtpars.Ny*(int64_t)mtpars.Nz;
        int64_t nt = blockDim.x*gridDim.x;
        int64_t tn = threadIdx.x + blockIdx.x*blockDim.x;
        int64_t I;
        int J;


        mtcube cube;

        for(J=0;J<15;J++)
        debugbuffer[tn*15+J] = 0;

        for(I=tn;I<psize;I+=nt)
        {
            mtgetcube(mtpars,I,cube);
            cube.eval_function(mtpars.pars);
            
            // debugbuffer[tn*15+0] = cube.xyz_min[0];
            // debugbuffer[tn*15+1] = cube.xyz_min[1];
            // debugbuffer[tn*15+2] = cube.xyz_min[2];
            // debugbuffer[tn*15+3] = cube.xyz_max[0];
            // debugbuffer[tn*15+4] = cube.xyz_max[1];
            // debugbuffer[tn*15+5] = cube.xyz_max[2];
            // debugbuffer[tn*15+6] = mtpars.pars.threshold;
            // debugbuffer[tn*15+7] = mtpars.Nx;
            // debugbuffer[tn*15+8] = I;
            // debugbuffer[tn*15+9] = psize;
            for(J=0;J<15;J++)
            {
                debugbuffer[tn*15+J] = cube.fevals[J];
            }
            

            
        }
    }

    __global__ void marchingtets_gpu_meshfill(marchingtet_pars mtpars, const int64_t *offsetbuffer, float *vertexbuffer)
    {
        int64_t psize = (int64_t)mtpars.Nx*(int64_t)mtpars.Ny*(int64_t)mtpars.Nz;
        int64_t nt = blockDim.x*gridDim.x;
        int64_t tn = threadIdx.x + blockIdx.x*blockDim.x;
        int64_t I;

        mtcube cube;

        int64_t offset = offsetbuffer[tn];

        for(I=tn;I<psize;I+=nt)
        {
            mtgetcube(mtpars,I,cube);
            cube.eval_function(mtpars.pars);
            cube.fill_trianglebuffer(vertexbuffer,offset); //this should be incrementing the offset
        }
    }

    __global__ void marchingtets_gpu_debug2(marchingtet_pars mtpars, float *outbuffer)
    {
        int64_t psize = (int64_t)mtpars.Nx*(int64_t)mtpars.Ny*(int64_t)mtpars.Nz;
        int64_t nt = blockDim.x*gridDim.x;
        int64_t tn = threadIdx.x + blockIdx.x*blockDim.x;
        int64_t I;
        int Ix,Iy,Iz;


        mtcube cube;

        for(I=tn;I<psize;I+=nt)
        {
            Ix = I%(mtpars.Nx);
            Iy = (I/(mtpars.Nx))%mtpars.Ny;
            Iz = (I/(mtpars.Nx*mtpars.Ny))%mtpars.Nz;

            if(Iz==(mtpars.Nz/2))
            {
                mtgetcube(mtpars,I,cube);
                cube.eval_function(mtpars.pars);
                outbuffer[Ix + Iy*mtpars.Nx] = cube.fevals[8];
            }
        }
    }

    __global__ void marchingtets_gpu_areacount(marchingtet_pars mtpars, float *areabuffer)
    {
        int64_t psize = (int64_t)mtpars.Nx*(int64_t)mtpars.Ny*(int64_t)mtpars.Nz;
        int64_t nt = blockDim.x*gridDim.x;
        int64_t tn = threadIdx.x + blockIdx.x*blockDim.x;
        int64_t I;

        mtcube cube;

        areabuffer[tn] = 0.0f;

        for(I=tn;I<psize;I+=nt)
        {
            mtgetcube(mtpars,I,cube);
            cube.eval_function(mtpars.pars);
            areabuffer[tn] += cube.count_triarea();
        }
    }




    void marchingtets_gpu_cleanup(int **dtricountbuffer, int **htricountbuffer, float **dvertexbuffer, int64_t **doffsetbuffer, int64_t **hoffsetbuffer)
    {
        if(*dtricountbuffer!=NULL) {cudaFree(*dtricountbuffer); *dtricountbuffer=NULL;}
        if(*htricountbuffer!=NULL) {delete[] *htricountbuffer; *htricountbuffer=NULL;}
        if(*doffsetbuffer!=NULL) {cudaFree(*doffsetbuffer); *doffsetbuffer=NULL;}
        if(*hoffsetbuffer!=NULL) {delete[] *hoffsetbuffer; *hoffsetbuffer=NULL;}
        if(*dvertexbuffer!=NULL) {cudaFree(*dvertexbuffer); *dvertexbuffer=NULL;}
        //if(*dmtpars!=NULL) {cudaFree(*dmtpars); *dmtpars=NULL;}
    }

    int64_t mt_count_tris_and_accum_offset(
        int *htricountbuffer,
        int *dtricountbuffer,
        int64_t *hoffsetbuffer,
        int64_t *doffsetbuffer,
        int nblocks, int nthreads
    )
    {
        int I;
        int64_t cnt;
        cudaMemcpy(htricountbuffer,dtricountbuffer,sizeof(int)*nblocks*nthreads,cudaMemcpyDeviceToHost);
        cnt = 0;
        for(I=0;I<nblocks*nthreads;I++)
        {
            hoffsetbuffer[I] = cnt*9;
            cnt += htricountbuffer[I];

            //printf("debug: hoffsetbuffer[%d]=%ld\n",I,hoffsetbuffer[I]);
        }

        cudaMemcpy(doffsetbuffer,hoffsetbuffer,sizeof(int64_t)*nblocks*nthreads,cudaMemcpyHostToDevice);

        return cnt;
    }

    void mt_debug1(float *debugbuffer, float *ddebugbuffer, int nblocks, int nthreads)
    {
        int I;
        cudaMemcpy(debugbuffer,ddebugbuffer,sizeof(float)*nblocks*nthreads,cudaMemcpyDeviceToHost);
        for(I=0;I<nblocks*nthreads;I++)
        {
            printf("debug buffer [%d]: %1.3f\n",I,debugbuffer[I]);
        }
        return;
    }

    void marchingtets_gpu(marchingtet_pars mtpars, bagoftriangles *mesh, int nblockdiv, int nthreads, int verbose)
    {
        //marchingtet_pars *dmtpars = NULL;
        int *dtricountbuffer = NULL;
        float *dvertexbuffer = NULL;
        int64_t *hoffsetbuffer = NULL;
        int64_t *doffsetbuffer = NULL;
        int nblocks;

        int64_t psize = (int64_t)mtpars.Nx*(int64_t)mtpars.Ny*(int64_t)mtpars.Nz;
        int *htricountbuffer = NULL;
        int64_t tricount;
        
        nblocks = (int)(((psize/(int64_t)nthreads)+1L)/(int64_t)nblockdiv+1L);

        cudaMalloc(&dtricountbuffer, nblocks*nthreads*sizeof(int));
        htricountbuffer = new(std::nothrow) int[nblocks*nthreads];
        cudaMalloc(&doffsetbuffer, nblocks*nthreads*sizeof(int64_t));
        hoffsetbuffer = new(std::nothrow) int64_t[nblocks*nthreads];

        //DEBUG
        // float *debugbuffer = NULL;
        // float *ddebugbuffer = NULL;
        // debugbuffer = new(std::nothrow) float[nblocks*nthreads*25];
        // cudaMalloc(&ddebugbuffer,sizeof(float)*nblocks*nthreads*25);


        if(dtricountbuffer==NULL || htricountbuffer==NULL)
        {
            printf("marchingtets_gpu error: failed to allocate counting buffers\n");
            marchingtets_gpu_cleanup(&dtricountbuffer,&htricountbuffer,&dvertexbuffer,&doffsetbuffer,&hoffsetbuffer);
            return;
        }

        // cudaMalloc(&dmtpars,sizeof(marchingtet_pars));
        // cudaMemcpy(dmtpars,&mtpars,sizeof(marchingtet_pars),cudaMemcpyHostToDevice);

        if(verbose>=2)
            printf("debug: launching marchingtets_gpu_tricount with <%d,%d>\n",nblocks,nthreads);
        cudaError_t err = cudaSuccess;
        marchingtets_gpu_tricount<<<nblocks,nthreads>>>(mtpars,dtricountbuffer);
        //marchingtets_gpu_debug<<<nblocks,nthreads>>>(mtpars,ddebugbuffer);
        cudaDeviceSynchronize();
        err = cudaGetLastError();
        if(err!=cudaSuccess)
        {
            printf("marchingtets_gpu_tricount error: %s\n",cudaGetErrorString(err));
        }

        //mt_debug1(debugbuffer,ddebugbuffer,nblocks,nthreads);

        tricount = mt_count_tris_and_accum_offset(htricountbuffer,dtricountbuffer,hoffsetbuffer,doffsetbuffer,nblocks,nthreads);

        if(verbose>=1)
            printf("marchingtets_gpu: triangle count = %ld\n",(long)tricount);

        mesh->vertices.resize(tricount*9);
        cudaMalloc(&dvertexbuffer,sizeof(float)*tricount*9);

        marchingtets_gpu_meshfill<<<nblocks,nthreads>>>(mtpars,doffsetbuffer,dvertexbuffer);
        cudaDeviceSynchronize();
        err = cudaGetLastError();
        if(err!=cudaSuccess)
        {
            printf("marchingtets_gpu_meshfill error: %s\n",cudaGetErrorString(err));
        }

        //DEBUG
        // int I;
        // for(I=0;I<tricount*3;I++)
        // {
        //     printf("vertex[%d]: (%1.3f, %1.3f, %1.3f)\n",I,
        //         mesh->vertices[I*3+0],mesh->vertices[I*3+1],mesh->vertices[I*3+2]);
        // }

        cudaMemcpy(mesh->vertices.data,dvertexbuffer,sizeof(float)*tricount*9,cudaMemcpyDeviceToHost);

        //DEBUG
        // delete[] debugbuffer; debugbuffer=NULL;
        // cudaFree(ddebugbuffer); ddebugbuffer = NULL;

        marchingtets_gpu_cleanup(&dtricountbuffer,&htricountbuffer,&dvertexbuffer,&doffsetbuffer,&hoffsetbuffer);
        return;
    }

    int64_t marchingtets_tricount_gpu(marchingtet_pars mtpars, int nblockdiv, int nthreads, int verbose)
    {
        //marchingtet_pars *dmtpars = NULL;
        int *dtricountbuffer = NULL;
        int64_t *hoffsetbuffer = NULL;
        int64_t *doffsetbuffer = NULL;
        float *dvertexbuffer = NULL;
        int nblocks;

        int64_t psize = (int64_t)mtpars.Nx*(int64_t)mtpars.Ny*(int64_t)mtpars.Nz;
        int *htricountbuffer = NULL;
        int64_t tricount = 0;
        
        nblocks = (int)(((psize/(int64_t)nthreads)+1L)/(int64_t)nblockdiv+1L);

        cudaMalloc(&dtricountbuffer, nblocks*nthreads*sizeof(int));
        htricountbuffer = new(std::nothrow) int[nblocks*nthreads];
        cudaMalloc(&doffsetbuffer, nblocks*nthreads*sizeof(int64_t));
        hoffsetbuffer = new(std::nothrow) int64_t[nblocks*nthreads];

        //DEBUG
        // float *debugbuffer = NULL;
        // float *ddebugbuffer = NULL;
        // debugbuffer = new(std::nothrow) float[nblocks*nthreads*25];
        // cudaMalloc(&ddebugbuffer,sizeof(float)*nblocks*nthreads*25);


        if(dtricountbuffer==NULL || htricountbuffer==NULL)
        {
            printf("marchingtets_gpu error: failed to allocate counting buffers\n");
            marchingtets_gpu_cleanup(&dtricountbuffer,&htricountbuffer,&dvertexbuffer,&doffsetbuffer,&hoffsetbuffer);
            return 0;
        }

        // cudaMalloc(&dmtpars,sizeof(marchingtet_pars));
        // cudaMemcpy(dmtpars,&mtpars,sizeof(marchingtet_pars),cudaMemcpyHostToDevice);

        //printf("debug: launching marchingtets_gpu_tricount with <%d,%d>\n",nblocks,nthreads);
        cudaError_t err = cudaSuccess;
        marchingtets_gpu_tricount<<<nblocks,nthreads>>>(mtpars,dtricountbuffer);
        cudaDeviceSynchronize();
        err = cudaGetLastError();
        if(err!=cudaSuccess)
        {
            printf("marchingtets_gpu_tricount error: %s\n",cudaGetErrorString(err));
        }

        //mt_debug1(debugbuffer,ddebugbuffer,nblocks,nthreads);

        tricount = mt_count_tris_and_accum_offset(htricountbuffer,dtricountbuffer,hoffsetbuffer,doffsetbuffer,nblocks,nthreads);

        if(verbose>=1)
            printf("marchingtets_gpu_tricount: triangle count = %ld\n",(long)tricount);

        marchingtets_gpu_cleanup(&dtricountbuffer,&htricountbuffer,&dvertexbuffer,&doffsetbuffer,&hoffsetbuffer);
        

        return tricount;
    }


    void marchingtets_area_gpu_cleanup(
        float **hareabuffer,
        float **dareabuffer
    )
    {
        if(*dareabuffer!=NULL) {cudaFree(*dareabuffer); *dareabuffer=NULL;}
        if(*hareabuffer!=NULL) {delete[] *hareabuffer; *hareabuffer=NULL;}
    }

    float marchingtets_area_gpu(marchingtet_pars mtpars, int nblockdiv, int nthreads, int verbose)
    {
        //marchingtet_pars *dmtpars = NULL;
        int nblocks;
        float *hareabuffer = NULL;
        float *dareabuffer = NULL;
        int I;
        float areasum = 0.0f;
        int64_t psize = (int64_t)mtpars.Nx*(int64_t)mtpars.Ny*(int64_t)mtpars.Nz;
        
        nblocks = (int)(((psize/(int64_t)nthreads)+1L)/(int64_t)nblockdiv+1L);

        cudaMalloc(&dareabuffer, nblocks*nthreads*sizeof(float));
        hareabuffer = new(std::nothrow) float[nblocks*nthreads];


        //DEBUG
        // float *debugbuffer = NULL;
        // float *ddebugbuffer = NULL;
        // debugbuffer = new(std::nothrow) float[nblocks*nthreads*25];
        // cudaMalloc(&ddebugbuffer,sizeof(float)*nblocks*nthreads*25);


        if(dareabuffer==NULL || hareabuffer==NULL)
        {
            printf("marchingtets_gpu error: failed to allocate counting buffers\n");
            marchingtets_area_gpu_cleanup(&hareabuffer,&dareabuffer);
            return 0.0f;
        }
        
        marchingtets_gpu_areacount<<<nblocks,nthreads>>>(mtpars,dareabuffer);
        //printf("debug: launching marchingtets_gpu_areacount with <%d,%d>\n",nblocks,nthreads);
        cudaError_t err = cudaSuccess;
        cudaDeviceSynchronize();
        err = cudaGetLastError();
        if(err!=cudaSuccess)
        {
            printf("marchingtets_gpu_areacount error: %s\n",cudaGetErrorString(err));
        }

        cudaMemcpy(hareabuffer,dareabuffer,nblocks*nthreads*sizeof(float),cudaMemcpyDeviceToHost);
        for(I=0;I<nblocks*nthreads;I++)
        {
            areasum += hareabuffer[I];
        }   
    
        //DEBUG
        // int I;
        // for(I=0;I<tricount*3;I++)
        // {
        //     printf("vertex[%d]: (%1.3f, %1.3f, %1.3f)\n",I,
        //         mesh->vertices[I*3+0],mesh->vertices[I*3+1],mesh->vertices[I*3+2]);
        // }


        //DEBUG
        // delete[] debugbuffer; debugbuffer=NULL;
        // cudaFree(ddebugbuffer); ddebugbuffer = NULL;

        marchingtets_area_gpu_cleanup(&hareabuffer,&dareabuffer);
        return areasum;
    }

    void marchingtets_cpu_tricount(int thread, int nthreads, marchingtet_pars mtpars, int *countbuffer)
    {


    }

    void marchingtets_cpu_meshfill(int thread, int nthreads, marchingtet_pars mtpars, float *vertexbuffer)
    {


    }

    void marchingtets_cpu_areacount(int thread, int nthreads, marchingtet_pars mtpars, float *areabuffer)
    {

    }

    void marchingtets_cpu(marchingtet_pars mtpars, bagoftriangles *mesh, int maxthreads)
    {


    }

    void test_marchingtets()
    {
        const char *fnameout = "./testout2.stl";
        bagoftriangles mesh;
        main_functionpars fpars;
        fpars.fractaliter = 64;
        fpars.threshold = 2.0f;
        fpars.freezethresh = 10.0f;

        marchingtet_pars mtpars;
        mtpars.xyz_min = cuvec3f(-1.2,-1.2,-1.2);
        mtpars.xyz_max = cuvec3f(1.2,1.2,1.2);
        mtpars.Nx = 125;
        mtpars.Ny = 125;
        mtpars.Nz = 125;
        mtpars.pars = fpars;

        float t0,t1, area;

        t0 = util::time_msec();
        marchingtets_gpu(mtpars,&mesh);
        t1 = util::time_msec();

        printf("marchingtets_gpu: %1.3f msec runtime %d psize\n",t1-t0,mtpars.Nx*mtpars.Ny*mtpars.Nz);

        t0 = util::time_msec();
        area = marchingtets_area_gpu(mtpars);
        t1 = util::time_msec();

        printf("marchingtets_area_gpu: %1.3f msec runtime %d psize\n",t1-t0,mtpars.Nx*mtpars.Ny*mtpars.Nz);
        printf("area=%1.3f, area/4pi=%1.3f\n",area,area/4.0f/pif);

        mesh.save_stl(fnameout);

    }
    


    void hausdorff_test1a(int fractaliter, int N, float &deltat, float &area, float &narea)
    {
        main_functionpars fpars;
        fpars.fractaliter = fractaliter;
        fpars.threshold = 2.0f;
        fpars.freezethresh = 10.0f;

        marchingtet_pars mtpars;
        mtpars.xyz_min = cuvec3f(-1.2,-1.2,-1.2);
        mtpars.xyz_max = cuvec3f(1.2,1.2,1.2);
        mtpars.Nx = N;
        mtpars.Ny = N;
        mtpars.Nz = N;
    
        mtpars.pars = fpars;

        float t0,t1;

        float dx = (mtpars.xyz_max[0]-mtpars.xyz_min[0])/((float)N);

        //printf("debug: dA=%1.3f\n",dx*dx);

        t0 = util::time_msec();
        area = marchingtets_area_gpu(mtpars);
        t1 = util::time_msec();
        narea = area/dx/dx;

        deltat=t1-t0;

        // printf("marchingtets_area_gpu: %1.3f msec runtime %d psize\n",t1-t0,mtpars.Nx*mtpars.Ny*mtpars.Nz);
        // printf("area=%1.3f, area/4pi=%1.3f\n",area,area/4.0f/pif);
    }

    void hausdorff_test()
    {
        int fiters[] = {32,64,128,256};
        //int fiter;
        int N = 32;

        int I,J;

        float deltat,area,narea;

        for(I=0;I<4;I++)
        {
            N=32;
            for(J=0;J<5;J++)
            {
                hausdorff_test1a(fiters[I],N,deltat,area,narea);
                printf("%d,%d,%1.6g,%1.6g,%1.6g\n",fiters[I],N,deltat,area,narea);
                N*=2;
            }
            printf("\n");
        }

    }

};//end namespace fractallevelset
};// end namespace amscuda
