// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT

#include <TestFramework.h>

#include <Image/ZoomImage.h>
#include <Image/Surface.h>
#include <Image/BlitSurface.h>
#include <Jolt/Core/Profiler.h>

//////////////////////////////////////////////////////////////////////////////////////////
// ImageFilter
//
// Abstract class and some implementations of a filter, essentially an 1D weighting function
// which is not zero for t e [-GetSupport(), GetSupport()] and zero for all other t
// The integrand is usually 1 although it is not required for this implementation,
// since the filter is renormalized when it is sampled.
//////////////////////////////////////////////////////////////////////////////////////////

class ImageFilter
{
public:
	// Destructor
	virtual			~ImageFilter() = default;

	// Get support of this filter (+/- the range the filter function is not zero)
	virtual float	GetSupport() const = 0;

	// Sample filter function at a certain point
	virtual float	GetValue(float t) const = 0;
};

class ImageFilterBox : public ImageFilter
{
	virtual float GetSupport() const override
	{
		return 0.5f;
	}

	virtual float GetValue(float t) const override
	{
		if (abs(t) <= 0.5f)
			return 1.0f;
		else
			return 0.0f;
	}
};

class ImageFilterTriangle : public ImageFilter
{
	virtual float GetSupport() const override
	{
		return 1.0f;
	}

	virtual float GetValue(float t) const override
	{
		t = abs(t);

		if (t < 1.0f)
			return 1.0f - t;
		else
			return 0.0f;
	}
};

class ImageFilterBell : public ImageFilter
{
	virtual float GetSupport() const override
	{
		return 1.5f;
	}

	virtual float GetValue(float t) const override
	{
		t = abs(t);

		if (t < 0.5f)
			return 0.75f - t * t;
		else if (t < 1.5f)
		{
			t = t - 1.5f;
			return 0.5f * t * t;
		}
		else
			return 0.0f;
	}
};

class ImageFilterBSpline : public ImageFilter
{
	virtual float GetSupport() const override
	{
		return 2.0f;
	}

	virtual float GetValue(float t) const override
	{
		t = abs(t);

		if (t < 1.0f)
		{
			float tt = t * t;
			return (0.5f * tt * t) - tt + (2.0f / 3.0f);
		}
		else if (t < 2.0f)
		{
			t = 2.0f - t;
			return (1.0f / 6.0f) * (t * t * t);
		}
		else
			return 0.0f;
	}
};

class ImageFilterLanczos3 : public ImageFilter
{
	virtual float GetSupport() const override
	{
		return 3.0f;
	}

	virtual float GetValue(float t) const override
	{
		t = abs(t);

		if (t < 3.0f)
			return Sinc(t) * Sinc(t / 3.0f);
		else
			return 0.0f;
	}

private:
	static float Sinc(float x)
	{
		x *= JPH_PI;

		if (abs(x) < 1.0e-5f)
			return 1.0f;

		return Sin(x) / x;
	}

};

class ImageFilterMitchell : public ImageFilter
{
	virtual float GetSupport() const override
	{
		return 2.0f;
	}

	virtual float GetValue(float t) const override
	{
		float tt = t * t;
		t = abs(t);

		if (t < 1.0f)
			return (7.0f * (t * tt) - 12.0f * tt + (16.0f / 3.0f)) / 6.0f;
		else if (t < 2.0f)
			return ((-7.0f / 3.0f) * (t * tt) + 12.0f * tt + -20.0f * t + (32.0f / 3.0f)) / 6.0f;
		else
			return 0.0f;
	}
};

static const ImageFilter &GetFilter(EFilter inFilter)
{
	static ImageFilterBox box;
	static ImageFilterTriangle triangle;
	static ImageFilterBell bell;
	static ImageFilterBSpline bspline;
	static ImageFilterLanczos3 lanczos3;
	static ImageFilterMitchell mitchell;

	switch (inFilter)
	{
		case FilterBox:								return box;
		case FilterTriangle:						return triangle;
		case FilterBell:							return bell;
		case FilterBSpline:							return bspline;
		case FilterLanczos3:						return lanczos3;
		case FilterMitchell:						return mitchell;
		default:				JPH_ASSERT(false);	return mitchell;
	}
}

//////////////////////////////////////////////////////////////////////////////////////////
// ZoomSettings
//////////////////////////////////////////////////////////////////////////////////////////

const ZoomSettings ZoomSettings::sDefault;

ZoomSettings::ZoomSettings() :
	mFilter(FilterMitchell),
	mWrapFilter(true),
	mBlur(1.0f)
{
}

bool ZoomSettings::operator == (const ZoomSettings &inRHS) const
{
	return mFilter == inRHS.mFilter
		&& mWrapFilter == inRHS.mWrapFilter
		&& mBlur == inRHS.mBlur;
}

//////////////////////////////////////////////////////////////////////////////////////////
// Resizing a surface
//////////////////////////////////////////////////////////////////////////////////////////

// Structure used for zooming
struct Contrib
{
	int			mOffset;				// Offset of this pixel (relative to start of scanline)
	int			mWeight;				// Weight of this pixel in 0.12 fixed point format
};

static void sPrecalculateFilter(const ZoomSettings &inZoomSettings, int inOldLength, int inNewLength, int inOffsetFactor, Array<Array<Contrib>> &outContrib)
{
	JPH_PROFILE("PrecalculateFilter");

	// Get filter
	const ImageFilter &filter = GetFilter(inZoomSettings.mFilter);

	// Get scale
	float scale = float(inNewLength) / inOldLength;

	float fwidth, fscale;
	if (scale < 1.0f)
	{
		// Minify, broaden filter
		fwidth = filter.GetSupport() / scale;
		fscale = scale;
	}
	else
	{
		// Enlarge, filter is always used as is
		fwidth = filter.GetSupport();
		fscale = 1.0f;
	}

	// Adjust filter for blur
	fwidth *= inZoomSettings.mBlur;
	fscale /= inZoomSettings.mBlur;
	float min_fwidth = 1.0f;
	if (fwidth < min_fwidth)
	{
		fwidth = min_fwidth;
		fscale = filter.GetSupport() / min_fwidth;
	}

	// Make room for a whole scanline
	outContrib.resize(inNewLength);

	// Loop over the whole scanline
	for (int i = 0; i < inNewLength; ++i)
	{
		// Compute center and left- and rightmost pixels affected
		float center = float(i) / scale;
		int left = int(floor(center - fwidth));
		int right = int(ceil(center + fwidth));

		// Reserve required elements
		Array<Contrib> &a = outContrib[i];
		a.reserve(right - left + 1);

		// Total sum of all weights, for renormalization of the filter
		int filter_sum = 0;

		// Compute the contributions for each
		for (int source = left; source <= right; ++source)
		{
			Contrib c;

			// Initialize the offset
			c.mOffset = source;

			// Compute weight at this position in 0.12 fixed point
			c.mWeight = int(4096.0f * filter.GetValue(fscale * (center - source)));
			if (c.mWeight == 0) continue;

			// Add weight to filter total
			filter_sum += c.mWeight;

			// Reflect the filter at the edges if the filter is not to be wrapped (clamp)
			if (!inZoomSettings.mWrapFilter && (c.mOffset < 0 || c.mOffset >= inOldLength))
				c.mOffset = -c.mOffset - 1;

			// Wrap the offset so that it falls within the image
			c.mOffset = (c.mOffset % inOldLength + inOldLength) % inOldLength;

			// Check that the offset falls within the image
			JPH_ASSERT(c.mOffset >= 0 && c.mOffset < inOldLength);

			// Multiply the offset with the specified factor
			c.mOffset *= inOffsetFactor;

			// Add the filter element
			a.push_back(c);
		}

		// Normalize the filter to 0.12 fixed point
		if (filter_sum != 0)
			for (uint n = 0; n < a.size(); ++n)
				a[n].mWeight = (a[n].mWeight * 4096) / filter_sum;
	}
}

static void sZoomHorizontal(RefConst<Surface> inSrc, Ref<Surface> ioDst, const ZoomSettings &inZoomSettings)
{
	JPH_PROFILE("ZoomHorizontal");

	// Check zoom parameters
	JPH_ASSERT(inSrc->GetHeight() == ioDst->GetHeight());
	JPH_ASSERT(inSrc->GetFormat() == ioDst->GetFormat());

	const int width = ioDst->GetWidth();
	const int height = ioDst->GetHeight();
	const int components = ioDst->GetNumberOfComponents();
	const int delta_s = -components;
	const int delta_d = ioDst->GetBytesPerPixel() - components;

	// Pre-calculate filter contributions for a row
	Array<Array<Contrib>> contrib;
	sPrecalculateFilter(inZoomSettings, inSrc->GetWidth(), ioDst->GetWidth(), inSrc->GetBytesPerPixel(), contrib);

	// Do the zoom
	for (int y = 0; y < height; ++y)
	{
		const uint8 *s = inSrc->GetScanLine(y);
		uint8 *d = ioDst->GetScanLine(y);

		for (int x = 0; x < width; ++x)
		{
			const Array<Contrib> &line = contrib[x];
			const size_t line_size_min_one = line.size() - 1;

			int c = components;
			do
			{
				int pixel = 0;

				// Apply the filter for one color component
				size_t j = line_size_min_one;
				do
				{
					const Contrib &cmp = line[j];
					pixel += cmp.mWeight * s[cmp.mOffset];
				}
				while (j--);

				// Clamp the pixel value
				if (pixel <= 0)
					*d = 0;
				else if (pixel >= (255 << 12))
					*d = 255;
				else
					*d = uint8(pixel >> 12);

				++s;
				++d;
			}
			while (--c);

			// Skip unused components if there are any
			s += delta_s;
			d += delta_d;
		}
	}
}

static void sZoomVertical(RefConst<Surface> inSrc, Ref<Surface> ioDst, const ZoomSettings &inZoomSettings)
{
	JPH_PROFILE("ZoomVertical");

	// Check zoom parameters
	JPH_ASSERT(inSrc->GetWidth() == ioDst->GetWidth());
	JPH_ASSERT(inSrc->GetFormat() == ioDst->GetFormat());

	const int width = ioDst->GetWidth();
	const int height = ioDst->GetHeight();
	const int components = ioDst->GetNumberOfComponents();
	const int delta_s = inSrc->GetBytesPerPixel() - components;
	const int delta_d = ioDst->GetBytesPerPixel() - components;

	// Pre-calculate filter contributions for a row
	Array<Array<Contrib>> contrib;
	sPrecalculateFilter(inZoomSettings, inSrc->GetHeight(), ioDst->GetHeight(), inSrc->GetStride(), contrib);

	// Do the zoom
	for (int y = 0; y < height; ++y)
	{
		const uint8 *s = inSrc->GetScanLine(0);
		uint8 *d = ioDst->GetScanLine(y);
		const Array<Contrib> &line = contrib[y];
		const size_t line_size_min_one = line.size() - 1;

		for (int x = 0; x < width; ++x)
		{
			int c = components;
			do
			{
				int pixel = 0;

				// Apply the filter for one color component
				size_t j = line_size_min_one;
				do
				{
					const Contrib &cmp = line[j];
					pixel += cmp.mWeight * s[cmp.mOffset];
				}
				while (j--);

				// Clamp the pixel value
				if (pixel <= 0)
					*d = 0;
				else if (pixel >= (255 << 12))
					*d = 255;
				else
					*d = uint8(pixel >> 12);

				++s;
				++d;
			}
			while (--c);

			// Skip unused components if there are any
			s += delta_s;
			d += delta_d;
		}
	}
}

bool ZoomImage(RefConst<Surface> inSrc, Ref<Surface> ioDst, const ZoomSettings &inZoomSettings)
{
	JPH_PROFILE("ZoomImage");

	// Get filter
	const ImageFilter &filter = GetFilter(inZoomSettings.mFilter);

	// Determine the temporary format that will require the least amount of components to be zoomed and the least amount of bytes pushed around
	ESurfaceFormat tmp_format;
	ESurfaceFormat src_format = inSrc->GetClosest8BitFormat();
	ESurfaceFormat dst_format = ioDst->GetClosest8BitFormat();
	const FormatDescription &src_desc = GetFormatDescription(src_format);
	const FormatDescription &dst_desc = GetFormatDescription(dst_format);
	if (src_desc.GetNumberOfComponents() < dst_desc.GetNumberOfComponents())
		tmp_format = src_format;
	else if (src_desc.GetNumberOfComponents() > dst_desc.GetNumberOfComponents())
		tmp_format = dst_format;
	else if (src_desc.GetBytesPerPixel() < dst_desc.GetBytesPerPixel())
		tmp_format = src_format;
	else
		tmp_format = dst_format;

	// Create temporary source buffer if necessary
	RefConst<Surface> src = inSrc;
	if (inSrc->GetFormat() != tmp_format)
	{
		Ref<Surface> tmp = new SoftwareSurface(inSrc->GetWidth(), inSrc->GetHeight(), tmp_format);
		if (!BlitSurface(inSrc, tmp))
			return false;
		src = tmp;
	}

	// Create temporary destination buffer if necessary
	Ref<Surface> dst = ioDst;
	if (ioDst->GetFormat() != tmp_format)
		dst = new SoftwareSurface(ioDst->GetWidth(), ioDst->GetHeight(), tmp_format);

	src->Lock(ESurfaceLockMode::Read);
	dst->Lock(ESurfaceLockMode::Write);

	if (src->GetWidth() == dst->GetWidth())
	{
		// Only vertical zoom required
		sZoomVertical(src, dst, inZoomSettings);
	}
	else if (src->GetHeight() == dst->GetHeight())
	{
		// Only horizontal zoom required
		sZoomHorizontal(src, dst, inZoomSettings);
	}
	else
	{
		// Determine most optimal order
		float operations_vh = float(dst->GetWidth()) * (filter.GetSupport() * src->GetHeight() + filter.GetSupport() * dst->GetHeight());
		float operations_hv = float(dst->GetHeight()) * (filter.GetSupport() * src->GetWidth() + filter.GetSupport() * dst->GetWidth());
		if (operations_vh < operations_hv)
		{
			// Create temporary buffer to hold the vertical scale
			Ref<Surface> tmp = new SoftwareSurface(src->GetWidth(), dst->GetHeight(), tmp_format);
			tmp->Lock(ESurfaceLockMode::ReadWrite);

			// First scale vertically then horizontally
			sZoomVertical(src, tmp, inZoomSettings);
			sZoomHorizontal(tmp, dst, inZoomSettings);

			tmp->UnLock();
		}
		else
		{
			// Create temporary buffer to hold the horizontal scale
			Ref<Surface> tmp = new SoftwareSurface(dst->GetWidth(), src->GetHeight(), tmp_format);
			tmp->Lock(ESurfaceLockMode::ReadWrite);

			// First scale horizontally then vertically
			sZoomHorizontal(src, tmp, inZoomSettings);
			sZoomVertical(tmp, dst, inZoomSettings);

			tmp->UnLock();
		}
	}

	src->UnLock();
	dst->UnLock();

	// Convert to destination if required
	if (dst != ioDst)
		if (!BlitSurface(dst, ioDst))
			return false;

	return true;
}
