/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#include "ExifBuilder.hpp"
#include <array>
#include <algorithm>
#include <bit>
#include <chrono>
#include <iterator>
#include <iomanip>
#include <sstream>

namespace
{
	constexpr int EXIF_HEADER_SIZE = 6;
	constexpr std::array<char, EXIF_HEADER_SIZE> EXIF_HEADER_AND_PADDING = { 'E', 'x', 'i', 'f', 0, 0 };

	constexpr int TIFF_HEADER_SIZE = 4;
	constexpr std::array<char, TIFF_HEADER_SIZE> TIFF_HEADER = { 0x4d, 0x4d, 0, 0x2a };

	constexpr bool IS_LITTLE_ENDIAN = std::endian::native == std::endian::little;

	enum class IFDTag : uint16_t
	{
		END = 0,
		MAKE = 0x010f,
		MODEL = 0x0110,
		ORIENTATION = 0x0112,
		X_RESOLUTION = 0x011a,
		Y_RESOLUTION = 0x011b,
		RESOLUTION_UNIT = 0x0128,
		SOFTWARE_USED = 0x0131,
		DATE_TAKEN = 0x0132,
		EXPOSURE_TIME = 0x829a,
		GPS_INFO_OFFSET = 0x8825,
	};

	enum class IFDGPSTag : uint16_t
	{
		LATITUDE_REF = 1,
		LATITUDE,
		LONGITUDE_REF,
		LONGITUDE,
		ALTITUDE_REF,
		ALTITUDE,
		TRACK_REF = 14,
		TRACK,
	};

	enum class IFDValueType
	{
		BYTE = 1,
		ASCII = 2,
		SHORT = 3,
		LONG_ = 4,
		RATIONAL = 5,
	};

	template<typename T>
	T EndianSwap(T value)
	{
		T result;

		char* ptr = reinterpret_cast<char*>(&value);
		std::reverse(ptr, ptr + sizeof(T));
		result = value;

		return result;
	}

	template<typename T>
	int Append(std::vector<uint8_t>& array, T value)
	{
		int offset = array.size();
		const int bytes = sizeof(T);
		char *c = (char *) &value;
		for (int i = 0; i < bytes; i++)
		{
			array.push_back(c[i]);
		}
		return offset;
	}

	int AppendU8(std::vector<uint8_t>& array, uint8_t value)
	{
		return Append<uint8_t>(array, value);
	}

	int AppendU16(std::vector<uint8_t>& array, uint16_t value)
	{
		if constexpr (IS_LITTLE_ENDIAN)
		{
			value = ::EndianSwap(value);
		}
		return Append<uint16_t>(array, value);
	}

	// no endian swap
	int AppendU32NES(std::vector<uint8_t>& array, uint32_t value)
	{
		return Append<uint32_t>(array, value);
	}

	int AppendU32(std::vector<uint8_t>& array, uint32_t value)
	{
		if constexpr (IS_LITTLE_ENDIAN)
		{
			value = ::EndianSwap(value);
		}
		return Append<uint32_t>(array, value);
	}

	template<int COUNT>
	int AppendVector(std::vector<uint8_t>& array, const std::array<char, COUNT>& values)
	{
		int offset = array.size();

		for (auto value: values)
		{
			array.push_back(value);
		}

		return offset;
	}

	int AppendVector(std::vector<uint8_t>& array, const std::vector<uint8_t>& values)
	{
		int offset = array.size();

		for (auto value: values)
		{
			array.push_back(value);
		}

		return offset;
	}

	int AppendVector(std::vector<uint8_t>& array, char* values, int count)
	{
		int offset = array.size();

		for (int i = 0; i < count; ++i)
		{
			array.push_back(values[i]);
		}

		return offset;
	}

	int AppendRational(std::vector<uint8_t>& array, const OpenVulkano::Image::RationalValue& rational)
	{
		int offset = array.size();

		AppendU32(array, rational.nominator);
		AppendU32(array, rational.denominator);

		return offset;
	}

	int AppendGPSCoords(std::vector<uint8_t>& array, const OpenVulkano::Image::GPSCoords& coords)
	{
		int offset = array.size();

		AppendU32(array, coords.degrees);
		AppendU32(array, 1);

		AppendU32(array, coords.minutes);
		AppendU32(array, 1);

		AppendU32(array, coords.seconds);
		AppendU32(array, 1);

		return offset;
	}

	void AppendTagAndValueType(std::vector<uint8_t>& array, uint16_t tag, uint16_t valueType)
	{
		AppendU16(array, tag);
		AppendU16(array, valueType);
	}

	void AddValueToU32AndEndianSwap(uint8_t *data, int valueToAdd)
	{
		uint32_t *ptr = (uint32_t *) data;
		*ptr += valueToAdd;
		*ptr = EndianSwap(*ptr);
	}

	int AppendTagValueTypeAndString(std::vector<uint8_t>& array, IFDTag tag, std::vector<uint8_t> &otherArray, const std::string& str)
	{
		AppendTagAndValueType(array, (uint16_t) tag, (uint16_t) IFDValueType::ASCII);
		AppendU32(array, str.size() + 1);
		int offset = AppendU32(array, otherArray.size() + 1);
		int offsetInData = AppendVector(otherArray, (char*) str.c_str(), str.size() + 1);
		uint32_t* ptr = (uint32_t*) (array.data() + offset);
		*ptr = offsetInData;
		return offset;
	}

	int AppendTagValueTypeAndRational(std::vector<uint8_t>& array, IFDTag tag, std::vector<uint8_t> &otherArray, const OpenVulkano::Image::RationalValue& value)
	{
		AppendTagAndValueType(array, (uint16_t) tag, (uint16_t) IFDValueType::RATIONAL);
		AppendU32(array, 1); // number of components
		int offset = AppendU32(array, otherArray.size());
		int offsetInData = AppendRational(otherArray, value);
		uint32_t* ptr = (uint32_t*) (array.data() + offset);
		*ptr = offsetInData;
		return offset;
	}

	void AppendTagValueTypeAndShort(std::vector<uint8_t>& array, IFDTag tag, uint16_t value)
	{
		AppendTagAndValueType(array, (uint16_t ) tag, (uint16_t) IFDValueType::SHORT);
		AppendU32(array, 1);
		AppendU16(array, value);
		AppendU16(array, 0); // padding
	}

	void AppendTagValueTypeAndByte(std::vector<uint8_t>& array, IFDGPSTag tag, IFDValueType valueType, uint16_t byte)
	{
		AppendTagAndValueType(array, (uint16_t) tag, (uint16_t) valueType);
		AppendU32(array, (valueType == IFDValueType::BYTE) ? 1 : 2); // 2 for N/S/E/W + \0, 1 for a single byte
		AppendU8(array, byte);
		AppendU8(array, 0); // padding
		AppendU8(array, 0); // padding
		AppendU8(array, 0); // padding
	}

	int AppendTagValueTypeAndGPSRational(std::vector<uint8_t>& array, IFDGPSTag tag, int numberOfComponents, int initialOffsetValue)
	{
		AppendTagAndValueType(array, (uint16_t) tag, (uint16_t) IFDValueType::RATIONAL);
		AppendU32(array, numberOfComponents); // number of components
		int offset = AppendU32NES(array, initialOffsetValue);
		return offset;
	}

	char CoordRefToChar(const std::variant<OpenVulkano::Image::LatitudeRef, OpenVulkano::Image::LongitudeRef>& ref)
	{
		char c = 0;
		if (std::holds_alternative<OpenVulkano::Image::LatitudeRef>(ref))
		{
			auto lat = std::get<OpenVulkano::Image::LatitudeRef>(ref);
			c = (lat == OpenVulkano::Image::LatitudeRef::NORTH ? 'N' : 'S');
		}
		else if (std::holds_alternative<OpenVulkano::Image::LongitudeRef>(ref))
		{
			auto lon = std::get<OpenVulkano::Image::LongitudeRef>(ref);
			c = (lon == OpenVulkano::Image::LongitudeRef::EAST ? 'E' : 'W');
		}
		else
		{
			throw std::runtime_error("An alternative does not contain neither LatitudeRef nor LongitudeRef!");
		}
		return c;
	}
}

namespace OpenVulkano::Image
{
	GPSCoords::GPSCoords(float decimalDegrees, bool isLatitude)
	{
		degrees = static_cast<int32_t>(decimalDegrees);

		float fractionalDegrees = decimalDegrees - degrees;
		minutes = static_cast<int32_t>(std::abs(fractionalDegrees) * 60);

		float fractionalMinutes = (std::abs(fractionalDegrees) * 60) - minutes;
		seconds = static_cast<int32_t>(fractionalMinutes * 60);

		if (isLatitude)
		{
			ref = (decimalDegrees < 0) ? LatitudeRef::SOUTH : LatitudeRef::NORTH;
		}
		else
		{
			ref = (decimalDegrees < 0) ? LongitudeRef::WEST : LongitudeRef::EAST;
		}

		degrees = std::abs(degrees);
	}

	GPSCoords::GPSCoords(int32_t degrees, int32_t minutes, int32_t seconds, LatitudeRef ref)
	    : degrees(degrees), minutes(minutes), seconds(seconds), ref(ref)
	{
	}

	GPSCoords::GPSCoords(int32_t degrees, int32_t minutes, int32_t seconds, LongitudeRef ref)
	    : degrees(degrees), minutes(minutes), seconds(seconds), ref(ref)
	{
	}

	void ExifBuilder::SetAltitude(float level)
	{
		altitudeIsAboveSeaLevel = level >= 0;
		altitude = std::abs(level);
	}

	void ExifBuilder::SetTime(std::time_t timestamp)
	{
		dateTaken = StringFromTime(timestamp);
	}

	std::vector<uint8_t> ExifBuilder::Build()
	{
		std::vector<uint8_t> result;
		std::vector<uint8_t> data; // the data that has ascii and rational values

		if (dateTaken.empty())
		{
			dateTaken = GetCurrentTimestamp();
		}

		AppendVector<EXIF_HEADER_SIZE>(result, EXIF_HEADER_AND_PADDING);
		AppendVector<TIFF_HEADER_SIZE>(result, TIFF_HEADER);

		int numberOfMainTags = 1; // 1 is for GPS Info tag
		numberOfMainTags += orientation != 0;
		numberOfMainTags += make != "";
		numberOfMainTags += model != "";
		numberOfMainTags += xResolution.nominator || xResolution.denominator;
		numberOfMainTags += yResolution.nominator || yResolution.denominator;
		numberOfMainTags += resolutionUnit != 0;
		numberOfMainTags += exposureTime.nominator || exposureTime.denominator;
		numberOfMainTags += softwareUsed != "";
		numberOfMainTags += dateTaken != "";

		AppendU32(result, 8); // Append offset to the ifd
		AppendU16(result, numberOfMainTags);

		std::vector<int> offsets;
		int gpsInfoOffset = 0;

		if (!make.empty())
		{
			offsets.push_back(AppendTagValueTypeAndString(result, IFDTag::MAKE, data, make));
		}

		if (!model.empty())
		{
			offsets.push_back(AppendTagValueTypeAndString(result, IFDTag::MODEL, data, model));
		}

		if (orientation != 0)
		{
			AppendTagValueTypeAndShort(result, IFDTag::ORIENTATION, orientation);
		}

		if (xResolution.nominator || xResolution.denominator)
		{
			offsets.push_back(AppendTagValueTypeAndRational(result, IFDTag::X_RESOLUTION, data, xResolution));
		}

		if (yResolution.nominator || yResolution.denominator)
		{
			offsets.push_back(AppendTagValueTypeAndRational(result, IFDTag::Y_RESOLUTION, data, yResolution));
		}

		if (exposureTime.nominator || exposureTime.denominator)
		{
			offsets.push_back(AppendTagValueTypeAndRational(result, IFDTag::EXPOSURE_TIME, data, exposureTime));
		}

		if (resolutionUnit != 0)
		{
			AppendTagValueTypeAndShort(result, IFDTag::RESOLUTION_UNIT, resolutionUnit);
		}

		if (!softwareUsed.empty())
		{
			offsets.push_back(AppendTagValueTypeAndString(result, IFDTag::SOFTWARE_USED, data, softwareUsed));
		}

		// NOTE(vb): For some reason windows file properties doesn't print date taken field!
		// Even though other software does provide this information without a problem
		offsets.push_back(AppendTagValueTypeAndString(result, IFDTag::DATE_TAKEN, data, dateTaken));

		// GPS Info offset
		AppendTagAndValueType(result, (uint16_t) IFDTag::GPS_INFO_OFFSET, (uint16_t) IFDValueType::LONG_);
		AppendU32(result, 1); // num components
		gpsInfoOffset = AppendU32(result, 0);

		// next ifd offset
		AppendU32(result, 0);

		int resultSize = result.size();
		AppendVector(result, data);

		// Resolve offsets
		{
			const int valueToAdd = resultSize - EXIF_HEADER_SIZE;
			for (const auto& offset: offsets)
			{
				AddValueToU32AndEndianSwap(result.data() + offset, valueToAdd);
			}
		}

		// Resolve GPS info offset
		{
			int ifdAndSubdataSize = result.size();
			uint32_t *ptr = (uint32_t *) (result.data() + gpsInfoOffset);
			*ptr = EndianSwap((uint32_t)(ifdAndSubdataSize - EXIF_HEADER_SIZE));
		}

		// Writing GPS Info structure
		int numberOfGPSInfoTags = 8;
		AppendU16(result, numberOfGPSInfoTags);
		offsets.resize(0);

		// Latitude Ref
		char latitudeRef = CoordRefToChar(latitude.ref);
		AppendTagValueTypeAndByte(result, IFDGPSTag::LATITUDE_REF, IFDValueType::ASCII, latitudeRef);

		// Latitude
		offsets.push_back(AppendTagValueTypeAndGPSRational(result, IFDGPSTag::LATITUDE, 3, 0)); // 0 * sizeof(RationalValue)

		// Longitude Ref
		char longitudeRef = CoordRefToChar(longitude.ref);
		AppendTagValueTypeAndByte(result, IFDGPSTag::LONGITUDE_REF, IFDValueType::ASCII, longitudeRef);

		// Longitude
		offsets.push_back(AppendTagValueTypeAndGPSRational(result, IFDGPSTag::LONGITUDE, 3, 24)); // 3 * sizeof(RationalValue)

		// Altitude Ref
		AppendTagValueTypeAndByte(result, IFDGPSTag::ALTITUDE_REF, IFDValueType::BYTE, altitudeIsAboveSeaLevel ? 0 : 1);

		// Altitude
		offsets.push_back(AppendTagValueTypeAndGPSRational(result, IFDGPSTag::ALTITUDE, 1, 48)); // 6 * sizeof(RationalValue)

		// Track Ref
		AppendTagValueTypeAndByte(result, IFDGPSTag::TRACK_REF, IFDValueType::ASCII, (trackRef == GPSTrackRef::TRUE_NORTH) ? 'T' : 'M');

		// Track
		offsets.push_back(AppendTagValueTypeAndGPSRational(result, IFDGPSTag::TRACK, 1, 56)); // 7 * sizeof(RationalValue)

		//

		{
			int sizeOfResultSoFar = result.size();
			const int valueToAdd = sizeOfResultSoFar - EXIF_HEADER_SIZE;
			for(const auto &offset : offsets)
			{
				AddValueToU32AndEndianSwap(result.data() + offset, valueToAdd);
			}
		}

		//

		AppendGPSCoords(result, latitude);
		AppendGPSCoords(result, longitude);

		AppendU32(result, altitude);
		AppendU32(result, 1); // denominator for altitude

		constexpr int TRACK_PRECISION = 10000;
		AppendU32(result, track * TRACK_PRECISION);
		AppendU32(result, TRACK_PRECISION);

		return result;
	}

	std::string ExifBuilder::StringFromTime(std::time_t time)
	{
		std::tm* timeInfo = std::localtime(&time);
		std::ostringstream oss;
		oss << std::put_time(timeInfo, "%Y:%m:%d %H:%M:%S");
		return oss.str();
	}

	std::string ExifBuilder::GetCurrentTimestamp()
	{
		auto now = std::chrono::system_clock::now();
		std::time_t currentTime = std::chrono::system_clock::to_time_t(now);
		return StringFromTime(currentTime);
	}
}