/*
 * Copyright (C) 2014 Brian Matherly (pez4brian@yahoo.com)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
#ifdef HAVE_CAIRO
#include <cairo.h>
#endif
#include "frei0r.hpp"
#include "frei0r_math.h"
#include "gradientlut.hpp"
#include <string>
#include <stdlib.h>

/**
This filter calculates NDVI or VI values from near-infrared + visible video.
The index values are mapped to a false color image.
*/

static inline double N2P(double ndvi) {
    // Convert an index value (-1.0 to 1.0) to position (0.0 to 1.0).
    return (ndvi + 1.0) / 2.0;
}

static unsigned int ColorIndex(const std::string& str) {
    // Convert a color initial to a component index.
    if(str == "r" || str == "R") {
        return 0;
    } else if(str == "g" || str == "G") {
        return 1;
    } else { // "b"
        return 2;
    }
}

class Ndvi : public frei0r::filter
{
public:
    Ndvi(unsigned int width, unsigned int height);
    virtual void update(double time,
                        uint32_t* out,
                        const uint32_t* in);

private:
    void initLut();
    double getComponent(uint8_t *sample, unsigned int chan, double offset, double scale);
    void setColor(uint8_t *sample, double index);
    void drawLegend(uint32_t *out);
    void drawRect( uint32_t* out, uint8_t r, uint8_t g, uint8_t b, unsigned int x, unsigned int y, unsigned int w, unsigned int h );
    void drawGradient( uint32_t* out, unsigned int x, unsigned int y, unsigned int w, unsigned int h );
    void drawText( uint32_t* out, std::string text, unsigned int x, unsigned int y, unsigned int textHeight );

    double paramLutLevels;
    std::string paramColorMap;
    double paramVisScale;
    double paramVisOffset;
    double paramNirScale;
    double paramNirOffset;
    std::string paramVisChan;
    std::string paramNirChan;
    std::string paramIndex;
    std::string paramLegend;
    unsigned int lutLevels;
    std::string colorMap;
    GradientLut gradient;
};

Ndvi::Ndvi(unsigned int width, unsigned int height)
 : paramLutLevels(256.0 / 1000.0)
 , paramColorMap("grayscale")
 , paramVisScale(0.1)
 , paramVisOffset(0.5)
 , paramNirScale(0.1)
 , paramNirOffset(0.5)
 , paramVisChan("b")
 , paramNirChan("r")
 , paramIndex("ndvi")
 , paramLegend("off")
 , lutLevels(0)
 , colorMap("")
 , gradient()
{
    register_param(paramColorMap,  "Color Map",
            "The color map to use. One of 'earth', 'grayscale', 'heat' or 'rainbow'.");
    register_param(paramLutLevels, "Levels",
            "The number of color levels to use in the false image (divided by 1000).");
    register_param(paramVisScale, "VIS Scale",
            "A scaling factor to be applied to the visible component (divided by 10).");
    register_param(paramVisOffset, "VIS Offset",
            "An offset to be applied to the visible component (mapped to [-100%, 100%].");
    register_param(paramNirScale, "NIR Scale",
            "A scaling factor to be applied to the near-infrared component (divided by 10).");
    register_param(paramNirOffset, "NIR Offset",
            "An offset to be applied to the near-infrared component (mapped to [-100%, 100%].");
    register_param(paramVisChan,  "Visible Channel",
            "The channel to use for the visible component. One of 'r', 'g', or 'b'.");
    register_param(paramNirChan,  "NIR Channel",
            "The channel to use for the near-infrared component. One of 'r', 'g', or 'b'.");
    register_param(paramIndex,  "Index Calculation",
            "The index calculation to use. One of 'ndvi' or 'vi'.");
    register_param(paramLegend,  "Legend",
            "Control legend display. One of 'off' or 'bottom'.");
}

void Ndvi::update(double time,
                  uint32_t* out,
                  const uint32_t* in) {
    uint8_t *inP = (uint8_t*)in;
    uint8_t *outP = (uint8_t*)out;
    double visScale = paramVisScale * 10.0;
    double visOffset =  (paramVisOffset * 510) - 255;
    double nirScale = paramNirScale * 10.0;
    double nirOffset = (paramNirOffset * 510) - 255;
    unsigned int visChan = ColorIndex(paramVisChan);
    unsigned int nirChan = ColorIndex(paramNirChan);

    initLut();

    if (paramIndex == "vi") {
        for (int i = 0; i < size; i++) {
            double vis =  getComponent(inP, visChan, visOffset, visScale);
            double nir =  getComponent(inP, nirChan, nirOffset, nirScale);
            double vi = (nir - vis) / 255.0;
            setColor(outP, vi);
            inP += 4;
            outP += 4;
        }
    } else { // ndvi
        for (int i = 0; i < size; i++) {
            double vis =  getComponent(inP, visChan, visOffset, visScale);
            double nir =  getComponent(inP, nirChan, nirOffset, nirScale);
            double ndvi = (nir - vis) / (nir + vis);
            setColor(outP, ndvi);
            inP += 4;
            outP += 4;
        }
    }

    if( paramLegend == "bottom" ) {
        drawLegend(out);
    }
}

void Ndvi::initLut() {
    // Only update the LUT if a parameter has changed.
    unsigned int paramLutLevelsInt = paramLutLevels * 1000.0 + 0.5;
    if (paramLutLevelsInt < 2) paramLutLevelsInt = 2;
    if (paramLutLevelsInt > 1000) paramLutLevelsInt = 1000;
    if (lutLevels == paramLutLevelsInt &&
        colorMap == paramColorMap) {
        return;
    } else {
        lutLevels = paramLutLevelsInt;
        colorMap = paramColorMap;
    }

    gradient.setDepth(lutLevels);

    if(colorMap == "earth") {
        GradientLut::Color water   = {0x30, 0x70, 0xd0};
        GradientLut::Color desert  = {0xd0, 0xc0, 0x90};
        GradientLut::Color grass   = {0x00, 0xc0, 0x20};
        GradientLut::Color forrest = {0x00, 0x30, 0x00};
        gradient.fillRange( N2P(-1.0), water,  N2P(-0.2), water  );
        gradient.fillRange( N2P(-0.2), water,  N2P(-0.1), desert );
        gradient.fillRange( N2P(-0.1), desert, N2P( 0.1), desert );
        gradient.fillRange( N2P( 0.1), desert, N2P( 0.4), grass );
        gradient.fillRange( N2P( 0.4), grass,  N2P( 1.0), forrest );
    } else if(colorMap == "heat") {
        GradientLut::Color n10 = {0x00, 0x00, 0x00};
        GradientLut::Color n08 = {0x10, 0x10, 0x70};
        GradientLut::Color n06 = {0x10, 0x20, 0xf0};
        GradientLut::Color n04 = {0x10, 0x60, 0xf0};
        GradientLut::Color n02 = {0x20, 0xa0, 0xc0};
        GradientLut::Color zer = {0x20, 0xb0, 0x20};
        GradientLut::Color p02 = {0x90, 0xf0, 0x10};
        GradientLut::Color p04 = {0xf0, 0xb0, 0x10};
        GradientLut::Color p06 = {0xf0, 0xa0, 0x10};
        GradientLut::Color p08 = {0xf0, 0x50, 0x10};
        GradientLut::Color p10 = {0xff, 0x00, 0x00};
        gradient.fillRange( N2P(-1.0), n10, N2P(-0.8), n08 );
        gradient.fillRange( N2P(-0.8), n08, N2P(-0.6), n06 );
        gradient.fillRange( N2P(-0.6), n06, N2P(-0.4), n04 );
        gradient.fillRange( N2P(-0.4), n04, N2P(-0.2), n02 );
        gradient.fillRange( N2P(-0.2), n02, N2P( 0.0), zer );
        gradient.fillRange( N2P( 0.0), zer, N2P( 0.2), p02 );
        gradient.fillRange( N2P( 0.2), p02, N2P( 0.4), p04 );
        gradient.fillRange( N2P( 0.4), p04, N2P( 0.6), p06 );
        gradient.fillRange( N2P( 0.6), p06, N2P( 0.8), p08 );
        gradient.fillRange( N2P( 0.8), p08, N2P( 1.0), p10 );
    } else if(colorMap == "rainbow") {
        GradientLut::Color violet = {0x7f, 0x00, 0xff};
        GradientLut::Color blue   = {0x00, 0x00, 0xff};
        GradientLut::Color green  = {0x00, 0xff, 0x00};
        GradientLut::Color yellow = {0xff, 0xff, 0x00};
        GradientLut::Color orange = {0xff, 0x7f, 0x00};
        GradientLut::Color red    = {0xff, 0x00, 0x00};
        gradient.fillRange( N2P(-1.0), violet, N2P(-0.6), blue   );
        gradient.fillRange( N2P(-0.6), blue,   N2P(-0.2), green   );
        gradient.fillRange( N2P(-0.2), green,   N2P( 0.2), yellow  );
        gradient.fillRange( N2P( 0.2), yellow,  N2P( 0.6), orange );
        gradient.fillRange( N2P( 0.6), orange, N2P( 1.0), red    );
    } else { // grayscale
        GradientLut::Color black = {0x00, 0x00, 0x00};
        GradientLut::Color white = {0xff, 0xff, 0xff};
        gradient.fillRange( N2P(-1.0), black, N2P( 1.0), white );
    }
}

inline double Ndvi::getComponent(uint8_t *sample, unsigned int chan, double offset, double scale)
{
    double c =  sample[chan];
    c = (c + offset) * scale;
    c = CLAMP(c, 0.0, 255.0);
    return c;
}

inline void Ndvi::setColor(uint8_t *sample, double index)
{
    double pos = N2P(index);
    const GradientLut::Color& falseColor = gradient[pos];
    sample[0] = falseColor.r;
    sample[1] = falseColor.g;
    sample[2] = falseColor.b;
    sample[3] = 0xff;
}

void Ndvi::drawLegend(uint32_t* out)
{
    unsigned int legendHeight = height / 20;

    // Black border above legend
    unsigned int borderHeight = legendHeight / 15;
    unsigned int borderY = height - legendHeight;
    drawRect( out, 0, 0, 0, 0, borderY, width, borderHeight );

    // Gradient
    unsigned int gradientHeight = legendHeight - borderHeight;
    unsigned int gradientY = height - gradientHeight;
    drawGradient( out, 0, gradientY, width, gradientHeight );

    // Text
    unsigned int textHeight = gradientHeight * 8 / 10;
    unsigned int textY = height - ( gradientHeight - textHeight ) / 2;
    unsigned int textX = width / 25;
    if (paramIndex == "vi") {
        drawText( out, "0", textX, textY , textHeight );
        drawText( out, "VI", width / 2, textY , textHeight );
        drawText( out, "1", width - textX, textY , textHeight );
    } else { // ndvi
        drawText( out, "-1", textX, textY , textHeight );
        drawText( out, "NDVI", width / 2, textY , textHeight );
        drawText( out, "1", width - textX, textY , textHeight );
    }
}

void Ndvi::drawRect( uint32_t* out, uint8_t r, uint8_t g, uint8_t b, unsigned int x, unsigned int y, unsigned int w, unsigned int h )
{
    for (unsigned int i = 0; i < h; i++) {
        uint8_t *sample = (uint8_t*)(out + ((i + y) * width) + x);
        for (unsigned int j = 0; j < w; j++) {
            sample[0] = r;
            sample[1] = g;
            sample[2] = b;
            sample += 4;
        }
    }
}

void Ndvi::drawGradient( uint32_t* out, unsigned int x, unsigned int y, unsigned int w, unsigned int h )
{
    for (unsigned int i = 0; i < w; i++) {
        double pos = (double)i / (double)w;
        const GradientLut::Color& falseColor = gradient[pos];
        uint8_t *sample = (uint8_t*)(out + (y * width) + x + i);
        for (unsigned int j = 0; j < h; j++) {
            sample[0] = falseColor.r;
            sample[1] = falseColor.g;
            sample[2] = falseColor.b;
            sample += width * 4;
        }
    }
}

void Ndvi::drawText( uint32_t* out, std::string text, unsigned int x, unsigned int y, unsigned int textHeight )
{
#ifdef HAVE_CAIRO
    int stride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, width);
    cairo_surface_t* surface = cairo_image_surface_create_for_data((unsigned char*)out,
                                                                   CAIRO_FORMAT_ARGB32,
                                                                   width,
                                                                   height,
                                                                   stride);
    cairo_t *cr = cairo_create(surface);
    cairo_text_extents_t te;
    cairo_font_extents_t fe;

    cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
    cairo_set_font_size(cr, textHeight);
    cairo_text_extents(cr, text.c_str(), &te);
    cairo_font_extents(cr, &fe);

    // Center text on x
    x -= te.width / 2;
    // Align bottom of text on y (assume no characters go below baseline)
    y -= (fe.height - fe.ascent + 1) / 2;

    cairo_move_to(cr, x, y);
    cairo_text_path(cr, text.c_str());
    cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
    cairo_fill_preserve(cr);
    cairo_set_source_rgb(cr, 0, 0, 0);
    cairo_set_line_width(cr, textHeight / 20);
    cairo_stroke (cr);
    cairo_surface_destroy (surface);
    cairo_destroy(cr);
#endif
}

frei0r::construct<Ndvi> plugin("NDVI filter",
            "This filter creates a false image from a visible + infrared source.",
            "Brian Matherly",
            0,2,
            F0R_COLOR_MODEL_RGBA8888);
