#include "gpu/gpu_types.h"
#include "gpu/gpu_renderer.h"
#include "log/log.h"
#include "math/nmath.h"
#include "memory/memory.h"

using namespace GS;
using namespace GS::GPU;

//------------------------------------------------------------------------------
Render::Texture *Renderer::CreateRenderTexture(
	const char *name,
	uint width,
	uint height,
	Render::Texture::Format format,
	Render::Texture::AA aa
)
{
	__NTRACE("CreateRenderTexture")
	// __LOG_W__ << "CreateRenderTexture: name=" << name << ", size=" << width << "x" << height << "\n";

	Render::Texture *tex = NewTexture(name);
	if (!tex)
	{
		__LOG_E__ << "CreateRenderTexture: NewTexture failed!\n";
		return nullptr;
	}
	// __LOG_W__ << "CreateRenderTexture: NewTexture succeeded, tex ptr=" << (void*)tex << "\n";

	const Render::Texture::Usage usage =
		Render::Texture::Usage(
			Render::Texture::IsRenderTarget |
			Render::Texture::IsShaderResource
		);

	// __LOG_W__ << "CreateRenderTexture: Calling tex->Create with format=" << format << ", aa=" << aa << "\n";
	if (tex->Create(nullptr, width, height, format, aa, usage))
	{
		// __LOG_W__ << "CreateRenderTexture: Success! Returning texture.\n";
		return tex;
	}

	__LOG_E__ << "CreateRenderTexture: tex->Create failed!\n";
	tex->Free();
	return nullptr;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
void Renderer::ClearTexture(Render::Texture *target, const Color &clear_color)
{
	__NTRACE("ClearTexture")
	// __LOG_W__ << "ClearTexture: Clearing target texture with color (" << clear_color.x << ", " << clear_color.y << ", " << clear_color.z << ", " << clear_color.w << ")\n";

	if (!target)
	{
		__LOG_E__ << "ClearTexture: target is null!\n";
		return;
	}

	const uint tex_w = target->GetWidth();
	const uint tex_h = target->GetHeight();

	// Use DrawSplineToTexture with clear_target=true to clear the texture
	// This is a workaround since Blit() doesn't work on RenderTarget textures
	// __LOG_W__ << "ClearTexture: Using DrawSplineToTexture method with clear...\n";

	// Create a dummy 2-point array (not used since we're clearing)
	Array<Vector2> dummy_points;
	dummy_points.Allocate(2);
	dummy_points[0] = Vector2(0, 0);
	dummy_points[1] = Vector2(1, 1);

	// Call DrawSplineToTexture with clear_target=true and zero width (no drawing)
	// This will fill the buffer with the clear color
	const uint pitch = tex_w * 4;
	const size_t buffer_size = pitch * tex_h;
	unsigned char *pixels = new unsigned char[buffer_size];

	// Convert color to 8-bit RGBA
	const auto r = (unsigned char)(clear_color.x * 255.0f);
	const auto g = (unsigned char)(clear_color.y * 255.0f);
	const auto b = (unsigned char)(clear_color.z * 255.0f);
	const auto a = (unsigned char)(clear_color.w * 255.0f);

	// __LOG_W__ << "ClearTexture: Filling buffer with RGBA(" << (int)r << ", " << (int)g << ", " << (int)b << ", " << (int)a << ")...\n";

	// Fill with clear color (ABGR format)
	unsigned int pixel_value = (a << 24) | (b << 16) | (g << 8) | r;
	unsigned int *pixel_buffer = (unsigned int *)pixels;
	for (uint i = 0; i < tex_w * tex_h; ++i)
	{
		pixel_buffer[i] = pixel_value;
	}

	// __LOG_W__ << "ClearTexture: Uploading via Blit...\n";
	target->Blit((const char *)pixels, tex_w, tex_h, 0, 0, Render::Texture::FormatRGBA8);

	delete[] pixels;

	// __LOG_W__ << "ClearTexture: Complete!\n";
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
void Renderer::BlitTextureToTexture(
	Render::Texture *src,
	Render::Texture *dst,
	const fRect *src_rect,
	const fRect *dst_rect
)
{
	__NTRACE("BlitTextureToTexture")

	if (!src || !dst)
		return;

	// Default rectangles (full texture)
	const fRect src_r = src_rect ? *src_rect : fRect(0, 0, 1, 1);
	const fRect dst_r = dst_rect ? *dst_rect : fRect(0, 0, 1, 1);

	// Save current state
	const fRect old_viewport = viewport;

	// Setup FBO for destination
	fx_fbo->SetColorTexture(dst);
	fx_fbo->SetDepthTexture(nullptr);
	SetCurrentFBO(fx_fbo);
	SetViewport(fRect(0, 0, (float)dst->GetWidth(), (float)dst->GetHeight()));

	// Use existing RenderFullscreenQuad infrastructure
	RenderFullscreenQuad(*single_texture_program, src_r, dst_r, src);

	// Restore state
	SetCurrentFBO(nullptr);
	SetViewport(old_viewport);
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
Vector2 Renderer::CatmullRomInterpolate(
	const Vector2 &p0,
	const Vector2 &p1,
	const Vector2 &p2,
	const Vector2 &p3,
	float t
) const
{
	// Catmull-Rom spline with tau = 0.5
	const float t2 = t * t;
	const float t3 = t2 * t;

	// Manual calculation for Vector2
	Vector2 result;
	result.x = 0.5f * (
		(2.0f * p1.x) +
		(-p0.x + p2.x) * t +
		(2.0f * p0.x - 5.0f * p1.x + 4.0f * p2.x - p3.x) * t2 +
		(-p0.x + 3.0f * p1.x - 3.0f * p2.x + p3.x) * t3
	);
	result.y = 0.5f * (
		(2.0f * p1.y) +
		(-p0.y + p2.y) * t +
		(2.0f * p0.y - 5.0f * p1.y + 4.0f * p2.y - p3.y) * t2 +
		(-p0.y + 3.0f * p1.y - 3.0f * p2.y + p3.y) * t3
	);
	return result;
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
void Renderer::TessellateSplineRibbon(
	const Array<Vector2> &control_points,
	float width_normalized,
	const Color &color,
	int samples_per_segment,
	bool soft_edge,
	SplineRibbon &out_ribbon
)
{
	__NTRACE("TessellateSplineRibbon")

	const int num_control = control_points.GetCount();
	if (num_control < 2)
		return;

	// For Catmull-Rom: we have num_control-1 segments
	const int num_segments = num_control - 1;
	if (num_segments < 1)
		return;

	// Allocate arrays for sampled points
	const int total_samples = num_segments * samples_per_segment + 1;
	Array<Vector2> samples;
	Array<Vector2> tangents;
	samples.Allocate(total_samples);
	tangents.Allocate(total_samples);

	// Sample the spline using Catmull-Rom interpolation
	int sample_idx = 0;
	for (int seg = 0; seg < num_segments; seg++)
	{
		// Get 4 control points (with duplication at boundaries)
		Vector2 p0 = (seg > 0) ? control_points[seg - 1] : control_points[seg];
		Vector2 p1 = control_points[seg];
		Vector2 p2 = control_points[seg + 1];
		Vector2 p3 = (seg < num_segments - 1) ? control_points[seg + 2] : control_points[seg + 1];

		for (int s = 0; s < samples_per_segment; s++)
		{
			const float t = (float)s / (float)samples_per_segment;
			samples[sample_idx] = CatmullRomInterpolate(p0, p1, p2, p3, t);

			// Compute tangent (derivative approximation)
			const float dt = 0.01f;
			const float t_next = (t + dt > 1.0f) ? 1.0f : t + dt;
			Vector2 p_next = CatmullRomInterpolate(p0, p1, p2, p3, t_next);
			Vector2 diff = p_next - samples[sample_idx];
			tangents[sample_idx] = diff.Normalized();

			sample_idx++;
		}
	}

	// Add final point
	samples[total_samples - 1] = control_points[num_control - 1];
	tangents[total_samples - 1] = tangents[total_samples - 2]; // Reuse last tangent

	// Build ribbon geometry
	out_ribbon.positions.Allocate(total_samples * 2);
	out_ribbon.colors.Allocate(total_samples * 2);

	for (int i = 0; i < total_samples; i++)
	{
		const Vector2 pos = samples[i];
		const Vector2 tangent = tangents[i];

		// Perpendicular normal (2D rotation by 90 degrees)
		Vector2 normal(-tangent.y, tangent.x);

		// Convert from normalized [0..1] texture space to clip space [-1..1]
		const float x_clip = pos.x * 2.0f - 1.0f;
		const float y_clip = 1.0f - pos.y * 2.0f;  // Flip Y (texture origin top-left)

		// Offset in clip space
		const Vector2 offset = normal * width_normalized;

		// Left and right vertices
		out_ribbon.positions[i * 2 + 0] = Vector4(
			x_clip - offset.x,
			y_clip - offset.y,
			0.0f,
			1.0f
		);
		out_ribbon.positions[i * 2 + 1] = Vector4(
			x_clip + offset.x,
			y_clip + offset.y,
			0.0f,
			1.0f
		);

		// Colors with soft edge via alpha gradient
		if (soft_edge)
		{
			// Apply alpha gradient on outer edges (30% opacity) vs center (full opacity)
			// But since we're using a ribbon with 2 vertices per sample, both get same alpha
			// To get true soft edge, we'd need a center vertex too, but for now just use the color as-is
			// TODO: Implement true soft edge with centerline vertex if needed
			out_ribbon.colors[i * 2 + 0] = color;
			out_ribbon.colors[i * 2 + 1] = color;
		}
		else
		{
			out_ribbon.colors[i * 2 + 0] = color;
			out_ribbon.colors[i * 2 + 1] = color;
		}
	}

	// Build indices (triangle list)
	const int num_quads = total_samples - 1;
	out_ribbon.indices.Allocate(num_quads * 6);

	for (int i = 0; i < num_quads; i++)
	{
		const ushort i0 = (ushort)(i * 2);
		const ushort i1 = (ushort)(i * 2 + 1);
		const ushort i2 = (ushort)((i + 1) * 2);
		const ushort i3 = (ushort)((i + 1) * 2 + 1);

		// Triangle 1
		out_ribbon.indices[i * 6 + 0] = i0;
		out_ribbon.indices[i * 6 + 1] = i2;
		out_ribbon.indices[i * 6 + 2] = i1;

		// Triangle 2
		out_ribbon.indices[i * 6 + 3] = i1;
		out_ribbon.indices[i * 6 + 4] = i2;
		out_ribbon.indices[i * 6 + 5] = i3;
	}
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
// Helper: Draw a simple line to texture pixel data (Bresenham-like)
static void DrawLineToPixels(unsigned char *pixels, uint width, uint height, uint pitch,
                              int x0, int y0, int x1, int y1,
                              unsigned char r, unsigned char g, unsigned char b, unsigned char a)
{
    const int dx = abs(x1 - x0);
    const int dy = abs(y1 - y0);
    const int sx = (x0 < x1) ? 1 : -1;
    const int sy = (y0 < y1) ? 1 : -1;
    int err = dx - dy;

    int x = x0, y = y0;
    while (true)
    {
        if (x >= 0 && x < (int)width && y >= 0 && y < (int)height)
        {
            int *pixel = (int*)(pixels + y * pitch + x * 4);

            // Read destination pixel components (packed as in existing code: ABGR)
            const unsigned int dst = *pixel;
            const unsigned char dst_r = (unsigned char)(dst & 0xFF);
            const unsigned char dst_g = (unsigned char)((dst >> 8) & 0xFF);
            const unsigned char dst_b = (unsigned char)((dst >> 16) & 0xFF);
            const unsigned char dst_a = (unsigned char)((dst >> 24) & 0xFF);

            // Source alpha (0..1)
            const float src_af = (float)a / 255.0f;
            const float inv = 1.0f - src_af;

            // Standard 'over' compositing: out = src*src_a + dst*(1-src_a)
            const unsigned char out_r = (unsigned char)(r * src_af + dst_r * inv + 0.5f);
            const unsigned char out_g = (unsigned char)(g * src_af + dst_g * inv + 0.5f);
            const unsigned char out_b = (unsigned char)(b * src_af + dst_b * inv + 0.5f);
            const unsigned char out_a = (unsigned char)(a + dst_a * inv + 0.5f);

            *pixel = (out_a << 24) | (out_b << 16) | (out_g << 8) | out_r;  // ABGR packing (kept as original)
        }

        if (x == x1 && y == y1) break;

        const int e2 = 2 * err;
        if (e2 > -dy) { err -= dy; x += sx; }
        if (e2 < dx)  { err += dx; y += sy; }
    }
}

void Renderer::DrawSplineToTexture(
    Render::Texture *target,
    const Array<Vector2> &control_points,
    float width_pixels,
    const Color &color,
    int samples_per_segment,
    bool clear_target,
    bool soft_edge,
    const Color *border_color,
    float border_width,
    float margin
)
{
    __NTRACE("DrawSplineToTexture")
    // __LOG_W__ << "Drawing spline to texture (CPU-based method)...\n";

    if (!target) {
        __LOG_E__ << "DrawSplineToTexture: target texture is null!\n";
        return;
    }

    if (control_points.GetCount() < 2) {
        __LOG_E__ << "DrawSplineToTexture: not enough control points!\n";
        return;
    }

    const uint tex_w = target->GetWidth();
    const uint tex_h = target->GetHeight();
    // __LOG_W__ << "Target texture size: " << tex_w << "x" << tex_h << "\n";

    // Convert color to 8-bit RGBA
    const auto r = (unsigned char)(color.x * 255.0f);
    const auto g = (unsigned char)(color.y * 255.0f);
    const auto b = (unsigned char)(color.z * 255.0f);
    const auto a = (unsigned char)(color.w * 255.0f);

    // __LOG_W__ << "Color: R=" << (int)r << " G=" << (int)g << " B=" << (int)b << " A=" << (int)a << "\n";

    // Convert border color if provided
    unsigned char border_r = 0, border_g = 0, border_b = 0, border_a = 0;
    if (border_color && border_width > 0.0f) {
        border_r = (unsigned char)(border_color->x * 255.0f);
        border_g = (unsigned char)(border_color->y * 255.0f);
        border_b = (unsigned char)(border_color->z * 255.0f);
        border_a = (unsigned char)(border_color->w * 255.0f);
        // __LOG_W__ << "Border Color: R=" << (int)border_r << " G=" << (int)border_g << " B=" << (int)border_b << " A=" << (int)border_a << " Width=" << border_width << "\n";
    }

    const int num_control = control_points.GetCount();
    const int num_segments = num_control - 1;

    // Adjust control points with margin to avoid clipping
    Array<Vector2> adjusted_points;
    adjusted_points.Allocate(num_control);

    if (margin > 0.0f) {
        // __LOG_W__ << "Applying margin: " << margin << "\n";

        // Calculate the margin in normalized coordinates [0..1]
        const float margin_x = margin / (float)tex_w;
        const float margin_y = margin / (float)tex_h;

        // Scale and offset control points to fit within margin bounds
        float min_x = 1.0f, max_x = 0.0f, min_y = 1.0f, max_y = 0.0f;
        for (int i = 0; i < num_control; i++) {
            if (control_points[i].x < min_x) min_x = control_points[i].x;
            if (control_points[i].x > max_x) max_x = control_points[i].x;
            if (control_points[i].y < min_y) min_y = control_points[i].y;
            if (control_points[i].y > max_y) max_y = control_points[i].y;
        }

        const float range_x = max_x - min_x;
        const float range_y = max_y - min_y;
        const float scale_x = (range_x > 0.0f) ? (1.0f - 2.0f * margin_x) / range_x : 1.0f;
        const float scale_y = (range_y > 0.0f) ? (1.0f - 2.0f * margin_y) / range_y : 1.0f;

        for (int i = 0; i < num_control; i++) {
            adjusted_points[i].x = margin_x + (control_points[i].x - min_x) * scale_x;
            adjusted_points[i].y = margin_y + (control_points[i].y - min_y) * scale_y;
        }
    } else {
        // No margin, use original points
        for (int i = 0; i < num_control; i++) {
            adjusted_points[i] = control_points[i];
        }
    }

    Array<Vector2> samples;
    samples.Allocate(num_segments * samples_per_segment + 1);

    // Sample the spline using Catmull-Rom interpolation with adjusted points
    int sample_idx = 0;
    for (int seg = 0; seg < num_segments; seg++)
    {
        Vector2 p0 = (seg > 0) ? adjusted_points[seg - 1] : adjusted_points[seg];
        Vector2 p1 = adjusted_points[seg];
        Vector2 p2 = adjusted_points[seg + 1];
        Vector2 p3 = (seg < num_segments - 1) ? adjusted_points[seg + 2] : adjusted_points[seg + 1];

        for (int s = 0; s < samples_per_segment; s++)
        {
            const float t = (float)s / (float)samples_per_segment;
            samples[sample_idx] = CatmullRomInterpolate(p0, p1, p2, p3, t);
            sample_idx++;
        }
    }
    samples[num_segments * samples_per_segment] = adjusted_points[num_control - 1];

    // __LOG_W__ << "Generated " << samples.GetCount() << " sampled points\n";

    // Create pixel buffer for drawing
    // __LOG_W__ << "Creating pixel buffer for CPU rendering...\n";

    const uint pitch = tex_w * 4;  // RGBA = 4 bytes per pixel
    const size_t buffer_size = pitch * tex_h;
    unsigned char *pixels = new unsigned char[buffer_size];

    // __LOG_W__ << "Pixel buffer created. Size=" << buffer_size << " Pitch=" << pitch << "\n";

    if (clear_target)
    {
        // Initialize with transparent black
        memset(pixels, 0, buffer_size);
    }
    else
    {
        // Read back the existing texture contents into the buffer so we can composite on top.
        // __LOG_W__ << "Reading existing texture pixels into buffer (for compositing)...\n";

        // Setup FBO for readback
        fx_fbo->SetColorTexture(target);
        fx_fbo->SetDepthTexture(nullptr);
        SetCurrentFBO(fx_fbo);

        fx_fbo->ReadColorPixels((char*)pixels, 0, 0, tex_w, tex_h);

        // Restore
        SetCurrentFBO(nullptr);
    }

    // __LOG_W__ << "Drawing spline lines...\n";

    // Draw border first (if specified)
    if (border_color && border_width > 0.0f) {
        // __LOG_W__ << "Drawing border with width: " << border_width << "\n";
        const float total_width = width_pixels + border_width * 2.0f;

        for (uint i = 0; i + 1 < samples.GetCount(); i++)
        {
            const int x0 = (int)(samples[i].x * (float)tex_w);
            const int y0 = (int)(samples[i].y * (float)tex_h);
            const int x1 = (int)(samples[i + 1].x * (float)tex_w);
            const int y1 = (int)(samples[i + 1].y * (float)tex_h);

            for (int w = -(int)total_width/2; w <= (int)total_width/2; w++)
            {
                DrawLineToPixels(pixels, tex_w, tex_h, pitch,
                    x0 + w, y0, x1 + w, y1, border_r, border_g, border_b, border_a);
            }
        }
    }

    // Draw main spline on top
    for (uint i = 0; i + 1 < samples.GetCount(); i++)
    {
        // Convert from normalized [0..1] to pixel coordinates
        const int x0 = (int)(samples[i].x * (float)tex_w);
        const int y0 = (int)(samples[i].y * (float)tex_h);
        const int x1 = (int)(samples[i + 1].x * (float)tex_w);
        const int y1 = (int)(samples[i + 1].y * (float)tex_h);

        // Draw line with width by drawing multiple parallel lines
        for (int w = -(int)width_pixels/2; w <= (int)width_pixels/2; w++)
        {
            DrawLineToPixels(pixels, tex_w, tex_h, pitch,
                x0 + w, y0, x1 + w, y1, r, g, b, a);
        }
    }

    // __LOG_W__ << "Uploading pixel buffer to texture...\n";
    // Upload to texture using Blit
    target->Blit((const char *)pixels, tex_w, tex_h, 0, 0, Render::Texture::FormatRGBA8);

    // __LOG_W__ << "Freeing pixel buffer...\n";
    delete[] pixels;
    // __LOG_W__ << "Buffer freed!\n";

    // __LOG_W__ << "DrawSplineToTexture complete!\n";
}
//------------------------------------------------------------------------------