// HARFANG(R) Copyright (C) 2021 Emmanuel Julien, NWNC HARFANG. Released under GPL/LGPL/Commercial Licence, see licence.txt for details.

#include "engine/picture.h"

#include <assert.h>

#include "foundation/cext.h"
#include "foundation/file.h"
#include "foundation/math.h"
#include "foundation/profiler.h"

#include "bimg/encode.h"
#include "bx/allocator.h"
#include "bx/file.h"

#include "stb_image.h"
#include "stb_image_write.h"
#include "stb_image_resize.h"

namespace hg {

int size_of(PictureFormat format) {
	static const int size_of_format[PF_Last] = {0, 3, 4, 16};
	return size_of_format[format];
}

size_t GetChannelCount(PictureFormat format) {
	static const size_t channel_count_format[PF_Last] = {0, 3, 4, 4};
	return channel_count_format[format];
}

//
Picture::Picture(uint16_t width, uint16_t height, PictureFormat format)
	: w(width), h(height), f(format), has_ownership(1), d(new uint8_t[w * h * size_of(f)]) {}

Picture::Picture(void *data, uint16_t width, uint16_t height, PictureFormat format) : w(width), h(height), f(format), d(reinterpret_cast<uint8_t *>(data)) {}

Picture::Picture(const Picture &pic)
	: w(pic.w), h(pic.h), f(pic.f), has_ownership(pic.has_ownership), d(pic.has_ownership ? new uint8_t[w * h * size_of(f)] : pic.d) {
	if (pic.has_ownership)
		std::copy(pic.d, pic.d + w * h * size_of(f), d);
}

Picture::Picture(Picture &&pic) noexcept : w(pic.w), h(pic.h), f(pic.f), has_ownership(pic.has_ownership), d(pic.d) {
	pic.w = 0;
	pic.h = 0;
	pic.f = PF_RGBA32;
	pic.has_ownership = 0;
	pic.d = nullptr;
}

Picture::~Picture() { Clear(); }

//
void Picture::SetData(void *data, uint16_t width, uint16_t height, PictureFormat format) {
	Clear();

	w = width;
	h = height;
	f = format;

	has_ownership = 0;
	d = reinterpret_cast<uint8_t *>(data);
}

void Picture::CopyData(const void *data, uint16_t width, uint16_t height, PictureFormat format) {
	ProfilerPerfSection section("Picture::CopyData");

	Clear();

	w = width;
	h = height;
	f = format;

	has_ownership = 1;
	d = new uint8_t[w * h * size_of(f)];
	std::copy(reinterpret_cast<const uint8_t *>(data), reinterpret_cast<const uint8_t *>(data) + w * h * size_of(f), d);
}

void Picture::TakeDataOwnership() {
	ProfilerPerfSection section("Picture::TakeDataOwnership");
	if (has_ownership || d == nullptr)
		return;

	auto *d_ = new uint8_t[w * h * size_of(f)];
	std::copy(d, d + w * h * size_of(f), d_);

	has_ownership = 1;
	d = d_;
}

//
Picture &Picture::operator=(const Picture &pic) {
	if (pic.has_ownership)
		CopyData(pic.d, pic.w, pic.h, pic.f);
	else
		SetData(pic.d, pic.w, pic.h, pic.f);
	return *this;
}

Picture &Picture::operator=(Picture &&pic) noexcept {
	Clear();

	w = pic.w;
	h = pic.h;
	f = pic.f;

	d = pic.d;
	has_ownership = pic.has_ownership;

	pic.w = 0;
	pic.h = 0;
	pic.f = PF_RGBA32;

	pic.has_ownership = 0;
	pic.d = nullptr;

	return *this;
}

//
Picture MakePictureView(void *data, uint16_t width, uint16_t height, PictureFormat format) {
	Picture pic;
	pic.SetData(data, width, height, format);
	return pic;
}

Picture MakePicture(const void *data, uint16_t width, uint16_t height, PictureFormat format) {
	Picture pic;
	pic.CopyData(data, width, height, format);
	return pic;
}

//
void Picture::Clear() {
	w = h = 0;
	f = PF_RGBA32;

	if (has_ownership)
		delete[](reinterpret_cast<uint8_t *>(d));
	has_ownership = 0;
	d = nullptr;
}

// TODO EJ implement these
Picture Crop(const Picture &picture, uint16_t width, uint16_t height) { return picture; }
Picture Resize(const Picture &picture, uint16_t width, uint16_t height) {
	Picture pic(width, height, picture.GetFormat());
	stbir_resize_uint8(picture.GetData(), picture.GetWidth(), picture.GetHeight(), picture.GetWidth() * size_of(picture.GetFormat()), 
		pic.GetData(), pic.GetWidth(),	pic.GetHeight(), pic.GetWidth() * size_of(pic.GetFormat()), size_of(pic.GetFormat()));
	
	return pic;
}

//
Color GetPixelRGBA(const Picture &pic, uint16_t x, uint16_t y) {
	if (x >= pic.GetWidth() || y >= pic.GetHeight())
		return Color::Zero;

	Color out = Color::Zero;
	const PictureFormat &fmt = pic.GetFormat();
	const int channel_count = GetChannelCount(fmt);
	const size_t offset = (x + y * pic.GetWidth()) * size_of(fmt);
	if (fmt == PF_RGBA32F) {
		const float *in = reinterpret_cast<float *>(pic.GetData() + offset);
		for (int i = 0; i < channel_count; i++) {
			out[i] = in[i];
		}
	} else {
		const uint8_t *in = pic.GetData() + offset;
		for (int i = 0; i < channel_count; i++) {
			out[i] = in[i] / 255.f;
		}
	}

	return out;
}

void SetPixelRGBA(Picture &pic, uint16_t x, uint16_t y, const Color &col) {
	if ((x >= pic.GetWidth()) || (y >= pic.GetHeight()))
		return;

	const PictureFormat &fmt = pic.GetFormat();
	const int channel_count = GetChannelCount(fmt);
	const size_t offset = (x + y * pic.GetWidth()) * size_of(fmt);
	if (fmt == PF_RGBA32F) {
		float *out = reinterpret_cast<float *>(pic.GetData() + offset);
		for (int i = 0; i < channel_count; i++) {
			out[i] = col[i];
		}
	} else {
		uint8_t *out = pic.GetData() + offset;
		for (int i = 0; i < channel_count; i++) {
			out[i] = uint8_t(Clamp(col[i], 0.f, 1.f) * 255.f);
		}
	}
}

//
struct STB_callbacks {
	ScopedFile file;
	stbi_io_callbacks clbk;
};

STB_callbacks open_STB_file(const char *path) {
	return {Open(path), {
							[](void *user, char *data, int size) -> int { return int(Read(*reinterpret_cast<ScopedFile *>(user), data, size)); },
							[](void *user, int n) -> void { Seek(*reinterpret_cast<ScopedFile *>(user), n, SM_Current); },
							[](void *user) -> int { return IsEOF(*reinterpret_cast<ScopedFile *>(user)) ? 0 : 1; },
						}};
}

static bool load_STB_picture(Picture &pic, const char *path) {
	ProfilerPerfSection section("load_STB_picture", path);

	auto cb = open_STB_file(path);
	if (!cb.file)
		return false;

	int x, y, n;
	const auto data = stbi_load_from_callbacks(&cb.clbk, &cb.file, &x, &y, &n, 4);
	if (!data)
		return false;

	pic.CopyData(data, x, y, PF_RGBA32);

	stbi_image_free(data);
	return true;
}

bool LoadJPG(Picture &pic, const char *path) { return load_STB_picture(pic, path); }
bool LoadPNG(Picture &pic, const char *path) { return load_STB_picture(pic, path); }
bool LoadGIF(Picture &pic, const char *path) { return load_STB_picture(pic, path); }
bool LoadPSD(Picture &pic, const char *path) { return load_STB_picture(pic, path); }
bool LoadTGA(Picture &pic, const char *path) { return load_STB_picture(pic, path); }
bool LoadBMP(Picture &pic, const char *path) { return load_STB_picture(pic, path); }

bool LoadPicture(Picture &pic, const char *path) { return load_STB_picture(pic, path); }

//
static void STB_write(void *user, void *data, int size) {
	ScopedFile *file = reinterpret_cast<ScopedFile *>(user);
	if (file) {
		Write(file->f, data, size);
	}
}

bool SavePNG(const Picture &pic, const char *path) {
	ProfilerPerfSection section("SavePNG", path);

	if (!pic.GetHeight() || !pic.GetWidth())
		return false;

	ScopedFile file(OpenWrite(path));
	if (!file)
		return false;

	return stbi_write_png_to_func(
			   STB_write, &file, pic.GetWidth(), pic.GetHeight(), size_of(pic.GetFormat()), pic.GetData(), pic.GetWidth() * size_of(pic.GetFormat())) != 0;
}

bool SaveBMP(const Picture &pic, const char *path) {
	ProfilerPerfSection section("SaveBMP", path);

	if (!pic.GetHeight() || !pic.GetWidth())
		return false;

	ScopedFile file(OpenWrite(path));
	if (!file)
		return false;

	return stbi_write_bmp_to_func(STB_write, &file, pic.GetWidth(), pic.GetHeight(), size_of(pic.GetFormat()), pic.GetData()) != 0;
}

class AlignedAllocator : public bx::AllocatorI {
public:
	AlignedAllocator(bx::AllocatorI *_allocator, size_t _minAlignment) : m_allocator(_allocator), m_minAlignment(_minAlignment) {}

	virtual void *realloc(void *_ptr, size_t _size, size_t _align, const char *_file, uint32_t _line) {
		return m_allocator->realloc(_ptr, _size, bx::max(_align, m_minAlignment), _file, _line);
	}

	bx::AllocatorI *m_allocator;
	size_t m_minAlignment;
};

static bool SaveBimg(const Picture &pic, const char *path, bool fast, bimg::TextureFormat::Enum format) {
	ProfilerPerfSection section("SaveBimg", path);

	if (!pic.GetHeight() || !pic.GetWidth())
		return false;

	bx::DefaultAllocator defaultAllocator;
	AlignedAllocator allocator(&defaultAllocator, 16);

	auto input_format = bimg::TextureFormat::RGBA8;
	switch (pic.GetFormat()) {
		case PF_RGB24:
			input_format = bimg::TextureFormat::RGB8;
			break;
		case PF_RGBA32:
			input_format = bimg::TextureFormat::RGBA8;
			break;
		case PF_RGBA32F:
			input_format = bimg::TextureFormat::RGBA32F;
			break;
		default:
			assert(false);
			return false;
	}

	uint16_t depth = 1; // not sure what it is
	auto input = bimg::imageAlloc(
		&allocator, input_format, pic.GetWidth(), pic.GetHeight(), depth, 1 /*_numLayers*/, false /*_cubeMap*/, false /*_hasMips*/, pic.GetData());

	auto output = bimg::imageEncode(&allocator, format, fast ? bimg::Quality::Fastest : bimg::Quality::Highest, *input);

	bimg::imageFree(input);

	bx::FileWriter writer;
	bx::Error err;
	if (bx::open(&writer, path, false, &err)) {
		bimg::imageWriteDds(&writer, *output, output->m_data, output->m_size, &err);
	}

	bimg::imageFree(output);

	return true;
}

bool SaveBC6H(const Picture &pic, const char *path, bool fast) { return SaveBimg(pic, path, fast, bimg::TextureFormat::BC6H); }

bool SaveBC7(const Picture &pic, const char *path, bool fast) { return SaveBimg(pic, path, fast, bimg::TextureFormat::BC7); }

bool SaveTGA(const Picture &pic, const char *path) {
	ProfilerPerfSection section("SaveTGA", path);

	if (!pic.GetHeight() || !pic.GetWidth())
		return false;

	ScopedFile file(OpenWrite(path));
	if (!file)
		return false;

	return stbi_write_tga_to_func(STB_write, &file, pic.GetWidth(), pic.GetHeight(), size_of(pic.GetFormat()), pic.GetData()) != 0;
}

bool SaveHDR(const Picture &pic, const char *path) {
	ProfilerPerfSection section("SaveHDR", path);

	if (!pic.GetHeight() || !pic.GetWidth())
		return false;

	ScopedFile file(OpenWrite(path));
	if (!file)
		return false;

	int comp = 4;
	assert(pic.GetFormat() == PF_RGBA32F);
	if (pic.GetFormat() != PF_RGBA32F)
		return false;

	return stbi_write_hdr_to_func(STB_write, &file, pic.GetWidth(), pic.GetHeight(), comp, (const float *)pic.GetData()) != 0;
}

} // namespace hg
