#include "Plotter.hh"

#include "FileOperations.hh"
#include "IntegerSetting.hh"
#include "MSXCliComm.hh"
#include "MSXMotherBoard.hh"
#include "PNG.hh"
#include "PlotterFont.hh"

#include "ScopedAssign.hh"
#include "gl_mat.hh"
#include "ranges.hh"
#include "serialize.hh"
#include "strCat.hh"

#include <algorithm>
#include <cmath>

namespace openmsx {

// Enable debug output by setting to 0
#if 1
template<typename... Args>
static void printDebug(Args&&...)
{
	// nothing
}
#else
template<typename... Args>
static void printDebug(Args&&... args)
{
	std::cerr << strCat(std::forward<Args>(args)...) << '\n';
}
#endif

MSXPlotter::MSXPlotter(MSXMotherBoard& motherBoard)
	: cliComm(motherBoard.getMSXCliComm())
	, dpiSetting(motherBoard.getSharedStuff<IntegerSetting>(
		"print-resolution",
		motherBoard.getCommandController(), "print-resolution",
		"resolution of the output image of emulated dot matrix printer in DPI",
		300, 72, 1200))
	, charSetSetting(
		motherBoard.getCommandController(),
		"plotter-charset",
		"character set for the MSX plotter",
		CharacterSet::International,
		EnumSetting<CharacterSet>::Map{
			{"international", CharacterSet::International},
			{"din",           CharacterSet::DIN}})
	, dipSwitch4Setting(
		motherBoard.getCommandController(),
		"plotter-dipswitch4",
		"dipswitch 4 setting for the MSX plotter",
		false)
	, penThicknessSetting(
		motherBoard.getCommandController(),
		"plotter-pen-thickness",
		"pen thickness for the MSX plotter",
		PenThickness::Standard,
		EnumSetting<PenThickness>::Map{
			{"standard", PenThickness::Standard},
			{"thick",    PenThickness::Thick}})
{
	resetSettings();
	ensurePrintPage();
}

MSXPlotter::~MSXPlotter()
{
	flushEmulatedPrinter();
}

zstring_view MSXPlotter::getName() const
{
	return "MSXPlotter";
}

void MSXPlotter::plugHelper(Connector& connector_, EmuTime time)
{
	PrinterCore::plugHelper(connector_, time);
	resetSettings();
	ensurePrintPage();
}

zstring_view MSXPlotter::getDescription() const
{
	return "Sony PRN-C41 Color Plotter (4 pens)";
}

MSXPlotter::CharacterSet MSXPlotter::getCharacterSet() const
{
	return charSetSetting.getEnum();
}

void MSXPlotter::setCharacterSet(CharacterSet cs)
{
	charSetSetting.setEnum(cs);
}

bool MSXPlotter::getDipSwitch4() const
{
	return dipSwitch4Setting.getBoolean();
}

void MSXPlotter::setDipSwitch4(bool enabled)
{
	dipSwitch4Setting.setBoolean(enabled);
}

void MSXPlotter::cyclePen()
{
	selectedPen = (selectedPen + 1) % 4;
}

void MSXPlotter::moveStep(gl::vec2 delta)
{
	gl::vec2 oldAbsPos = penPosition + origin;
	gl::vec2 newAbsPos = gl::clamp(oldAbsPos + delta, gl::vec2{0.0f}, PLOT_AREA);
	penPosition = newAbsPos - origin;
}

void MSXPlotter::ejectPaper()
{
	flushEmulatedPrinter();
	resetSettings(); // Resets pen pos and other states
	ensurePrintPage();
}

void MSXPlotter::write(uint8_t data)
{
	printDebug("Plotter: received 0x", hex_string<2>(data),
	           " mode=", (mode == Mode::TEXT ? "TEXT" : "GRAPHIC"),
	           " escState=", int(escState));

	// Handle ESC sequence state first
	switch (escState) {
	using enum EscState;
	case ESC: // Got ESC, waiting for next char
		if (data == '#') {
			// ESC # = enter graphic mode
			printDebug("Plotter: entering GRAPHIC mode");
			mode = Mode::GRAPHIC;
			graphicCmdBuffer.clear();
			escState = NONE;
		} else if (data == '$') {
			// ESC $ = return to text mode, execute any pending graphics
			printDebug("Plotter: returning to TEXT mode");
			if (!graphicCmdBuffer.empty()) {
				executeGraphicCommand();
				graphicCmdBuffer.clear();
			}
			liftPen(); // end any open path on mode change
			mode = Mode::TEXT;
			escState = NONE;
		} else if (data == 'C') {
			// ESC C = color set, wait for color digit
			escState = ESC_C;
		} else {
			// Unknown ESC sequence, ignore
			cliComm.printWarning(
				"Plotter: unknown ESC sequence 0x", hex_string<2>(data));
			escState = NONE;
		}
		break;

	case ESC_C: // Got ESC C, waiting for color digit '0'-'3'
		if ('0' <= data && data <= '3') {
			liftPen();
			selectedPen = data - '0';
			printDebug("Plotter: selected pen ", selectedPen);
			terminatorSkip = TerminatorSkip::START;
		}
		escState = NONE;
		break;

	case EscState::ESC_S: // Got DC2 (0x12)
		if (data <= 15) {
			// Raw byte 0-15
			charScale = data;
			updateLineFeed();
			printDebug("Plotter: text scale set to ", charScale);
			terminatorSkip = TerminatorSkip::START;
			escState = NONE;
		} else if ('0' <= data && data <= '9') {
			// ASCII digit
			unsigned val = data - '0';
			if (val == 0 || val >= 2) {
				// '0' or '2'-'9' -> single digit, final
				charScale = val;
				updateLineFeed();
				printDebug("Plotter: text scale set to ", charScale);
				terminatorSkip = TerminatorSkip::START;
				escState = NONE;
			} else {
				// '1' -> could be start of "0"-"15"
				pendingScaleDigit = val;
				escState = ESC_S_EXP_DIGIT;
			}
		} else {
			// Invalid scale parameter, ignore command but process char
			escState = NONE;
			processTextMode(data);
		}
		break;

	case ESC_S_EXP_DIGIT: // Got DC2 + '1', waiting for potential second digit
		if ('0' <= data && data <= '9') {
			unsigned val = pendingScaleDigit * 10 + (data - '0');
			if (val <= 15) {
				charScale = val;
				updateLineFeed();
				printDebug("Plotter: text scale set to ", charScale);
				terminatorSkip = TerminatorSkip::START;
				escState = NONE;
			} else {
				// > 15, valid first digit but invalid second.
				// e.g. "19". Interpret first digit as scale, second as text.
				// Or ignore? for now ignore
				escState = NONE;
				processTextMode(data);
			}
		} else {
			// Not a digit. e.g. "1A".
			// Execute the pending '1' as scale.
			charScale = pendingScaleDigit;
			updateLineFeed();
			printDebug("Plotter: text scale set to ", charScale);
			terminatorSkip = TerminatorSkip::START;
				// Should we skip terminators here too if the first digit was
				// valid? The command technically executed properly with the
				// single digit. But followed by 'A', user probably didn't
				// intend a terminator sequence immediately. However, strictly
				// speaking, if '1' sets scale, command is done.
			escState = NONE;
			processTextMode(data);
		}
		break;

	case NONE:
		// Route to appropriate mode handler
		if (mode == Mode::TEXT) {
			processTextMode(data);
		} else {
			processGraphicMode(data);
		}
		break;
	}
}

void MSXPlotter::forceFormFeed()
{
	flushEmulatedPrinter();
}

void MSXPlotter::processTextMode(uint8_t data)
{
	using enum TerminatorSkip;
	if (terminatorSkip != NONE) {
		if (data == 0x0D) { // CR
			terminatorSkip = SEEN_CR;
			return;
		} else if (data == 0x0A) { // LF
			terminatorSkip = NONE;
			return;
		} else {
			terminatorSkip = NONE;
			// continue to process data
		}
	}

	if (printNext) {
		// CHR$(1) prefix: print alternate character (0-31) by subtracting 64
		// e.g., CHR$(1)+CHR$(65) prints character 1 (65-64=1)
		drawCharacter(uint8_t(data - 64));
		printNext = false;
		return;
	}

	switch (data) {
	case 0x01: // Header / Literal prefix
		printNext = true;
		break;
	case 0x08: // BS - Backspace
		penPosition.x = std::max(penPosition.x - float(8 + 2 * charScale), 0.0f);
		break;
	case 0x0d: // CR - Carriage return, if dipswitch 4 is set, also do a line feed
		penPosition.x = 0.0f;
		if (!getDipSwitch4()) break;
		[[fallthrough]];
	case 0x0a: // LF - Line feed
		penPosition.y -= lineFeed;
		if (penPosition.y < -1354.0f) { // Page end reached
			flushEmulatedPrinter();
			ensurePrintPage();
			penPosition.y = 30.0f; // Reset to top of new page
		}
		break;
	case 0x0b: // VT - Line up (feed paper to previous line)
		penPosition.y = std::min(penPosition.y + lineFeed, 30.0f);
		break;
	case 0x0C: // FF - Form feed (top of form)
		flushEmulatedPrinter();
		ensurePrintPage();
		penPosition = {0.0f, 30.0f};
		break;
	case 0x12: // DC2 - Scale set prefix (wait for next char)
		escState = EscState::ESC_S;
		break;
	case 0x1b: // ESC - Start escape sequence for mode (text/graphics)
		escState = EscState::ESC;
		break;
	default:
		// Printable character in text mode (Allow all >= 32)
		if (data >= 32) {
			// Check for line wrap before drawing
			// Estimate width: 6 steps base * scaleFactor
			// Or simply check current position
			if (penPosition.x >= RIGHT_BORDER) {
				// Auto CR/LF
				penPosition.x = 0.0f;
				penPosition.y -= lineFeed;
				if (penPosition.y < -1354.0f) {
					flushEmulatedPrinter();
					ensurePrintPage();
					penPosition.y = 30.0f;
				}
			}
			drawCharacter(data);
		}
		break;
	}
}

void MSXPlotter::processGraphicMode(uint8_t data)
{
	// In graphic mode, accumulate command chars until delimiter
	// Commands are ASCII text like "J100,-100,50,-100" terminated by CR/LF or
	// next command

	if (data == one_of(0x0d, 0x0a)) {
		// CR or LF terminates command
		executeGraphicCommand();
	} else if (data == 0x1b) {
		// ESC in graphic mode - start escape sequence
		executeGraphicCommand();
		escState = EscState::ESC;
	} else if (32 <= data && data < 127) {
		// Accumulate command characters
		graphicCmdBuffer += char(data);

		// Execute immediately if buffer starts with known command and we hit a
		// letter (next command) Commands start with a letter, followed by numbers
		// and commas
		if (graphicCmdBuffer.size() > 1) {
			// Special handling for P command which takes arbitrary text
			bool isPrintCmd = (graphicCmdBuffer[0] == 'P');

			char lastChar = graphicCmdBuffer.back();
			bool isAlpha  = ('a' <= lastChar && lastChar <= 'z') ||
			                ('A' <= lastChar && lastChar <= 'Z');
			if (!isPrintCmd && isAlpha && lastChar != one_of('E', 'e', graphicCmdBuffer.front())) {
				// New command starting, execute previous
				char nextCmd = graphicCmdBuffer.back();
				graphicCmdBuffer.pop_back();
				executeGraphicCommand();
				graphicCmdBuffer = nextCmd;
			}
		}
	}
}

void MSXPlotter::executeGraphicCommand()
{
	if (graphicCmdBuffer.empty()) return;

	printDebug("Plotter: executing graphic command '", graphicCmdBuffer, "'");

	char cmd = graphicCmdBuffer[0];
	std::string args = graphicCmdBuffer.substr(1);

	// Parse comma-separated numbers (integers or floats)
	std::vector<float> coords;
	size_t pos = 0;
	while (pos < args.size()) {
		// Skip whitespace and commas
		while (pos < args.size() && (args[pos] == ',' || args[pos] == ' ')) {
			++pos;
		}
		if (pos >= args.size()) break;

		// Try to parse a number
		size_t nextPos;
		try {
			// Using stod is easiest for double parsing, but we need to check if it
			// actually parses std::stod throws if no conversion. Manually check if
			// valid start of number? Valid: digit, +, -, or .
			char c = args[pos];
			if (('0' <= c && c <= '9') || c == one_of('-', '+', '.')) {
				// Check if it's just a lonely '+' or '-' or '.' which stof might handle
				// or reject Standard library stof parses.
				float val = std::stof(args.substr(pos), &nextPos);
				coords.push_back(val);
				pos += nextPos;
			} else {
				// Not a number, skip this character (it might be part of P command
				// text) For P command, we ideally shouldn't be parsing coords here, but
				// we do it generic. Just skip.
				++pos;
			}
		} catch (...) {
			// parsing failed, skip char
			++pos;
		}
	}

	// Any command other than D/J ends an open drawing path before command
	// side-effects such as color changes.
	if (cmd != 'D' && cmd != 'J') {
		liftPen();
	}

	switch (cmd) {
	case 'H': // Home - move to origin
		printDebug("Plotter: H - Home");
		penPosition = gl::vec2{0.0f};
		break;

	case 'M': // Move (absolute) - M x,y
		if (coords.size() >= 2) {
			printDebug("Plotter: M - Move to (", coords[0], ",", coords[1], ")");
			moveTo({coords[0], coords[1]});
		}
		break;

	case 'R': // Relative move - R dx,dy
		if (coords.size() >= 2) {
			printDebug("Plotter: R - Relative move (", coords[0], ",", coords[1], ")");
			moveTo(penPosition + gl::vec2{coords[0], coords[1]});
		}
		break;

	case 'D': // Draw (absolute) - D x,y[,x,y,...]
		// The D command draws a series of absolute line segments
		printDebug("Plotter: D - Draw absolute, ", coords.size() / 2, " segments, penDown=", penDown);
		setPenDown(true);
		for (size_t i = 0; i + 1 < coords.size(); i += 2) {
			gl::vec2 target{coords[i], coords[i + 1]};
			lineTo(target);
		}
		setPenDown(false);
		break;

	case 'J': // Draw relative - J dx,dy[,dx,dy,...]
		// The J command draws a series of relative line segments
		printDebug("Plotter: J - Draw relative, ", coords.size() / 2, " segments, penDown=", penDown);
		setPenDown(true);
		for (size_t i = 0; i + 1 < coords.size(); i += 2) {
			lineTo(penPosition + gl::vec2{coords[i], coords[i + 1]});
		}
		setPenDown(false);
		break;

	case 'S': // Scale set - S n (0-15)
		if (!coords.empty()) {
			charScale = std::clamp(int(coords[0]), 0, 15);
			updateLineFeed();
			printDebug("Plotter: S - Scale set to ", charScale);
		}
		break;

	case 'L': // Line type - L n (0-15)
		if (!coords.empty()) {
			lineType = uint8_t(std::clamp(int(coords[0]), 0, 15));
			dashDistance = 0.0f; // Reset pattern phase
			printDebug("Plotter: L - Line type set to ", lineType);
		}
		break;

	case 'Q': // Alpha rotate - Q n (0-3)
		if (!coords.empty()) {
			// If a character was just printed, undo the gap
			// (the last char before Q should not have the trailing gap)
			if (pendingCharGap > 0.0f) {
				switch (pendingGapRotation) {
				case 0: penPosition.x -= pendingCharGap; break;
				case 1: penPosition.y += pendingCharGap; break;
				case 2: penPosition.x += pendingCharGap; break;
				case 3: penPosition.y -= pendingCharGap; break;
				}
				printDebug("Plotter: Q - Removed pending gap ", pendingCharGap);
				pendingCharGap = 0.0f;
			}
			rotation = std::clamp(int(coords[0]), 0, 3);
			printDebug("Plotter: Q - Rotation set to ", rotation);
		}
		break;

	case 'I': // Initialize
		// Sets current pen position as origin
		origin += penPosition;
		penPosition = gl::vec2{0.0f};
		printDebug("Plotter: I - Origin set to current pos. New Origin=(", origin.x, ",", origin.y, ")");
		break;

	case 'A': // All initialize
		printDebug("Plotter: A - All Initialize (Reset)");
		resetSettings();
		break;

	case 'F': { // New line
		// Move to top of next line using max height encountered.
		// If empty line, default to standard line feed (12 steps + scale effect?).
		// If maxLineHeight is set, use it.

		// Default drop if no characters printed: Height + Gap
		// Height = 6 * (1+S), Gap = 2 * (1+S) => Total 8 * (1+S)
		// For Scale 1: 8 * 2 = 16 steps.
		float drop = (maxLineHeight > 0.0f) ? maxLineHeight : 6.0f * (1.0f + float(charScale));

		// Add inter-line spacing (Gap)
		drop += 2.0f * (1.0f + float(charScale));

		// Apply Line Feed and Carriage Return based on rotation
		switch (rotation) {
		case 0: // Normal: Feed Y- (Down), CR X=0
			penPosition.y -= drop;
			if (penPosition.y < -1354.0f) { // Page End check
				flushEmulatedPrinter();
				ensurePrintPage();
				penPosition.y = 30.0f;
			}
			penPosition.x = 0.0f;
			break;
		case 1: // 90 CW (Down): Feed X- (Left), CR Y=0
			penPosition.x = std::max(penPosition.x - drop, 0.0f);
			penPosition.y = 0.0f;
			break;
		case 2: // 180 (Left): Feed Y+ (Up), CR X=0
			penPosition.y = std::min(penPosition.y + drop, 30.0f);
			penPosition.x = 0.0f;
			break;
		case 3: // 270 CW (Up): Feed X+ (Right), CR Y=0
			penPosition.x = std::min(penPosition.x + drop, RIGHT_BORDER);
			penPosition.y = 0.0f;
			break;
		}

		maxLineHeight = 0.0f;
		printDebug("Plotter: F - New Line, drop=", drop, " rot=", rotation);
		break;
	}
	case 'P': // Print - P chrs
		if (!args.empty()) {
			// Characters after 'P' are printed literally, including spaces.
			// CHR$(1) prefix is used to print alternate characters (0-31).
			std::string_view text = args;
			printDebug("Plotter: P - Printing '", text, "'");

			bool altChar = false;
			for (auto c : text) {
				if (uint8_t(c) == 0x01) {
					altChar = true;
				} else if (altChar) {
					// CHR$(1)+CHR$(N) prints character N-64
					drawCharacter(uint8_t(c) - 64);
					altChar = false;
				} else {
					drawCharacter(uint8_t(c));
				}
			}
		}
		break;

	case 'C': // Color select - C n (0-3)
		if (!coords.empty() && 0 <= coords[0] && coords[0] <= 3) {
			auto newPen = unsigned(coords[0]);
			if (newPen != selectedPen) {
				printDebug("Plotter: C - Select color ", newPen, " (Pen change delay applied)");
				selectedPen = uint8_t(newPen);
			}
		}
		break;

	default:
		// Unknown command, ignore
		cliComm.printWarning("Plotter: unknown graphic command '", cmd, "'");
		break;
	}

	if (cmd != 'Q' && cmd != 'P') {
		pendingCharGap = 0.0f;
	}
	graphicCmdBuffer.clear();
}

gl::vec2 MSXPlotter::toPaperPos(gl::vec2 plotterPos) const
{
	// Margin constants: center the plotting area on the paper.
	// MARGIN_X = (PAPER_WIDTH_STEPS - PLOT_AREA_WIDTH) / 2 = 45 steps (9mm)
	// MARGIN_Y = (PAPER_HEIGHT_STEPS - PLOT_AREA_HEIGHT) / 2 = 48 steps (9.6mm)

	// Convert logical plotter steps to physical paper steps with margins.
	// Origin (0,0) is bottom-left of the plotting area.
	// X-axis: MarginX + LogicalX
	// Y-axis: (PaperHeight - MarginY) - LogicalY (inverted for top-down PNG)
	gl::vec2 paperPos = gl::vec2{MARGIN.x, FULL_AREA.y - MARGIN.y}
	                  + gl::vec2{1.0f, -1.0f} * (plotterPos + origin);
	return paperPos * pixelSize;
}

float MSXPlotter::penRadius() const
{
	// Sony PRN-C41 uses pens that are likely ~0.4-0.5mm wide.
	// Standard step is 0.2mm. Set dot size to 200% of step size for solid
	// lines.
	// radius = 0.2mm -> line width = 0.4mm,  or 0.6mm for thick line
	return pixelSize * ((penThicknessSetting.getEnum() == PenThickness::Thick) ? 1.5f : 1.0f);
}

gl::vec3 MSXPlotter::penColor() const
{
	return inkColors[selectedPen];
}

void MSXPlotter::drawDot(gl::vec2 pos)
{
	ensurePrintPage();
	assert(paper);
	paper->draw_dot(toPaperPos(pos), penRadius(), penColor());
}

void MSXPlotter::setPenDown(bool newState)
{
	if (newState) {
		// Only the first lowering of a stroke emits a touchdown dot.
		if (!penDown) {
			penDown = true;
			drawDot(penPosition);
		}
	} else {
		liftPen();
	}
}

void MSXPlotter::liftPen()
{
	penDown = false;
}

void MSXPlotter::drawLine(gl::vec2 from, gl::vec2 to)
{
	assert(penDown);

	ensurePrintPage();
	if (0 < lineType && lineType < 15) {
		auto halfPeriod = float(lineType + 2);
		drawDashedLine(from, to, halfPeriod);
	} else {
		drawSolidLine(from, to);
		dashDistance += length(to - from); // needed ?
	}
}

void MSXPlotter::drawDashedLine(gl::vec2 A, gl::vec2 B, float halfPeriod)
{
	if (A == B) return;

	gl::vec2 AB = B - A;
	float length2 = gl::length2(AB);
	float invLen = gl::rsqrt(length2);
	float len = length2 * invLen; // for dashDistance, avoids separate sqrt

	float period = 2.0f * halfPeriod;
	float phase = std::fmod(dashDistance, period);
	dashDistance += len;

	float t_start = (phase < halfPeriod) ? -phase : (period - phase);
	float t_end = t_start + halfPeriod;

	// Work in normalized parameter space [0,1] to avoid per-iteration multiplies
	float period_alpha = period * invLen;
	float alpha_start = t_start * invLen;
	float alpha_end = t_end * invLen;

	// First segment (may be partial at line start)
	if (alpha_start < 1.0f) {
		drawSolidLine(A + AB * std::max(0.0f, alpha_start), A + AB * std::min(1.0f, alpha_end));
		alpha_start += period_alpha;
		alpha_end += period_alpha;
	}

	// Middle segments (fully inside [0,1], no clipping)
	while (alpha_end < 1.0f) {
		drawSolidLine(A + AB * alpha_start, A + AB * alpha_end);
		alpha_start += period_alpha;
		alpha_end += period_alpha;
	}

	// Last segment (partial at line end)
	if (alpha_start < 1.0f) {
		drawSolidLine(A + AB * alpha_start, B);
	}
}

void MSXPlotter::drawSolidLine(gl::vec2 A, gl::vec2 B)
{
	assert(paper);
	paper->draw_motion(toPaperPos(A), toPaperPos(B), penRadius(), penColor());
}

void MSXPlotter::ensurePrintPage()
{
	if (paper) return;

	static constexpr auto MM_PER_INCH = 25.4f;
	auto paperSize = trunc(A4_SIZE / MM_PER_INCH * float(dpiSetting->getInt()));
	pixelSize = float(paperSize.x) / FULL_AREA.x; // pixels are round (x==y)
	paper = std::make_unique<PlotterPaper>(paperSize);
}

void MSXPlotter::flushEmulatedPrinter()
{
	liftPen();
	if (!paper) return;

	if (!paper->empty()) {
		try {
			static constexpr std::string_view PRINT_DIR = "prints";
			static constexpr std::string_view PRINT_EXTENSION = ".png";
			auto filename = FileOperations::getNextNumberedFileName(PRINT_DIR, "page", PRINT_EXTENSION);

			auto rgb = paper->getRGB();
			auto size = rgb.size();
			small_buffer<const uint8_t*, 4096> rowPointers(std::views::transform(xrange(size.y),
				[&](int y) { return &rgb.getLine(y).data()->x; }));

			PNG::saveRGB(size.x, rowPointers, filename);

			cliComm.printInfo("Printed to ", filename);
		} catch (MSXException& e) {
			cliComm.printWarning("Failed to print: ", e.getMessage());
		}
	}
	paper.reset();
}

void MSXPlotter::resetSettings()
{
	mode = Mode::TEXT;
	escState = EscState::NONE;
	selectedPen = 0;
	penDown = false;
	penPosition = gl::vec2{0.0f, 30.0f};
	origin = gl::vec2{0.0f, 1354.0f};
	lineType = 0;
	dashDistance = 0.0f;
	rotation = 0;
	pendingCharGap = 0.0f;
	pendingGapRotation = 0;
	charScale = 1; // Default scale is 1
	maxLineHeight = 0.0f;
	graphicCmdBuffer.clear();
	printNext = false;
	terminatorSkip = TerminatorSkip::NONE;

	// Set up printer settings for plotter
	updateLineFeed();
}

void MSXPlotter::moveTo(gl::vec2 pos)
{
	penPosition = pos;
}

void MSXPlotter::lineTo(gl::vec2 pos)
{
	drawLine(penPosition, pos);
	penPosition = pos;
}

void MSXPlotter::drawCharacter(uint8_t c)
{
	ScopedAssign savedLineType(lineType, 0);
	ScopedAssign savedDashDistance(dashDistance, dashDistance); // value doesn't matter, just restore at the end
	// Character glyphs are already vector strokes; keep the pen logically down
	// during the call without adding extra touchdown dwell dots.
	ScopedAssign savedPenState(penDown, true);

	auto fontVariant = [&] {
		switch (getCharacterSet()) {
		using enum CharacterSet;
		default:
		case International: return PlotterFontVariant::International;
		case DIN:           return PlotterFontVariant::DIN;
		}
	}();
	auto glyph = decodePRNC41Glyph(fontVariant, c);

	float scaleFactor = 1.0f + float(charScale);
	float unitX = 0.8f * scaleFactor;
	float unitY = 1.0f * scaleFactor;

	maxLineHeight = std::max(maxLineHeight, 6.0f * unitY);

	// Transform local (u, v) [0..width, 0..height] to global (px, py)
	// Cursor is at the START of the character baseline.
	// u=0 is at cursor, character extends in reading direction (+u).
	// v=0 is at baseline, character extends upward (+v).
	auto transform = [&](gl::vec2 p) -> gl::vec2 {
		// Each rotation matrix has columns [uDir, vDir] for that rotation
		static constexpr std::array<gl::mat2, 4> rotations = {{
			gl::mat2{gl::vec2{ 1.0f,  0.0f}, gl::vec2{ 0.0f,  1.0f}}, //   0°
			gl::mat2{gl::vec2{ 0.0f, -1.0f}, gl::vec2{ 1.0f,  0.0f}}, //  90° CW
			gl::mat2{gl::vec2{-1.0f,  0.0f}, gl::vec2{ 0.0f, -1.0f}}, // 180°
			gl::mat2{gl::vec2{ 0.0f,  1.0f}, gl::vec2{-1.0f,  0.0f}}, // 270° CW
		}};
		return penPosition + rotations[rotation] * p;
	};

	gl::vec2 glyphPos{0.0f, 0.0f};
	for (const auto& command : glyph) {
		gl::vec2 nextGlyphPos = glyphPos + gl::vec2{
			command.delta.x * unitX,
			command.delta.y * unitY,
		};
		if (command.type == PlotterFontCommandType::Draw) {
			drawLine(transform(glyphPos), transform(nextGlyphPos));
		}
		glyphPos = nextGlyphPos;
	}
	// Advance cursor to next character position
	// Always use full width (char + gap). If Q rotation follows, it will undo the
	// gap.
	float charWidthOnly = 5.0f * unitX;      // just the character
	float charGap = 2.0f * scaleFactor;      // gap between characters
	float charAdvance = charWidthOnly + charGap; // total advance

	penPosition = transform({charAdvance, 0.0f});

	// Track the gap so Q command can undo it if it follows
	pendingCharGap = charGap;
	pendingGapRotation = rotation;

	printDebug("Plotter: Rotation ", rotation,
	           " | charWidth ", charWidthOnly,
	           " | charGap ", charGap,
	           " | charAdvance ", charAdvance,
	           " | plotter.x ", penPosition.x,
	           " | plotter.y ", penPosition.y);
}

void MSXPlotter::updateLineFeed()
{
	// Base line feed for scale 1 is 18 (9.0 * 2).
	// Formula: 9.0 * (1.0 + Scale) seems consistent.
	// Scale 0: 9 * 1 = 9
	// Scale 1: 9 * 2 = 18
	lineFeed = 9.0f * (1.0f + float(charScale));
}

// Serialization and registration macros
INSTANTIATE_SERIALIZE_METHODS(MSXPlotter);
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, MSXPlotter, "MSXPlotter");

} // namespace openmsx
