/*
 *  This file is part of RawTherapee.
 *
 *  Copyright (c) 2004-2010 Gabor Horvath <hgabor@rawtherapee.com>
 *
 *  RawTherapee 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 3 of the License, or
 *  (at your option) any later version.
 *
 *  RawTherapee 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 RawTherapee.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "improcfun.h"

#include "alignedbuffer.h"
#include "color.h"
#include "imagefloat.h"
#include "labimage.h"
#include "opthelper.h"
#include "rt_math.h"
#include "procparams.h"
#include "sleef.h"

//#define PROFILE

#ifdef PROFILE
#   include <iostream>
#endif

namespace
{

using ProcParams = rtengine::procparams::ProcParams;
using FramingParams = rtengine::procparams::FramingParams;

using Basis = FramingParams::Basis;
using BorderSizing = FramingParams::BorderSizing;
using FramingMethod = FramingParams::FramingMethod;

enum class Orientation { LANDSCAPE, PORTRAIT };
enum class Side { WIDTH, HEIGHT };

struct Dimensions
{
    double width;
    double height;

    Dimensions() : width(0), height(0) {}
    Dimensions(double w, double h) : width(w), height(h) {}

    bool isDegenerate() const { return width <= 0.0 || height <= 0.0; }

    double aspectRatio() const {
        if (isDegenerate()) return 1.0;
        else return static_cast<double>(width) / static_cast<double>(height);
    }

    Orientation orient() const {
        return width >= height ? Orientation::LANDSCAPE : Orientation::PORTRAIT;
    }

    bool inside(const Dimensions& other) const {
        return width <= other.width && height <= other.height;
    }

    void rotate(Orientation newOrient) {
        if (newOrient != orient()) {
            std::swap(width, height);
        }
    }

    bool operator==(const Dimensions& other) const {
        return width == other.width && height == other.height;
    }
    bool operator!=(const Dimensions& other) const { return !(*this == other); }

    Dimensions intersect(const Dimensions& other) const {
        return Dimensions(std::min(width, other.width), std::min(height, other.height));
    }

    void debug(const char* prefix) const {
        printf("%s w=%f h=%f ar=%f\n", prefix, width, height, aspectRatio());
    }
};

struct ResizeArgs
{
    Dimensions size;
    double scale = 1.0;

    ResizeArgs(const Dimensions& aSize, double aScale) : size(aSize), scale(aScale) {}
};

class Framing
{
public:
    Framing(const ProcParams& params, int fullWidth, int fullHeight);

    ResizeArgs adjustResizeForFraming(const ResizeArgs& resize) const;
    Dimensions computeFramedSize(const Dimensions& imgSize) const;

private:
    Dimensions clampResize(const Dimensions& imgSize, const Dimensions& bounds) const;
    ResizeArgs adjustResize(const ResizeArgs& resize, const Dimensions& newSize) const;
    Dimensions computeRelativeImageBBoxInFrame(const Dimensions& imgSize,
                                               const Dimensions& framedSize) const;
    Dimensions computeUniformRelativeImageBBox(const Dimensions& imgSize,
                                               const Dimensions& framedSize) const;
    ResizeArgs resizeForFixedFrame(const ResizeArgs& resize) const;
    ResizeArgs resizeForBBox(const ResizeArgs& resize) const;
    Dimensions computeSizeWithBorders(const Dimensions& imgSize) const;

    const ProcParams& allParams;
    const FramingParams framing;  // Make a copy to sanitize inputs
    const Dimensions postCropImageSize;
    const Dimensions maxUpscalingBBox;
};

// Downscaling limit is 32x32px
constexpr double MIN_DOWNSCALE_PX = 32.0;
// Upscaling limit is 16x image size
constexpr double MAX_UPSCALE_FACTOR = 16.0;

int computeSize(int dim, double scale)
{
    return static_cast<int>(static_cast<double>(dim) * scale + 0.5);
}

std::pair<double, double> computeImgAndBorderSize(double frameSize, double scale)
{
    // frame_len = img_len + 2 * scale * img_len = (1 + 2 * scale) * img_len
    double imgFrameScale = (1.0 + 2.0 * scale);
    double imgSize = frameSize / imgFrameScale;
    double borderSize = scale * imgSize;

    return {imgSize, borderSize};
}

Orientation orient(const FramingParams& params, const Dimensions& imgSize)
{
    switch (params.orientation) {
        case FramingParams::Orientation::LANDSCAPE:
            return Orientation::LANDSCAPE;
        case FramingParams::Orientation::PORTRAIT:
            return Orientation::PORTRAIT;
        case FramingParams::Orientation::AS_IMAGE:
        default:
            return imgSize.orient();
    }
}

double flipAspectRatioByOrientation(double aspectRatio, Orientation orient)
{
    switch (orient) {
        case Orientation::LANDSCAPE:
            return aspectRatio >= 1.0 ? aspectRatio : 1.0 / aspectRatio;
        case Orientation::PORTRAIT:
            return aspectRatio <= 1.0 ? aspectRatio : 1.0 / aspectRatio;
        default:
            return aspectRatio;
    }
}

Side autoPickBasis(const FramingParams& params, const Dimensions& imgSize)
{
    if (imgSize.isDegenerate()) {
        if (imgSize.width <= 0) return Side::HEIGHT;
        else return Side::WIDTH;
    }

    Orientation imgOrient = imgSize.orient();
    double imgAspectRatio = imgSize.aspectRatio();
    Orientation frameOrient = orient(params, imgSize);
    double frameAspectRatio = flipAspectRatioByOrientation(params.aspectRatio, frameOrient);

    if (frameOrient == imgOrient) {
        // Pick the more constrained side (i.e. hits 0 border width first)
        return imgAspectRatio >= frameAspectRatio ? Side::WIDTH : Side::HEIGHT;
    } else if (imgOrient == Orientation::LANDSCAPE) {
        // Image in landscape, frame in portrait
        return Side::WIDTH;
    } else {
        // Image in portrait, frame in landscape
        return Side::HEIGHT;
    }
}

Side pickReferenceSide(const FramingParams& params, const Dimensions& imgSize)
{
    switch (params.basis) {
        case Basis::WIDTH:
            return Side::WIDTH;
        case Basis::HEIGHT:
            return Side::HEIGHT;
        case Basis::LONG:
            return imgSize.width >= imgSize.height ? Side::WIDTH : Side::HEIGHT;
        case Basis::SHORT:
            return imgSize.width <= imgSize.height ? Side::WIDTH : Side::HEIGHT;
        case Basis::AUTO:
        default:
            return autoPickBasis(params, imgSize);
    }
}

constexpr bool INSIDE_BBOX = true;
constexpr bool OUTSIDE_BBOX = false;

Dimensions clampToBBox(const Dimensions& img, const Dimensions& bbox, bool clampInside)
{
    double widthScale = 1.0;
    double heightScale = 1.0;
    if (bbox.width > 0) {
        widthScale = img.width / bbox.width;
    }
    if (bbox.height > 0) {
        heightScale = img.height / bbox.height;
    }

    Dimensions newSize = img;

    if (clampInside) {
        // If a side exceeds the bbox, scale down to bbox
        double scale = std::max(widthScale, heightScale);
        if (scale > 1.0) {
            if (widthScale >= heightScale) {
                newSize.width = bbox.width;
                newSize.height = img.height / widthScale;
            } else {
                newSize.width = img.width / heightScale;
                newSize.height = bbox.height;
            }
        }
    } else {
        // If a side is within the bbox, scale up to bbox
        double scale = std::min(widthScale, heightScale);
        if (scale < 1.0) {
            if (widthScale <= heightScale) {
                newSize.width = bbox.width;
                newSize.height = img.height / widthScale;
            } else {
                newSize.width = img.width / heightScale;
                newSize.height = bbox.height;
            }
        }
    }

    return newSize;
}

Dimensions downscaleToTouchBBox(const Dimensions& img, const Dimensions& bbox)
{
    if (bbox.isDegenerate()) return Dimensions(0, 0);
    if (!bbox.inside(img)) return img;

    double widthScale = img.width / bbox.width;
    double heightScale = img.height / bbox.height;

    Dimensions downscaled;
    if (widthScale <= heightScale) {
        downscaled.width = bbox.width;
        downscaled.height = img.height / widthScale;
    } else {
        downscaled.height = bbox.height;
        downscaled.width = img.width / heightScale;
    }
    return downscaled;
}

Dimensions upscaleToBBox(const Dimensions& img, const Dimensions& bbox)
{
    if (bbox.isDegenerate()) return Dimensions(0, 0);
    if (!img.inside(bbox)) return img;

    double widthScale = img.width / bbox.width;
    double heightScale = img.height / bbox.height;

    Dimensions upscaled;
    if (widthScale >= heightScale) {
        upscaled.width = bbox.width;
        upscaled.height = img.height / widthScale;
    } else {
        upscaled.height = bbox.height;
        upscaled.width = img.width / heightScale;
    }

    return upscaled;
}

double orientAspectRatio(const FramingParams& framing, const Dimensions& imgSize)
{
    double aspectRatio = framing.aspectRatio;
    if (aspectRatio == FramingParams::AS_IMAGE_ASPECT_RATIO) {
        aspectRatio = imgSize.aspectRatio();
    }

    Orientation borderOrient = orient(framing, imgSize);
    if ((borderOrient == Orientation::PORTRAIT && aspectRatio > 1.0) ||
            (borderOrient == Orientation::LANDSCAPE && aspectRatio < 1.0)) {
        aspectRatio = 1.0 / aspectRatio;
    }
    return aspectRatio;
}

Dimensions fromAspectRatio(const Dimensions& size, double aspectRatio)
{
    Dimensions result;
    if (aspectRatio >= 1.0) {
        result.height = size.height;
        result.width = result.height * aspectRatio;
    } else {
        result.width = size.width;
        result.height = result.width / aspectRatio;
    }
    return result;
}

FramingParams sanitize(const FramingParams& dirty)
{
    FramingParams framing = dirty;
    framing.framedWidth = std::max(static_cast<int>(MIN_DOWNSCALE_PX), framing.framedWidth);
    framing.framedHeight = std::max(static_cast<int>(MIN_DOWNSCALE_PX), framing.framedHeight);
    framing.relativeBorderSize = std::max(0.0, std::min(1.0, framing.relativeBorderSize));
    framing.minWidth = std::max(0, framing.minWidth);
    framing.minHeight = std::max(0, framing.minHeight);
    framing.absWidth = std::max(0, framing.absWidth);
    framing.absHeight = std::max(0, framing.absHeight);
    return framing;
}

Framing::Framing(const ProcParams& params, int fullWidth, int fullHeight) :
    allParams(params),
    framing(sanitize(params.framing)),
    postCropImageSize(params.crop.enabled ?
        Dimensions(params.crop.w, params.crop.h) :
        Dimensions(fullWidth, fullHeight)),
    maxUpscalingBBox(Dimensions(
        computeSize(postCropImageSize.width, MAX_UPSCALE_FACTOR),
        computeSize(postCropImageSize.height, MAX_UPSCALE_FACTOR)))
{
}

Dimensions Framing::clampResize(const Dimensions& imgSize, const Dimensions& bounds) const
{
    // Don't adjust above upscaling limit.
    //
    // If the upscaling limit is contained inside the target bounds, scale
    // down the bounds to outside the upscaling limit. This is needed since
    // scaling the bounds to fit inside the upscaling bbox may artificially
    // reduce the upscaling limit due to aspect ratio differences.
    Dimensions clampedBounds = maxUpscalingBBox.inside(bounds) ?
        downscaleToTouchBBox(bounds, maxUpscalingBBox) :
        clampToBBox(bounds, maxUpscalingBBox, INSIDE_BBOX);

    if (!imgSize.inside(clampedBounds)) {
        // Downscale large images to fit inside bounds (only if above limit)

        Dimensions minSize(MIN_DOWNSCALE_PX, MIN_DOWNSCALE_PX);
        if (!minSize.inside(imgSize)) {
            // Skip images below downscaling limit
            return imgSize;
        } else if (!minSize.inside(clampedBounds)) {
            // Go as small as possible without exceeding downscaling limit
            return downscaleToTouchBBox(imgSize, minSize);
        } else {
            // Downscale large images to fit inside bounds
            return clampToBBox(imgSize, clampedBounds, INSIDE_BBOX);
        }
    } else {
        // Consider upscaling...
        if (!framing.allowUpscaling ||
                imgSize.width == clampedBounds.width ||
                imgSize.height == clampedBounds.height) {
            return imgSize;
        } else {
            return upscaleToBBox(imgSize, clampedBounds);
        }
    }
}

ResizeArgs Framing::adjustResize(const ResizeArgs& resize, const Dimensions& bbox) const
{
    Dimensions newSize = clampResize(resize.size, bbox);
    double newScale = newSize.width / postCropImageSize.width;
    return ResizeArgs(newSize, newScale);
}

Dimensions Framing::computeRelativeImageBBoxInFrame(const Dimensions& imgSize,
                                                    const Dimensions& framedSize) const
{
    if (imgSize.isDegenerate() || framedSize.isDegenerate()) {
        return Dimensions(0, 0);
    }

    double imgAspectRatio = imgSize.aspectRatio();

    Side side = pickReferenceSide(framing, imgSize);
    double scale = framing.relativeBorderSize;

    // Compute image and border lengths on basis side
    double frameBasis = side == Side::WIDTH ? framedSize.width : framedSize.height;
    double frameOther = side == Side::WIDTH ? framedSize.height : framedSize.width;

    auto computedSizes = computeImgAndBorderSize(frameBasis, scale);
    double imgBasis = computedSizes.first;

    // Compute image and border lengths for the non-basis side
    double imgBasisToOther = side == Side::WIDTH ? 1.0 / imgAspectRatio : imgAspectRatio;
    double imgOther = imgBasis * imgBasisToOther;

    // Find the maximum allowed image size considering min size limits
    double maxImageBasis = frameBasis;
    double maxImageOther = frameOther;
    if (framing.minSizeEnabled) {
        double minBorderBasis = static_cast<double>(
            side == Side::WIDTH ? framing.minWidth : framing.minHeight);
        double minBorderOther = static_cast<double>(
            side == Side::WIDTH ? framing.minHeight : framing.minWidth);

        maxImageOther = std::floor(frameOther - 2.0 * minBorderOther);
        maxImageBasis = std::floor(frameBasis - 2.0 * minBorderBasis);
    }

    // Image is too large to satisfy requirements:
    //   a. Min border size limit not satisfied
    //   b. Basis size is too small for the requested aspect ratio
    //      (i.e. original image clipped)
    //
    // Resize the image so that it fits in bounds
    if (imgOther > maxImageOther) {
        imgOther = maxImageOther;
        imgBasis = imgOther / imgBasisToOther;
    }
    if (imgBasis > maxImageBasis) {
        imgBasis = maxImageBasis;
        imgOther = imgBasis * imgBasisToOther;
    }

    if (side == Side::WIDTH) {
        return Dimensions(imgBasis, imgOther);
    } else {
        return Dimensions(imgOther, imgBasis);
    }
}

Dimensions Framing::computeUniformRelativeImageBBox(const Dimensions& imgSize,
                                                    const Dimensions& framedSize) const
{
    if (imgSize.isDegenerate() || framedSize.isDegenerate()) {
        return Dimensions(0, 0);
    }

    Side side = pickReferenceSide(framing, imgSize);
    double scale = framing.relativeBorderSize;

    // Compute image and border lengths on basis side
    double frameBasis = side == Side::WIDTH ? framedSize.width : framedSize.height;
    double frameOther = side == Side::WIDTH ? framedSize.height : framedSize.width;

    auto computedSizes = computeImgAndBorderSize(frameBasis, scale);
    double imgBasis = computedSizes.first;
    double border = computedSizes.second;

    // Compute image and border lengths for the non-basis side
    double imgAspectRatio = imgSize.aspectRatio();
    double imgBasisToOther = side == Side::WIDTH ? 1.0 / imgAspectRatio : imgAspectRatio;
    double imgOther = imgBasis * imgBasisToOther;

    // If the frame doesn't constrain the non-basis side length, we just need
    // to check the border minimum size. However, if the non-basis side is
    // constrained, we need to adjust the image size to fit while still
    // maintaining the border scale w.r.t. the basis side.
    double totalOther = imgOther + 2.0 * border;
    if (totalOther > frameOther) {
        // Let:
        // imgOther = imgBasis * imgBasisToOther
        // border = imgBasis * scale
        //
        // Want:
        // frameOther = imgOther + 2 * border
        //            = imgBasis * imgBasisToOther + 2 * scale * imgBasis
        //            = imgBasis * (imgBasisToOther + 2 * scale)
        //
        // Rearrange:
        // imgBasis = frameOther / (imgBasisToOther + 2 * scale)
        imgBasis = frameOther / (imgBasisToOther + 2.0 * scale);
        imgOther = imgBasis * imgBasisToOther;
        border = imgBasis * scale;
    }

    // Find the maximum allowed image size considering min size limits
    double maxImageBasis = frameBasis;
    double maxImageOther = frameOther;
    if (framing.minSizeEnabled) {
        double minBorder = static_cast<double>(
            side == Side::WIDTH ? framing.minWidth : framing.minHeight);

        maxImageBasis = std::floor(frameBasis - 2.0 * minBorder);
        maxImageOther = std::floor(frameOther - 2.0 * minBorder);
    }

    if (imgOther > maxImageOther) {
        imgOther = maxImageOther;
        imgBasis = imgOther / imgBasisToOther;
    }
    if (imgBasis > maxImageBasis) {
        imgBasis = maxImageBasis;
        imgOther = imgBasis * imgBasisToOther;
    }

    if (side == Side::WIDTH) {
        return Dimensions(imgBasis, imgOther);
    } else {
        return Dimensions(imgOther, imgBasis);
    }
}

ResizeArgs Framing::adjustResizeForFraming(const ResizeArgs& resize) const
{
    if (!framing.enabled) return resize;

    switch (framing.framingMethod) {
        case FramingMethod::BBOX:
            return resizeForBBox(resize);
        case FramingMethod::FIXED_SIZE:
            return resizeForFixedFrame(resize);
        case FramingMethod::STANDARD:
        default:
            // No limits on framed size so do nothing
            return resize;
    }
}

ResizeArgs Framing::resizeForFixedFrame(const ResizeArgs& args) const
{
    double framedWidth = framing.framedWidth;
    double framedHeight = framing.framedHeight;
    Dimensions frameSize(framedWidth, framedHeight);

    Dimensions bbox;
    if (framing.borderSizingMethod == BorderSizing::FIXED_SIZE) {
        auto length = [](double frame, double border) {
            return std::max(0.0, frame - 2.0 * border);
        };

        bbox = {
            length(framedWidth, framing.absWidth),
            length(framedHeight, framing.absHeight)
        };
    } else if (framing.borderSizingMethod == BorderSizing::UNIFORM_PERCENTAGE) {
        bbox = computeUniformRelativeImageBBox(args.size, frameSize);
    } else {
        bbox = computeRelativeImageBBoxInFrame(args.size, frameSize);
    }

    return adjustResize(args, bbox);
}

ResizeArgs Framing::resizeForBBox(const ResizeArgs& args) const
{
    Dimensions boundary(framing.framedWidth, framing.framedHeight);

    Dimensions bbox;
    if (framing.borderSizingMethod == BorderSizing::FIXED_SIZE) {
        auto length = [](double frame, double border) {
            return std::max(0.0, frame - 2.0 * border);
        };

        bbox = {
            length(boundary.width, framing.absWidth),
            length(boundary.height, framing.absHeight)
        };
    } else if (framing.borderSizingMethod == BorderSizing::UNIFORM_PERCENTAGE) {
        bbox = computeUniformRelativeImageBBox(args.size, boundary);
    } else {
        // For the requested aspect ratio, it must fit inside the requested
        // bounding box
        double aspectRatio = orientAspectRatio(framing, args.size);
        Dimensions ratioBBox = fromAspectRatio(boundary, aspectRatio);
        ratioBBox = clampToBBox(ratioBBox, boundary, INSIDE_BBOX);

        // Now we have the true max bounds for the framed image. Determine how the
        // original image fits inside these bounds. This process is the same as
        // in the fixed frame mode.
        bbox = computeRelativeImageBBoxInFrame(args.size, ratioBBox);
    }

    return adjustResize(args, bbox);
}

Dimensions Framing::computeFramedSize(const Dimensions& imgSize) const
{
    if (!framing.enabled) return imgSize;

    // For constrained frame sizes, check if the image size (without frame)
    // exceeds the constrained size. This indicates that a combination of
    // parameters caused the downscaling limit to be hit. In which case,
    // just return the original image size (i.e. don't insert border).
    //
    // If the image fits the constrained size, assume that previous resize
    // calculations were correct and trim off any excess borders. The excess
    // may be from rounding errors or hitting some downscaling limit.
    switch (framing.framingMethod) {
        case FramingMethod::BBOX:
        {
            Dimensions fixed(framing.framedWidth, framing.framedHeight);
            if (imgSize.inside(fixed)) {
                Dimensions framedSize = computeSizeWithBorders(imgSize);
                return clampToBBox(framedSize, fixed, INSIDE_BBOX);
            } else {
                return imgSize;
            }
        }
        case FramingMethod::FIXED_SIZE:
        {
            Dimensions fixed(framing.framedWidth, framing.framedHeight);
            return imgSize.inside(fixed) ? fixed : imgSize;
        }
        case FramingMethod::STANDARD:
        default:
            return computeSizeWithBorders(imgSize);
    }
}

Dimensions Framing::computeSizeWithBorders(const Dimensions& imgSize) const
{
    if (framing.borderSizingMethod == BorderSizing::FIXED_SIZE) {
        return Dimensions(imgSize.width + 2.0 * framing.absWidth,
                          imgSize.height + 2.0 * framing.absHeight);
    }

    Side side = pickReferenceSide(framing, imgSize);
    double scale = framing.relativeBorderSize;

    if (framing.borderSizingMethod == BorderSizing::UNIFORM_PERCENTAGE) {
        double borderSize = 0;
        if (side == Side::WIDTH) {
            borderSize = scale * imgSize.width;
            if (framing.minSizeEnabled && borderSize < framing.minWidth) {
                borderSize = framing.minWidth;
            }
        } else {
            borderSize = scale * imgSize.height;
            if (framing.minSizeEnabled && borderSize < framing.minHeight) {
                borderSize = framing.minHeight;
            }
        }

        return Dimensions(imgSize.width + 2.0 * borderSize,
                          imgSize.height + 2.0 * borderSize);
    }

    double aspectRatio = orientAspectRatio(framing, imgSize);

    Dimensions framedSize;
    if (side == Side::WIDTH) {
        framedSize.width = (1.0 + 2.0 * scale) * imgSize.width;
        framedSize.height = framedSize.width / aspectRatio;
    } else {
        framedSize.height = (1.0 + 2.0 * scale) * imgSize.height;
        framedSize.width = framedSize.height * aspectRatio;
    }

    // Check if the computed frame size satsifies the requested aspect ratio
    // without cutting off the original image. If the image is cut off, use
    // the smallest frame that preserves the original image and still
    // satisfies the requested aspect ratio.
    Dimensions minFramedSize = fromAspectRatio(imgSize, aspectRatio);
    Dimensions limit = imgSize;
    if (framing.minSizeEnabled) {
        limit.width += 2.0 * framing.minWidth;
        limit.height += 2.0 * framing.minHeight;
    }
    minFramedSize = clampToBBox(minFramedSize, limit, OUTSIDE_BBOX);

    if (minFramedSize.inside(framedSize)) {
        return framedSize;
    } else {
        return minFramedSize;
    }
}

}  // namespace

namespace rtengine
{

static inline float Lanc (float x, float a)
{
    if (x * x < 1e-6f) {
        return 1.0f;
    } else if (x * x > a * a) {
        return 0.0f;
    } else {
        x = static_cast<float> (rtengine::RT_PI) * x;
        return a * xsinf (x) * xsinf (x / a) / (x * x);
    }
}

void ImProcFunctions::Lanczos (const Imagefloat* src, Imagefloat* dst, float scale)
{

    const float delta = 1.0f / scale;
    const float a = 3.0f;
    const float sc = min (scale, 1.0f);
    const int support = static_cast<int> (2.0f * a / sc) + 1;

#ifdef _OPENMP
    #pragma omp parallel
#endif
    {
        // storage for precomputed parameters for horisontal interpolation
        float * wwh = new float[support * dst->getWidth()];
        int * jj0 = new int[dst->getWidth()];
        int * jj1 = new int[dst->getWidth()];

        // temporal storage for vertically-interpolated row of pixels
        float * lr = new float[src->getWidth()];
        float * lg = new float[src->getWidth()];
        float * lb = new float[src->getWidth()];

        // Phase 1: precompute coefficients for horisontal interpolation

        for (int j = 0; j < dst->getWidth(); j++) {

            // x coord of the center of pixel on src image
            float x0 = (static_cast<float> (j) + 0.5f) * delta - 0.5f;

            // weights for interpolation in horisontal direction
            float * w = wwh + j * support;

            // sum of weights used for normalization
            float ws = 0.0f;

            jj0[j] = max (0, static_cast<int> (floorf (x0 - a / sc)) + 1);
            jj1[j] = min (src->getWidth(), static_cast<int> (floorf (x0 + a / sc)) + 1);

            // calculate weights
            for (int jj = jj0[j]; jj < jj1[j]; jj++) {
                int k = jj - jj0[j];
                float z = sc * (x0 - static_cast<float> (jj));
                w[k] = Lanc (z, a);
                ws += w[k];
            }

            // normalize weights
            for (int k = 0; k < support; k++) {
                w[k] /= ws;
            }
        }

        // Phase 2: do actual interpolation
#ifdef _OPENMP
        #pragma omp for
#endif

        for (int i = 0; i < dst->getHeight(); i++) {

            // y coord of the center of pixel on src image
            float y0 = (static_cast<float> (i) + 0.5f) * delta - 0.5f;

            // weights for interpolation in y direction
            float w[support];
            for (auto& f : w) {
                f = 0.f;
            }

            // sum of weights used for normalization
            float ws = 0.0f;

            int ii0 = max (0, static_cast<int> (floorf (y0 - a / sc)) + 1);
            int ii1 = min (src->getHeight(), static_cast<int> (floorf (y0 + a / sc)) + 1);

            // calculate weights for vertical interpolation
            for (int ii = ii0; ii < ii1; ii++) {
                int k = ii - ii0;
                float z = sc * (y0 - static_cast<float> (ii));
                w[k] = Lanc (z, a);
                ws += w[k];
            }

            // normalize weights
            for (int k = 0; k < support; k++) {
                w[k] /= ws;
            }

            // Do vertical interpolation. Store results.
            for (int j = 0; j < src->getWidth(); j++) {

                float r = 0.0f, g = 0.0f, b = 0.0f;

                for (int ii = ii0; ii < ii1; ii++) {
                    int k = ii - ii0;

                    r += w[k] * src->r (ii, j);
                    g += w[k] * src->g (ii, j);
                    b += w[k] * src->b (ii, j);
                }

                lr[j] = r;
                lg[j] = g;
                lb[j] = b;
            }

            // Do horizontal interpolation
            for (int j = 0; j < dst->getWidth(); j++) {

                float * wh = wwh + support * j;

                float r = 0.0f, g = 0.0f, b = 0.0f;

                for (int jj = jj0[j]; jj < jj1[j]; jj++) {
                    int k = jj - jj0[j];

                    r += wh[k] * lr[jj];
                    g += wh[k] * lg[jj];
                    b += wh[k] * lb[jj];
                }

                dst->r (i, j) = /*CLIP*/ (r);//static_cast<int> (r));
                dst->g (i, j) = /*CLIP*/ (g);//static_cast<int> (g));
                dst->b (i, j) = /*CLIP*/ (b);//static_cast<int> (b));
            }
        }

        delete[] wwh;
        delete[] jj0;
        delete[] jj1;
        delete[] lr;
        delete[] lg;
        delete[] lb;
    }
}


void ImProcFunctions::Lanczos (const LabImage* src, LabImage* dst, float scale)
{
    const float delta = 1.0f / scale;
    constexpr float a = 3.0f;
    const float sc = min(scale, 1.0f);
    const int support = static_cast<int> (2.0f * a / sc) + 1;

    // storage for precomputed parameters for horizontal interpolation
    float* const wwh = new float[support * dst->W];
    int* const jj0 = new int[dst->W];
    int* const jj1 = new int[dst->W];

    // Phase 1: precompute coefficients for horizontal interpolation
    for (int j = 0; j < dst->W; j++) {

        // x coord of the center of pixel on src image
        float x0 = (static_cast<float> (j) + 0.5f) * delta - 0.5f;

        // weights for interpolation in horizontal direction
        float * w = wwh + j * support;

        // sum of weights used for normalization
        float ws = 0.0f;

        jj0[j] = max (0, static_cast<int> (floorf (x0 - a / sc)) + 1);
        jj1[j] = min (src->W, static_cast<int> (floorf (x0 + a / sc)) + 1);

        // calculate weights
        for (int jj = jj0[j]; jj < jj1[j]; jj++) {
            int k = jj - jj0[j];
            float z = sc * (x0 - static_cast<float> (jj));
            w[k] = Lanc (z, a);
            ws += w[k];
        }

        // normalize weights
        for (int k = 0; k < support; k++) {
            w[k] /= ws;
        }
    }

#ifdef _OPENMP
    #pragma omp parallel
#endif
    {
        // temporal storage for vertically-interpolated row of pixels
        AlignedBuffer<float> aligned_buffer_ll(src->W);
        AlignedBuffer<float> aligned_buffer_la(src->W);
        AlignedBuffer<float> aligned_buffer_lb(src->W);
        float* const lL = aligned_buffer_ll.data;
        float* const la = aligned_buffer_la.data;
        float* const lb = aligned_buffer_lb.data;
        // weights for interpolation in y direction
        float w[support] ALIGNED64;
        memset(w, 0, sizeof(w));

        // Phase 2: do actual interpolation
#ifdef _OPENMP
        #pragma omp for
#endif

        for (int i = 0; i < dst->H; i++) {
            // y coord of the center of pixel on src image
            float y0 = (static_cast<float> (i) + 0.5f) * delta - 0.5f;

            // sum of weights used for normalization
            float ws = 0.0f;

            int ii0 = max (0, static_cast<int> (floorf (y0 - a / sc)) + 1);
            int ii1 = min (src->H, static_cast<int> (floorf (y0 + a / sc)) + 1);

            // calculate weights for vertical interpolation
            for (int ii = ii0; ii < ii1; ii++) {
                int k = ii - ii0;
                float z = sc * (y0 - static_cast<float> (ii));
                w[k] = Lanc (z, a);
                ws += w[k];
            }

            // normalize weights
            for (int k = 0; k < support; k++) {
                w[k] /= ws;
            }

            // Do vertical interpolation. Store results.
            int j = 0;
#ifdef __SSE2__
            __m128 Lv, av, bv, wkv;

            for (j = 0; j < src->W - 3; j += 4) {
                Lv = ZEROV;
                av = ZEROV;
                bv = ZEROV;

                for (int ii = ii0; ii < ii1; ii++) {
                    int k = ii - ii0;
                    wkv = F2V(w[k]);
                    Lv += wkv * LVFU(src->L[ii][j]);
                    av += wkv * LVFU(src->a[ii][j]);
                    bv += wkv * LVFU(src->b[ii][j]);
                }

                STVF(lL[j], Lv);
                STVF(la[j], av);
                STVF(lb[j], bv);
            }
#endif

            for (; j < src->W; ++j) {
                float Ll = 0.0f, La = 0.0f, Lb = 0.0f;

                for (int ii = ii0; ii < ii1; ++ii) {
                    int k = ii - ii0;

                    Ll += w[k] * src->L[ii][j];
                    La += w[k] * src->a[ii][j];
                    Lb += w[k] * src->b[ii][j];
                }

                lL[j] = Ll;
                la[j] = La;
                lb[j] = Lb;
            }

            // Do horizontal interpolation
            for (int x = 0; x < dst->W; ++x) {
                float * wh = wwh + support * x;
                float Ll = 0.0f, La = 0.0f, Lb = 0.0f;

                for (int jj = jj0[x]; jj < jj1[x]; ++jj) {
                    int k = jj - jj0[x];

                    Ll += wh[k] * lL[jj];
                    La += wh[k] * la[jj];
                    Lb += wh[k] * lb[jj];
                }

                dst->L[i][x] = Ll;
                dst->a[i][x] = La;
                dst->b[i][x] = Lb;
            }
        }
    }
    delete[] jj0;
    delete[] jj1;
    delete[] wwh;
}

double ImProcFunctions::resizeScale (const ProcParams* params, int fw, int fh, int &imw, int &imh)
{
    imw = fw;
    imh = fh;

    if (!params || !params->resize.enabled) {
        return 1.0;
    }

    // get the resize parameters
    int refw, refh;
    double dScale;

    if (params->crop.enabled && params->resize.appliesTo == "Cropped area") {
        // the resize values applies to the crop dimensions
        refw = params->crop.w;
        refh = params->crop.h;
    } else {
        // the resize values applies to the image dimensions
        // if a crop exists, it will be resized to the calculated scale
        refw = fw;
        refh = fh;
    }

    switch (params->resize.dataspec) {
        case (1):
            // Width
            dScale = (double)params->resize.width / (double)refw;
            break;

        case (2):
            // Height
            dScale = (double)params->resize.height / (double)refh;
            break;

        case (3):

            // FitBox
            if ((double)refw / (double)refh > (double)params->resize.width / (double)params->resize.height) {
                dScale = (double)params->resize.width / (double)refw;
            } else {
                dScale = (double)params->resize.height / (double)refh;
            }
            dScale = (dScale > 1.0 && !params->resize.allowUpscaling) ? 1.0 : dScale;

            break;
            
        case (4):
        
            // Long Edge
            if (refw > refh) {
                dScale = (double)params->resize.longedge / (double)refw;
            } else {
                dScale = (double)params->resize.longedge / (double)refh;
            }
            break;
            
        case (5):
        
            // Short Edge
            if (refw > refh) {
                dScale = (double)params->resize.shortedge / (double)refh;
            } else {
                dScale = (double)params->resize.shortedge / (double)refw;
            }
            break;

        default:
            // Scale
            dScale = params->resize.scale;
            break;
    }

    if (params->crop.enabled && params->resize.appliesTo == "Full image") {
        imw = params->crop.w;
        imh = params->crop.h;
    } else {
        imw = refw;
        imh = refh;
    }

    if (fabs (dScale - 1.0) <= 1e-5) {
        return 1.0;
    } else {
        imw = computeSize(imw, dScale);
        imh = computeSize(imh, dScale);
        return dScale;
    }
}

void ImProcFunctions::resize (Imagefloat* src, Imagefloat* dst, float dScale)
{
#ifdef PROFILE
    time_t t1 = clock();
#endif

    if (params->resize.method != "Nearest" ) {
        Lanczos (src, dst, dScale);
    } else {
        // Nearest neighbour algorithm
#ifdef _OPENMP
        #pragma omp parallel for if (multiThread)
#endif

        for (int i = 0; i < dst->getHeight(); i++) {
            int sy = i / dScale;
            sy = LIM (sy, 0, src->getHeight() - 1);

            for (int j = 0; j < dst->getWidth(); j++) {
                int sx = j / dScale;
                sx = LIM (sx, 0, src->getWidth() - 1);
                dst->r (i, j) = src->r (sy, sx);
                dst->g (i, j) = src->g (sy, sx);
                dst->b (i, j) = src->b (sy, sx);
            }
        }
    }

#ifdef PROFILE
    time_t t2 = clock();
    std::cout << "Resize: " << params->resize.method << ": "
              << (float) (t2 - t1) / CLOCKS_PER_SEC << std::endl;
#endif
}

ImProcFunctions::FramingData ImProcFunctions::framing(const FramingArgs& args) const
{
    FramingData result;
    result.enabled = false;
    result.imgWidth = args.resizeWidth;
    result.imgHeight = args.resizeHeight;
    result.scale = args.resizeScale;
    result.framedWidth = args.resizeWidth;
    result.framedHeight = args.resizeHeight;

    if (!args.params || !args.params->resize.enabled) return result;
    if (!args.params->framing.enabled) return result;

    // For these calculations, try to keep everything as doubles to minimize
    // rounding errors from intermediate steps!

    Framing util(*params, args.cropWidth, args.cropHeight);
    ResizeArgs resize(Dimensions(args.resizeWidth, args.resizeHeight), args.resizeScale);
    ResizeArgs adjusted = util.adjustResizeForFraming(resize);
    Dimensions framedSize = util.computeFramedSize(adjusted.size);

    result.enabled = true;
    result.imgWidth = std::round(adjusted.size.width);
    result.imgHeight = std::round(adjusted.size.height);
    result.scale = adjusted.scale;
    result.framedWidth = std::round(framedSize.width);
    result.framedHeight = std::round(framedSize.height);

    return result;
}

// Draws the border around the input image.
// It should be called after gamma correction.
Imagefloat* ImProcFunctions::drawFrame(Imagefloat* rgb, const FramingParams& params,
                                       const FramingData& dims) const
{
    if (rgb->getWidth() > dims.framedWidth || rgb->getHeight() >  dims.framedHeight) {
        return rgb;
    }
    if (rgb->getWidth() == dims.framedWidth && rgb->getHeight() == dims.framedHeight) {
        return rgb;
    }

    Imagefloat* framed = new Imagefloat(dims.framedWidth, dims.framedHeight);

    // Color::gamma2curve expects a 16-bit value, but the GUI sliders are
    // using 8-bit values. Step up the user value to 16-bits.
    auto clip = [](int v) -> int {
        int sanitized = std::max(0, std::min(v, 255));

        double normalized = static_cast<double>(sanitized) / 255.0;
        return normalized * 65535.0;
    };

    float r = Color::gamma2curve[clip(params.borderRed)];
    float g = Color::gamma2curve[clip(params.borderGreen)];
    float b = Color::gamma2curve[clip(params.borderBlue)];

#ifdef _OPENMP
    #pragma omp parallel for if (multiThread)
#endif
    for (int i = 0; i < framed->getHeight(); i++) {
        for (int j = 0; j < framed->getWidth(); j++) {
            framed->r(i, j) = r;
            framed->g(i, j) = g;
            framed->b(i, j) = b;
        }
    }

    auto offset = [](int inner, int outer) {
        double u = inner;
        double v = outer;
        return static_cast<int>(std::round((v - u) / 2.0));
    };
    int rowOffset = offset(rgb->getHeight(), framed->getHeight());
    int colOffset = offset(rgb->getWidth(), framed->getWidth());

#ifdef _OPENMP
    #pragma omp parallel for if (multiThread)
#endif
    for (int i = 0; i < rgb->getHeight(); i++) {
        for (int j = 0; j < rgb->getWidth(); j++) {
            int row = i + rowOffset;
            int col = j + colOffset;

            framed->r(row, col) = rgb->r(i, j);
            framed->g(row, col) = rgb->g(i, j);
            framed->b(row, col) = rgb->b(i, j);
        }
    }

    delete rgb;
    return framed;
}

}  // namespace rtengine
