/*
* Copyright (c) 2017, BSC (Barcelona Supercomputing Center)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of the <organization> nor the
*       names of its contributors may be used to endorse or promote products
*       derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY BSC ''AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL <copyright holder> BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include <stdlib.h>
#include <unistd.h>

// General definitions
#include "matmul.h"
// General definitions which are required to build the FPGA accelerators
#include "matmul.fpga.h"

#pragma omp task device(smp) no_copy_deps
#pragma omp task out([BSIZE*BSIZE]v)
void setBlock (elem_t* v, const elem_t val) {
   for (unsigned int i = 0; i < BSIZE*BSIZE; ++i) {
      v[i] = val;
   }
}

#pragma omp task device(smp) no_copy_deps
#pragma omp task in([BSIZE*BSIZE]v)
void checkBlock (unsigned int * check_ok, elem_t* v, const elem_t val, const float threshold )
{
   const elem_t maxv = val * (1.0 + (val < 0 ? -threshold : threshold));
   const elem_t minv = val * (1.0 - (val < 0 ? -threshold : threshold));
   for (unsigned int i = 0; i < BSIZE*BSIZE && ( *check_ok ); ++i) {
      elem_t tmp = v[i];
      if (tmp > maxv || tmp < minv) {
         *check_ok = FALSE;
         fprintf(stderr, "ERROR:\t Expected a %lf but found %lf.\n", (double)val, (double)tmp);
      }
   }
}

#pragma omp target device(fpga) copy_deps num_instances(1)
#pragma omp task in([BSIZE*BSIZE]a, [BSIZE*BSIZE]b) inout([BSIZE*BSIZE]c)
void matmulBlock(elem_t *a, elem_t *b, elem_t *c) {
   unsigned int i, j, k;

   for (i = 0; i < BSIZE; i++) {
      for (j = 0; j < BSIZE; j++) {
         elem_t sum = c[i*BSIZE + j];
         for (k = 0; k < BSIZE; k++) {
            sum += a[i*BSIZE + k] * b[k*BSIZE + j];
         }
         c[i*BSIZE + j] = sum;
      }
   }
}


int main(int argc, char** argv) {
   if (argc < 2) {
      usage(argv[0]);
      exit(1);
   }

   unsigned int const b2size = BSIZE*BSIZE;
   unsigned int const msize = atoi(argv[1]);
   unsigned int const m2size = msize*msize;
   unsigned char const check = argc > 2 ? atoi(argv[2]) : 1;
   if (msize%BSIZE != 0) {
      fprintf(stderr, "ERROR:\t<matrix size> must be multiple of <block size>\n");
      usage(argv[0]);
      exit(1);
   }

   size_t s = m2size*sizeof(elem_t);
   elem_t* a = (elem_t *)(malloc(s));
   elem_t* b = (elem_t *)(malloc(s));
   elem_t* c = (elem_t *)(malloc(s));

   if (a == NULL || b == NULL || c == NULL) {
      fprintf(stderr, "ERROR:\tCannot allocate memory for the matrices\n");
      exit(1);
   }


   for (unsigned int i = 0; i < m2size/b2size; i++) {
      setBlock(&a[i*b2size], (elem_t)VAL_A + i);
      setBlock(&b[i*b2size], (elem_t)VAL_B - i);
      setBlock(&c[i*b2size], (elem_t)VAL_C);
   }

   #pragma omp taskwait
   double t_start = wall_time();

   for (unsigned int i = 0; i < msize/BSIZE; i++) {
//NOTE: Assuming that the following task will be executed in a shared memory environment.
//      Otherwise, it must define the input and output data of child tasks.
{
      for (unsigned int j = 0; j < msize/BSIZE; j++) {
         unsigned int const ci = j*b2size + i*BSIZE*msize;
         for (unsigned int k = 0; k < msize/BSIZE; k++) {
            unsigned int const ai = k*b2size + i*BSIZE*msize;
            unsigned int const bi = j*b2size + k*BSIZE*msize;
            matmulBlock(&a[ai], &b[bi], &c[ci]);
         }
      }
}
   }

   #pragma omp taskwait
   double t_end = wall_time();

   unsigned int check_ok = TRUE;
   if (check) {
      printf( "=================== CHECKING ===================== \n" );
      for (unsigned int i = 0; i < msize/BSIZE && check_ok; i++) {
         for (unsigned int j = 0; j < msize/BSIZE && check_ok; j++) {
            elem_t val = VAL_C;
            for (unsigned int k = 0; k < msize/BSIZE; k++) {
               unsigned int const ai = k*b2size + i*BSIZE*msize;
               unsigned int const bi = j*b2size + k*BSIZE*msize;
               val += a[ai]*b[bi]*BSIZE;
            }
            unsigned int const ci = j*b2size + i*BSIZE*msize;
            checkBlock(&check_ok, &c[ci], val, THRESHOLD);
         }
      }
   }

   #pragma omp taskwait

   if (check) {
      if (check_ok) {
         printf( "Output matrix is OK!\n" );
      } else {
         printf( "Output matrix is Wrong!\n" );
      }
      printf( "================================================== \n" );
   }

   free(a);
   free(b);
   free(c);

   printf( "==================== RESULTS ===================== \n" );
   printf( "  Benchmark: %s (%s)\n", "Matmul", "OmpSs" );
   printf( "  Elements type: %s\n", ELEM_T_STR );
   printf( "  Execution time (secs): %f\n", t_end - t_start );
   printf( "================================================== \n" );

}

