// This file is part of necsim project which is released under MIT license.
// See file **LICENSE.txt** or visit https://opensource.org/licenses/MIT) for full license details
/**
 * @author Samuel Thompson
 * @file LandscapeMetricsCalculator.cpp
 * @brief Contains the LandscapeMetricsCalculator class for calculating landscape metrics.
 *
 * @copyright <a href="https://opensource.org/licenses/MIT"> MIT Licence.</a>
 */
#include "necsim/Matrix.h"
#include "necsim/Map.h"
#include "LandscapeMetricsCalculator.h"

double LandscapeMetricsCalculator::calculateMNN()
{
    vector<double> distances;
    for(unsigned long i = 0; i < getRows(); i++)
    {
        for(unsigned long j = 0; j < getCols(); j++)
        {
            if(matrix[i][j] != 0)
            {
                distances.push_back(findNearestNeighbourDistance(i, j));
            }
        }
    }
    if(distances.empty())
    {
        throw FatalException("No cells found on map. Cannot calculate distances.");
    }
    return accumulate(distances.begin(), distances.end(), 0.0) / distances.size();
}

void LandscapeMetricsCalculator::checkMinDistance(Cell &home_cell, const long &x, const long &y, double &min_distance)
{
    Cell this_cell{};
    this_cell.x = x;
    this_cell.y = y;
    if(this_cell != home_cell && matrix[y][x] > 0)
    {
        auto distance = distanceBetweenCells(home_cell, this_cell);
        if(distance >= 1.0)
        {
            min_distance = min(min_distance, distance);
        }
    }
}

double LandscapeMetricsCalculator::findNearestNeighbourDistance(const long &row, const long &col)
{
    long upper = min(static_cast<long>(getRows() - 1), row + 1);
    long lower = max(static_cast<long>(0), row - 1);
    long left = max(static_cast<long>(0), col - 1);
    long right = min(static_cast<long>(getCols() - 1), col + 1);
    long x = left;
    long y = lower;
    double min_distance = getRows() * getCols();
    Cell home_cell{};
    home_cell.x = col;
    home_cell.y = row;
    while(true)
    {
        x = left;
        y = lower;
        double min_distance_possible = min(right - left, upper - lower);
        // Four dimensions to check in
        while(x < right)
        {
            x++;
            checkMinDistance(home_cell, x, y, min_distance);
        }
        while(y < upper)
        {
            y++;
            checkMinDistance(home_cell, x, y, min_distance);
        }
        while(x > left)
        {
            x--;
            checkMinDistance(home_cell, x, y, min_distance);
        }
        while(y > lower)
        {
            y--;
            checkMinDistance(home_cell, x, y, min_distance);
        }
        // Expand the grid by 1 in all dimensions.
        if(lower > 0)
        {
            lower--;
        }
        if(upper < static_cast<long>(getRows()) - 1)
        {
            upper++;
        }
        if(left > 0)
        {
            left--;
        }
        if(right < static_cast<long>(getCols()) - 1)
        {
            right++;
        }
#ifdef DEBUG
        if(left < 0 || left >= getCols() ||
                right < 0 || right >= getCols() ||
                lower < 0 || lower >= getRows() ||
                upper < 0 || upper >= getRows())
        {
            stringstream ss;
            ss << "Bounds out of range. Please report this bug." << endl;
            ss << "t, b, l, r: " << upper << ", " << lower << ", " << left << ", " << right << endl;
            throw FatalException(ss.str());
        }
#endif
        if(min_distance < min_distance_possible)
        {
            break;
        }
    }
    return min_distance;
}

void LandscapeMetricsCalculator::createCellList()
{
    for(unsigned long i = 0; i < getRows(); i++)
    {
        for(unsigned long j = 0; j < getCols(); j++)
        {
            if(matrix[i][j] != 0)
            {
                Cell cell;
                cell.x = j;
                cell.y = i;
                all_cells.emplace_back(cell);
            }
        }
    }
    stringstream ss;
    ss << "Detected " << all_cells.size() << " cells" << endl;
    writeInfo(ss.str());
}

double LandscapeMetricsCalculator::calculateClumpiness()
{
    createCellList();
    double P = static_cast<double>(all_cells.size()) / static_cast<double>(getCols() * getRows());
    unsigned long totalAdj = calculateNoAdjacencies();
    unsigned long totalNonAdj = (all_cells.size() * 8) - totalAdj;
    double minPerimeter = calculateMinPerimeter();
    double G = totalAdj / (totalAdj + totalNonAdj - minPerimeter);
    if(G < P && P < 0.5)
    {
        return (G - P) / P;
    }
    else if(P == 1.0)
    {
        return 1.0;
    }
    else
    {
        return (G - P) / (1 - P);
    }
}

unsigned long LandscapeMetricsCalculator::calculateNoAdjacencies()
{
    unsigned long totalAdj = 0;
    for(const auto &home_cell : all_cells)
    {
        for(long x = -1; x <= 1; x++)
        {
            for(long y = -1; y <= 1; y++)
            {
                if(!(x == 0 && y == 0))
                {
                    Cell this_cell;
                    this_cell.x = x + home_cell.x;
                    this_cell.y = y + home_cell.y;
                    if(this_cell.x >= 0 && this_cell.x < static_cast<long>(getCols()) &&
                       this_cell.y >= 0 && this_cell.y < static_cast<long>(getRows()))
                    {
                        if(matrix[this_cell.y][this_cell.x] >= 1.0)
                        {
                            totalAdj++;
                        }
                    }
                }
            }
        }
    }
    return totalAdj;
}

double LandscapeMetricsCalculator::calculateMinPerimeter()
{
    // Based on http://www.umass.edu/landeco/research/fragstats/documents/Metrics/
    // Contagion%20-%20Interspersion%20Metrics/Metrics/C115%20-%20CLUMPY.htm
    double largestIntegerSquare = floor(pow(all_cells.size(), 0.5));
    double m = all_cells.size() - pow(largestIntegerSquare, 2);
    if(m == 0)
    {
        return 4 * largestIntegerSquare;
    }
    else if(pow(largestIntegerSquare, 2.0) < all_cells.size() &&
            all_cells.size() <= largestIntegerSquare * (1 + largestIntegerSquare))
    {
        return (4 * largestIntegerSquare) + 2;
    }
    else
    {
        return (4 * largestIntegerSquare) + 4;
    }
}




