// Copyright (c) 2026 - 2026 kio@little-bat.de
// BSD-2-Clause license
// https://opensource.org/licenses/BSD-2-Clause

#include "Console.h"
#include "Devices/FileSystem.h"
#include "Graphics/ColorMap.h"
#include "Graphics/Printer<Canvas>.h"
#include "Graphics/gif/GifDecoder.h"
#include "Graphics/img/ImageDecoder.h"
#include "Video/FrameBuffer<Pixmap>.h"
#include "Video/HamImageInfo.h"
#include "Video/HorizontalLayout.h"
#include "Video/MultiSpritesPlane.h"
#include "Video/VerticalLayout.h"
#include "common/Array.h"
#include "common/Dispatcher.h"
#include "common/Xoshiro128.h"
#include "common/cstrings.h"
#include "common/malloc.h"
#include "common/trace.h"
#include <cmath>

using namespace kilipili::Video;
using namespace kilipili;


namespace kilipili
{

void Console::show_file(cstr path)
{
	trace(__func__);
	if (lceq(extension_from_path(path), ".gif")) return show_gif_file(path);
	if (lceq(extension_from_path(path), ".img")) return show_img_file(path);
	if (lceq(extension_from_path(path), ".ham")) return show_ham_image(path);
	throw "unknown file";
}

void Console::show_ham_image(cstr path)
{
	trace(__func__);

	VgaMode	  old_vga = vga_mode;
	ColorMode old_cm  = pixmap->colormode;

	ScreenSize ss = screensize_160x120;

	{
		constexpr int		 max_size	= 240000; // limited by ram
		constexpr int		 max_width	= 800;	  // probably won't work
		constexpr int		 max_height = 600;
		constexpr ScreenSize max_ss		= screensize_800x600;

		HamImageInfo info(openFile(path));
		int			 w = info.width;
		int			 h = info.height;
		if (h > max_height || w > max_width || w * h > max_size) throw "image too big to display";
		while (ss < max_ss && (w > vga_modes[ss]->width || h > vga_modes[ss]->height)) ss = ScreenSize(ss + 1);

		Error e = switch_vga_mode_ham8(*vga_modes[ss], info);
		pixmap->clear(0);
		if (e) throw e;
	}

	Error error = NO_ERROR;

	try
	{
		FilePtr		 file = openFile(path);
		HamImageInfo info(file);

		Pixmap<colormode_i8>* pixmap = reinterpret_cast<Pixmap<colormode_i8>*>(this->pixmap.ptr());
		file->read(pixmap->pixmap, info.width * info.height);
		file = nullptr;

		printf("heap largest free block = %u\n", heap_largest_free_block());

		uint min, max, avg;
		LoadSensor::getLoad(1, min, avg, max); // reset loads

		waitchar();

		LoadSensor::getLoad(1, min, avg, max);
		min = (min + 50000) / 100000;
		max = (max + 50000) / 100000;
		avg = (avg + 50000) / 100000;
		printf(
			"load core 1: %u.%u, %u.%u, %u.%uMHz (min,avg,max)\n", //
			min / 10, min % 10, avg / 10, avg % 10, max / 10, max % 10);
	}
	catch (Error e)
	{
		error = e;
	}
	catch (std::exception& e)
	{
		panic("exception in line %i: %s", __LINE__, e.what());
	}
	catch (...)
	{
		panic("unknown exception in line %i", __LINE__);
	}
	//catch (...)
	//{
	//	error = "unknown exception";
	//}

	if (error) printf("%s\n", error);
	switch_vga_mode(old_vga, old_cm, true);
	textvdu->cls();
	if (error) textvdu->printf("%s\n", error);
}

void Console::show_img_file(cstr path)
{
	trace(__func__);

	VgaMode	  old_vga = vga_mode;
	ColorMode old_cm  = pixmap->colormode;

	ScreenSize ss = screensize_160x120;
	ColorMode  cm = colormode_i8;

	{
		FilePtr		 file = openFile(path);
		ImageDecoder img(file);
		if (!img.isa_img_file()) throw "not an img file";
		cm				  = img.has_cmap ? colormode_i8 : colormode_rgb;
		ScreenSize max_ss = img.pixelsize == 1 ? screensize_512x384 : screensize_320x192;
		while (ss < max_ss && (img.image_width > vga_modes[ss]->width || img.image_height > vga_modes[ss]->height))
			ss = ScreenSize(ss + 1);
	}

	if (Error e = switch_vga_mode(*vga_modes[ss], cm, false)) throw e;
	pixmap->clear(0);

	Error error = NO_ERROR;

	try
	{
		FilePtr		 file = openFile(path);
		ImageDecoder img(file);
		assert(img.isa_img_file());

		int x0 = (vga_modes[ss]->width - img.image_width) / 2;
		int y0 = (vga_modes[ss]->height - img.image_height) / 2;

		img.decodeImage(*pixmap, x0, y0);
		assert(img.has_cmap && img.cmapsize <= 256);
		system_colormap.reset(img.global_cmap);

		waitchar();
	}
	catch (Error e)
	{
		error = e;
	}
	catch (std::exception& e)
	{
		panic("exception in line %i: %s", __LINE__, e.what());
	}
	catch (...)
	{
		panic("unknown exception in line %i", __LINE__);
	}
	//catch (...)
	//{
	//	error = "unknown exception";
	//}

	if (error) printf("%s\n", error);
	switch_vga_mode(old_vga, old_cm, true);
	textvdu->cls();
	if (error) textvdu->printf("%s\n", error);
}

void Console::show_gif_file(cstr path)
{
	trace(__func__);

	VgaMode	  old_vga = vga_mode;
	ColorMode old_cm  = pixmap->colormode;

	ScreenSize ss = screensize_160x120;
	ColorMode  cm = colormode_i8;

	{
		FilePtr	   file = openFile(path);
		GifDecoder gif(file);
		if (!gif.isa_gif_file) throw "not a gif file";
		while (ss < screensize_512x384 &&
			   (gif.image_width > vga_modes[ss]->width || gif.image_height > vga_modes[ss]->height))
			ss = ScreenSize(ss + 1);
	}

	if (Error e = switch_vga_mode(*vga_modes[ss], cm, false)) throw e;
	pixmap->clear(0);

	Error error = NO_ERROR;

	try
	{
		FilePtr	   file = openFile(path);
		GifDecoder gif(file);
		assert(gif.isa_gif_file);

		pixmap->clear(gif.background_color);

		int x0	  = (vga_modes[ss]->width - gif.image_width) / 2;
		int y0	  = (vga_modes[ss]->height - gif.image_height) / 2;
		int delay = 0;
		do {
			while (in_vblank)
			{
				if (getchar_wto(1000) >= 0) goto x;
			}
			while (!in_vblank)
			{
				if (getchar_wto(1000) >= 0) goto x;
			}

			CC when = now();
			delay	= gif.decode_frame(*pixmap, system_colormap.colors, x0, y0);
			when += (delay - 2) * 10000;

			if (getchar_wto(when - now()) >= 0) goto x;
		}
		while (delay);

		waitchar();
	}
	catch (Error e)
	{
		error = e;
	}
	catch (std::exception& e)
	{
		panic("exception in line %i: %s", __LINE__, e.what());
	}
	catch (...)
	{
		panic("unknown exception in line %i", __LINE__);
	}
	//catch (...)
	//{
	//	error = "unknown exception";
	//}

x:
	if (error) printf("%s\n", error);
	switch_vga_mode(old_vga, old_cm, true);
	textvdu->cls();
	if (error) textvdu->printf("%s\n", error);
}


void Console::run_tiles_demo()
{
	trace(__func__);

	enum {
		grass,
		grassLD,
		grassLU,
		grassRD,
		grassRU,
		ground,
		grndB,
		grndBL,
		grndBR,
		grndL,
		grndLF,
		grndR,
		grndRF,
		plateau,
		plateauL,
		plateauR,
		water,
		waterT,
		air
	};
	static constexpr cstr tile_names[] = {
		"grass.gif",
		"grass left dn.gif",
		"grass left up.gif",
		"grass right dn.gif",
		"grass right up.gif",
		"ground.gif",
		"ground bottom.gif",
		"ground bottom left.gif",
		"ground bottom right.gif",
		"ground left.gif",
		"ground left foot.gif",
		"ground right.gif",
		"ground right foot.gif",
		"platform.gif",
		"platform left.gif",
		"platform right.gif",
		"water.gif",
		"water top.gif",
	};
	enum { bush1, bush2, bush3, bush4, pilz1, pilz2, crate, sign1, sign2, tree1, tree2, tree3, stone };
	static constexpr cstr object_names[] = {
		"bush1.gif", "bush2.gif", "bush3.gif", "bush4.gif", "mushroom1.gif", "mushroom2.gif", "crate.gif",
		"sign1.gif", "sign2.gif", "tree1.gif", "tree2.gif", "tree3.gif",	 "stone.gif",
	};

	using namespace Video;
	static constexpr ColorMode BGCM = colormode_i4;
	using Sprite					= Video::Sprite<Shape>;

	shut_down();

	RCPtr<Pixmap<BGCM>> background = new Pixmap<BGCM>(512, 384);
	pixmap						   = background;
	//colors.reset(newColorMap(BGCM));
	framebuffer = new FrameBuffer<Pixmap<BGCM>>(background, system_colormap.as<BGCM>());
	addVideoPlane(framebuffer);
	startVideo(vga_mode_512x384_60, 240 MHz, 16 /*DEFAULT_SCANLINE_BUFFER_SIZE*/);
	textvdu = new TextVDU(pixmap);
	//textvdu->cls();
	//textvdu->printf("%s\n", textvdu->identify());
	RCPtr<MultiSpritesPlane<Sprite, HasZ>> grid = new MultiSpritesPlane<Sprite, HasZ>();
	addVideoPlane(grid);
	mousepointer = new MousePointer(MOUSE_BUSY);
	addVideoPlane(mousepointer);

	{
		setWorkDir("sdcard:/tiles/gif");
		FilePtr	   file = openFile("bg-512.gif");
		GifDecoder gif(file);
		assert(gif.isa_gif_file);
		//gif.decode_image(*background); // true color
		//gif.decode_image(*background, colors.get()); // ic
		while (gif.decode_frame(*background, system_colormap.colors)) { gif.loop_count = 0; }
	}

	//	Array<Shape> tile_shapes(18u);
	Shape* tile_shapes = new Shape[18];
	setWorkDir("/tiles/gif/tiles");
	for (uint i = 0; i < 18; i++)
	{
		FilePtr						file = openFile(tile_names[i]);
		GifDecoder					gif(file);
		Color						cmap[256];
		int							transp;
		RCPtr<Pixmap<colormode_i8>> tile = new Pixmap<colormode_i8>(32, 32);
		gif.decode_image(*tile, cmap, &transp, 0, 0);
		tile_shapes[i] = Shape(*tile, transp, Dist(0, 0), cmap);
	}

	//Array<Shape> object_shapes(13u);
	Shape* object_shapes = new Shape[13];
	setWorkDir("/tiles/gif/objects");
	for (uint i = 0; i < 13; i++)
	{
		FilePtr						file = openFile(object_names[i]);
		GifDecoder					gif(file);
		Color						cmap[256];
		int							transp;
		RCPtr<Pixmap<colormode_i8>> object = new Pixmap<colormode_i8>(gif.image_width, gif.image_height);
		gif.decode_image(*object, cmap, &transp, 0, 0);
		object_shapes[i] = Shape(*object, transp, Dist(0, object->height), cmap);
	}

	// clang-format off
	static constexpr uint8 gamefield_def[12][17] = {
													{air,air,air,air,				air,air,air,air,			air,air,air,air,				air,air,air,air,				air},
													{air,air,air,air,				air,air,air,air,			air,air,air,air,				air,air,air,air,				air},
													{grass,grass,grass,grassRD,		air,air,air,air,			air,air,air,air,				air,grassLD,grass,grass,		grass},
													{grndB,grndB,grndB,grndBR,		air,air,air,air,			air,plateauL,plateauR,air,		air,grndL,ground,ground,		ground},
													{air,air,air,air,				air,air,plateauL,plateauR,	air,air,air,air,				air,grndBL,grndB,grndB,			grndBR},
													{air,air,air,air,				air,air,air,air,			air,air,air,air,				air,air,air,air,				air},
													
													{air,air,plateauL,plateau,		plateauR,air,air,air,		air,air,air,air,				air,air,air,air,				air},
													
													{air,air,air,air,				air,air,air,grassLD,		grass,grass,grassRD,air,		air,air,air,air,				air},
													{air,air,air,air,				air,air,air,grndL,			ground,ground,grndR,air,		air,air,air,air,				air},
													{grassLD,grass,grassRD,air,		air,air,grassLD,grass,		grass,grass,grass,grassRD,		air,air,air,air,				air},
													{grass,grass,grass,grassRD,		waterT,waterT,grndL,ground,	ground,ground,ground,grndRF,	grassLU,grassRD,waterT,waterT,	grassLD},
													{ground,ground,ground,grndRF,	water,water,grndLF,ground,	ground,ground,ground,ground,	ground,grndRF,water,water,		grndLF},
													};
	// clang-format on

	Sprite* tile_sprites[12][17] = {{nullptr}};
	for (int x = 0; x < 17; x++)
		for (int y = 0; y < 12; y++)
		{
			if (gamefield_def[y][x] == air) continue;
			tile_sprites[y][x] = new Sprite(tile_shapes[gamefield_def[y][x]], Point(x * 32, y * 32), 100);
			grid->add(tile_sprites[y][x]);
		}

	struct ObjectDef
	{
		int obj, x, y, z = 100;
	};
	static constexpr int	   w = 32, h = 32; //grid
	static constexpr ObjectDef object_def[] = {
		{bush1, w * 1 + 0, h * 2, 90},
		{tree2, w * 0 + 0, h * 2, 91},
		{bush2, w * 0 + 5, h * 2, 95},
		{stone, w * 1 + 5, h * 2, 96},
		{pilz2, w * 2 - 5, h * 2, 97},
		{pilz2, w * 2 + 0, h * 2, 98},
		//
		{bush1, w * 14 + 15, h * 2, 90},
		{sign1, w * 14 - 5, h * 2, 91},
		{pilz1, w * 14 - 5, h * 2, 92},
		{pilz2, w * 14 + 10, h * 2, 93},
		//
		{bush4, w * 9 + 6, h * 3, 90},
		{bush3, w * 10 - 5, h * 3, 91},
		{pilz1, w * 9 + 4, h * 3, 92},
		{pilz1, w * 10 + 7, h * 3, 93},
		//
		{tree3, w * 6 - 4, h * 4, 90},
		{bush4, w * 6 + 7, h * 4, 91},
		{bush3, w * 7 - 4, h * 4, 92},
		//
		{crate, w * 3 + 0, h * 6, 90},
		{crate, w * 3 + 19, h * 6, 90},
		{crate, w * 3 + 10, h * 6 - 19, 90},
		{bush4, w * 4 - 4, h * 6, 91},
		//
		{sign2, w * 0 + 8, h * 9, 90},
		{tree3, w * 2 + 4, h * 10 + 8, 190},
		{tree3, w * 1 - 4, h * 10, 191},
		{bush1, w * 2 + 0, h * 10, 192},
		{tree2, w * 1 + 8, h * 10, 193},
		{bush2, w * 1 + 10, h * 10, 194},
		{bush1, w * 3 - 10, h * 10 + 5, 195},
		{pilz1, w * 1 + 6, h * 10, 196},
		{pilz2, w * 3 + 4, h * 10, 197},
		//
		{tree1, w * 8, h * 7, 90},
		{pilz2, w * 8 - 6, h * 7, 91},
		//
		{stone, w * 7 - 2, h * 9, 101},
		{tree2, w * 10, h * 9, 101},
		{tree3, w * 9 - 4, h * 9, 102},
		{bush3, w * 10 - 5, h * 9, 103},
		{pilz2, w * 9 + 8, h * 9, 104},
		//
		{crate, w * 12 + 2, h * 10, 90},
		{tree2, w * 12 + 4, h * 10, 91},
		{bush4, w * 12 + 6, h * 10, 92},
		{bush4, w * 13 + 6, h * 10, 93},
	};

	Sprite* object_sprites[NELEM(object_def)] = {nullptr};
	for (uint i = 0; i < NELEM(object_def); i++)
	{
		const ObjectDef& odef  = object_def[i];
		Shape&			 shape = object_shapes[odef.obj];
		object_sprites[i]	   = new Sprite(shape, Point(odef.x, odef.y), uint16(odef.z));
		grid->add(object_sprites[i]);
	}

	waitchar();

	//TODO

	removeVideoPlane(grid);
	for (int x = 0; x < 17; x++)
		for (int y = 0; y < 12; y++) //
			delete tile_sprites[y][x];
	for (uint i = 0; i < NELEM(object_sprites); i++) //
		delete object_sprites[i];
	delete[] tile_shapes;
	delete[] object_shapes;
}


void Console::test_layout()
{
	removeVideoPlane(mousepointer);
	removeVideoPlane(framebuffer);
	pixmap		 = nullptr;
	framebuffer	 = nullptr;
	textvdu		 = nullptr;
	mousepointer = nullptr;
	system_colormap.reset(vga8_colors);

	try
	{
		if ((0)) // hor-2
		{
			// we make 2 horizontal parts with different constraints:

			int w1 = vga_mode.width * 60 / 100 / 8 * 8;
			int w2 = vga_mode.width - w1;
			int h1 = vga_mode.height;

			constexpr ColorMode cm1 = colormode_a1w8;
			constexpr ColorMode cm2 = colormode_i8;

			RCPtr pm1 = new Pixmap<cm1>(w1, h1, attrheight_12px);
			RCPtr pm2 = new Pixmap<cm2>(w2, h1, attrheight_12px);

			RCPtr vp1 = new FrameBuffer(pm1);
			RCPtr vp2 = new FrameBuffer(pm2);

			framebuffer	 = new HorizontalLayout(vp1, vp2, w1);
			mousepointer = new MousePointer;
			addVideoPlane(framebuffer);
			addVideoPlane(mousepointer);

			textvdu = new TextVDU(pm2);
			//textvdu->cls();
			textvdu->print("layout RIGHT\n");
			for (textvdu->fgcolor = 0; textvdu->fgcolor < 16; textvdu->fgcolor++)
			{
				for (textvdu->bgcolor = 0; textvdu->bgcolor < 16; textvdu->bgcolor++) textvdu->printChar('X');
				textvdu->newLine();
			}

			textvdu = new TextVDU(pm1);
			//textvdu->cls();
			textvdu->print("Welcome to a 1x2 layout\n");
			textvdu->print("change video mode to leave\n");
			pixmap = pm1;
		}
		else if ((0)) // vert-3
		{
			// we make 3 planes vertically:

			int w1 = vga_mode.width;
			int h1 = vga_mode.height * 20 / 100;
			int h2 = vga_mode.height * 60 / 100;
			int h3 = vga_mode.height - h1 - h2;

			constexpr ColorMode cm1 = colormode_i1;
			constexpr ColorMode cm2 = colormode_a1w8;
			constexpr ColorMode cm3 = colormode_i1;
			system_colormap.as<colordepth_1bpp>()->reset();

			RCPtr pm1 = new Pixmap<cm1>(w1, h1, attrheight_12px);
			RCPtr pm2 = new Pixmap<cm2>(w1, h2, attrheight_12px);
			RCPtr pm3 = new Pixmap<cm3>(w1, h3, attrheight_12px);

			RCPtr vp1 = new FrameBuffer(pm1);
			RCPtr vp2 = new FrameBuffer(pm2);
			RCPtr vp3 = new FrameBuffer(pm3);

			framebuffer = new VerticalLayout(vp1, vp2, vp3, h1, h2);
			addVideoPlane(framebuffer);
			mousepointer = new MousePointer;
			addVideoPlane(mousepointer);

			textvdu = new TextVDU(pm1);
			textvdu->cls();
			textvdu->print("layout TOP\n");

			textvdu = new TextVDU(pm3);
			textvdu->cls();
			textvdu->print("layout BOTTOM\n");

			textvdu = new TextVDU(pm2);
			textvdu->cls();
			textvdu->print("Welcome to a 3x1 layout\n");
			textvdu->print("change video mode to leave\n");
			pixmap = pm2;
		}
		else if ((0)) // 2x2
		{
			// we make 2 planes vertically and in each part 2 horizontally with different constraints:

			int w1 = vga_mode.width * 60 / 100 / 8 * 8;
			int w2 = vga_mode.width - w1;
			int h1 = vga_mode.height * 20 / 100;
			int h2 = vga_mode.height - h1;

			constexpr ColorMode cm1 = colormode_i1;
			constexpr ColorMode cm2 = colormode_i4;
			constexpr ColorMode cm3 = colormode_a1w8;
			constexpr ColorMode cm4 = colormode_i8;

			RCPtr<Pixmap<cm1>> pm1 = new Pixmap<cm1>(w1, h1, attrheight_12px);
			RCPtr<Pixmap<cm2>> pm2 = new Pixmap<cm2>(w2, h1, attrheight_12px);
			RCPtr<Pixmap<cm3>> pm3 = new Pixmap<cm3>(w1, h2, attrheight_12px);
			RCPtr<Pixmap<cm4>> pm4 = new Pixmap<cm4>(w2, h2, attrheight_12px);

			RCPtr<VideoPlane> vp1 = new FrameBuffer(pm1);
			RCPtr<VideoPlane> vp2 = new FrameBuffer(pm2);
			RCPtr<VideoPlane> vp3 = new FrameBuffer(pm3);
			RCPtr<VideoPlane> vp4 = new FrameBuffer(pm4);

			RCPtr<HorizontalLayout<2>> hl1 = new HorizontalLayout(vp1, vp2, w1);
			RCPtr<HorizontalLayout<2>> hl2 = new HorizontalLayout(vp3, vp4, w1);

			framebuffer = new VerticalLayout(hl1, hl2, h1);
			addVideoPlane(framebuffer);
			mousepointer = new MousePointer;
			addVideoPlane(mousepointer);

			textvdu = new TextVDU(pm1);
			textvdu->cls();
			textvdu->print("layout TL\n");

			textvdu = new TextVDU(pm2);
			textvdu->cls();
			textvdu->print("layout TR\n");

			textvdu = new TextVDU(pm4);
			textvdu->cls();
			textvdu->print("layout MR\n");

			textvdu = new TextVDU(pm3);
			textvdu->cls();
			textvdu->print("Welcome to a 3x2 layout\n");
			textvdu->print("change video mode to leave\n");
			pixmap = pm3;
		}
		else if ((0)) // 2x3
		{
			// we make 3 planes vertically and in each part 2 horizontally with different constraints:

			int w1 = vga_mode.width * 60 / 100 / 8 * 8;
			int w2 = vga_mode.width - w1;
			int h1 = vga_mode.height * 20 / 100;
			int h2 = vga_mode.height * 60 / 100;
			int h3 = vga_mode.height - h1 - h2;

			constexpr ColorMode cm1 = colormode_i1;
			constexpr ColorMode cm2 = colormode_i4;
			constexpr ColorMode cm3 = colormode_a1w8;
			constexpr ColorMode cm4 = colormode_a2w8;
			constexpr ColorMode cm5 = colormode_i4;
			constexpr ColorMode cm6 = colormode_i8;

			RCPtr<Pixmap<cm1>> pm1 = new Pixmap<cm1>(w1, h1, attrheight_12px);
			RCPtr<Pixmap<cm2>> pm2 = new Pixmap<cm2>(w2, h1, attrheight_12px);
			RCPtr<Pixmap<cm3>> pm3 = new Pixmap<cm3>(w1, h2, attrheight_12px);
			RCPtr<Pixmap<cm4>> pm4 = new Pixmap<cm4>(w2, h2, attrheight_12px);
			RCPtr<Pixmap<cm5>> pm5 = new Pixmap<cm5>(w1, h3, attrheight_12px);
			RCPtr<Pixmap<cm6>> pm6 = new Pixmap<cm6>(w2, h3, attrheight_12px);

			RCPtr<VideoPlane> vp1 = new FrameBuffer(pm1);
			RCPtr<VideoPlane> vp2 = new FrameBuffer(pm2);
			RCPtr<VideoPlane> vp3 = new FrameBuffer(pm3);
			RCPtr<VideoPlane> vp4 = new FrameBuffer(pm4);
			RCPtr<VideoPlane> vp5 = new FrameBuffer(pm5);
			RCPtr<VideoPlane> vp6 = new FrameBuffer(pm6);

			RCPtr<HorizontalLayout<2>> hl1 = new HorizontalLayout(vp1, vp2, w1);
			RCPtr<HorizontalLayout<2>> hl2 = new HorizontalLayout(vp3, vp4, w1);
			RCPtr<HorizontalLayout<2>> hl3 = new HorizontalLayout(vp5, vp6, w1);

			framebuffer = new VerticalLayout(hl1, hl2, hl3, h1, h2);
			addVideoPlane(framebuffer);
			mousepointer = new MousePointer;
			addVideoPlane(mousepointer);

			textvdu = new TextVDU(pm1);
			textvdu->cls();
			textvdu->print("layout TL\n");

			textvdu = new TextVDU(pm2);
			textvdu->cls();
			textvdu->print("layout TR\n");

			textvdu = new TextVDU(pm4);
			textvdu->cls();
			textvdu->print("layout MR\n");

			textvdu = new TextVDU(pm5);
			textvdu->cls();
			textvdu->print("layout BL\n");

			textvdu = new TextVDU(pm6);
			textvdu->cls();
			textvdu->print("layout BR\n");

			textvdu = new TextVDU(pm3);
			textvdu->cls();
			textvdu->print("Welcome to a 3x2 layout\n");
			textvdu->print("change video mode to leave\n");
			pixmap = pm3;
		}
		else // 3x3
		{
			// we make 3 planes vertically and in each part 3 horizontally with different constraints:

			int w1 = vga_mode.width * 10 / 100 / 8 * 8;
			int w2 = vga_mode.width * 80 / 100 / 8 * 8;
			int w3 = vga_mode.width - w1 - w2;
			int h1 = 2 * 12;
			int h2 = vga_mode.height - 4 * 12;
			int h3 = 2 * 12;

			constexpr ColorMode cm1 = colormode_i4;
			constexpr ColorMode cm2 = colormode_i1;
			constexpr ColorMode cm3 = colormode_i8;
			constexpr ColorMode cm4 = colormode_a2w8;
			constexpr ColorMode cm5 = colormode_a1w8;
			constexpr ColorMode cm6 = colormode_a2w8;
			constexpr ColorMode cm7 = colormode_i4;
			constexpr ColorMode cm8 = colormode_rgb;
			constexpr ColorMode cm9 = colormode_i2;
			system_colormap.as<colormode_i2>()->reset();
			system_colormap.as<colormode_i1>()->reset();

			RCPtr<Pixmap<cm1>> pm1 = new Pixmap<cm1>(w1, h1, attrheight_12px);
			RCPtr<Pixmap<cm2>> pm2 = new Pixmap<cm2>(w2, h1, attrheight_12px);
			RCPtr<Pixmap<cm3>> pm3 = new Pixmap<cm3>(w3, h1, attrheight_12px);
			RCPtr<Pixmap<cm4>> pm4 = new Pixmap<cm4>(w1, h2, attrheight_12px);
			RCPtr<Pixmap<cm5>> pm5 = new Pixmap<cm5>(w2, h2, attrheight_12px);
			RCPtr<Pixmap<cm6>> pm6 = new Pixmap<cm6>(w3, h2, attrheight_12px);
			RCPtr<Pixmap<cm7>> pm7 = new Pixmap<cm7>(w1, h3, attrheight_12px);
			RCPtr<Pixmap<cm8>> pm8 = new Pixmap<cm8>(w2, h3, attrheight_12px);
			RCPtr<Pixmap<cm9>> pm9 = new Pixmap<cm9>(w3, h3, attrheight_12px);

			RCPtr<VideoPlane> vp1 = new FrameBuffer(pm1);
			RCPtr<VideoPlane> vp2 = new FrameBuffer(pm2);
			RCPtr<VideoPlane> vp3 = new FrameBuffer(pm3);
			RCPtr<VideoPlane> vp4 = new FrameBuffer(pm4);
			RCPtr<VideoPlane> vp5 = new FrameBuffer(pm5);
			RCPtr<VideoPlane> vp6 = new FrameBuffer(pm6);
			RCPtr<VideoPlane> vp7 = new FrameBuffer(pm7);
			RCPtr<VideoPlane> vp8 = new FrameBuffer(pm8);
			RCPtr<VideoPlane> vp9 = new FrameBuffer(pm9);

			RCPtr<HorizontalLayout<3>> hl1 = new HorizontalLayout(vp1, vp2, vp3, w1, w2);
			RCPtr<HorizontalLayout<3>> hl2 = new HorizontalLayout(vp4, vp5, vp6, w1, w2);
			RCPtr<HorizontalLayout<3>> hl3 = new HorizontalLayout(vp7, vp8, vp9, w1, w2);

			framebuffer = new VerticalLayout(hl1, hl2, hl3, h1, h2);
			addVideoPlane(framebuffer);
			mousepointer = new MousePointer;
			addVideoPlane(mousepointer);

			textvdu = new TextVDU(pm1);
			textvdu->cls();
			textvdu->print("layout TL");

			textvdu = new TextVDU(pm2);
			textvdu->cls();
			textvdu->print("layout TM");

			textvdu = new TextVDU(pm3);
			textvdu->cls();
			textvdu->print("layout TR");

			textvdu = new TextVDU(pm4);
			textvdu->cls();
			textvdu->print("layout ML");

			//textvdu = new TextVDU(pm5);
			//textvdu->cls();
			//textvdu->print("layout BL");

			textvdu = new TextVDU(pm6);
			textvdu->cls();
			textvdu->print("layout MR");

			textvdu = new TextVDU(pm7);
			textvdu->cls();
			textvdu->print("layout BL");

			textvdu = new TextVDU(pm8);
			textvdu->cls();
			textvdu->print("layout BM");

			textvdu = new TextVDU(pm9);
			textvdu->cls();
			textvdu->print("layout BR");

			textvdu = new TextVDU(pm5);
			textvdu->cls();
			textvdu->print("Welcome to a 3x3 layout\n");
			textvdu->print("change video mode to leave\n");
			pixmap = pm3;
		}
	}
	catch (Error e)
	{
		panic("test_layout: %s", e);
	}
	catch (std::exception& e)
	{
		panic("exception in line %i: %s", __LINE__, e.what());
	}
	catch (...)
	{
		panic("unknown exception in line %i", __LINE__);
	}
	//catch (...)
	//{
	//	panic("test_layout: unknown exception");
	//}
}


constexpr float		  pi = std::numbers::pi_v<float>;
inline float		  sin(int r) { return sinf(float(r) * pi / 180); }
inline float		  cos(int r) { return cosf(float(r) * pi / 180); }
inline constexpr Dist operator*(const Dist& a, coord f) { return Dist(a.dx * f, a.dy * f); }
inline constexpr Dist operator/(const Dist& a, coord f) { return Dist(a.dx / f, a.dy / f); }
inline constexpr Dist operator*(const Dist& a, float f) { return Dist(coord(float(a.dx) * f), coord(float(a.dy) * f)); }

inline uint idx_color(int n) { return uint(n); }
inline uint rgb_color(int n) { return uint(system_colormap[n]); }


void Console::demo_draw_circles()
{
	enum VgaColors {
		black,
		red,
		green,
		yellow,
		blue,
		magenta,
		cyan,
		light_grey,
		dark_grey,
		bright_red,
		bright_green,
		bright_yellow,
		bright_blue,
		bright_magenta,
		bright_cyan,
		white,
	};

	system_colormap.reset(vga8_colors);
	bool isic  = is_indexed_color(pixmap->colormode);
	auto color = isic ? idx_color : rgb_color;
	pixmap->clear(color(white));

	Xoshiro128 rng {uint(now())};

	for (int r = 200; r > 0; r--)
	{
		int bgcolor = 240 + rng.random(16);
		int fgcolor = rng.random(240);
		if (system_colormap[fgcolor] == white) continue;
		if (system_colormap[fgcolor] == system_colormap[bgcolor]) continue;

		int x = rng.random(pixmap->width) - r / 2;
		int y = rng.random(pixmap->height) - r / 2;
		pixmap->fillCircle(x, y, r, r, color(bgcolor), 0); // TODO: w,h statt p2
		pixmap->drawCircle(x, y, r, r, color(fgcolor), 1);
	}
}

void Console::demo_draw_rectangles()
{
	enum VgaColors {
		black,
		red,
		green,
		yellow,
		blue,
		magenta,
		cyan,
		light_grey,
		dark_grey,
		bright_red,
		bright_green,
		bright_yellow,
		bright_blue,
		bright_magenta,
		bright_cyan,
		white,
	};

	system_colormap.reset(vga8_colors);
	bool isic  = is_indexed_color(pixmap->colormode);
	auto color = isic ? idx_color : rgb_color;
	pixmap->clear(color(white));

	Xoshiro128 rng {uint(now())};

	for (int r = 200; r > 0; r--)
	{
		int bgcolor = 240 + rng.random(16);
		int fgcolor = rng.random(240);
		if (system_colormap[fgcolor] == white) continue;
		if (system_colormap[fgcolor] == system_colormap[bgcolor]) continue;

		int x = rng.random(pixmap->width) - r / 2;
		int y = rng.random(pixmap->height) - r / 2;
		pixmap->fillRect(x, y, r, r, color(bgcolor), 0);
		pixmap->drawRect(x, y, r, r, color(fgcolor), 1);
	}
}

void Console::demo_draw_lines()
{
	trace(__func__);

	enum VgaColors {
		black,
		red,
		green,
		yellow,
		blue,
		magenta,
		cyan,
		light_grey,
		dark_grey,
		bright_red,
		bright_green,
		bright_yellow,
		bright_blue,
		bright_magenta,
		bright_cyan,
		white,
	};

	assert(pixmap);

	system_colormap.reset(vga8_colors);
	bool isic  = is_indexed_color(pixmap->colormode);
	auto color = isic ? idx_color : rgb_color;
	pixmap->clear(color(white));

	Xoshiro128 rng {uint(now())};
	for (int i = 0; i < 200; i++)
	{
		int x1		= rng.random(pixmap->width);
		int x2		= rng.random(pixmap->width);
		int y1		= rng.random(pixmap->height);
		int y2		= rng.random(pixmap->height);
		int fgcolor = rng.random(256);
		if (system_colormap[fgcolor] == white) continue;
		pixmap->drawLine(x1, y1, x2, y2, color(fgcolor), 1);
	}
}

void Console::test_floodfill(cstr cmd)
{
	cmd += 14; // skip "test floodfill"
	int test;
	if (eq(cmd, " 1")) test = 1;
	else if (eq(cmd, " 2")) test = 2;
	else if (eq(cmd, " 3")) test = 3;
	else
		return textvdu->print(
			"  available options:\n"
			"  test floodfill 1 ... 4\n");

	enum VgaColors {
		black,
		red,
		green,
		yellow,
		blue,
		magenta,
		cyan,
		light_grey,
		dark_grey,
		bright_red,
		bright_green,
		bright_yellow,
		bright_blue,
		bright_magenta,
		bright_cyan,
		white,
	};

	system_colormap.reset(vga8_colors);
	bool isic  = is_indexed_color(pixmap->colormode);
	auto color = isic ? idx_color : rgb_color;
	pixmap->clear(color(white));

	Xoshiro128 rng {uint(now())};
	auto	   light_color = [&]() { return color(rng.random(7) + 8); }; // vga: random light color

	int h = pixmap->height;
	int w = pixmap->width;

	switch (test)
	{
	default:
	case 1: // star, black = fg
	{
		for (int i = 0; i < 30; i++) pixmap->drawHLine(0, h * i / 30, w, light_color(), 0);
		for (int i = 0; i < 40; i++) pixmap->drawVLine(w * i / 40, 0, h, light_color(), 0);
		int r = w * 11 / 20;
		pixmap->drawCircle(w / 2 - r, h / 2 - r, r * 2, r * 2, color(black), 1);
		Point triangle[4] = {Point(w / 2, h / 2), Point(), Point(), Point(w / 2, h / 2)};
		r				  = r * 18 / 30;
		const int d		  = 20;
		for (int i = 0; i < d; i += 2)
		{
			float x		= sin(360 / d * i) * float(r);
			float y		= cos(360 / d * i) * float(r);
			triangle[1] = Point(w / 2 + coord(x), h / 2 + coord(y));
			x			= sin(360 / d * ++i) * float(r);
			y			= cos(360 / d * i--) * float(r);
			triangle[2] = Point(w / 2 + coord(x), h / 2 + coord(y));
			pixmap->drawPolygon(triangle, 4, ::black, 1);
		}
		textvdu->moveTo(0, 0);
		textvdu->print("<key>");
		while (getc() <= 0) Dispatcher::run(100);
		pixmap->floodFill(0, h / 2, ::black, 1);
		break;
	}
	case 2: // rhombs with yellow dot: black = fg
	{
		for (int y = 0; y < h; y += 4)	   //
			for (int x = 0; x < w; x += 4) //
			{
				pixmap->setPixel(x, y, color(yellow), 0);
				pixmap->setPixel(x + 1, y, color(black), 1);
				pixmap->setPixel(x, y + 1, color(black), 1);
				pixmap->setPixel(x - 1, y, color(black), 1);
				pixmap->setPixel(x, y - 1, color(black), 1);
			}
		textvdu->moveTo(0, 0);
		textvdu->print("<key>");
		while (getc() <= 0) Dispatcher::run(100);
		pixmap->floodFill(w / 2 + 2, h / 2 + 2, color(black), 1);
		break;
	}
	case 3: // grid of dots, black = fg
	{
		for (int y = 0; y < h; y += 2)	   //
			for (int x = 0; x < w; x += 2) //
				pixmap->setPixel(x, y, color(black), 1);
		textvdu->moveTo(0, 0);
		textvdu->print("<key>");
		while (getc() <= 0) Dispatcher::run(100);
		pixmap->floodFill(w / 2 + 1, h / 2 + 1, color(black), 1);
		break;
	}
		//case 3: // star, white = bg
		//case 4: // rhombs with red dot: white = bg
		//case 5: // grid of dots, white = bg
	}
}


static void draw_beziers1(Canvas* canvas, const Point& p0, const Point& p3, uint color, bool fp)
{
	Dist  dist = (p0 - p3) * 3 / 4;
	float d	   = sqrtf(float(dist.dx * dist.dx + dist.dy * dist.dy));

	for (int deg = 0; deg < 360; deg += 11)
	{
		Point p2 = p3 + Dist(int(sin(deg) * d), int(cos(deg) * d));
		if (fp) canvas->drawBezier_f(p0, p0, p2, p3, color, 1);
		else canvas->drawBezier(p0, p0, p2, p3, color, 1);
	}
}

static void draw_beziers2(Canvas* canvas, const Point& p0, const Point& p3, uint color, bool fp)
{
	Dist d = p3 - p0;

	float deg = -50 * pi / 180;
	float s	  = sinf(deg);
	float c	  = cosf(deg);
	Dist  d1  = Dist(int(float(d.dx) * c - float(d.dy) * s), int(float(d.dy) * c + float(d.dx) * s));

	deg		= +30 * pi / 180;
	s		= sinf(deg);
	c		= cosf(deg);
	Dist d2 = Dist(int(float(d.dx) * c - float(d.dy) * s), int(float(d.dy) * c + float(d.dx) * s));

	for (float f = 0.2f; f <= 5; f *= 1.1f)
	{
		if (fp) canvas->drawBezier_f(p0, p0 + d1 * f, p3 - d2 * (1.0f / f), p3, color, 1);
		else canvas->drawBezier(p0, p0 + d1 * f, p3 - d2 * (1.0f / f), p3, color, 1);
	}
}

static void draw_beziers3(Canvas* canvas, const Point& p0, const Point& p3, uint color, bool fp)
{
	Dist d = p3 - p0;

	float deg = -50 * pi / 180;
	float s	  = sinf(deg);
	float c	  = cosf(deg);
	Dist  d1  = Dist(int(float(d.dx) * c - float(d.dy) * s), int(float(d.dy) * c + float(d.dx) * s));

	deg		= +30 * pi / 180;
	s		= sinf(deg);
	c		= cosf(deg);
	Dist d2 = Dist(int(float(d.dx) * c - float(d.dy) * s), int(float(d.dy) * c + float(d.dx) * s));

	for (float f = 0.2f; f <= 5; f *= 1.1f)
	{
		if (fp) canvas->drawBezier_f(p0, p0 + d1 * f, p3 - d2 * f, p3, color, 1);
		else canvas->drawBezier(p0, p0 + d1 * f, p3 - d2 * f, p3, color, 1);
	}
}

void Console::test_bezier(int test)
{
	system_colormap.reset(vga8_colors);
	enum VgaColors {
		black,
		red,
		green,
		yellow,
		blue,
		magenta,
		cyan,
		light_grey,
		dark_grey,
		bright_red,
		bright_green,
		bright_yellow,
		bright_blue,
		bright_magenta,
		bright_cyan,
		white
	};

	bool isic  = is_indexed_color(pixmap->colormode);
	auto color = isic ? idx_color : rgb_color;

	Point mp  = Point(pixmap->width / 2, pixmap->height / 2);
	float rad = float(pixmap->height / 2);

	int	 num_frames = 0;
	long total_time = 0;

	for (int deg = 0;; deg += 1)
	{
		pixmap->clear(::white);

		Point p1 = mp + Dist {int(rad * sin(deg)), int(rad * cos(deg))};
		Point p2 = mp + Dist {int(rad * sin(deg + 90)), int(rad * cos(deg + 90))};

		CC start = now();
		switch (test)
		{
		default:
		case '1': draw_beziers1(pixmap, p1, p2, color(green), 0); break;
		case '2': draw_beziers2(pixmap, p1, p2, color(green), 0); break;
		case '3': draw_beziers3(pixmap, p1, p2, color(green), 0); break;
		case 'a': draw_beziers1(pixmap, p1, p2, color(bright_red), 1); break;
		case 'b': draw_beziers2(pixmap, p1, p2, color(bright_red), 1); break;
		case 'c': draw_beziers3(pixmap, p1, p2, color(bright_red), 1); break;
		}
		total_time += now() - start;
		num_frames++;

		pixmap->drawLine(p1, p2, color(bright_red), 1);

		while (!in_vblank) { Dispatcher::run(100); }

		int c = getc();
		if (c >= '1' && c <= '3') test = c;
		else if (c >= 'a' && c <= 'c') test = c;
		else if (c > 0) break;
	}

	uint time_per_frame = uint(total_time / num_frames);
	::printf("time per frame = %u usec\n", time_per_frame);
	textvdu->moveTo(0, 0);
	textvdu->printf("time per frame = %u.%3u msec\n", time_per_frame / 1000, time_per_frame % 1000);
}

void Console::test_bezier(cstr cmd)
{
	if (eq(cmd, "test bezier 1")) test_bezier('1');
	else if (eq(cmd, "test bezier 2")) test_bezier('2');
	else if (eq(cmd, "test bezier 3")) test_bezier('3');
	else if (lceq(cmd, "test bezier a")) test_bezier('a');
	else if (lceq(cmd, "test bezier b")) test_bezier('b');
	else if (lceq(cmd, "test bezier c")) test_bezier('c');
	else
		textvdu->print(
			"available options:\n"
			"test bezier 1 ... 3 | a ... c\n");
}

void Console::test_triangles()
{
	if (is_attribute_mode(pixmap->colormode))
	{
		textvdu->print("attribute modes are not suited for this test. try \"video 640x480 i4\"\n");
		return;
	}

	enum ZxColors {
		black,
		blue,
		red,
		magenta,
		green,
		cyan,
		yellow,
		light_grey,
		dark_grey,
		light_blue,
		light_red,
		light_magenta,
		light_green,
		light_cyan,
		light_yellow,
		light_white
	};

	system_colormap.reset(zx_colors);
	bool isic  = is_indexed_color(pixmap->colormode);
	auto color = isic ? idx_color : rgb_color;

	Point triangles[] = {
		{8, 0}, {10, 10}, {0, 4},  // left pointing
		{1, 0}, {10, 6},  {0, 10}, // right pointing
		{0, 0}, {10, 0},  {7, 10}, // top=hline
		{8, 0}, {10, 10}, {1, 10}  // bottom=hline
	};

	int x0	   = 5;
	int y0	   = pixmap->height / 50;
	int width  = pixmap->width - pixmap->height - x0; // left strip
	int height = (pixmap->height - 2 * y0) / 4;

	for (int i = 0; i < 12; i += 3)
	{
		Point p1 {x0 + triangles[i + 0].x * width / 10, y0 + triangles[i + 0].y * height / 10};
		Point p2 {x0 + triangles[i + 1].x * width / 10, y0 + triangles[i + 1].y * height / 10};
		Point p3 {x0 + triangles[i + 2].x * width / 10, y0 + triangles[i + 2].y * height / 10};
		pixmap->fillRect(x0, y0, width, height, color(light_green), 0);
		pixmap->drawTriangle(p1, p2, p3, color(light_red), 1);
		pixmap->fillTriangle(p1, p2, p3, color(light_grey), 1);
		y0 += height + 2;
	}

	height = pixmap->height;
	width  = height;
	Rect  rect(pixmap->width - pixmap->height, 0, width, height);
	Point mp = rect.center();
	float ri = float(height / 2 / 5); // inner radius
	float ro = float(height / 2 - 5); // outer radius

	for (int w0 = 0;; w0 += 1)
	{
		pixmap->fillRect(rect, light_grey, 0);
		for (int w = 0; w < 360; w += 60)
		{
			Point p1 = mp + Dist(int(ri * sin(w0 + w - 30)), int(ri * cos(w0 + w - 30)));
			Point p2 = mp + Dist(int(ri * sin(w0 + w + 30)), int(ri * cos(w0 + w + 30)));
			Point p3 = mp + Dist(int(ro * sin(w0 + w)), int(ro * cos(w0 + w)));
			Point p4 = mp + Dist(int(ro * sin(w0 + w + 60)), int(ro * cos(w0 + w + 60)));
			pixmap->fillTriangle(p2, p3, p4, color(light_blue + w / 60), 1); // outer triangles
			pixmap->fillTriangle(p1, p2, p3, color(light_red + w / 60), 1);	 // star
			pixmap->fillTriangle(p1, p2, mp, color(blue + w / 60), 1);		 // inner triangles
		}

		while (!in_vblank) { Dispatcher::run(100); }
		int c = getc();
		if (c > 0) break;
	}

	//system_colormap.reset(default_colors[get_colordepth(pixmap->colormode)]);
}

void Console::test_line_clipping(cstr cmd)
{
	cmd += 13; // skip "test clipping"
	int test;
	if (eq(cmd, " 1")) test = 1;
	else if (eq(cmd, " 2")) test = 2;
	else if (eq(cmd, " 3")) test = 3;
	else if (eq(cmd, " 4")) test = 4;
	else
		return textvdu->print(
			"available options:\n"
			"test clipping 1 ... 4\n");

	int w4 = pixmap->width / 4;
	int h4 = pixmap->height / 4;
	if (pixmap->attrheight != attrheight_none) h4 -= h4 % pixmap->attrheight;

	if (pixmap->colormode != DEFAULT_COLORMODE)
	{
		textvdu->printf("line clipping test only works with default colormode\n");
		return;
	}
	Pixmap<DEFAULT_COLORMODE>* pixmap_cm = static_cast<Pixmap<DEFAULT_COLORMODE>*>(pixmap.ptr());

	enum VgaColors {
		black,
		red,
		green,
		yellow,
		blue,
		magenta,
		cyan,
		light_grey,
		dark_grey,
		bright_red,
		bright_green,
		bright_yellow,
		bright_blue,
		bright_magenta,
		bright_cyan,
		white
	};

	system_colormap.reset(vga8_colors);
	bool isic  = is_indexed_color(pixmap->colormode);
	auto color = isic ? idx_color : rgb_color;

	pixmap->clear(color(light_grey));
	Pixmap<DEFAULT_COLORMODE> pixmap2(*pixmap_cm, Rect(w4, h4, 2 * w4, 2 * h4));
	pixmap2.clear(::white);

	int w = pixmap2.width;
	int h = pixmap2.height;

	switch (test)
	{
	default:
	case 1:
	{
		pixmap2.drawLine(w / 2, 0, w / 2, h - 1, color(bright_yellow), 1);
		pixmap2.drawLine(0, h / 2, w - 1, h / 2, color(bright_yellow), 1);
		pixmap2.drawLine(0, 0, w - 1, h - 1, color(bright_green), 1);
		pixmap2.drawLine(0, h - 1, w - 1, 0, color(bright_green), 1);
		pixmap2.drawRect(w / 2, h / 2, 2, 2, color(bright_red), 1);

		pixmap->drawLine(w4 + 10, h4 + 20, w4 + w + 10, h4 + 30, color(bright_cyan), 1);
		pixmap2.drawLine(10, 20, w + 10, 30, color(black), 1);

		pixmap->drawLine(w4 - 20, h4 + 22, w4 + w - 10, h4 + 35, color(bright_cyan), 1);
		pixmap2.drawLine(-20, 22, w - 10, 35, color(black), 1);

		pixmap->drawLine(w4 - 30, h4 + 25, w4 + w + 10, h4 + 43, color(bright_cyan), 1);
		pixmap2.drawLine(-30, 25, w + 10, 43, color(black), 1);

		pixmap->drawLine(w4 + 10, h4 + h - 3, w4 + w - 10, h4 + h + 7, color(bright_cyan), 1);
		pixmap2.drawLine(10, h - 3, w - 10, h + 7, color(black), 1);

		pixmap->drawLine(w4 + 10, h4 + h - 7, w4 + w - 3, h4 + h + 3, color(bright_cyan), 1);
		pixmap2.drawLine(10, h - 7, w - 3, h + 3, color(black), 1);

		pixmap->drawLine(w4 + 7, h4 + h - 5, w4 + w - 3, h4 + h + 6, color(bright_cyan), 1);
		pixmap2.drawLine(7, h - 5, w - 3, h + 6, color(black), 1);

		pixmap->drawLine(w4 + 10, h4 - 7, w4 + w - 10, h4 + 3, color(bright_cyan), 1);
		pixmap2.drawLine(10, -7, w - 10, 3, color(black), 1);

		pixmap->drawLine(w4 - 10, h4 - 5, w4 + w + 11, h4 + 6, color(bright_cyan), 1);
		pixmap2.drawLine(-10, -5, w + 11, 6, color(black), 1);
		break;
	}
	case 2:
	case 3:
	{
		coord mx = pixmap->width / 2;
		coord my = pixmap->height / 2;

		for (float ro = 40; ro < float(pixmap->width / 2); ro += 17)
		{
			float ri = ro * 6 / 7;

			constexpr int num_vertices = 33;
			static int	  w0		   = 0;
			w0++;
			Array<Point> points(2 * num_vertices + 1);

			for (int i = 0; i < num_vertices * 2;)
			{
				int w		= i * 180 / num_vertices;
				points[i++] = Point(mx + int(ri * sin(w0 + w)), my + int(ri * cos(w0 + w)));
				w			= i * 180 / num_vertices;
				points[i++] = Point(mx + int(ro * sin(w0 + w)), my + int(ro * cos(w0 + w)));
			}
			points[2 * num_vertices] = points[0];

			if (test == 2) points.revert();

			pixmap->drawPolygon(&points[0], points.count(), color(bright_cyan), 1);
			for (uint i = 0; i < points.count(); i++) { points[i] -= Dist(w4, h4); }
			pixmap2.drawPolygon(&points[0], points.count(), color(bright_blue), 1);
		}
		break;
	}
	case 4:
	{
		Xoshiro128 rng {uint(now())};

		for (;;)
		{
			for (int i = 0; i < 250; i++)
			{
				Point p1(rng.random(pixmap->width), rng.random(pixmap->height));
				Point p2(rng.random(pixmap->width), rng.random(pixmap->height));
				pixmap->drawLine(p1, p2, bright_cyan, 1);
				pixmap2.drawLine(p1 - Dist(w4, h4), p2 - Dist(w4, h4), color(black), 1);
			}
			textvdu->moveTo(0, 0);
			textvdu->print("<enter>\n");

			int c = waitchar();
			if (c != 13) break;
			pixmap->clear(color(light_grey));
			pixmap2.clear(color(white));
		}
		break;
	}
	}
}

void Console::test_polygons()
{
	enum VgaColors {
		black,
		red,
		green,
		yellow,
		blue,
		magenta,
		cyan,
		light_grey,
		dark_grey,
		bright_red,
		bright_green,
		bright_yellow,
		bright_blue,
		bright_magenta,
		bright_cyan,
		white
	};

	system_colormap.reset(vga8_colors);
	bool isic  = is_indexed_color(pixmap->colormode);
	auto color = isic ? idx_color : rgb_color;

	Point triangles[] = {
		{8, 0}, {10, 10}, {0, 4},  // left pointing
		{1, 0}, {10, 6},  {0, 10}, // right pointing
		{0, 0}, {10, 0},  {7, 10}, // top=hline
		{8, 0}, {10, 10}, {1, 10}  // bottom=hline
	};

	int x0	   = 5;
	int y0	   = pixmap->height / 50;
	int width  = pixmap->width - pixmap->height - x0; // left strip
	int height = (pixmap->height - 2 * y0) / 4;

	for (int i = 0; i < 12; i += 3)
	{
		Point points[3] = {
			{x0 + triangles[i + 0].x * width / 10, y0 + triangles[i + 0].y * height / 10},
			{x0 + triangles[i + 1].x * width / 10, y0 + triangles[i + 1].y * height / 10},
			{x0 + triangles[i + 2].x * width / 10, y0 + triangles[i + 2].y * height / 10}};
		pixmap->fillRect(x0, y0, width, height, color(bright_green), 0);
		pixmap->drawTriangle(points, color(bright_red), 1);
		pixmap->fillPolygon(points, 3, color(light_grey), 1);
		y0 += height + 2;
	}

	height = (pixmap->height - 20) / 2;
	width  = height;

	for (int iy = 0; iy < 2; iy++)
	{
		for (int ix = 0; ix < 2; ix++)
		{
			y0 = 20 + iy * height;
			x0 = pixmap->width - 2 * width + ix * width;
			pixmap->fillRect(x0, y0, width, height, color(256 - 6 + iy * 2 + ix * 2), 0); // vga color!
			switch (iy * 2 + ix)
			{
			default:
			case 0: // a rectangle
			{
				Point p[6] = {
					{x0 + 2, y0 + 2}, //
					{x0 + width / 2, y0 + 2},
					{x0 + width - 4, y0 + 2},
					{x0 + 2, y0 + height - 4},
					{x0 + width / 2, y0 + height - 4},
					{x0 + width - 4, y0 + height - 4}};
				Point p2[4] = {
					{x0 + width - 4, y0 + 2},
					{x0 + 2, y0 + 2},
					{x0 + 2, y0 + height - 4},
					{x0 + width - 4, y0 + height - 4}};
				pixmap->fillPolygon(p2, 4, color(red), 1);
				pixmap->fillPolygon(p, 6, color(green), 1);
				break;
			}
			case 1: // pentagon and pentagram
			{
				x0 += width / 2;
				y0 += height / 2;
				{
					Point points[8];
					float r = float(width * 90 / 200);
					for (int i = 0; i < 7; i++)
					{
						int w	  = 123 + i * 360 / 7;
						points[i] = Point(x0 + int(r * sin(w)), y0 + int(r * cos(w)));
					}
					points[7] = points[0];
					//pixmap->drawPolygon(points, 8, color(light_red));
					pixmap->fillPolygon(points, 8, color(green), 1);
				}
				{
					Point points[8];
					float r = float(width * 85 / 200);
					for (int i = 0; i < 7; i++)
					{
						int w	  = 123 + 2 * i * 360 / 7;
						points[i] = Point(x0 + int(r * sin(w)), y0 + int(r * cos(w)));
					}
					points[7] = points[0];
					pixmap->drawPolygon(points, 8, color(bright_red), 1);
					pixmap->fillPolygon(points, 8, color(bright_green), 1);
				}
				{
					Point points[8];
					float r = float(width * 80 / 200);
					for (int i = 0; i < 7; i++)
					{
						int w	  = 123 + 3 * i * 360 / 7;
						points[i] = Point(x0 + int(r * sin(w)), y0 + int(r * cos(w)));
					}
					points[7] = points[0];
					pixmap->drawPolygon(points, 8, color(bright_red), 1);
					pixmap->fillPolygon(points, 8, color(bright_yellow), 1);
				}
				break;
			}
			case 3: // house with openings
			{
				Point p[] = {
					{-2, 1},  {-2, 0},	{-5, 0},  {-5, -3},					   // l.u. ecke des hauses
					{-4, -3},												   //start l tür
					{-4, -1}, {-3, -1}, {-3, -3}, {-2, -3}, {-2, -1},		   //start l fenster
					{0, -1},  {0, -2},	{-2, -2}, {-2, -3}, {2, -3},		   //start r fenster
					{2, -1},  {1, -1},	{1, -2},  {2, -2},	{2, -3},  {3, -3}, //start r tür
					{3, -1},  {4, -1},	{4, -3},  {5, -3},					   // r.u. ecke des hauses
					{5, 0},	  {4, 0},	{4, 1},	  {5, 1},	{1, 3},			   //first, start rundfenster
					{1, 2},	  {0, 1},	{1, 0},	  {2, 1},	{1, 2},	  {1, 3},  //first
					{-3, 1},  {-2, 1},
				};
				x0 += width / 2;
				y0 += height / 2;
				for (uint i = 0; i < NELEM(p); i++)
				{
					p[i].x = x0 + p[i].x * width / 11;
					p[i].y = y0 + p[i].y * width / -10;
				}
				pixmap->drawPolygon(p, NELEM(p), color(bright_red), 1);
				pixmap->fillPolygon(p, NELEM(p), color(bright_yellow), 0);
				break;
			}
			case 2: // saw tooth, very many points
			{
				Array<Point> p1(0u, 100);
				for (int x = 0; x < width; x += 10)
				{
					p1.append(Point(x0 + x, y0 + height / 2));
					p1.append(Point(x0 + x, y0 + height / 2 - 20));
				}
				Array<Point> p2(p1);
				p2.revert();
				for (uint i = 0; i < p2.count(); i++)
				{
					p2[i].x += 3;
					p2[i].y += 14;
				}
				p1.append(p2);
				pixmap->drawPolygon(&p1[0], p1.count(), color(white), 1);
				pixmap->fillPolygon(&p1[0], p1.count(), color(black), 0);

				for (uint i = 0; i < p1.count(); i++)
				{
					p1[i].x += 5;
					p1[i].y += 30;
				}
				pixmap->fillPolygon(&p1[0], p1.count(), color(black), 0);
				pixmap->drawPolygon(&p1[0], p1.count(), color(bright_green), 1);
				break;
			}
			}
		}
	}
}

// ############################# MP3 #########################################

#if 0 // use Mp3Decoder?

template<uint nc>
class PipedAdapter : public AudioSource<nc>
{
public:
	Queue<AudioSample<nc>, 4 * 1024> queue;
	int								 eof = false;

	uint getAudio(AudioSample<nc>* buffer, uint num_frames) noexcept override;
	//void setSampleRate(float /*new_sample_frequency*/) noexcept override {}
};

template<uint nc>
uint PipedAdapter<nc>::getAudio(AudioSample<nc>* buffer, uint num_frames) noexcept
{
	// provide audio data in AudioController callback:

	trace(__func__);

	uint n = queue.read(buffer, num_frames);
	if (n == num_frames) return n;
	if (eof) return n; // -> will remove us from the AudioController
	AudioSample<nc> s = n ? buffer[n - 1] : AudioSample<nc>();
	while (n < num_frames) buffer[n++] = s;
	return n;
}

class Mp3Decoder : public Audio::Mp3Decoder
{
public:
	Mp3Decoder(File* f) : file(f)
	{
		piped_audiosource  = new PipedAdapter<hw_num_channels>;
		samplerate_adapter = new SampleRateAdapter(piped_audiosource, float(sample_rate));
		addAudioSource(samplerate_adapter);
	}
	~Mp3Decoder() //
	{
		removeAudioSource(samplerate_adapter);
	}

	virtual uint input(uchar* data, uint count) override;
	virtual void output(const MadHeader*, const MadPcmBuffer*) override;

	uint		sample_rate = 44100;
	RCPtr<File> file;

	RCPtr<PipedAdapter<hw_num_channels>>	  piped_audiosource;
	RCPtr<SampleRateAdapter<hw_num_channels>> samplerate_adapter;
};

static_assert(sizeof(signed long long) == 8);
static_assert(sizeof(signed long) == 4);
static_assert(sizeof(signed int) == 4);


uint Mp3Decoder::input(uchar* data, uint count)
{
	// callback from mad mp3 encoder:
	// read more data from file:

	trace(__func__);

	uint n = file->read(data, count, true);
	if (n == 0) piped_audiosource->eof = true;
	return n;
}

static inline Sample mp3_scale(mad_fixed_t sample) // scale mp3 output to int16
{
	static_assert(sizeof(mad_fixed_t) == sizeof(int32));
	static_assert(sizeof(Sample) == sizeof(int16));

	sample += (1 << (MAD_F_FRACBITS - 16));		  // round
	sample = sample >> (MAD_F_FRACBITS + 1 - 16); // quantize

	return sample == Sample(sample) ? Sample(sample) : sample < 0 ? -0x8000 : +0x7fff;
}

static_assert(sizeof(mad_fixed_t) == 4);

void Mp3Decoder::output(const MadHeader*, const MadPcmBuffer* pcm)
{
	// callback from mad mp3 encoder:
	// output audio:

	trace(__func__);

	uint			   nchannels = pcm->channels;
	uint			   nsamples	 = pcm->length;
	const mad_fixed_t* left_ch	 = pcm->samples[0];
	const mad_fixed_t* right_ch	 = pcm->samples[1];

	if (sample_rate != pcm->samplerate)
	{
		printf("pcm.f = %u Hz\n", pcm->samplerate);
		while (piped_audiosource->queue.avail()) Dispatcher::run(500);
		sample_rate = pcm->samplerate;
		samplerate_adapter->setSourceSampleRate(float(sample_rate));
	}

	//printf("<%u> ", nsamples);

	while (nsamples--)
	{
		while (piped_audiosource->queue.free() == 0) Dispatcher::run(500);
		if (nchannels == 1) piped_audiosource->queue.put(HwAudioSample(mp3_scale(*left_ch++)));
		else piped_audiosource->queue.put(HwAudioSample(mp3_scale(*left_ch++), mp3_scale(*right_ch++)));
	}
}

void Console::play_mp3(cstr arg)
{
	trace(__func__);
	debugstr("%s: stack_free = %i\n", __func__, stack_free());
	debugstr("%s: stack min free = %i\n", __func__, calc_stack_guard_min_free(0));

	arg += *arg == ' ';

	uint min, avg, max;								// Hz
	LoadSensor::getLoad(0 /*core*/, min, avg, max); // reset counters

	Mp3Decoder decoder(Devices::openFile(arg));
	startAudio(true);

	Error error = decoder.play();
	if (error) fprintf(stderr, "mp3 decoder failure: %s\n", error);
	else fprintf(stderr, "mp3 decoder finished ok\n");

	LoadSensor::getLoad(0 /*core*/, min, avg, max); // get counters
	min = (min + 50000) / 100000;
	max = (max + 50000) / 100000;
	avg = (avg + 50000) / 100000;

	debugstr(
		"mp3 load: %u.%u, %u.%u, %u.%uMHz (min,avg,max)\n", //
		min / 10, min % 10, avg / 10, avg % 10, max / 10, max % 10);

	decoder.piped_audiosource->eof = true; // safety only
	while (decoder.piped_audiosource->queue.avail()) Dispatcher::run(100);
	stopAudio(true);
}

#endif

} // namespace kilipili
