/**
 * @author Samuel Thompson
 * @file DataMask.cpp
 * @brief  Contains the DataMask class for describing the spatial sampling pattern on a landscape.
 *
 * @copyright <a href="https://opensource.org/licenses/MIT"> MIT Licence.</a>
 */

#include "DataMask.h"
#include "Landscape.h"
#include "Logging.h"

namespace necsim
{
    DataMask::DataMask() : inputfile(""), isNullSample(true), isGridOffset(false), x_offset(0), y_offset(0), x_dim(0),
                           y_dim(0), mask_x_dim(0), mask_y_dim(0), getProportionfptr(nullptr), sample_mask(),
                           sample_mask_exact()
    {
        getProportionfptr = &DataMask::getBoolProportion;
    }

    DataMask &DataMask::operator=(const DataMask &other)
    {
        sample_mask = other.sample_mask;
        sample_mask_exact = other.sample_mask_exact;
        inputfile = other.inputfile;
        // True if the sample mask is true everywhere
        isNullSample = other.isNullSample;
        // True if the grid is smaller than the sample mask
        isGridOffset = other.isGridOffset;
        x_offset = other.x_offset;
        y_offset = other.y_offset;
        // Stores the size of the grid which is stored as a full species lineage_indices
        x_dim = other.x_dim;
        y_dim = other.y_dim;
        // Stores the size of the samplemask from which spatially sampling is read
        mask_x_dim = other.mask_x_dim;
        mask_y_dim = other.mask_y_dim;

        getProportionfptr = &DataMask::getBoolProportion;
        return *this;
    }

    DataMask &DataMask::operator=(DataMask &&other) noexcept
    {
        sample_mask = std::move(other.sample_mask);
        sample_mask_exact = std::move(other.sample_mask_exact);
        inputfile = std::move(other.inputfile);
        // True if the sample mask is true everywhere
        isNullSample = other.isNullSample;
        // True if the grid is smaller than the sample mask
        isGridOffset = other.isGridOffset;
        x_offset = other.x_offset;
        y_offset = other.y_offset;
        // Stores the size of the grid which is stored as a full species lineage_indices
        x_dim = other.x_dim;
        y_dim = other.y_dim;
        // Stores the size of the samplemask from which spatially sampling is read
        mask_x_dim = other.mask_x_dim;
        mask_y_dim = other.mask_y_dim;

        getProportionfptr = &DataMask::getBoolProportion;
        return *this;
    }

    bool DataMask::isNull()
    {
        return isNullSample;
    }

    void DataMask::setup(const shared_ptr<SimParameters> sim_parameters)
    {
#ifdef DEBUG
        if((sim_parameters->grid_x_size > sim_parameters->sample_x_size
            || sim_parameters->grid_y_size > sim_parameters->sample_y_size) && !isNullSample)
        {
            writeLog(50,
                     "Grid size: " + std::to_string(sim_parameters->grid_x_size) + ", "
                     + std::to_string(sim_parameters->grid_y_size));
            writeLog(50,
                     "Sample mask size: " + std::to_string(sim_parameters->sample_x_size) + ", "
                     + std::to_string(sim_parameters->sample_y_size));
            throw FatalException("Datamask dimensions do not make sense");
        }
#endif // DEBUG
        inputfile = sim_parameters->sample_mask_file;
        x_dim = sim_parameters->grid_x_size;
        y_dim = sim_parameters->grid_y_size;
        mask_x_dim = sim_parameters->sample_x_size;
        mask_y_dim = sim_parameters->sample_y_size;
        x_offset = sim_parameters->sample_x_offset;
        y_offset = sim_parameters->sample_y_offset;
        if(x_dim != mask_x_dim || y_dim != mask_y_dim)
        {
            isGridOffset = true;
        }
        else
        {
            isGridOffset = false;
        }
    }

    bool DataMask::checkCanUseDefault(const shared_ptr<SimParameters> sim_parameters)
    {
        if(sim_parameters->sample_mask_file == "null")
        {
            if(sim_parameters->fine_map_x_size == sim_parameters->sample_x_size
               && sim_parameters->fine_map_y_size == sim_parameters->sample_y_size
               && sim_parameters->fine_map_x_offset == 0 && sim_parameters->fine_map_y_offset == 0)
            {
                isNullSample = true;
            }
            else
            {
                isNullSample = false;
            }
        }
        else
        {
            isNullSample = sim_parameters->sample_mask_file == "none";
        }
        return isNullSample;
    }

    void DataMask::importBooleanMask(unsigned long xdim,
                                     unsigned long ydim,
                                     unsigned long mask_xdim,
                                     unsigned long mask_ydim,
                                     unsigned long xoffset,
                                     unsigned long yoffset,
                                     string inputfile_in)
    {
        shared_ptr<SimParameters> tmp_sim_parameters = make_shared<SimParameters>();
        tmp_sim_parameters->sample_mask_file = inputfile_in;
        tmp_sim_parameters->grid_x_size = xdim;
        tmp_sim_parameters->grid_y_size = ydim;
        tmp_sim_parameters->sample_x_size = mask_xdim;
        tmp_sim_parameters->sample_y_size = mask_ydim;
        tmp_sim_parameters->sample_x_offset = xoffset;
        tmp_sim_parameters->sample_y_offset = yoffset;
        setup(tmp_sim_parameters);
        isNullSample = inputfile_in == "null" || inputfile_in == "none";
        if(!isNullSample)
        {
            doImport();
        }
    }

    void DataMask::doImport()
    {
        sample_mask.setSize(mask_y_dim, mask_x_dim);
        sample_mask.import(inputfile);
        sample_mask.close();
        completeBoolImport();
    }

    void DataMask::completeBoolImport()
    {
        mask_x_dim = sample_mask.getCols();
        mask_y_dim = sample_mask.getRows();
        getProportionfptr = &DataMask::getBoolProportion;
    }

    void DataMask::setupNull(const shared_ptr<SimParameters> mapvarin)
    {
        sample_mask.setSize(mapvarin->fine_map_y_size, mapvarin->fine_map_x_size);
        for(unsigned long i = 0; i < sample_mask.getRows(); i++)
        {
            for(unsigned long j = 0; j < sample_mask.getCols(); j++)
            {
                sample_mask.setValue(i, j, i + y_offset < mask_y_dim && j + x_offset < mask_x_dim);
            }
        }
        completeBoolImport();
    }

    void DataMask::importSampleMask(const shared_ptr<SimParameters> mapvarin)
    {
        setup(mapvarin);
        if(!checkCanUseDefault(mapvarin))
        {
            if(inputfile == "null")
            {
                setupNull(mapvarin);
            }
            else if(mapvarin->uses_spatial_sampling)
            {
#ifdef DEBUG
                writeLog(10, "Using spatial sampling.");
                writeLog(10, "Mask dimensions: " + std::to_string(mask_x_dim) + ", " + std::to_string(mask_y_dim));
#endif // DEBUG
                sample_mask_exact.setSize(mask_y_dim, mask_x_dim);
                sample_mask_exact.import(inputfile);
                sample_mask_exact.close();
                mask_x_dim = sample_mask_exact.getCols();
                mask_y_dim = sample_mask_exact.getRows();
                getProportionfptr = &DataMask::getSampleProportion;
            }
            else
            {
                doImport();
            }
        }
        else
        {
            if(mapvarin->uses_spatial_sampling)
            {
                // This could perhaps be a warning, but I'd prefer to have the warning/prohibit potential in python
                // and throw a full exception here.
                throw FatalException("Cannot use a spatial sampling routine when the map file is null.");
            }
            getProportionfptr = &DataMask::getNullProportion;
        }
    }

    bool DataMask::getVal(const long &x, const long &y, const long &xwrap, const long &ywrap) const
    {
        long xval = x + (xwrap * x_dim) + x_offset;
        long yval = y + (ywrap * y_dim) + y_offset;
        if(isNullSample)
        {
            return true;
        }
#ifdef DEBUG
        if(xval < 0 || xval >= (long) mask_x_dim || yval < 0 || yval >= (long) mask_y_dim)
        {
            std::stringstream ss;
            ss << "Get value on samplemask requested for non index." << std::endl;
            ss << "x, y: " << x << ", " << y << std::endl;
            ss << "dimensions x,y: " << mask_x_dim << ", " << mask_y_dim << std::endl;
            ss << "x, y wrap: " << xwrap << ", " << ywrap << std::endl;
            ss << "xval, yval: " << xval << ", " << yval << std::endl;
            ss << "offsets x, y: " << x_offset << ", " << y_offset << std::endl;
            writeLog(50, ss);
            ss.str("Get value on samplemask requested for non index.");
            throw std::out_of_range (ss.str());
        }
#endif
        return sample_mask.getCopy(yval, xval);
    }

    double DataMask::getNullProportion(const long &x, const long &y, const long &xwrap, const long &ywrap) const
    {
        return 1.0;
    }

    double DataMask::getBoolProportion(const long &x, const long &y, const long &xwrap, const long &ywrap) const
    {

        if(getVal(x, y, xwrap, ywrap))
        {
            return 1.0;
        }
        else
        {
            return 0.0;
        }
    }

    double DataMask::getSampleProportion(const long &x, const long &y, const long &xwrap, const long &ywrap) const
    {
#ifdef DEBUG
        if(isNullSample || sample_mask_exact.getCols() == 0)
        {
            throw std::out_of_range ("Cannot get the exact value from a samplemask if we are using a null mask, or the "
                               "exact samplemask has not been properly imported.");
        }
#endif // DEBUG
        long xval = x + (xwrap * x_dim) + x_offset;
        long yval = y + (ywrap * y_dim) + y_offset;
        return sample_mask_exact.getCopy(yval, xval);
    }

    double DataMask::getExactValue(const long &x, const long &y, const long &xwrap, const long &ywrap) const
    {
        return (this->*getProportionfptr)(x, y, xwrap, ywrap);
    }

    void DataMask::convertBoolean(shared_ptr<Landscape> map1, const double &deme_sampling, const double &generation)
    {
        // Clear the old boolean object and set the new size
        sample_mask.setSize(y_dim, x_dim);
        for(unsigned long y = 0; y < y_dim; y++)
        {
            for(unsigned long x = 0; x < x_dim; x++)
            {
                long tmp_x = x;
                long tmp_y = y;
                long tmp_xwrap = 0;
                long tmp_ywrap = 0;
                recalculateCoordinates(tmp_x, tmp_y, tmp_xwrap, tmp_ywrap);
                double density = map1->getVal(tmp_x, tmp_y, tmp_xwrap, tmp_ywrap, generation) * deme_sampling;
                sample_mask.setValue(y, x, density >= 1.0);
            }
        }
    }

    void DataMask::clearSpatialMask()
    {
        sample_mask_exact.setSize(0, 0);
    }

    void DataMask::recalculateCoordinates(long &x, long &y, long &x_wrap, long &y_wrap)
    {
        if(isGridOffset)
        {
            x_wrap = (long) ((floor((x - (double) x_offset) / (double) x_dim)));
            y_wrap = (long) ((floor((y - (double) y_offset) / (double) y_dim)));
            x += -x_offset - (x_wrap * x_dim);
            y += -y_offset - (y_wrap * y_dim);
        }
    }
}