/******************************************************************************
 *
 * Project:  Marching squares
 * Purpose:  Classes for contour generation
 * Author:   Hugo Mercier, <hugo dot mercier at oslandia dot com>
 *
 ******************************************************************************
 * Copyright (c) 2018, Oslandia <infos at oslandia dot com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#ifndef MARCHING_SQUARES_CONTOUR_GENERATOR_H
#define MARCHING_SQUARES_CONTOUR_GENERATOR_H

#include <vector>
#include <algorithm>

#include "gdal.h"

#include "utility.h"
#include "point.h"
#include "square.h"

namespace marching_squares
{

template <typename ContourWriter, typename LevelGenerator>
class ContourGenerator
{
  public:
    ContourGenerator(size_t width, size_t height, bool hasNoData,
                     double noDataValue, ContourWriter &writer,
                     LevelGenerator &levelGenerator)
        : width_(width), height_(height), hasNoData_(hasNoData),
          noDataValue_(noDataValue), previousLine_(), writer_(writer),
          levelGenerator_(levelGenerator)
    {
        previousLine_.resize(width_);
        std::fill(previousLine_.begin(), previousLine_.end(), NaN);
    }

    CPLErr feedLine(const double *line)
    {
        if (lineIdx_ <= height_)
        {
            feedLine_(line);
            if (lineIdx_ == height_)
            {
                // last line
                feedLine_(nullptr);
            }
        }
        return CE_None;
    }

  private:
    size_t width_;
    size_t height_;
    bool hasNoData_;
    double noDataValue_;

    size_t lineIdx_ = 0;

    std::vector<double> previousLine_;

    ContourWriter &writer_;
    LevelGenerator &levelGenerator_;

    class ExtendedLine
    {
      public:
        ExtendedLine(const double *line, size_t size, bool hasNoData,
                     double noDataValue)
            : line_(line), size_(size), hasNoData_(hasNoData),
              noDataValue_(noDataValue)
        {
        }

        double value(int idx) const
        {
            if (line_ == nullptr)
                return NaN;
            if (idx < 0 || idx >= int(size_))
                return NaN;
            double v = line_[idx];
            if (hasNoData_ && v == noDataValue_)
                return NaN;
            return v;
        }

      private:
        const double *line_;
        size_t size_;
        bool hasNoData_;
        double noDataValue_;
    };

    void feedLine_(const double *line)
    {
        writer_.beginningOfLine();

        ExtendedLine previous(&previousLine_[0], width_, hasNoData_,
                              noDataValue_);
        ExtendedLine current(line, width_, hasNoData_, noDataValue_);
        for (int colIdx = -1; colIdx < int(width_); colIdx++)
        {
            const ValuedPoint upperLeft(colIdx + 1 - .5, lineIdx_ - .5,
                                        previous.value(colIdx));
            const ValuedPoint upperRight(colIdx + 1 + .5, lineIdx_ - .5,
                                         previous.value(colIdx + 1));
            const ValuedPoint lowerLeft(colIdx + 1 - .5, lineIdx_ + .5,
                                        current.value(colIdx));
            const ValuedPoint lowerRight(colIdx + 1 + .5, lineIdx_ + .5,
                                         current.value(colIdx + 1));

            Square(upperLeft, upperRight, lowerLeft, lowerRight)
                .process(levelGenerator_, writer_);
        }
        if (line != nullptr)
            std::copy(line, line + width_, previousLine_.begin());
        lineIdx_++;

        writer_.endOfLine();
    }
};

template <typename ContourWriter, typename LevelGenerator>
inline ContourGenerator<ContourWriter, LevelGenerator> *
newContourGenerator(size_t width, size_t height, bool hasNoData,
                    double noDataValue, ContourWriter &writer,
                    LevelGenerator &levelGenerator)
{
    return new ContourGenerator<ContourWriter, LevelGenerator>(
        width, height, hasNoData, noDataValue, writer, levelGenerator);
}

template <typename ContourWriter, typename LevelGenerator>
class ContourGeneratorFromRaster
    : public ContourGenerator<ContourWriter, LevelGenerator>
{
  public:
    ContourGeneratorFromRaster(const GDALRasterBandH band, bool hasNoData,
                               double noDataValue, ContourWriter &writer,
                               LevelGenerator &levelGenerator)
        : ContourGenerator<ContourWriter, LevelGenerator>(
              GDALGetRasterBandXSize(band), GDALGetRasterBandYSize(band),
              hasNoData, noDataValue, writer, levelGenerator),
          band_(band)
    {
    }

    bool process(GDALProgressFunc progressFunc = nullptr,
                 void *progressData = nullptr)
    {
        size_t width = GDALGetRasterBandXSize(band_);
        size_t height = GDALGetRasterBandYSize(band_);
        std::vector<double> line;
        line.resize(width);

        for (size_t lineIdx = 0; lineIdx < height; lineIdx++)
        {
            if (progressFunc &&
                progressFunc(double(lineIdx) / height, "Processing line",
                             progressData) == FALSE)
                return false;

            CPLErr error =
                GDALRasterIO(band_, GF_Read, 0, int(lineIdx), int(width), 1,
                             &line[0], int(width), 1, GDT_Float64, 0, 0);
            if (error != CE_None)
            {
                CPLDebug("CONTOUR", "failed fetch %d %d", int(lineIdx),
                         int(width));
                return false;
            }
            this->feedLine(&line[0]);
        }
        if (progressFunc)
            progressFunc(1.0, "", progressData);
        return true;
    }

  private:
    const GDALRasterBandH band_;

    ContourGeneratorFromRaster(const ContourGeneratorFromRaster &) = delete;
    ContourGeneratorFromRaster &
    operator=(const ContourGeneratorFromRaster &) = delete;
};

}  // namespace marching_squares

#endif
