#include "src/rhdc/core/libpl-rhdc.hpp"

#include <thread>
#include <mutex>
#include <vector>
#include <cstring>
#include <unordered_map>
#include <algorithm>
#include <valarray>
#include <vector>
#include <QImage>
#include "src/polyfill/byteswap.hpp"
#include "src/rhdc/core/credentials.hpp"
#include "src/rhdc/web/api-helpers.hpp"
#include "src/rhdc/web/api.hpp"
#include "src/core/rate-limiter.hpp"
#include "src/core/qthread.hpp"
#include "src/core/buffer.hpp"
#include "src/core/time.hpp"

constexpr ubyte LPL_OKAY = 0;
constexpr ubyte LPL_COMMAND_NOT_FOUND = 1;
constexpr ubyte LPL_INVALID_PAYLOAD_SIZE = 2;
constexpr ubyte LPL_BROKEN_PIPE = 3;
constexpr ubyte LPL_BAD_STATE = 4;
constexpr ubyte LPL_WAIT = 5;
constexpr ubyte LPL_INVALID_PAYLOAD = 6;

enum class AsyncStatus : ubyte {
	NotStarted = 0,
	Pending = 1,
	Ready = 2,
	Error = 3,
	Retry = 4
};

enum class ImageFlags : uint32_t {
	Size_16x16 = 0x10000000u,
	Size_32x32 = 0x20000000u,
	Size_64x64 = 0x40000000u,
	Format_RGBA16 = 0x100000u,
	Format_RGBA32 = 0x200000u,
	Opt_DITHER = 0x0001u,
	Opt_NO_FILTERING = 0x0002u,
	Opt_NO_ALPHA = 0x0004u,
	Opt_ZOOM = 0x0008u,

	Mask_Size = Size_16x16 | Size_32x32 | Size_64x64,
	Mask_Format = Format_RGBA16 | Format_RGBA32,
	Mask_Opt = Opt_DITHER | Opt_NO_FILTERING | Opt_NO_ALPHA | Opt_ZOOM,
	Mask_All = Mask_Size | Mask_Format | Mask_Opt,
	Mask_None = 0u
};
DEFINE_FLAG_OPERATIONS( ImageFlags, uint32_t );

enum class RhdcLibplCommandId : ubyte {
	GET_USERNAME = 0x00,
	GET_AVATAR_16_ASYNC = 0x01,
	GET_AVATAR_32_ASYNC = 0x02,
	TRY_GET_AVATAR_ASYNC = 0x03
};

struct CachedAvatar {
	QImage image;
	AsyncStatus status;
	ubyte errorCode;

	inline CachedAvatar() : status( AsyncStatus::NotStarted ) {}
};

struct AvatarArgs {
	string username;
	ImageFlags flags;

	inline bool operator==( const AvatarArgs &other ) const {
		return username == other.username && flags == other.flags;
	}
};

class ColorF {

	private:
	std::valarray<float> m_color;

	inline ColorF( float a, float r, float g, float b ) {
		m_color = std::valarray<float>({ a, r, g, b });
	}

	inline ColorF( std::valarray<float> &&color ) {
		m_color = std::move( color );
	}

	public:
	inline ColorF() {
		m_color = std::valarray<float>({ 0.f, 0.f, 0.f, 0.f });
	}

	inline ColorF( const ColorF &other ) {
		m_color = other.m_color;
	}

	inline ColorF( ColorF &&other ) {
		m_color = std::move( other.m_color );
	}

	inline float alpha() const noexcept {
		return m_color[0];
	}

	static ColorF fromARGB32( uint pixel ) noexcept {
		return ColorF(
			(float)(pixel >> 24) / (float)0xFF,
			(float)((pixel >> 16) & 0xFF) / (float)0xFF,
			(float)((pixel >> 8) & 0xFF) / (float)0xFF,
			(float)(pixel & 0xFF) / (float)0xFF
		);
	}

	static ColorF fromRGBA5551( ushort pixel ) noexcept {
		return ColorF(
			(pixel & 1) ? 1.f : 0.f,
			(float)(pixel >> 11) / (float)0x1F,
			(float)((pixel >> 6) & 0x1F) / (float)0x1F,
			(float)((pixel >> 1) & 0x1F) / (float)0x1F
		);
	}

	ushort toRGBA5551() const noexcept {
		std::valarray<float> c = (m_color * (float)0x1f) + 0.5f;
		return(
			((ushort)std::clamp( (int)c[1], 0, 0x1F ) << 11) |
			((ushort)std::clamp( (int)c[2], 0, 0x1F ) << 6) |
			((ushort)std::clamp( (int)c[3], 0, 0x1F ) << 1) |
			((m_color[0] >= 0.5f) ? 1u : 0u)
		);
	}

	uint toARGB24( ubyte alpha ) const noexcept {
		std::valarray<float> c = (m_color * (float)0xff) + 0.5f;
		return(
			((uint)alpha << 24) |
			((uint)std::clamp( (int)c[1], 0, 0xFF ) << 16) |
			((uint)std::clamp( (int)c[2], 0, 0xFF ) << 8) |
			(uint)std::clamp( (int)c[3], 0, 0xFF )
		);
	}

	inline ColorF operator+( const ColorF &other ) const {
		return ColorF( m_color + other.m_color );
	}

	inline ColorF operator-( const ColorF &other ) const {
		return ColorF( m_color - other.m_color );
	}

	inline ColorF operator*( float x ) const {
		return ColorF( m_color * x );
	}

	inline ColorF operator/( float x ) const {
		return ColorF( m_color / x );
	}

	inline ColorF &operator+=( const ColorF &other ) {
		m_color += other.m_color;
		return *this;
	}

};

template<> struct std::hash<AvatarArgs> {
	inline size_t operator()(const AvatarArgs &id) const noexcept {
		return std::hash<string>()( id.username ) ^ std::hash<uint32_t>()( (uint32_t)id.flags );
	}
};

static std::mutex s_lock;
static int s_sessionId = 0;

static RateLimiter s_burstLimiter( 50, 1000ll );
static RateLimiter s_mainLimiter( 250, 60000ll );

static string s_username;
static bool s_usernameFetched;
static HashMap<string,CachedAvatar> s_rhdcAvatarCache;
static HashMap<AvatarArgs,Buffer> s_processedAvatarCache;

static inline bool writeHeader( const WritableFifoPipe &pipe, ubyte status1, ubyte status2, ushort payloadSize, std::error_code &err ) {
	const ubyte header[4] = { status1, status2, (ubyte)(payloadSize >> 8), (ubyte)(payloadSize & 0xff) };
	return pipe.write( 4, header, err );
}

void clearRhdcLibplCache() {
	const std::lock_guard lock( s_lock );
	s_sessionId++;
	s_username.clear();
	s_processedAvatarCache.clear();
	s_rhdcAvatarCache.clear();
	s_usernameFetched = false;
	s_burstLimiter.clear();
	s_mainLimiter.clear();
}


static bool handleGetMyUsernameRequest( const WritableFifoPipe &pipe, std::error_code &err ) {
	const std::lock_guard lock( s_lock );

	if( !s_usernameFetched ) {
		s_username = ApiUtil::identity().username;
		if( s_username.empty() ) {
			s_username = RhdcCredentials::load().username;
		}
		s_usernameFetched = true;
	}

	if( s_username.empty() ) {
		return writeHeader( pipe, LPL_OKAY, 1, 0, err );
	}

	if( !writeHeader( pipe, LPL_OKAY, 0, (ushort)s_username.length() + 1, err ) ) return false;
	return pipe.write( s_username.length() + 1, s_username.c_str(), err );
}

static bool handleGetAvatarRequest(
	const WritableFifoPipe &pipe,
	const AvatarArgs &args,
	std::error_code &err
) {
	for( char c : args.username ) {
		if( c == '-' || c == '_' ) continue;
		if( c >= 'a' && c <= 'z' ) continue;
		if( c >= 'A' && c <= 'Z' ) continue;
		if( c >= '0' && c <= '9' ) continue;
		return writeHeader( pipe, LPL_INVALID_PAYLOAD, 1, 0, err );
	}

	const std::lock_guard lock( s_lock );
	if( s_processedAvatarCache.count( args ) != 0 ) {
		const Buffer &avatar = s_processedAvatarCache.at( args );
		if( !writeHeader( pipe, LPL_OKAY, 0, (ushort)avatar.size(), err ) ) return false;
		return pipe.write( avatar.size(), avatar.data(), err );
	} else if( s_rhdcAvatarCache.count( args.username ) != 0 ) {
		CachedAvatar &source = s_rhdcAvatarCache.at( args.username );
		if( source.status == AsyncStatus::Ready ) {
			if( (args.flags & ~ImageFlags::Mask_All) != ImageFlags::Mask_None ) {
				return writeHeader( pipe, LPL_INVALID_PAYLOAD, 0, 0, err );
			}

			int avatarSize;
			switch( args.flags & ImageFlags::Mask_Size ) {
				case ImageFlags::Size_16x16: avatarSize = 16; break;
				case ImageFlags::Size_32x32: avatarSize = 32; break;
				case ImageFlags::Size_64x64: avatarSize = 64; break;
				default: return writeHeader( pipe, LPL_INVALID_PAYLOAD, 0, 0, err );
			}

			QImage avatar = source.image.scaled(
				avatarSize,
				avatarSize,
				Flags::has( args.flags, ImageFlags::Opt_ZOOM ) ? Qt::KeepAspectRatioByExpanding : Qt::KeepAspectRatio,
				Flags::has( args.flags, ImageFlags::Opt_NO_FILTERING ) ? Qt::FastTransformation : Qt::SmoothTransformation
			);

			if( avatar.width() != avatarSize || avatar.height() != avatarSize ) {
				avatar = avatar.copy( (avatar.width() - avatarSize) / 2, (avatar.height() - avatarSize) / 2, avatarSize, avatarSize );
			}

#if QT_VERSION >= QT_VERSION_CHECK( 5, 13, 0 )
			avatar.convertTo( QImage::Format_ARGB32 );
#else
			avatar = avatar.convertToFormat( QImage::Format_ARGB32 );
#endif

			uint *pixels = (uint*)avatar.bits();
			if( Flags::has( args.flags, ImageFlags::Opt_NO_ALPHA ) ) {
				// Replace alpha transparency with black
				for( int i = 0; i < avatarSize * avatarSize; i++ ) {
					switch( pixels[i] >> 24 ) {
						case 0xff:
							continue;
						case 0x00:
							pixels[i] = 0xFF000000u;
							break;
						default:
							const ColorF c = ColorF::fromARGB32( pixels[i] );
							pixels[i] = (c * c.alpha()).toARGB24( 0xff );
							break;
					}

				}
			} else {
				// Set the colour of fully transparent pixels bordering visible pixels to match the colour of their neighbours.
				// This fixes unwanted outlines when using texture filtering on the N64 with transparent textures.
				for( int i = 0; i < avatarSize * avatarSize; i++ ) {
					if( (pixels[i] >> 24) != 0 ) continue;
					const int x = i % avatarSize;
					const int y = i / avatarSize;

					ColorF c;
					int samples = 0;

					if( x > 0 && (pixels[i-1] >> 24) != 0u ) {
						c += ColorF::fromARGB32( pixels[i-1] );
						samples++;
					}

					if( x < avatarSize - 1 && (pixels[i+1] >> 24) != 0u ) {
						c += ColorF::fromARGB32( pixels[i+1] );
						samples++;
					}

					if( y > 0 && (pixels[i-avatarSize] >> 24) != 0u ) {
						c += ColorF::fromARGB32( pixels[i-avatarSize] );
						samples++;
					}

					if( y < avatarSize - 1 && (pixels[i+avatarSize] >> 24) != 0u ) {
						c += ColorF::fromARGB32( pixels[i+avatarSize] );
						samples++;
					}

					if( samples != 0 ) {
						pixels[i] = (c / (float)samples).toARGB24( 0 );
						continue;
					}

					if( x > 0 && y > 0 && (pixels[i-1-avatarSize] >> 24) != 0u ) {
						c += ColorF::fromARGB32( pixels[i-1-avatarSize] );
						samples++;
					}

					if( x < avatarSize - 1 && y > 0 && (pixels[i+1-avatarSize] >> 24) != 0u ) {
						c += ColorF::fromARGB32( pixels[i+1-avatarSize] );
						samples++;
					}

					if( x > 0 && y < avatarSize - 1 && (pixels[i-1+avatarSize] >> 24) != 0u ) {
						c += ColorF::fromARGB32( pixels[i-1+avatarSize] );
						samples++;
					}

					if( x < avatarSize - 1 && y < avatarSize && (pixels[i+1+avatarSize] >> 24) != 0u ) {
						c += ColorF::fromARGB32( pixels[i+1+avatarSize] );
						samples++;
					}

					if( samples == 0 ) continue;
					pixels[i] = (c / (float)samples).toARGB24( 0 );
				}
			}

			Buffer avatarData;
			if( Flags::has( args.flags, ImageFlags::Format_RGBA16 ) ) {
				// Convert to RGBA5551
				avatarData = Buffer( (size_t)avatar.sizeInBytes() >> 1 );
				if( Flags::has( args.flags, ImageFlags::Opt_DITHER ) ) {
					std::vector<ColorF> colors;
					colors.reserve( avatarSize * avatarSize );
					for( int i = 0; i < avatarSize * avatarSize; i++ ) {
						colors.push_back( ColorF::fromARGB32( pixels[i] ) );
					}

					for( int i = 0; i < avatarSize * avatarSize; i++ ) {
						const ushort c = colors[i].toRGBA5551();
						const ColorF diff = colors[i] - ColorF::fromRGBA5551( c );
						const int x = i % avatarSize;
						const int y = i / avatarSize;

						if( x != avatarSize - 1 ) colors[i+1] += diff * (7.f / 16.f);
						if( y != avatarSize - 1 ) {
							if( x != 0 ) colors[i+avatarSize-1] += diff * (3.f / 16.f);
							colors[i+avatarSize] += diff * (5.f / 16.f);
							if( x != avatarSize - 1) colors[i+avatarSize+1] += diff * (1.f / 16.f);
						}

						((ushort*)avatarData.data())[i] = htons( c );
					}
				} else {
					for( int i = 0; i < avatarSize * avatarSize; i++ ) {
						((ushort*)avatarData.data())[i] = htons( ColorF::fromARGB32( pixels[i] ).toRGBA5551() );
					}
				}
			} else {
				avatarData = Buffer( (size_t)avatar.sizeInBytes() );
				for( size_t i = 0; i < (avatarData.size() >> 2); i++ ) {
					((uint*)avatarData.data())[i] = htonl( (pixels[i] >> 24) | ((pixels[i] & 0x00FFFFFFu) << 8) );
				}
			}

			if( !writeHeader( pipe, LPL_OKAY, 0, (ushort)avatarData.size(), err ) ) return false;
			if( !pipe.write( avatarData.size(), avatarData.data(), err ) ) return false;
			s_processedAvatarCache[args] = std::move( avatarData );
			return true;
		}

		switch( source.status ) {
			case AsyncStatus::NotStarted:
				break;
			case AsyncStatus::Pending:
				return writeHeader( pipe, LPL_WAIT, 0, 0, err );
			case AsyncStatus::Retry:
				source.status = AsyncStatus::Ready;
				[[fallthrough]];
			default:
				return writeHeader( pipe, LPL_OKAY, source.errorCode, 0, err );
		}
	}

	if( !s_burstLimiter.check() ) {
		return writeHeader( pipe, LPL_OKAY, 4, 0, err );
	} else if( !s_mainLimiter.check() ) {
		s_burstLimiter.pop();
		return writeHeader( pipe, LPL_OKAY, 4, 0, err );
	}

	const int sessionId = s_sessionId;
	const string username = args.username;
	QtThread::safeAsync( [username, sessionId]() {
		s_rhdcAvatarCache[username].status = AsyncStatus::Pending;
		RhdcApi::getAvatarAsync(
			username,
			[=](const QByteArray &imgData) {
				const std::lock_guard lock( s_lock );
				if( sessionId != s_sessionId ) return;
				if( s_rhdcAvatarCache[username].image.loadFromData( imgData, "PNG" ) ) {
					s_rhdcAvatarCache[username].status = AsyncStatus::Ready;
					s_rhdcAvatarCache[username].errorCode = 0;
				} else {
					s_rhdcAvatarCache[username].status = AsyncStatus::Error;
					s_rhdcAvatarCache[username].errorCode = 3;
				}
			},
			[=](ApiErrorType error) {
				const std::lock_guard lock( s_lock );
				if( sessionId != s_sessionId ) return;
				if( error == ApiErrorType::NotFound ) {
					s_rhdcAvatarCache[username].status = AsyncStatus::Error;
					s_rhdcAvatarCache[username].errorCode = 1;
				} else {
					s_rhdcAvatarCache[username].status = AsyncStatus::Retry;
					s_rhdcAvatarCache[username].errorCode = 2;
				}
			}
		);
	});

	return writeHeader( pipe, LPL_WAIT, 0, 0, err );
}

bool handleRhdcLibplRequest(
	ubyte commandId,
	ushort payloadSize,
	const ubyte *payload,
	const WritableFifoPipe &pipe,
	std::error_code &err
) {
	switch( (RhdcLibplCommandId)commandId ) {
		case RhdcLibplCommandId::GET_USERNAME:
			return handleGetMyUsernameRequest( pipe, err );
		case RhdcLibplCommandId::GET_AVATAR_16_ASYNC:
			if( payloadSize == 0 ) return handleGetAvatarRequest( pipe, AvatarArgs{ "self", ImageFlags::Size_32x32 | ImageFlags::Format_RGBA16 | ImageFlags::Opt_ZOOM }, err );
			if( payloadSize > 30 ) return writeHeader( pipe, LPL_INVALID_PAYLOAD_SIZE, 1, 0, err );
			return handleGetAvatarRequest( pipe, AvatarArgs{ string( (const char*)payload, payloadSize ), ImageFlags::Size_32x32 | ImageFlags::Format_RGBA16 | ImageFlags::Opt_ZOOM }, err );
		case RhdcLibplCommandId::GET_AVATAR_32_ASYNC:
			if( payloadSize == 0 ) return handleGetAvatarRequest( pipe, AvatarArgs{ "self", ImageFlags::Size_32x32 | ImageFlags::Format_RGBA32 | ImageFlags::Opt_ZOOM }, err );
			if( payloadSize > 30 ) return writeHeader( pipe, LPL_INVALID_PAYLOAD_SIZE, 1, 0, err );
			return handleGetAvatarRequest( pipe, AvatarArgs{ string( (const char*)payload, payloadSize ), ImageFlags::Size_32x32 | ImageFlags::Format_RGBA32 | ImageFlags::Opt_ZOOM }, err );
		case RhdcLibplCommandId::TRY_GET_AVATAR_ASYNC:
			if( payloadSize < 5 || payloadSize > 34 ) return writeHeader( pipe, LPL_INVALID_PAYLOAD_SIZE, 1, 0, err );
			return handleGetAvatarRequest( pipe, AvatarArgs{ string( (const char*)&payload[4], payloadSize - 4 ), (ImageFlags)ntohl( *(const uint*)payload ) }, err );
		default:
			return writeHeader( pipe, LPL_COMMAND_NOT_FOUND, 0, 0, err );
	}
}
