/*
	Реализация спецификаций CLDC версии 1.1 (JSR-139), MIDP версии 2.1 (JSR-118)
	и других спецификаций для функционирования компактных приложений на языке
	Java (мидлетов) в среде программного обеспечения Малик Эмулятор.

	Copyright © 2016, 2019 Малик Разработчик

	Это свободная программа: вы можете перераспространять ее и/или изменять
	ее на условиях Меньшей Стандартной общественной лицензии GNU в том виде,
	в каком она была опубликована Фондом свободного программного обеспечения;
	либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.

	Эта программа распространяется в надежде, что она будет полезной,
	но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
	или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
	общественной лицензии GNU.

	Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
	вместе с этой программой. Если это не так, см.
	<https://www.gnu.org/licenses/>.
*/


package malik.emulator.midp;

import java.io.*;
import java.lang.ref.*;
import javax.microedition.lcdui.*;
import malik.emulator.i18n.*;

public class ConsoleScreen extends CustomDisplayable
		implements PrintListener
{
	private static final class HelpCommand extends ConsoleCommand
	{
		HelpCommand()
		{
			super("?",
					"Использование:\n" +
					"  ?\n" +
					"  ? <команда>\n" +
					"Без аргументов – получение списка доступных команд, " +
					"с аргументом – получение справки по команде, указанной в аргументе.");
		}

		public void run(String[] arguments)
		{
			int i;
			int len;
			ConsoleCommand[] commands;
			ConsoleCommand command;
			ConsoleScreen console;
			String name;
			if((console = getConsoleScreen()) == null)
			{
				return;
			}
			if(arguments.length <= 0)
			{
				/* Список доступных команд */
				console.print("Список доступных команд:\n");
				commands = console.consoleCommands;
				len = console.consoleCommandsCount;
				for(i = 0; i < len; i++)
				{
					if((command = commands[i]).isHide())
					{
						continue;
					}
					console.print("  " + command.getName() + '\n');
				}
				console.print("Попробуйте так же\n" +
						"  ? <команда>\n" +
						"для получения справки по конкретной команде.\n");
				console.printHelp();
			} else
			{
				/* Справка по определённой команде */
				if((command = console.getConsoleCommand(name = arguments[0])) == null ||
						command.isHide())
				{
					console.print("Неизвестная команда " + name + '\n');
					return;
				}
				console.print(command.getHelp());
				console.println();
			}
		}
	}

	public static final int DEFAULT_MAXIMUM_LINES = 200;
	public static final int DEFAULT_BACKGROUND_COLOR = 0x000000;
	public static final int DEFAULT_TITLE_COLOR = 0xc0d0c0;
	public static final int DEFAULT_OUTPUT_COLOR = 0xc0c0c0;
	public static final int DEFAULT_INPUT_COLOR = 0xffffff;
	public static final Font DEFAULT_OUTPUT_FONT;
	public static final Font DEFAULT_INPUT_FONT;
	public static final Command BACK_COMMAND;
	private static final char SPACE = 32;
	private static final Command[] COMMANDS;
	private static final Command RUN_COMMAND;
	private static final String GREETING = "Команда: ";

	static
	{
		Command[] commands;
		Command run;
		Command back;
		Font font = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_SMALL);
		commands = new Command[] {
				run = new Command("Выполнить", Command.OK, 0),
				back = new Command("Назад", Command.BACK, 0)
		};
		DEFAULT_OUTPUT_FONT = font;
		DEFAULT_INPUT_FONT = font;
		BACK_COMMAND = back;
		COMMANDS = commands;
		RUN_COMMAND = run;
	}

	private static String toString(char[] src, int offset, int length)
	{
		char next;
		int i;
		int len;
		char[] str;
		Array.copy(src, offset, str = new char[len = length], 0, len);
		for(i = len - 1; i-- > 0; )
		{
			if(str[i] == '\\' && ((next = str[i + 1]) == '"' || next == 'n' || next == '\\'))
			{
				Array.copy(str, i + 1, str, i, len-- - i - 1);
				if(next == 'n')
				{
					str[i] = '\n';
				}
			}
		}
		return new String(str, 0, len);
	}


	private int backgroundColor;
	private int titleColor;
	private int inputColor;
	private int inputTop;
	private int outputColor;
	private int outputLinesCount;
	private int outputFirstLineIndex;
	private int lastCommandIndex;
	private int lastCommandsCount;
	private int consoleCommandsCount;
	private String[] lastCommands;
	private ConsoleCommand[] consoleCommands;
	private TextOutputMultilinedColor output;
	private TextInput input;
	private Font inputFont;
	private Font outputFont;
	private Reference reference;

	public ConsoleScreen(String title, CommandListener listener)
	{
		this(false, title, listener, DEFAULT_MAXIMUM_LINES, DEFAULT_BACKGROUND_COLOR,
				DEFAULT_TITLE_COLOR, DEFAULT_OUTPUT_COLOR, DEFAULT_OUTPUT_FONT,
				DEFAULT_INPUT_COLOR, DEFAULT_INPUT_FONT);
	}

	public ConsoleScreen(boolean fullScreenMode, String title, CommandListener listener,
			int maximumLines, int backgroundColor, int titleColor,
			int outputColor, Font outputFont, int inputColor, Font inputFont)
	{
		super(fullScreenMode, title, null, COMMANDS, null, listener);
		int t;
		int sh;
		int infh;
		int outfh;
		ConsoleCommand[] list;
		Reference ref;
		Font inf;
		Font outf;
		sh = super.getHeight();
		infh = (inf = inputFont == null ? DEFAULT_INPUT_FONT : inputFont).getHeight();
		outfh = (outf = outputFont == null ? DEFAULT_OUTPUT_FONT : outputFont).getHeight();
		maximumLines = maximumLines < 10 ? 10 : (maximumLines > 1000 ? 1000 : maximumLines);
		this.backgroundColor = backgroundColor;
		this.titleColor = titleColor;
		this.inputColor = inputColor;
		this.inputTop = t = sh - (infh + 1);
		this.outputColor = outputColor;
		this.outputLinesCount = t / outfh;
		this.consoleCommandsCount = 1;
		this.lastCommands = new String[50];
		this.consoleCommands = list = new ConsoleCommand[9];
		this.output = new TextOutputMultilinedColor(0x0400, maximumLines, outputColor);
		this.input = new TextInput(null, 1000, Input.ANY, 1000);
		this.inputFont = inf;
		this.outputFont = outf;
		this.reference = ref = new WeakReference(this);
		(list[0] = new HelpCommand()).setConsoleScreen(ref);
	}

	public void addCommand(Command command)
	{
		throw new IllegalStateException("ConsoleScreen.addCommand: " +
				"консольный экран не поддерживает дополнительные команды.");
	}

	public void setDefaultCommand(Command command)
	{
		throw new IllegalStateException("ConsoleScreen.setDefaultCommand: " +
				"консольный экран не поддерживает дополнительные команды.");
	}

	public void setTicker(Ticker ticker)
	{
		throw new IllegalStateException("ConsoleScreen.setTicker: " +
				"консольный экран не поддерживает бегущие строки.");
	}

	public void write(byte[] src, int offset, int length)
	{
		char[] ac;
		TextOutputMultilinedColor output;
		synchronized(output = this.output)
		{
			output.append(ac = Helper.byteToCharArray(src, offset, length), 0, ac.length);
			checkLength(output);
		}
		repaint(CLIENT);
	}

	public void print(char src)
	{
		TextOutputMultilinedColor output;
		synchronized(output = this.output)
		{
			output.append(String.valueOf(src));
			checkLength(output);
		}
		repaint(CLIENT);
	}

	public void print(char[] src)
	{
		TextOutputMultilinedColor output;
		synchronized(output = this.output)
		{
			output.append(src, 0, src != null ? src.length : 0);
			checkLength(output);
		}
		repaint(CLIENT);
	}

	public void print(String src)
	{
		TextOutputMultilinedColor output;
		synchronized(output = this.output)
		{
			output.append(src);
			checkLength(output);
		}
		repaint(CLIENT);
	}

	public void println()
	{
		TextOutputMultilinedColor output;
		synchronized(output = this.output)
		{
			output.append(String.valueOf('\n'));
			checkLength(output);
		}
		repaint(CLIENT);
	}

	public void printHelp()
	{
		print("Регистр команд и аргументов учитывается. Аргументы пишутся через пробел, " +
				"в них допустимы так называемые escape-последовательности \\\\, \\\", \\n; аргу" +
				"менты можно заключать в кавычки (\"…\"), если внутри них содержатся пробелы.\n" +
				"Стрелками \u2191 и \u2193 можно прокручивать текст консоли, " +
				"стрелками \u2190 и \u2192 можно прокручивать последние набранные команды,\n" +
				"Enter – выполнить набранную команду,\nEsc – выход из консоли.\n");
	}

	public void clear()
	{
		TextOutputMultilinedColor output;
		synchronized(output = this.output)
		{
			output.setColor(outputColor);
			output.clear();
			output.split(outputFont, super.getWidth());
			scrollToLastLine();
			repaint(CLIENT);
		}
	}

	public void setColor(int color)
	{
		TextOutputMultilinedColor output;
		synchronized(output = this.output)
		{
			output.setColor(color);
		}
	}

	public void setBackgroundColor(int backgroundColor)
	{
		this.backgroundColor = backgroundColor;
		repaint();
	}

	public void setTitleColor(int titleColor)
	{
		this.titleColor = titleColor;
		repaint(TITLE);
	}

	public void setOutputColor(int outputColor)
	{
		this.outputColor = outputColor;
	}

	public void setInputColor(int inputColor)
	{
		this.inputColor = inputColor;
		repaint(CLIENT);
	}

	public void setOutputFont(Font outputFont)
	{
		this.outputFont = outputFont == null ? DEFAULT_OUTPUT_FONT : outputFont;
		super.setEventSizeChanged();
		repaint(CLIENT);
	}

	public void setInputFont(Font inputFont)
	{
		this.inputFont = inputFont == null ? DEFAULT_INPUT_FONT : inputFont;
		super.setEventSizeChanged();
		repaint(CLIENT);
	}

	public void addConsoleCommand(ConsoleCommand command)
	{
		int len;
		ConsoleCommand[] c;
		if(command == null)
		{
			throw new NullPointerException("ConsoleScreen.addConsoleCommand: " +
					"параметр command равен нулевой ссылке.");
		}
		if(command.getConsoleScreen() != null)
		{
			throw new IllegalArgumentException("ConsoleScreen.addConsoleCommand: " +
					"параметр command уже принадлежит консоли.");
		}
		synchronized(super.getMonitor())
		{
			if((len = consoleCommandsCount) == (c = consoleCommands).length)
			{
				Array.copy(c, 0, c = consoleCommands = new ConsoleCommand[(len << 1) - 1], 0, len);
			}
			c[len++] = command;
			consoleCommandsCount = len;
			command.setConsoleScreen(reference);
		}
	}

	public int getBackgroundColor()
	{
		return backgroundColor;
	}

	public int getTitleColor()
	{
		return titleColor;
	}

	public int getOutputColor()
	{
		return outputColor;
	}

	public int getInputColor()
	{
		return inputColor;
	}

	public Font getOutputFont()
	{
		return outputFont;
	}

	public Font getInputFont()
	{
		return inputFont;
	}

	protected void paint(Graphics render)
	{
		int width;
		int inputLeft;
		int inputTop;
		int outputFirstLineIndex;
		int outputLinesCount;
		int inputLength;
		char[] inputChars;
		Font inputFont = this.inputFont;
		Font outputFont = this.outputFont;
		TextOutputMultilinedColor output;
		TextInput input = this.input;
		render.setFont(outputFont);
		synchronized(output = this.output)
		{
			if(output.getLineStart(outputLinesCount = output.getLinesCount()) < output.getLength())
			{
				output.split(outputFont, super.getWidth());
				output.deleteStartLines();
				scrollToLastLine();
			}
			if((outputLinesCount = output.getLinesCount()) > 0 &&
					outputLinesCount > (outputFirstLineIndex = this.outputFirstLineIndex))
			{
				output.paintLines(render, 0, 0, outputFirstLineIndex,
						Math.min(outputLinesCount - outputFirstLineIndex, this.outputLinesCount));
			}
		}
		render.setFont(inputFont);
		render.setColor(inputColor);
		render.drawLine(0, inputTop = this.inputTop, (width = super.getWidth()) - 1, inputTop++);
		render.drawString(GREETING, 0, inputTop, Graphics.LEFT | Graphics.TOP);
		inputLeft = inputFont.stringWidth(GREETING);
		inputLength = input.getLength();
		inputChars = input.getChars();
		if(inputLength < inputChars.length)
		{
			inputChars[inputLength++] = '_';
		}
		render.clipRect(inputLeft, inputTop, width - inputLeft, super.getHeight() - inputTop);
		render.drawChars(inputChars, 0, inputLength,
				Math.min(inputLeft, width - inputFont.charsWidth(inputChars, 0, inputLength)),
				inputTop, Graphics.LEFT | Graphics.TOP);
	}

	protected void paintTitle(Graphics render, int width, int height, String title)
	{
		render.setColor(backgroundColor);
		render.fillRect(0, 0, width--, --height);
		render.setColor(titleColor);
		render.drawLine(0, height, width, height);
		render.drawString(title, 1, 2, Graphics.LEFT | Graphics.TOP);
	}

	protected void paintClient(Graphics render, int width, int height,
			int clipLeft, int clipTop, int clipWidth, int clipHeight)
	{
		Graphics clientRender;
		render.setColor(backgroundColor);
		render.fillRect(0, 0, width, height);
		(clientRender = getClientGraphics()).reset();
		clientRender.setClip(clipLeft, clipTop, clipWidth, clipHeight);
		paint(clientRender);
	}

	protected void paintCommands(Graphics render, int width, int height,
			Command[] commands, int pressedIndex, Font defaultCommandFont)
	{
		int w;
		int a;
		int b;
		b = (w = width) - (a = w / 3);
		render.setColor(backgroundColor);
		render.fillRect(0, 0, width, height);
		render.setColor(inputColor);
		paintCommand(render, 0, 0, a, height, commands[0], false, pressedIndex == 0);
		paintCommand(render, b, 0, w - b, height, commands[2], false, pressedIndex == 2);
		render.setFont(defaultCommandFont);
		paintCommand(render, a, 0, b - a, height, commands[1], true, pressedIndex == 1);
	}

	protected void paintCommand(Graphics render, int left, int top, int width, int height,
			Command command, boolean asDefault, boolean asPressed)
	{
		if(command != null)
		{
			render.drawRoundRect(left, top, width - 1, height - 1, 2, 2);
			render.drawString(
					command.getTruncatedLabel(render.getFont(), width - 4),
					left + (width / 2) + (asPressed ? 1 : 0),
					top + (asPressed ? 3 : 2) + (asDefault ? -1 : 1),
					Graphics.HCENTER | Graphics.TOP);
		}
	}

	protected void onSizeChanged(int width, int height)
	{
		int t;
		int infh = inputFont.getHeight();
		inputTop = t = height - (infh + 1);
		outputLinesCount = t / outputFont.getHeight();
		super.onSizeChanged(width, height);
	}

	protected void onKeyPressed(int key, int charCode)
	{
		if(!super.keyHandling(key))
		{
			switch(key)
			{
			case MIDletProxy.KEY_UP:
				{
					int firstLineIndex;
					if((firstLineIndex = outputFirstLineIndex) > 0)
					{
						outputFirstLineIndex = firstLineIndex - 1;
						repaint(CLIENT);
					}
				}
				return;
			case MIDletProxy.KEY_DOWN:
				{
					int firstLineIndex;
					if((firstLineIndex = outputFirstLineIndex) <
							output.getLinesCount() - outputLinesCount)
					{
						outputFirstLineIndex = firstLineIndex + 1;
						repaint(CLIENT);
					}
				}
				return;
			case MIDletProxy.KEY_LEFT:
				{
					int count = lastCommandsCount + 1;
					int index = lastCommandIndex = (lastCommandIndex + count - 1) % count;
					TextInput input;
					(input = this.input).setString(lastCommands[index]);
					input.setCaretPosition(input.getLength());
					repaint(CLIENT);
				}
				return;
			case MIDletProxy.KEY_RIGHT:
				{
					int count = lastCommandsCount + 1;
					int index = lastCommandIndex = (lastCommandIndex + 1) % count;
					TextInput input;
					(input = this.input).setString(lastCommands[index]);
					input.setCaretPosition(input.getLength());
					repaint(CLIENT);
				}
				return;
			default:
				{
					TextInput input;
					(input = this.input).keyPressed(key, charCode);
					if(input.isChanged())
					{
						repaint(CLIENT);
					}
				}
				return;
			}
		}
		super.onKeyPressed(key, charCode);
	}

	protected void onKeyRepeated(int key, int charCode)
	{
		if(!super.keyHandling(key))
		{
			switch(key)
			{
			case MIDletProxy.KEY_UP:
				{
					int firstLineIndex;
					if((firstLineIndex = outputFirstLineIndex) > 0)
					{
						outputFirstLineIndex = firstLineIndex - 1;
						repaint(CLIENT);
					}
				}
				return;
			case MIDletProxy.KEY_DOWN:
				{
					int firstLineIndex;
					if((firstLineIndex = outputFirstLineIndex) <
							output.getLinesCount() - outputLinesCount)
					{
						outputFirstLineIndex = firstLineIndex + 1;
						repaint(CLIENT);
					}
				}
				return;
			default:
				{
					TextInput input;
					(input = this.input).keyPressed(key, charCode);
					if(input.isChanged())
					{
						repaint(CLIENT);
					}
				}
				return;
			}
		}
		super.onKeyRepeated(key, charCode);
	}

	protected void onKeyReleased(int key)
	{
		switch(key)
		{
		case MIDletProxy.KEY_ESCAPE:
			if(super.keyHandling(key))
			{
				break;
			}
			super.setEventCommandAction(BACK_COMMAND);
			return;
		case MIDletProxy.KEY_ENTER:
			if(super.keyHandling(key))
			{
				break;
			}
			super.setEventCommandAction(RUN_COMMAND);
			return;
		case MIDletProxy.KEY_SHIFT:
		case MIDletProxy.KEY_LSHIFT:
		case MIDletProxy.KEY_RSHIFT:
			input.keyReleased(key);
			return;
		}
		super.onKeyReleased(key);
	}

	protected void onCommandAction(Command command)
	{
		if(command == RUN_COMMAND)
		{
			runCommand();
			return;
		}
		super.onCommandAction(command);
	}

	private void runCommand()
	{
		boolean q;
		boolean cq;
		char p;
		char c;
		int i;
		int i0;
		int len;
		int argsLen;
		int argsIndex;
		int argsStart;
		char[] line;
		String[] args;
		String name;
		TextInput input;
		ConsoleCommand command;
		/* инициализация */
		len = (input = this.input).getLength();
		line = input.getChars();
		i0 = -1;
		argsStart = -1;
		/* считывание имени команды */
		for(i = 0; i <= len; i++)
		{
			if((c = i < len ? line[i] : (char) 0) > 32 && (i <= 0 || line[i - 1] <= 32))
			{
				i0 = i;
			}
			if(i > 0 && c <= 32 && line[i - 1] > 32)
			{
				argsStart = i + 1;
				break;
			}
		}
		if(i0 < 0 || argsStart < 0)
		{
			return;
		}
		name = new String(line, i0, argsStart - i0 - 1);
		/* считывание аргументов команды */
		argsLen = 0;
		for(q = false, p = line[argsStart - 1], i = argsStart; i <= len; p = c, i++)
		{
			c = i < len ? line[i] : (char) 0;
			if(cq = c == '"' && p != '\\')
			{
				q = !q;
				c = SPACE;
			}
			if(cq ? !q : (q ? i == len : c <= 32 && p > 32))
			{
				argsLen++;
			}
		}
		args = new String[argsLen];
		argsIndex = 0;
		for(q = false, p = line[argsStart - 1], i = argsStart; i <= len; p = c, i++)
		{
			c = i < len ? line[i] : (char) 0;
			if(cq = c == '"' && p != '\\')
			{
				q = !q;
				c = SPACE;
			}
			if(cq ? q : (!q) && c > 32 && p <= 32)
			{
				i0 = cq ? i + 1 : i;
			}
			if(cq ? !q : (q ? i == len : c <= 32 && p > 32))
			{
				args[argsIndex++] = toString(line, i0, i - i0);
			}
		}
		if(argsIndex < argsLen)
		{
			return;
		}
		/* добавление команды в список недавно набранных */
		addLastCommand(String.valueOf(line, 0, len).intern());
		input.clear();
		/* выполнение команды */
		setColor(inputColor);
		print((new StringBuffer(GREETING)).append(line, 0, len).append('\n').toString());
		serviceRepaints();
		setColor(outputColor);
		if((command = getConsoleCommand(name)) == null)
		{
			print("Неизвестная команда " + name + '\n');
			return;
		}
		repaint(CLIENT);
		command.run(args);
	}

	private void addLastCommand(String lastCommand)
	{
		int len;
		String[] list;
		if((len = lastCommandsCount) == (list = lastCommands).length - 1)
		{
			Array.copy(list, 1, list, 0, len-- - 2);
		}
		list[len++] = lastCommand;
		lastCommandIndex = len;
		lastCommandsCount = len;
	}

	private void scrollToLastLine()
	{
		outputFirstLineIndex = Math.max(output.getLinesCount() - outputLinesCount, 0);
	}

	private void checkLength(TextOutputMultilinedColor output)
	{
		if(output.getLength() >= 25000)
		{
			output.split(outputFont, super.getWidth());
			output.deleteStartLines();
			scrollToLastLine();
		}
	}

	private ConsoleCommand getConsoleCommand(String name)
	{
		int i;
		ConsoleCommand[] commands;
		ConsoleCommand command;
		for(commands = consoleCommands, i = consoleCommandsCount; i-- > 0; )
		{
			if(name.equals((command = commands[i]).getName()))
			{
				return command;
			}
		}
		return null;
	}
}
