#include "ppsspp_config.h"

#import "draw_text_cocoa.h"

#if PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS)

#import <Foundation/Foundation.h>
#import <CoreText/CoreText.h>
#import <CoreGraphics/CoreGraphics.h>

#if PPSSPP_PLATFORM(MAC)
#import <AppKit/AppKit.h>
#define ColorType NSColor
#else
#import <UIKit/UIKit.h>
#define ColorType UIColor
#endif

#include "Common/System/Display.h"
#include "Common/GPU/thin3d.h"
#include "Common/Data/Hash/Hash.h"
#include "Common/Data/Text/WrapText.h"
#include "Common/Data/Encoding/Utf8.h"
#include "Common/Render/Text/draw_text.h"
#include "Common/Render/Text/draw_text_cocoa.h"

#include "Common/Log.h"
#include "Common/StringUtils.h"

enum {
	MAX_TEXT_WIDTH = 4096,
	MAX_TEXT_HEIGHT = 512
};

// for future OpenEmu support
#ifndef PPSSPP_FONT_BUNDLE
#define PPSSPP_FONT_BUNDLE [NSBundle mainBundle]
#endif

class TextDrawerFontContext {
public:
	TextDrawerFontContext(const FontStyle &_style, float _dpiScale) : style(_style), dpiScale(_dpiScale) {
		FontStyleFlags styleFlags = style.flags;
		std::string fontName = GetFontNameForFontStyle(style, &styleFlags);

		// Create an attributed string with string and font information
		CGFloat fontSize = ceilf((style.sizePts / dpiScale) * 1.25f);
		INFO_LOG(Log::G3D, "Creating cocoa typeface '%s' size %d (effective size %0.1f)", fontName.c_str(), style.sizePts, fontSize);

		CTFontSymbolicTraits traits = 0;
		if (styleFlags & FontStyleFlags::Bold)   traits |= kCTFontTraitBold;
		if (styleFlags & FontStyleFlags::Italic) traits |= kCTFontTraitItalic;

		CTFontRef base = CTFontCreateWithName(CFStringCreateWithCString(kCFAllocatorDefault, fontName.c_str(), kCFStringEncodingUTF8), fontSize, nil);
		CTFontRef font = CTFontCreateCopyWithSymbolicTraits(base, fontSize, NULL, traits, traits); // desired & mask
		if (!font) {
			// Skip the traits.
			font = base;
		} else {
			CFRelease(base);
		}

		_dbg_assert_(font != nil);
		// CTFontRef font = CTFontCreateUIFontForLanguage(kCTFontUIFontSystem, fontSize, nil);
		attributes = @{
			(__bridge id)kCTFontAttributeName: (__bridge id)font,
			(__bridge id)kCTForegroundColorFromContextAttributeName: (__bridge id)kCFBooleanTrue,
		};
		CFRelease(font);
	}
	~TextDrawerFontContext() {
		Destroy();
	}
	void Destroy() {
	}

	NSDictionary* attributes = nil;
	std::string fname;

	FontStyle style;
	float dpiScale;
};

TextDrawerCocoa::TextDrawerCocoa(Draw::DrawContext *draw) : TextDrawer(draw) {
	// Register fonts with CoreText
	// We only need to do this once.
	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
		std::vector<std::string> allFonts = GetAllFontFilenames();

		for (const auto &fontName : allFonts) {
			// Convert C++ string to NSString
			NSString *fontFileName = [NSString stringWithUTF8String:fontName.c_str()];

			// Get the font URL from the bundle
			NSURL *fontURL = [PPSSPP_FONT_BUNDLE URLForResource:fontFileName
													withExtension:@"ttf"
													subdirectory:@"assets"];
			if (!fontURL) {
				NSLog(@"Font URL not found for %@", fontFileName);
				continue;
			}

			// Optional: Print font descriptors for debugging
			CFArrayRef descs = CTFontManagerCreateFontDescriptorsFromURL((__bridge CFURLRef)fontURL);
			if (descs) {
				CFIndex count = CFArrayGetCount(descs);
				NSLog(@"Found %ld font descriptor(s) for %@", count, fontFileName);

				for (CFIndex i = 0; i < count; ++i) {
					CTFontDescriptorRef desc = (CTFontDescriptorRef)CFArrayGetValueAtIndex(descs, i);
					CFTypeRef attr = CTFontDescriptorCopyAttribute(desc, kCTFontNameAttribute);
					if (attr && CFGetTypeID(attr) == CFStringGetTypeID()) {
						CFStringRef name = (CFStringRef)attr;
						NSLog(@"Descriptor #%ld: %@", i, name);
						CFRelease(name);
					} else {
						NSLog(@"Descriptor #%ld: Unknown or non-string attribute", i);
						if (attr) CFRelease(attr);
					}
				}

				CFRelease(descs);
			} else {
				NSLog(@"Failed to retrieve font descriptors for %@", fontFileName);
			}

			// Register the font
			CTFontManagerRegisterFontsForURL((__bridge CFURLRef)fontURL, kCTFontManagerScopeProcess, NULL);
		}
	});
}

TextDrawerCocoa::~TextDrawerCocoa() {
	ClearCache();
	ClearFonts();
}

// TODO: Share with other backends.
void TextDrawerCocoa::SetOrCreateFont(const FontStyle &style) {
	if (style.sizePts <= 0) {
		return;
	}

	auto iter = fontMap_.find(style);
	if (iter != fontMap_.end()) {
		fontStyle_ = style;
		return;
	}

	TextDrawerFontContext *font = new TextDrawerFontContext(style, dpiScale_);

	fontMap_[style] = std::unique_ptr<TextDrawerFontContext>(font);
	fontStyle_ = style;
}

void TextDrawerCocoa::ClearFonts() {
	for (auto &iter : fontMap_) {
		iter.second->dpiScale = dpiScale_;
		iter.second->Destroy();
	}
	fontMap_.clear();
}

// AI added some error checks here.
void TextDrawerCocoa::MeasureStringInternal(std::string_view str, float *w, float *h) {
	auto iter = fontMap_.find(fontStyle_);
	NSDictionary *attributes = nil;
	if (iter != fontMap_.end()) {
		attributes = iter->second->attributes;
	}

	std::vector<std::string_view> lines;
	SplitString(str, '\n', lines);

	float extW = 0, extH = 0;
	for (auto &line : lines) {
		if (line.empty()) {
			// Handle empty lines by measuring the string "Wg" which typically has a good ascent and descent.
			NSString *emptyLineString = @"Wg";
			NSAttributedString* emptyAs = [[NSAttributedString alloc] initWithString:emptyLineString attributes:attributes];
			CTLineRef emptyCtline = CTLineCreateWithAttributedString((CFAttributedStringRef)emptyAs);
			CGFloat ascent, descent, leading;
			CTLineGetTypographicBounds(emptyCtline, &ascent, &descent, &leading);
			CFRelease(emptyCtline);
			extH += (float)(ascent + descent + leading); // Use actual line height for empty lines
			continue;
		}
		// Handle potential UTF-8 conversion failure
		NSString *string = [[NSString alloc] initWithBytes:line.data()
											 length:line.size()
											 encoding:NSUTF8StringEncoding];

		// If UTF-8 fails, fallback to Windows-1252 or lossy conversion to prevent nil
		if (!string) {
			string = [[NSString alloc] initWithBytes:line.data()
									   length:line.size()
									   encoding:NSASCIIStringEncoding];
		}
		// Skip empty or failed strings to prevent NSConcreteAttributedString crash
		if (!string) {
			continue;
		}
		NSAttributedString* as = [[NSAttributedString alloc] initWithString:string attributes:attributes];
		// Safety check for AttributedString allocation
		if (!as) {
			continue;
		}

		CTLineRef ctline = CTLineCreateWithAttributedString((CFAttributedStringRef)as);
		// Core Text safety check
		if (!ctline) {
			continue;
		}

		CGFloat ascent, descent, leading;
		double fWidth = CTLineGetTypographicBounds(ctline, &ascent, &descent, &leading);

		if (fWidth > extW)
			extW = (float)fWidth;
		extH += (float)(ascent + descent + leading); // Included leading for better vertical spacing
		CFRelease(ctline);  // Needed to avoid memory leak.
	}

	*w = ceilf(extW);
	*h = ceilf(extH);
}

bool TextDrawerCocoa::DrawStringBitmap(std::vector<uint8_t> &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, std::string_view str, int align, bool fullColor) {
	if (str.empty()) {
		bitmapData.clear();
		return false;
	}

	auto iter = fontMap_.find(fontStyle_);
	if (iter == fontMap_.end()) {
		return false;
	}

	// INFO_LOG(Log::System, "Rasterizing %.*s", (int)str.length(), str.data());

	NSString* string = [[NSString alloc] initWithBytes:str.data() length:str.length() encoding: NSUTF8StringEncoding];

	// If UTF-8 fails, fallback to ASCII to prevent nil
	if (!string) {
		string = [[NSString alloc] initWithBytes:str.data() length:str.length() encoding:NSASCIIStringEncoding];
	}
	// Skip if string creation failed
	if (!string) {
		return false;
	}

	NSDictionary* attributes = iter->second->attributes;
	NSAttributedString* as = [[NSAttributedString alloc] initWithString:string attributes:attributes];

	// Safety check for AttributedString allocation
	if (!as) {
		return false;
	}

	// Figure out how big an image we need.
	// We re-use MeasureString here.
	float w, h;
	MeasureString(str, &w, &h);
	// Reverse the DPI scale that MeasureString baked in.
	w /= (dpiScale_ * fontScaleX_);
	h /= (dpiScale_ * fontScaleY_);

	int width = (int)ceilf(w);
	int height = (int)ceilf(h);
	if (width <= 0 || height <= 0) {
		WARN_LOG(Log::G3D, "Text '%.*s' caused a zero size image", (int)str.length(), str.data());
		return false;
	}

	// Round width and height upwards to the closest multiple of 4.
	int bmWidth = (width + 1 + 3) & ~3;
	int bmHeight = (height + 1 + 3) & ~3;

	uint32_t *bitmap = new uint32_t[bmWidth * bmHeight];
	memset(bitmap, 0, bmWidth * bmHeight * 4);

	// Create the context and fill it with white background
	CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
	CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedLast;
	CGContextRef ctx = CGBitmapContextCreate(bitmap, bmWidth, bmHeight, 8, bmWidth*4, space, bitmapInfo);
	CGColorSpaceRelease(space);

	// Safety check for CGContext creation
	if (!ctx) {
		WARN_LOG(Log::G3D, "Failed to create CGBitmapContext");
		delete [] bitmap;
		return false;
	}

	// CGContextSetRGBStrokeColor(ctx, 1.0, 1.0, 1.0, 1.0); // white background
	CGContextSetStrokeColorWithColor(ctx, [ColorType whiteColor].CGColor);
	CGContextSetFillColorWithColor(ctx, [ColorType whiteColor].CGColor);

	std::vector<std::string_view> lines;
	SplitString(str, '\n', lines);

	float lineY = 0.0;
	for (std::string_view line : lines) {
		NSString *string = [[NSString alloc] initWithBytes:line.data() length:line.size() encoding: NSUTF8StringEncoding];

		// If UTF-8 fails, fallback to ASCII to prevent nil
		if (!string) {
			string = [[NSString alloc] initWithBytes:line.data() length:line.size() encoding:NSASCIIStringEncoding];
		}
		// Skip empty or failed strings to prevent crash
		if (!string) {
			continue;
		}

		NSAttributedString* as = [[NSAttributedString alloc] initWithString:string attributes:attributes];

		// Safety check for AttributedString allocation
		if (!as) {
			continue;
		}

		CTLineRef ctline = CTLineCreateWithAttributedString((CFAttributedStringRef)as);

		// Core Text safety check
		if (!ctline) {
			continue;
		}

		CGFloat ascent, descent, leading;
		double fWidth = CTLineGetTypographicBounds(ctline, &ascent, &descent, &leading);

		// Draw the text
		CGFloat x = 0.0;
		CGFloat y = bmHeight - lineY - ascent;  // from bottom???
		CGContextSetTextPosition(ctx, x, y);
		CTLineDraw(ctline, ctx);
		CFRelease(ctline);

		lineY += ascent + descent + leading;
	}

	entry.texture = nullptr;
	entry.width = width;
	entry.height = height;
	entry.bmWidth = bmWidth;
	entry.bmHeight = bmHeight;
	entry.lastUsedFrame = frameCount_;

	// data now contains the bytes in RGBA, presumably.
	// Convert the bitmap to a Thin3D compatible array of 16-bit pixels. Can't use a single channel format
	// because we need white. Well, we could using swizzle, but not all our backends support that.
	if (texFormat == Draw::DataFormat::R8G8B8A8_UNORM || texFormat == Draw::DataFormat::B8G8R8A8_UNORM) {
		bitmapData.resize(entry.bmWidth * entry.bmHeight * sizeof(uint32_t));
		uint32_t *bitmapData32 = (uint32_t *)&bitmapData[0];
		for (int y = 0; y < entry.bmHeight; y++) {
			for (int x = 0; x < entry.bmWidth; x++) {
				uint32_t color = bitmap[bmWidth * y + x];
				if (fullColor) {
					bitmapData32[entry.bmWidth * y + x] = RGBAToPremul8888(color);
				} else {
					// Don't know why we'd end up here, but let's support it.
					bitmapData32[entry.bmWidth * y + x] = AlphaToPremul8888(color);
				}
			}
		}
	} else if (texFormat == Draw::DataFormat::B4G4R4A4_UNORM_PACK16 || texFormat == Draw::DataFormat::R4G4B4A4_UNORM_PACK16) {
		_dbg_assert_(!fullColor);
		bitmapData.resize(entry.bmWidth * entry.bmHeight * sizeof(uint16_t));
		uint16_t *bitmapData16 = (uint16_t *)&bitmapData[0];
		for (int y = 0; y < entry.bmHeight; y++) {
			for (int x = 0; x < entry.bmWidth; x++) {
				uint8_t bAlpha = (uint8_t)(bitmap[bmWidth * y + x] & 0xff);
				bitmapData16[entry.bmWidth * y + x] = AlphaToPremul4444(bAlpha);
			}
		}
	} else if (texFormat == Draw::DataFormat::A4R4G4B4_UNORM_PACK16) {
		_dbg_assert_(!fullColor);
		bitmapData.resize(entry.bmWidth * entry.bmHeight * sizeof(uint16_t));
		uint16_t *bitmapData16 = (uint16_t *)&bitmapData[0];
		for (int y = 0; y < entry.bmHeight; y++) {
			for (int x = 0; x < entry.bmWidth; x++) {
				uint8_t bAlpha = (uint8_t)(bitmap[bmWidth * y + x] & 0xff);
				bitmapData16[entry.bmWidth * y + x] = AlphaToPremul4444(bAlpha);
			}
		}
	} else if (texFormat == Draw::DataFormat::R8_UNORM) {
		_dbg_assert_(!fullColor);
		bitmapData.resize(entry.bmWidth * entry.bmHeight);
		for (int y = 0; y < entry.bmHeight; y++) {
			for (int x = 0; x < entry.bmWidth; x++) {
				uint8_t bAlpha = bitmap[bmWidth * y + x] & 0xff;
				bitmapData[entry.bmWidth * y + x] = bAlpha;
			}
		}
	} else {
		_assert_msg_(false, "Bad TextDrawer format");
	}

	CGContextRelease(ctx);
	delete [] bitmap;
	return true;
}

#endif
