import AABB2D from '../../lib/ts/termdraw/AABB2D.ts';
import * as ansi from '../../lib/ts/termdraw/ansi.ts';
import { boundsToSize, sizeToBounds } from '../../lib/ts/termdraw/boundsutils.ts';
import { BDC_PROP_VALUES, BOX_DRAWING_CHAR_PROPS, LineStyle } from '../../lib/ts/termdraw/boxcharprops.ts';
import { makeChildLineBorderGenerator } from '../../lib/ts/termdraw/boxdrawcomponents2.ts';
import BoxDrawr from '../../lib/ts/termdraw/BoxDrawr.ts';
import { RegionFillOptions } from '../../lib/ts/termdraw/components2.ts';
import { AbstractComponentWrapper, AbstractRasterable, BoundedRasterable, FixedRasterable, FlexChild, makeBorderedAbstractRasterable, makeChildBorderGenerator, makeFlex, makeSolidGenerator, PackedRasterable, PaddingRasterable, RegionFillingRasterableGenerator, SizedRasterable } from '../../lib/ts/termdraw/components2.ts';
import TextRaster2, { Style } from '../../lib/ts/termdraw/TextRaster2.ts';
import { createUniformRaster, drawTextToRaster, textToRaster } from '../../lib/ts/termdraw/textraster2utils.ts';
import Vec2D from '../../lib/ts/termdraw/Vec2D.ts';
import { vec2dsAreEqual } from '../../lib/ts/termdraw/vecutils.ts';
import KeyEvent from '../../lib/ts/terminput/KeyEvent.ts';
import { AbstractAppInstance, DenoStdinLike, PossiblyTUIAppContext, PossiblyTUIAppSpawner, runTuiApp, TUIAppRunnerContext, Waitable } from '../../lib/ts/tuiappframework3.ts';
import WatchableVariable, { makeReadonlyWatchable } from '../../lib/ts/WatchableVariable.ts';

//// Misc helper functions

function sleep(ms:number) {
	return new Promise((resolve,_reject) => {
		setTimeout(resolve, ms);
	});
}

function clamp(val:number, min:number, max:number) : number {
	return val < min ? min : val > max ? max : val;
}

function clampSize(val:Vec2D<number>, min:Vec2D<number>, max:Vec2D<number>) : Vec2D<number> {
	return {
		x: clamp(val.x, min.x, max.x),
		y: clamp(val.y, min.y, max.y),
	};
}

class DemoAppInstance extends AbstractAppInstance<KeyEvent,number> {
	_requestCleanExit(result:number) {
		// could be overridden to say goodbye first or use a signal or something idk
		this._resolve(result);
	}
	// deno-lint-ignore no-explicit-any
	_abort(reason:any) {
		this._reject(reason);
	}
	
	override handleInput(input:KeyEvent) {
		if( input.key == "q" ) {
			this._requestCleanExit(0);
		}
		if(input.key == "c" && input.ctrlKey) {
			this._abort(new Error("Aborted by user"));
		}
	}
}

class EchoAppInstance extends DemoAppInstance {
	#textLines : string[];
	constructor(textLines:string[], ctx:PossiblyTUIAppContext) {
		super(ctx)
		this.#textLines = textLines;
		this._handleRunResult(this._run());
	}
	
	protected async _run() : Promise<number> {
		await this._ctx.writeOut("Hello.  I will echo some stuff shortly.\n");
		await sleep(500);
		this._ctx.setScene({
			rasterForSize: size => {
				// TODO: Make a component framework or something lol
				const idealSize = {
					x: this.#textLines.map(l => l.length).reduce((a,b) => Math.max(a,b), 0) + 4,
					y: this.#textLines.length + 2,
				}
				let rast = createUniformRaster(clampSize(idealSize, {x:0, y:0}, size), " ", ansi.RED_BACKGROUND);
				for( let i=0; i<this.#textLines.length; ++i ) {
					rast = drawTextToRaster(rast, {x:2, y:i+1}, this.#textLines[i], ansi.RED_BACKGROUND);
				}
				return rast;
			}
		});
		await sleep(1000);
		await this._ctx.writeOut("Now we're back to regular output.\n");
		return 0;
	}
}

class ClockAppInstance extends DemoAppInstance {
	_inputKeyMessage : string;
	
	constructor(ctx:PossiblyTUIAppContext) {
		super(ctx);
		this._inputKeyMessage = "";
		this._handleRunResult(this._run());
	}
	
	override handleInput(input: KeyEvent): void {
		this._inputKeyMessage = "Key pressed: " + JSON.stringify(input);
		super.handleInput(input);
		this._redraw();
	}
	
	_redraw() : void {
		const now = new Date();
		const textLines : string[] = [
			now.toString(),
			this._inputKeyMessage,
		];
		this._ctx.setScene({
			rasterForSize: (size) => {
				const idealSize = {
					x: textLines.map(l => l.length).reduce((a,b) => Math.max(a,b), 0) + 4,
					y: textLines.length + 2,
				}
				let rast = createUniformRaster(clampSize(idealSize, {x:0,y:0}, size), " ", ansi.RED_BACKGROUND);
				for( let i=0; i<textLines.length; ++i ) {
					rast = drawTextToRaster(rast, {x:1, y:i+1}, textLines[i], ansi.RED_BACKGROUND);
				}
				return rast;
			}
		});
	}
	
	async _run() : Promise<number> {
		let i = 0;
		while(i < 10 && !this._exited) {
			// Hmm: Could use an abort signal or something.
			this._redraw();
			await sleep(1000);
			++i;
		}
		return 0;
	}
}

/**
 * App that demonstrates reading from stdin
 * 
 * TODO: Use TBD component system to lay out/render
 */

interface WCAppState {
	currentInputName : string|undefined;
	byteCount : number;
	lineCount : number;
	status : "unstarted"|"reading"|"done";
}

class WCAppInstance extends DemoAppInstance implements SizedRasterable {
	#stdin : ReadableStream<Uint8Array>|undefined;
	#inputNames : string[];
	#sceneCache : SizedRasterable|undefined;
	constructor(ctx:PossiblyTUIAppContext, inputNames:string[]) {
		super(ctx);
		this.#stdin = ctx.stdin;
		this.#inputNames = inputNames;
		this._handleRunResult(this._run());
	}
	
	override _cleanup() : Promise<void> {
		if( typeof this._refreshTimer == 'number' ) {
			clearInterval(this._refreshTimer);
			this._refreshTimer = undefined;
		}
		// this.#reader.cancel("WCAppInstance#_cleanup()");
		return Promise.resolve();
	}
	
	_appState : WCAppState = {
		status: "unstarted",
		currentInputName: undefined,
		byteCount: 0,
		lineCount: 0,
	};
	
	_refreshTimer : number|undefined;
	
	_generateScene() : SizedRasterable {
		const now = new Date();
		
		const currentInputLine : [string,Style] =
			this._appState.status == "done" ? ['Reached end of input!', ansi.BRIGHT_GREEN_TEXT] :
			this._appState.status == "unstarted" ? ['', ansi.YELLOW_TEXT] :
			[`Reading ${this._appState.currentInputName}`, ansi.YELLOW_TEXT];
		
		const textLines : [string,Style][] = [
			[now.toString()                    ,ansi.UNDERLINED + ansi.BRIGHT_WHITE_TEXT + ansi.RED_BACKGROUND], // For demonstration's sake
			currentInputLine,
			[`Read ${this._appState.byteCount} bytes`   ,ansi.FAINT + ansi.BLUE_TEXT  ],
			[`Read ${this._appState.lineCount} lines`   ,ansi.BOLD  + ansi.BLUE_TEXT  ],
		];
		const idealSize = {
			x: textLines.map(l => l[0].length).reduce((a,b) => Math.max(a,b), 0),
			y: textLines.length + 2,
		}
		let rast = createUniformRaster(idealSize, " ", ansi.BLUE_BACKGROUND);
		for( let i=0; i<textLines.length; ++i ) {
			rast = drawTextToRaster(rast, {x:0, y:i}, textLines[i][0], textLines[i][1]);
		}
		const border = makeSolidGenerator(" ", ansi.RED_BACKGROUND);
		const content = new FixedRasterable(rast);
		const bordered = makeBorderedAbstractRasterable(border, 1, content);
		
		return new AbstractComponentWrapper(bordered);
	}
	
	get _scene() : SizedRasterable {
		if( this.#sceneCache == undefined ) {
			this.#sceneCache = this._generateScene();
		}
		return this.#sceneCache;
	}
	
	rasterForSize(size: Vec2D<number>): TextRaster2 {
		return this._scene.rasterForSize(size);
	}
	
	_updateView() {
		this.#sceneCache = undefined;
		// Could cache the scene and invalidate it at this point.
		this._ctx.setScene(this);
	}
	
	_patchState(patch : Partial<WCAppState>) {
		this._appState = {
			...this._appState,
			...patch
		};
		this._updateView();
	}
	
	async _run() : Promise<number> {
		this._refreshTimer = setInterval(this._updateView.bind(this), 1000);
		this._updateView();
		
		for( const inputName of this.#inputNames ) {
			const readable : ReadableStream<Uint8Array>|undefined = await (inputName == '-' ? Promise.resolve(this.#stdin) : (Deno.open(inputName, {read:true})).then(f => f.readable));
			if( readable == undefined ) {
				this._ctx.writeOut(`Failed to open '${inputName}'`);
				return 1;
			}
			this._patchState({
				status: "reading",
				currentInputName: inputName,
			});
			await sleep(500);
			try {
				for await( const chunk of readable ) {
					const byteCount = this._appState.byteCount + chunk.length;
					let lineCount = this._appState.lineCount;
					for( const byte of chunk ) {
						if( byte == 0x0A ) ++lineCount;
					}
					this._patchState({
						lineCount,
						byteCount
					});
					await sleep(500);
				}
			} finally {
				readable.cancel();
			}
		}
		await sleep(1000);
		this._patchState({
			currentInputName: undefined,
			status: "done"
		})
		await sleep(1000);
		return 0;
	}
}

const fixedSpace = new FixedRasterable(textToRaster(" ", ""));
const flexySpace = makeSolidGenerator(" ", "");
const flexyOneSpace = new PaddingRasterable({x0:0, y0:0, x1:1, y1:1}, flexySpace);

const oneCharPad = {
	component: fixedSpace,
	flexGrowAlong: 0,
	flexGrowAcross: 0,
	flexShrinkAlong: 1,
	flexShrinkAcross: 0,
};

function mkSimpleTextRasterable(spans:{text:string, style:Style}[], background=blackBackground) : AbstractRasterable {
	return makeFlex(
		"right",
		background,
		spans.map(span => {
			const rast = textToRaster(span.text, span.style);
			return {
				component: new FixedRasterable(rast),
				flexGrowAlong: 0,
				flexGrowAcross: 0,
				flexShrinkAlong: 0,
				flexShrinkAcross: 0,
			}
		}),
	);
}

function mkTextRasterable(spans:{text:string, style:Style}[], background=blackBackground) : AbstractRasterable {
	return makeFlex("right", background, [
		...spans.map(span => {
			const rast = textToRaster(span.text, span.style);
			return {
				component: new FixedRasterable(rast),
				flexGrowAlong: 0,
				flexGrowAcross: 0,
				flexShrinkAlong: 0,
				flexShrinkAcross: 0,
			}
		}),
		{
			// This bit is necessary because there's not yet any way
			// to tell a component to align itself right or left
			// (default is to center everything)
			// the padding ensures that it fills the entire space,
			// so alignment is irrelevant.
			component: flexySpace,
			flexGrowAlong: 1,
			flexGrowAcross: 0,
			flexShrinkAlong: 1,
			flexShrinkAcross: 0,
		}
	], {
		alongBeforeSpace: 1,
		alongAfterSpace: 1,
	}); // Maybe add a padding one at the end
}

class SizedLineBorderRasterable implements BoundedRasterable {
	readonly bounds : AABB2D<number>;
	readonly #bdcLineStyle : LineStyle;
	readonly #lineStyle  : Style;
	constructor(region:AABB2D<number>, bdcLineStyle:LineStyle, lineStyle:Style) {
		this.bounds = region;
		this.#bdcLineStyle = bdcLineStyle;
		this.#lineStyle = lineStyle;
	}
	rasterForRegion(region: AABB2D<number>): TextRaster2 {
		const size = boundsToSize(region);
		const boxDrawr = new BoxDrawr(size.x, size.y);
		const x0 = this.bounds.x0 - region.x0;
		const y0 = this.bounds.y0 - region.y0;
		const x1 = this.bounds.x1 - region.x0 - 1;
		const y1 = this.bounds.y1 - region.y0 - 1;
		const ls = this.#bdcLineStyle;
		boxDrawr.addLine(x0, y0, x1, y0, ls);
		boxDrawr.addLine(x1, y0, x1, y1, ls);
		boxDrawr.addLine(x1, y1, x0, y1, ls);
		boxDrawr.addLine(x0, y1, x0, y0, ls);
		return boxDrawr.contentToRaster(this.#lineStyle);
	}
}

// TODO: Use this to draw some cool bordered boxes
function lineBorder(bdcLineStyle:LineStyle, lineStyle:Style) : AbstractRasterable&PackedRasterable&RegionFillingRasterableGenerator {
	return {
		bounds: {x0: -1, y0:-1, x1: 1, y1: 1},
		pack() { return this; },
		fillSize(size:Vec2D<number>) {
			return this.fillRegion(sizeToBounds(size));
		},
		fillRegion(region:AABB2D<number>) {
			return new SizedLineBorderRasterable(region, bdcLineStyle, lineStyle);
		},
	}
}

function lineBordered(bdcLineStyle:LineStyle, lineStyle:Style, interior:AbstractRasterable) : AbstractRasterable {
	return makeBorderedAbstractRasterable(
		lineBorder(bdcLineStyle, lineStyle),
		1, interior
	);
}

const blackBackground = makeSolidGenerator(" ", ansi.BLACK_BACKGROUND);
const blueBackground = makeSolidGenerator(" ", ansi.BLUE_BACKGROUND);
/**
 * I use cyanBackground to mark regions that should be hidden;
 * if cyan background shows up, there's a problem somewhere!
 */
const cyanBackground = makeSolidGenerator("!", ansi.CYAN_BACKGROUND);
const redBackground = makeSolidGenerator(" ", ansi.RED_BACKGROUND);
/** toBeLined = placeholder until I figure out how to draw lines */
const toBeLined = makeChildLineBorderGenerator(BDC_PROP_VALUES.LIGHT, ansi.WHITE_TEXT);

interface BoxesAppOptions {
	wrapperBorderEnabled: boolean;
	wrapperFlexEnabled: boolean;
	/** Quit after drawing the scene once? */
	autoQuit: boolean;
}

class BoxesAppInstance extends DemoAppInstance implements SizedRasterable {
	#options : BoxesAppOptions;
	constructor(ctx:PossiblyTUIAppContext, options:BoxesAppOptions) {
		super(ctx);
		
		this.#options = options;
		
		ctx.setScene(this);
	}
	
	_buildScene(size:Vec2D<number>) : SizedRasterable {
		// Background used for all the elements,
		// though it won't show through for most of them:
		const defaultBg = blueBackground;
		
		const welcomeSpan = mkTextRasterable([
			{text:"Welcome to boxes!", style:ansi.FAINT+ansi.YELLOW_TEXT},
		])
		const sizeSpan = mkTextRasterable([
			{text:"Screen size: ", style:ansi.WHITE_TEXT},
			{text:size.x +" x " +size.y, style:ansi.BRIGHT_WHITE_TEXT},
		]);
		const rigidFlexOpts = {
			flexGrowAlong: 0,
			flexGrowAcross: 0,
			flexShrinkAlong: 0,
			flexShrinkAcross: 0,
		};
		const gaseousFlexOpts = {
			flexGrowAlong: 100,
			flexGrowAcross: 100,
			flexShrinkAlong: 100,
			flexShrinkAcross: 100,
		};
		const genericFlexOpts = {
			flexGrowAlong: 1,
			flexGrowAcross: 1,
			flexShrinkAlong: 0,
			flexShrinkAcross: 0,
		};
		const texto = makeFlex("down", defaultBg, [
			{component: welcomeSpan, ...rigidFlexOpts},
			{component: sizeSpan   , ...rigidFlexOpts},
		]);
		const boxes = makeFlex("down", defaultBg, [
			{component: lineBordered(BDC_PROP_VALUES.LIGHT, ansi.WHITE_TEXT, texto), ...rigidFlexOpts},
			// TODO: Instead of solid, make boxes
			{component: lineBordered(BDC_PROP_VALUES.LIGHT, ansi.BOLD+ansi.RED_TEXT  , makeSolidGenerator("2", ansi.RED_TEXT  )), ...genericFlexOpts},
			{component: lineBordered(BDC_PROP_VALUES.LIGHT, ansi.BOLD+ansi.GREEN_TEXT, makeSolidGenerator("3", ansi.GREEN_TEXT)), ...genericFlexOpts},
			{component: lineBordered(BDC_PROP_VALUES.LIGHT, ansi.BOLD+ansi.BLUE_TEXT , makeSolidGenerator("4", ansi.BLUE_TEXT )), ...genericFlexOpts},
		]);
		let root : AbstractRasterable = boxes;
		if( this.#options.wrapperFlexEnabled ) {
			root = makeFlex("right", defaultBg, [
				{component: root, ...genericFlexOpts}
			]);
		}
		if( this.#options.wrapperBorderEnabled ) {
			root = lineBordered(BDC_PROP_VALUES.DOUBLE, ansi.BOLD+ansi.BRIGHT_RED_TEXT, boxes);
		}
		
		// Can this work here?
		if( this.#options.autoQuit ) this._requestCleanExit(0);
		
		return new AbstractComponentWrapper(root);
	}
	
	rasterForSize(size: Vec2D<number>): TextRaster2 {
		return this._buildScene(size).rasterForSize(size);
	}
}

interface StatusData {
	name: string;
	status?: string;
	lastSeen?: Date;
	recentMessages: string[];
}

function statusDataToAR(thing:StatusData) : AbstractRasterable {
	const statusSections : FlexChild<AbstractRasterable>[] = [];
	// Note that along = down, across = L-R
	// Status line
	statusSections.push({
		component: makeFlex("right",
			blackBackground,
			[
				{
					component: mkSimpleTextRasterable([{text: thing.name, style: ansi.BRIGHT_WHITE_TEXT}]),
					flexGrowAlong: 0, flexGrowAcross: 0, flexShrinkAlong: 0, flexShrinkAcross: 0
				},
				{
					component: flexyOneSpace,
					flexGrowAlong: 1, flexGrowAcross: 0, flexShrinkAlong: 1, flexShrinkAcross: 0
				},
				{
					component: mkSimpleTextRasterable([{
						text: thing.status ?? "?",
						style:
							thing.status == "online" ? ansi.BRIGHT_GREEN_TEXT :
							thing.status == "offline" ? ansi.RED_TEXT :
							ansi.YELLOW_TEXT
					}]),
					flexGrowAlong: 0, flexGrowAcross: 0, flexShrinkAlong: 0, flexShrinkAcross: 0
				},
			],
			{
				alongBeforeSpace: 1,
				alongAfterSpace: 1,
			}
		),
		flexGrowAlong: 0,
		flexGrowAcross: 1,
		flexShrinkAlong: 1,
		flexShrinkAcross: 1,
	});
	// Last seen line
	statusSections.push({
		component: mkTextRasterable([
			{
				text: "last seen: " + (thing.lastSeen == undefined ? "never" : thing.lastSeen.toISOString()),
				style: ansi.BRIGHT_BLACK_TEXT
			}
		]),
		flexGrowAlong: 0,
		flexGrowAcross: 0,
		flexShrinkAlong: 1,
		flexShrinkAcross: 1,
	});
	const messagesChildren : FlexChild<AbstractRasterable>[] = [];
	// TODO: Put messages in its own area that when shrunk
	// just shows fewer of them; might need to add a 'gravity' prop or something
	for( const msg of thing.recentMessages ) {
		messagesChildren.push({
			// TODO: Show the most recent message in different color
			// so I can check that sorting is working
			component: mkTextRasterable([
				{text: msg, style: ansi.WHITE_TEXT},
			]),
			flexGrowAlong: 0,
			flexGrowAcross: 1,
			flexShrinkAlong: 0,
			flexShrinkAcross: 1
		});
	}
	statusSections.push({
		// This causes trouble with the current flex layout system.
		// Maybe it would help to tell this flexbox that it's okay
		// to be small, don't bother wrapping, and to prioritize showing the bottom children;
		// or maybe it shouldn't be a flex at all!
		component: makeFlex("down", cyanBackground, messagesChildren),
		flexGrowAlong: 1,
		flexGrowAcross: 1,
		flexShrinkAlong: 1,
		flexShrinkAcross: 1,
	})
	return makeFlex("down", cyanBackground, statusSections);
}
function statusDatasToAR(things:StatusData[]) : AbstractRasterable {
	return makeFlex("down", toBeLined, things.map(sd => ({
		// TODO: Allow them to grow across, but not along!
		component: statusDataToAR(sd),
		flexGrowAlong: 0,
		flexGrowAcross: 1,
		flexShrinkAlong: 1,
		flexShrinkAcross: 1,
	})), {
		space: 1
	});
}

class StatusMockupAppInstance extends DemoAppInstance implements SizedRasterable {
	#statusDatas : StatusData[];
	
	constructor(ctx:PossiblyTUIAppContext) {
		super(ctx);
		this.#statusDatas = [
			{
				name: "bill", status: "online",
				lastSeen: new Date(),
				recentMessages: [
					"Hi there",
					"I'm Bill"
				]
			},
			{
				name: "ted", status: "offline",
				lastSeen: undefined,
				recentMessages: []
			},
		];
		ctx.setScene(this);
	}
	
	_buildScene(_size:Vec2D<number>) : SizedRasterable {
		let main = statusDatasToAR(this.#statusDatas);
		const demoNestedFlexBox = true;
		if( demoNestedFlexBox ) {
			main = makeFlex("down", blueBackground, [
				{
					component: main,
					flexGrowAlong: 0,
					flexGrowAcross: 0,
					flexShrinkAcross: 0,
					flexShrinkAlong: 0,
				}
			]);
		}
		return new AbstractComponentWrapper(main);
	}
	
	rasterForSize(size: Vec2D<number>): TextRaster2 {
		return this._buildScene(size).rasterForSize(size);
	}
}

//// Over-engineered process spawning stuff

const HELP_TEXT_LINES = [
	"Usage: tuidemo3 [--capture-input] [--output-mode={screen|lines}] <appname>",
	"",
	"A few simple apps to demonstrate a TUI app framework",
	"",
	"Options:",
	"  --capture-input ; Use 'raw mode'; application will parse key events",
	"                  ; (use 'q' to quit)",
	"  --output-mode=<mode>> ; Request the app operate with the given output mode",
	"                        ; (some apps will ignore this)",
	"",
	"Output modes (maybe badly named):",
	"  screen ; 'fullscreen'; app to use terminal as 2D canvas",
	"  lines  ; Output one line at a time, like a teletype",
	"",
	"Apps:",
	"  help   ; print this help text and exit",
	"  hello  ; Print some text, sleep a while, exit",
	"  clock  ; Show a clock, updated every second",
	"  wc     ; Read from stdin, print counts of bytes/lines read",
];

interface TopArgs {
	appName : string;
	outputMode? : "screen"|"lines";
	inputMode? : "none"|"push-key-events";
	screenSizeOverride? : Vec2D<number>;
	appArgs : string[];
}

function parseTopArgs(args:string[]) : TopArgs {
	let appArgs : string[] = [];
	let appName : string = "no-app-specified";
	let outputMode : undefined|"screen"|"lines"         = undefined;
	let inputMode  : undefined|"none"|"push-key-events" = undefined;
	let screenSizeOverride : undefined|Vec2D<number>    = undefined;
	let m : RegExpExecArray|null;
	for( let i=0; i<args.length; ++i ) {
		if( args[i] == '--help' ) {
			appName = 'help';
		} else if( args[i] == '--capture-input' ) {
			inputMode = "push-key-events";
		} else if( (m = /^--output-mode=(screen|lines)$/.exec(args[i])) != null ) {
			outputMode = m[1] as "screen"|"lines";
		} else if( (m = /^--screen-size=(?<x>\d+),(?<y>\d+)$/.exec(args[i])) != null ) {
			screenSizeOverride = {x:+m.groups!["x"], y: +m.groups!["y"]};
		} else if( args[i].startsWith("-") ) {
			appArgs.push(args[i]);
		} else {
			appName = args[i];
			appArgs = appArgs.concat(args.slice(i+1));
			break;
		}
	}
	return { appName, outputMode, inputMode, screenSizeOverride, appArgs };
}

// For compatibility with Deno.spawn, which may be less important
// now that I'm cramming other things into TUIAppSpawnContext context anyway.
interface DenoProcLikeSpawnContext {
	stdin  : DenoStdinLike;
	stdout : typeof Deno.stdout;
	stderr : typeof Deno.stderr;
}

// Ideally this would be mostly the same as what `runTuiApp` needs,
// minus the app's own preferences.  Still thinking on how
// stdin/stdout/stderr should be represented, since they are not always available.
type TUIAppSpawnContext = DenoProcLikeSpawnContext & Pick<TUIAppRunnerContext, "screenSizeVar"|"registerCleanup"|"unregisterCleanup">;

interface Spawner<C,R> {
	spawn(ctx:C) : R;
}

function tuiAppToProcLike<R>(
	app  : PossiblyTUIAppSpawner<PossiblyTUIAppContext, Waitable<R>, KeyEvent>,
	ctxOverrides : {
		outputMode: "screen"|"lines",
		screenSizeVar?: TUIAppSpawnContext["screenSizeVar"]
	}
) : Spawner<TUIAppSpawnContext, Waitable<R>> {
	return {
		spawn(ctx:TUIAppSpawnContext) : Waitable<R> {
			const exitCodePromise = runTuiApp(app, {
				stdin: ctx.stdin,
				stdout: ctx.stdout.writable.getWriter(),
				outputMode: ctxOverrides.outputMode,
				screenSizeVar: ctxOverrides.screenSizeVar ?? ctx.screenSizeVar,
				registerCleanup: ctx.registerCleanup,
				unregisterCleanup: ctx.unregisterCleanup,
			});
			return {
				wait() { return exitCodePromise }
			};
		}
	}
}

function echoAndExitApp(toStdout:string[], toStderr:string[], exitCode:number) : Spawner<DenoProcLikeSpawnContext,Waitable<number>> {
	const textEncoder = new TextEncoder();
	return {
		spawn(ctx:DenoProcLikeSpawnContext) : Waitable<number> {
			async function run() : Promise<number> {
				if( toStdout.length > 0 ) {
					const writer = ctx.stdout.writable.getWriter();
					for( const line of toStdout ) {
						await writer.write(textEncoder.encode(line+"\n"));
					}
				}
				if( toStderr.length > 0 ) {
					const writer = ctx.stderr.writable.getWriter();
					for( const line of toStderr ) {
						await writer.write(textEncoder.encode(line+"\n"));
					}
				}
				return exitCode;
			}
			const prom = run();
			return { wait: () => prom }
		}
	}
}

function makeDenoScreenSizeVariable(initalValue:Vec2D<number>, refreshInterval:number) : WatchableVariable<Vec2D<number>> {
	return makeReadonlyWatchable(
		initalValue,
		(abortSignal, setter) => {
			const refreshScreenSize = () => {
				const cs = Deno.consoleSize();
				const vec = {x:cs.columns, y:cs.rows};
				setter(vec);
			}
			
			let signalListenerAdded : boolean = false;
			let interval : number|undefined = undefined;
			
			refreshScreenSize();
			
			try {
				Deno.addSignalListener("SIGWINCH", refreshScreenSize);
				signalListenerAdded = true;
			} catch( e ) {
				interval = setInterval(refreshScreenSize, refreshInterval);
			}
			
			abortSignal.addEventListener("abort", () => {
				if(signalListenerAdded) Deno.removeSignalListener("SIGWINCH", refreshScreenSize);
				if(interval) clearInterval(interval);
			});
		},
		vec2dsAreEqual
	);
}

function parseMain(args:string[]) : Spawner<DenoProcLikeSpawnContext,Waitable<number>> {
	const topArgs = parseTopArgs(args);
	
	const ctxOverrides : Partial<Pick<TUIAppSpawnContext,"screenSizeVar">> = {};
	if( topArgs.screenSizeOverride != undefined ) ctxOverrides.screenSizeVar = makeReadonlyWatchable(topArgs.screenSizeOverride);
	
	if( topArgs.appName == "help" ) {
		return echoAndExitApp([], HELP_TEXT_LINES, 0);
	} else if( topArgs.appName == "clock" ) {
		return tuiAppToProcLike({
			inputMode: topArgs.inputMode ?? "none",
			spawn: (ctx:PossiblyTUIAppContext)  => new ClockAppInstance(ctx),
		}, {
			outputMode: topArgs.outputMode ?? "screen",
			...ctxOverrides,
		});
	} else if( topArgs.appName == "hello" ) {
		return tuiAppToProcLike({
			inputMode: topArgs.inputMode ?? "none",
			spawn: (ctx:PossiblyTUIAppContext) => new EchoAppInstance([
				"Hello, world!",
				"How's it going?"
			], ctx),
		}, {
			outputMode: topArgs.outputMode ?? "screen",
			...ctxOverrides,
		});
	} else if( topArgs.appName == "wc" ) {
		const inputFiles : string[] = [];
		let usingStdin = false;
		for( const arg of topArgs.appArgs ) {
			if( arg == '-' ) {
				usingStdin = true;
			} else if( !arg.startsWith("-") ) {
				inputFiles.push(arg);
				// Means 'wc should read from stdin'.
				// TODO: Allow apps to deal with their own arguments.
			} else {
				throw new Error(`Unrecognized argument to 'wc': '${arg}'`);
			}
		}
		return tuiAppToProcLike({
			inputMode: usingStdin || topArgs.inputMode == undefined ? 'none' : topArgs.inputMode,
			spawn: (ctx:PossiblyTUIAppContext) => new WCAppInstance(ctx, inputFiles),
		}, {
			outputMode: topArgs.outputMode ?? "screen",
			...ctxOverrides,
		});
	} else if( topArgs.appName == "boxes" ) {
		const boxesAppOpts : BoxesAppOptions = {
			wrapperBorderEnabled: false,
			wrapperFlexEnabled: false,
			autoQuit: false,
		}
		for( const arg of topArgs.appArgs ) {
			// Hmm: Could allow these in arbitrary order.
			if( arg == "--wrap-with-flex-row" ) {
				boxesAppOpts.wrapperFlexEnabled = true;
			} else if( arg == "--wrap-with-border" ) {
				boxesAppOpts.wrapperBorderEnabled = true;
			} else if( arg == "--quit" ) {
				boxesAppOpts.autoQuit = true;
			} else {
				throw new Error(`Unrecognized argument to 'boxes': '${arg}'`);
			}
		}
		return tuiAppToProcLike({
			inputMode: topArgs.inputMode ?? "none",
			spawn: (ctx:PossiblyTUIAppContext) => new BoxesAppInstance(ctx, boxesAppOpts),
		}, {
			outputMode: topArgs.outputMode ?? "screen",
			...ctxOverrides,
		});
	} else if( topArgs.appName == "status-mockup" ) {
		return tuiAppToProcLike({
			inputMode: topArgs.inputMode ?? "none",
			spawn: (ctx:PossiblyTUIAppContext) => new StatusMockupAppInstance(ctx),
		}, {
			outputMode: topArgs.outputMode ?? "screen",
			...ctxOverrides,
		});
	} else if( topArgs.appName == "no-app-specified" ) {
		return echoAndExitApp([], ["No app specified; try 'help'"], 1);
	} else {
		return echoAndExitApp([], [`Unrecognized command '${topArgs.appName}'`], 1);
	}
}

// TODO: Some kind of component library that
//   automatically lays things out, can draw boxes, tables, etc

if( import.meta.main ) {
	const spwanCtx : TUIAppSpawnContext = {
		stdin : Deno.stdin ,
		stdout: Deno.stdout,
		stderr: Deno.stderr,
		screenSizeVar: makeDenoScreenSizeVariable({x: 40, y: 20}, 1000),
		registerCleanup: (cb : ()=>void) => {
			Deno.addSignalListener("SIGINT", cb);
		},
		unregisterCleanup: (cb : ()=>void) => {
			Deno.removeSignalListener("SIGINT", cb);
		},
	};
	
	Deno.exit(await parseMain(Deno.args).spawn(spwanCtx).wait())
}
