/**
 * @brief Gaussian Pyramid for Michael Ashikhmin tone mapping operator
 *
 * This file is a part of LuminanceHDR package, based on pfstmo.
 * ----------------------------------------------------------------------
 * Copyright (C) 2003,2004 Grzegorz Krawczyk
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * ----------------------------------------------------------------------
 *
 * @author Akiko Yoshida, <yoshida@mpi-sb.mpg.de>
 *
 * $Id: pyramid.h,v 1.4 2004/11/16 13:40:45 yoshida Exp $
 */

#ifndef PYRAMID_ASHIKHMIN_H
#define PYRAMID_ASHIKHMIN_H

#include <stdio.h>

#include <Libpfs/array2d.h>
#include <Libpfs/array2d_fwd.h>

using namespace pfs;

class Pyramid {  // each level of a Gaussian pyramid
   public:
    Pyramid() : width(0), height(0), size(0), kernel_size(0), lambda(0.), GP(NULL), flag(0) {
    };

    ~Pyramid(){
        //    delete GP;
    };

    int width;
    int height;
    int size;
    int kernel_size;

    double lambda;

    pfs::Array2Df *GP;
    int flag;

    inline double getPixel(int x, int y) { return (*GP)(x, y); }

    inline void setPixel(int x, int y, double val) { (*GP)(x, y) = val; }
};

////////////////////////////////////////////////////////

class GaussianPyramid {
   public:
    GaussianPyramid() {}

    GaussianPyramid(pfs::Array2Df *lum_map, int im_height, int im_width) {
        // set a=0.4 (considered by Burt and Adelson, 1983).
        // obtained by Outer((0.05, 0.25, 0.4, 0.25, 0.05),(0.05, 0.25, 0.4,
        // 0.25,
        // 0.05))
        g_weights[0][0] = g_weights[0][4] = 0.0025;
        g_weights[0][1] = g_weights[0][3] = 0.0125;
        g_weights[0][2] = 0.0200;

        g_weights[1][0] = g_weights[1][4] = 0.0125;
        g_weights[1][1] = g_weights[1][3] = 0.0625;
        g_weights[1][2] = 0.1000;

        g_weights[2][0] = g_weights[2][4] = 0.0200;
        g_weights[2][1] = g_weights[2][3] = 0.1000;
        g_weights[2][2] = 0.1600;

        g_weights[3][0] = g_weights[3][4] = 0.0125;
        g_weights[3][1] = g_weights[3][3] = 0.0625;
        g_weights[3][2] = 0.1000;

        g_weights[4][0] = g_weights[4][4] = 0.0025;
        g_weights[4][1] = g_weights[4][3] = 0.0125;
        g_weights[4][2] = 0.0200;

        constructPyramid(lum_map, im_width, im_height);
    }

    ~GaussianPyramid() {
        for (int i = 0; i < PYRAMID; i++) delete p[i].GP;
    }

    double NoInterpolateLum(int newX, int newY, Pyramid *pl) {

        double lum;

        if (newX < pl->width - 1 && newY < pl->height - 1)
            lum = pl->getPixel(newX, newY);
        else if (newX < pl->width - 1 && newY >= pl->height) {
            lum = pl->getPixel(newX, pl->height - 1);
        } else if (newX >= pl->width && newY < pl->height - 1) {
            lum = pl->getPixel(pl->width - 1, newY);
        } else
            lum = pl->getPixel(pl->width - 1, pl->height - 1);

        return lum;
    }

    double InterpolateLum(double newX, double newY, Pyramid *pl) {
        int X_int = (int)newX;
        int Y_int = (int)newY;

        double dx, omdx, dy, omdy;
        dx = newX - (double)X_int;
        omdx = 1.0 - dx;
        dy = newY - (double)Y_int;
        omdy = 1.0 - dy;
        double lum;

        if (X_int < pl->width - 1 && Y_int < pl->height - 1)
            lum = omdx * omdy * pl->getPixel(X_int, Y_int) +
                  dx * omdy * pl->getPixel(X_int + 1, Y_int) +
                  omdx * dy * pl->getPixel(X_int, Y_int + 1) +
                  dx * dy * pl->getPixel(X_int + 1, Y_int + 1);
        else if (X_int < pl->width - 1 && Y_int >= pl->height) {
            Y_int = pl->height - 1;
            lum = omdx * pl->getPixel(X_int, Y_int) +
                  dx * pl->getPixel(X_int + 1, Y_int);
        } else if (X_int >= pl->width && Y_int < pl->height - 1) {
            X_int = pl->width - 1;
            lum = omdy * pl->getPixel(X_int, Y_int) +
                  dy * pl->getPixel(X_int, Y_int + 1);
        } else
            lum = pl->getPixel(pl->width - 1, pl->height - 1);

        return lum;
    }

    void Interpolate(int bottom, int top) {
        //         fprintf(stderr, "Interpolating kernel_size=%2d and
        //         kernel_size=%2d\n", p[bottom].kernel_size,
        //         p[top].kernel_size);
        for (int i = bottom + 1; i < top && i < PYRAMID; i++) {
            //      fprintf(stderr, "into %2d\n", i+1);

            int new_kernel_size = i + 1;
            double lambda =
                1.0 -
                (double)(p[bottom].kernel_size - new_kernel_size) /
                    (double)(p[bottom].kernel_size - p[top].kernel_size) * 0.5;
            int new_w = (int)((double)p[bottom].width * lambda);
            int new_h = (int)((double)p[bottom].height * lambda);
            initializeNewLevel(i, new_w, new_h, new_kernel_size, lambda * p[bottom].lambda);

#ifdef _OPENMP
            #pragma omp parallel for schedule(dynamic,16)
#endif
            for (int y = 0; y < p[i].height; y++) {
                for (int x = 0; x < p[i].width; x++) {
                    // top
                    int newX = x / (lambda + lambda);
                    int newY = y / (lambda + lambda);
                    double top_lum = NoInterpolateLum(newX, newY, &p[top]);

                    // bottom
                    newX = (int)((double)x / lambda);
                    newY = (int)((double)y / lambda);
                    double bottom_lum = NoInterpolateLum(newX, newY, &p[bottom]);

/* old implementation. It casts the values to int and calls InterPolateLum, where no interpolation is made because the values are int => nonsense
                    int newX = (int)((double)x / (lambda + lambda));
                    int newY = (int)((double)y / (lambda + lambda));
                    double top_lum = InterpolateLum(newX, newY, &p[top]);

                    // bottom
                    newX = (int)((double)x / lambda);
                    newY = (int)((double)y / lambda);
                    double bottom_lum = InterpolateLum(newX, newY, &p[bottom]);
*/
                    (*p[i].GP)(x, y) = (1.0 - lambda) * top_lum + lambda * bottom_lum;
                }
            }
        }
    }

    void InterpolatePyramid() {
        int bottom = 0, top = bottom + 1;
        while (bottom < PYRAMID - 1) {
            while (p[top].flag == 0) top++;

            Interpolate(bottom, top);

            bottom = top;
            top++;
        }
    }

    int constructNext(int current_index) {
        int w = (int)(p[current_index].width / 2);
        int h = (int)(p[current_index].height / 2);

        int k_size;
        if (p[current_index].kernel_size == 1)
            k_size = 5;
        else
            k_size = 2 * p[current_index].kernel_size;

        int next_index = k_size - 1;
        double new_lambda = p[current_index].lambda * 0.5;
        initializeNewLevel(next_index, w, h, k_size, new_lambda);

        // apply 5*5 kernel
        int X, Y;
#ifdef _OPENMP
        #pragma omp parallel for private(X,Y)
#endif
        for (int y = 0; y < h; y++) {
            for (int x = 0; x < w; x++) {
                double sum = 0.0;
                for (int n = -2; n < 3; n++) {
                    for (int m = -2; m < 3; m++) {
                        if (2 * x + m >= 0 &&
                            2 * x + m < p[current_index].width)
                            X = 2 * x + m;
                        else if (2 * x + m < 0)
                            X = 0;
                        else
                            X = p[current_index].width - 1;

                        if (2 * y + n >= 0 &&
                            2 * y + n < p[current_index].height)
                            Y = 2 * y + n;
                        else if (2 * y + n < 0)
                            Y = 0;
                        else
                            Y = p[current_index].height - 1;

                        sum += g_weights[m + 2][n + 2] *
                               p[current_index].getPixel(X, Y);
                    }
                }

                p[next_index].setPixel(x, y, sum);
            }
        }
        return next_index;
    }

    void showPyramid() {
        fprintf(stderr, "showPyramid()...\n");
        for (int i = 0; i < PYRAMID; i++)
            fprintf(stderr,
                    "index=%2d: kernel_size=%2d, w h=%4d %4d, lambda=%f\n", i,
                    p[i].kernel_size, p[i].width, p[i].height, p[i].lambda);
    }

    void initializeNewLevel(int index, int w, int h, int k_size,
                            double lambda) {
        p[index].width = w;
        p[index].height = h;
        p[index].size = w * h;
        p[index].kernel_size = k_size;
        p[index].lambda = lambda;
        p[index].GP = new pfs::Array2Df(w, h);
        p[index].flag = 1;
    }

    void constructPyramid(pfs::Array2Df *lum_map, int im_width, int im_height) {
        initializeNewLevel(0, im_width, im_height, 1, 1.0);
        for (int y = 0; y < im_height; y++)
            for (int x = 0; x < im_width; x++)
                p[0].setPixel(x, y, (*lum_map)(x, y));

        int current_index = 0;
        while (p[current_index].kernel_size < PYRAMID)
            current_index = constructNext(current_index);

        InterpolatePyramid();

        // showPyramid();
    }

    static const int PYRAMID = 20;
    Pyramid p[PYRAMID];
    double g_weights[5][5];
};

#endif
