/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
#pragma once
#ifndef STENCIL_HPP
#define STENCIL_HPP

#include <algorithm>
#include <cstdio>
#include <functional>
#include <future>
#include <iostream>
#include <utility>
#include <vector>

#include <ff/ff.hpp>
// importand order in includes
#include <ff/map.hpp>
#include <ff/parallel_for.hpp>

#include "task.hpp"
#include "threadPool.hpp"

template <typename T> class Stencil : public ff::ff_Map<Task<T>> {
  public:
    Stencil(std::function<T(std::vector<T>)> convolution,
            std::vector<std::pair<int, int>> neighborhood, int iterations);
    Stencil(std::function<T(std::vector<T>)> convolution,
            std::vector<std::pair<int, int>> neighborhood, int iterations,
            int maxworkers);
    Task<T> *svc(Task<T> *);
    std::vector<std::vector<T>> *operator()(std::vector<std::vector<T>> *matrix,
                                            int iterations);
    void stdthread(std::vector<std::shared_future<Task<T> *>> *ResultsVector,
                   std::vector<std::promise<Task<T> *> *> *OutputVector);
    void sequential(std::vector<std::shared_future<Task<T> *>> *ResultsVector,
                    std::vector<std::promise<Task<T> *> *> *OutputVector);

  private:
    Task<T> *svc_helper(Task<T> *t, int iterations);
    void constructor_helper(std::vector<std::pair<int, int>> neighborhood);

    std::function<T(std::vector<T>)> Convolution;
    std::vector<std::pair<int, int>> Neighborhood;
    int Iterations = 0;
    std::vector<int> Borders{0, 0, 0, 0}; // left, top, right, bottom
    int MaxWorkers = 0;
};

template <typename T>
Stencil<T>::Stencil(std::function<T(std::vector<T>)> convolution,
                    std::vector<std::pair<int, int>> neighborhood,
                    int iterations)
    : ff::ff_Map<Task<T>>(), Convolution(convolution), Neighborhood({{0, 0}}),
      Iterations(iterations) {
    constructor_helper(neighborhood);
}

template <typename T>
Stencil<T>::Stencil(std::function<T(std::vector<T>)> convolution,
                    std::vector<std::pair<int, int>> neighborhood,
                    int iterations, int maxworkers)
    : ff::ff_Map<Task<T>>(maxworkers), Convolution(convolution),
      Neighborhood({{0, 0}}), Iterations(iterations), MaxWorkers(maxworkers) {
    constructor_helper(neighborhood);
}

template <typename T>
void Stencil<T>::constructor_helper(
    std::vector<std::pair<int, int>> neighborhood) {
    // copies neighborhood and adds the default element (0,0)
    Neighborhood.insert(Neighborhood.end(), neighborhood.begin(),
                        neighborhood.end());

    // finds the boundaries of the neighborhood
    auto frst = [](std::pair<int, int> a, std::pair<int, int> b) {
        return a.first < b.first;
    };
    auto scnd = [](std::pair<int, int> a, std::pair<int, int> b) {
        return a.second < b.second;
    };

    if (neighborhood.size() > 0) {
        Borders[0] =
            std::abs(std::min(0, (*std::min_element(Neighborhood.begin(),
                                                    Neighborhood.end(), frst))
                                     .first));
        Borders[1] =
            std::abs(std::min(0, (*std::min_element(Neighborhood.begin(),
                                                    Neighborhood.end(), scnd))
                                     .second));
        Borders[2] =
            std::abs(std::max(0, (*std::max_element(Neighborhood.begin(),
                                                    Neighborhood.end(), frst))
                                     .first));
        Borders[3] =
            std::abs(std::max(0, (*std::max_element(Neighborhood.begin(),
                                                    Neighborhood.end(), scnd))
                                     .second));
    }
}

// svc function for fastflow library
template <typename T> Task<T> *Stencil<T>::svc(Task<T> *task) {
    task = svc_helper(task, this->Iterations);
    ff::ff_node::ff_send_out(task);
    return this->GO_ON;
}

// operator to apply to vector<vector<T>>
template <typename T>
std::vector<std::vector<T>> *
Stencil<T>::operator()(std::vector<std::vector<T>> *matrix, int iterations) {
    if ((*matrix).size() == 0 || (*matrix)[0].size() == 0) {
        return matrix;
    }
    std::vector<std::vector<T>> * arena = new std::vector<std::vector<T>>();
    *arena = *matrix;

    Task<T> *task = new Task<T>(arena, (*arena).size(), (*arena)[0].size());
    task = svc_helper(task, iterations);

    *matrix = *task->VectorData;
    delete task;
    return matrix;
}

// function for std thread
// ResultsVector: vector of futures where another thread will put the tasks
// OutputVector: where to put the processed tasks
template <typename T>
void Stencil<T>::stdthread(
    std::vector<std::shared_future<Task<T> *>> *ResultsVector,
    std::vector<std::promise<Task<T> *> *> *OutputVector) {
    if (!ResultsVector || !OutputVector) {
        std::cerr << "Error: input is NULL [Stencil<T>::stdthread]"
                  << std::endl;
        return;
    }
    ThreadPool thread_pool(MaxWorkers);
    int MaxThreads = thread_pool.numberOfThreads();
    int count = 0;

    // for each task, create a new arena where to store the new computed values
    // then send jobs to the threadpool
    for (auto result : *ResultsVector) {
        Task<T> *task = result.get();
        int niter = Iterations;
        int delta = task->Rows / MaxThreads;

        std::vector<std::vector<T>> *arena = new std::vector<std::vector<T>>(0);
        *arena = *(task->VectorData); // copy all from VectorData

        while (niter > 0) {
            for (int l = 0; l < MaxThreads - 1; ++l) {
                thread_pool.addJob([&, l, delta] {
                    for (int x = l * delta; x < (l+1) * delta; ++x) {
                        for (int y = 0; y < task->Cols; ++y) {
                            if (x < Borders[1] || x >= task->Rows - Borders[3] ||
                                y < Borders[0] || y >= task->Cols - Borders[2]) {
                                continue;
                            }
                            std::vector<T> n;
                            n.resize(Neighborhood.size());
                            std::transform(
                                Neighborhood.begin(), Neighborhood.end(),
                                n.begin(),
                                [&task, x, y](std::pair<int, int> e) {
                                  return (*task->VectorData)[x + e.second]
                                                            [y + e.first];
                                });
                            (*arena)[x][y] = Convolution(n);
                        }
                    }
                });
            }
            thread_pool.addJob([&, delta, MaxThreads] {
                for (int x = (MaxThreads - 1) * delta;
                     x < task->Rows; ++x) {
                    for (int y = 0; y < task->Cols; ++y) {
                        if (x < Borders[1] || x >= task->Rows - Borders[3] ||
                            y < Borders[0] || y >= task->Cols - Borders[2]) {
                            continue;
                        }
                        std::vector<T> n;
                        n.resize(Neighborhood.size());
                        std::transform(
                            Neighborhood.begin(), Neighborhood.end(), n.begin(),
                            [&task, x, y](std::pair<int, int> e) {
                                return (
                                    *task->VectorData)[x + e.second][y + e.first];
                            });
                        (*arena)[x][y] = Convolution(n);
                    }
                }
            });
            thread_pool.waitEnd();
            std::swap(task->VectorData, arena);
            --niter;
        }
        delete (arena);
        // set the value of the promise
        (*OutputVector)[count]->set_value(task);
        ++count;
    }
}

template <typename T>
void Stencil<T>::sequential(
    std::vector<std::shared_future<Task<T> *>> *ResultsVector,
    std::vector<std::promise<Task<T> *> *> *OutputVector) {
    if (!ResultsVector || !OutputVector) {
        std::cerr << "Error: input is NULL [Stencil<T>::stdthread]"
                  << std::endl;
        return;
    }
    int count = 0;

    for (auto result : *ResultsVector) {
        Task<T> *task = result.get();
        int niter = Iterations;

        std::vector<std::vector<T>> *arena = new std::vector<std::vector<T>>(0);
        *arena = *(task->VectorData); // copy all from VectorData

        while (niter > 0) {
            for (int x = 0; x < task->Rows; ++x) {
                for (int y = 0; y < task->Cols; ++y) {
                    if (x < Borders[1] || x >= task->Rows - Borders[3] ||
                        y < Borders[0] || y >= task->Cols - Borders[2]) {
                        continue;
                    }
                    std::vector<T> n;
                    n.resize(Neighborhood.size());
                    std::transform(
                        Neighborhood.begin(), Neighborhood.end(), n.begin(),
                        [&task, x, y](std::pair<int, int> e) {
                            return (
                                *task->VectorData)[x + e.second][y + e.first];
                        });
                    (*arena)[x][y] = Convolution(n);
                }
            }
            std::swap(task->VectorData, arena);
            --niter;
        }

        delete (arena);
        (*OutputVector)[count]->set_value(task);
        ++count;
    }
}

template <typename T> Task<T> *Stencil<T>::svc_helper(Task<T> *task, int iterations) {
    int niter = iterations;

    std::vector<std::vector<T>> *arena = new std::vector<std::vector<T>>(0);
    *arena = *(task->VectorData);

    while (niter > 0) {
        parallel_for(
            0, task->Rows,
            [&](const int x) {
              for (int y = 0; y < task->Cols; ++y) {
                if (x < Borders[1] || x >= task->Rows - Borders[3] ||
                    y < Borders[0] || y >= task->Cols - Borders[2]) {
                    continue;
                }
                std::vector<T> n;
                n.resize(Neighborhood.size());
                std::transform(
                    Neighborhood.begin(), Neighborhood.end(), n.begin(),
                    [&task, x, y](std::pair<int, int> e) {
                        return (*task->VectorData)[x + e.second][y + e.first];
                    });
                (*arena)[x][y] = Convolution(n);
              }
            },
            MaxWorkers);
        std::swap(task->VectorData, arena);
        --niter;
    }
    delete (arena);
    return task;
}

#endif /* STENCIL_HPP */
