/*
	Реализация спецификаций 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.application;

import java.util.*;
import malik.emulator.util.*;

public final class Run extends Object
		implements ThreadTerminationListener
{
	/* Следующие методы будут вызываться только из главного потока (т.е. сериализовано):
	 * void java.lang.Runnable.run()
	 * void java.lang.ThreadTerminationListener.onThreadTermination(java.lang.Thread,
	 *         java.lang.Throwable)
	 * void malik.emulator.application.AppProxy.appLaunch()
	 * void malik.emulator.application.AppProxy.appTerminate()
	 * void malik.emulator.application.AppProxy.keyPressed(int, int)
	 * void malik.emulator.application.AppProxy.keyRepeated(int, int)
	 * void malik.emulator.application.AppProxy.keyReleased(int)
	 * void malik.emulator.application.AppProxy.pointerPressed(int, int, int)
	 * void malik.emulator.application.AppProxy.pointerDragged(int, int)
	 * void malik.emulator.application.AppProxy.pointerReleased(int, int, int)
	 * void malik.emulator.application.AppProxy.windowResize(int, int)
	 * void malik.emulator.application.AppProxy.windowShow()
	 * void malik.emulator.application.AppProxy.windowHide()
	 */
	static final int EVENT_APP_LAUNCH = 0;
	static final int EVENT_APP_TERMINATE = 1;
	static final int EVENT_KEY_PRESSED = 4;
	static final int EVENT_KEY_RELEASED = 6;
	static final int EVENT_POINTER_PRESSED = 8;
	static final int EVENT_POINTER_DRAGGED = 9;
	static final int EVENT_POINTER_RELEASED = 10;
	static final int EVENT_WINDOW_RESIZE = 12;
	static final int EVENT_WINDOW_SHOW = 13;
	static final int EVENT_WINDOW_HIDE = 14;
	private static final Run INSTANCE;

	static
	{
		INSTANCE = new Run();
	}

	public static Run getInstance()
	{
		return INSTANCE;
	}

	static void main()
	{
		ThreadTerminationListenerCollection.instance.addListener(new ThreadTerminationHandler(INSTANCE));
		INSTANCE.execute();
	}

	private static AppProxy getStartAppProxyInstance()
			throws AppProxyNotFoundException
	{
		AppProxy result;
		String className = System.getSystemProperty("malik.emulator.application.run.proxy.class");
		try
		{
			result = (AppProxy) Class.forName(className).newInstance();
		}
		catch(NullPointerException e)
		{
			throw new AppProxyNotFoundException(
					"Отсутствует важное системное свойство: " +
					"malik.emulator.application.run.proxy.class. " +
					"Проверьте файл config.properties и исходный код системных библиотек.");
		}
		catch(ClassNotFoundException e)
		{
			throw new AppProxyNotFoundException(
					"Не удалось найти класс для обработки событий: " + className + ".");
		}
		catch(InstantiationException e)
		{
			throw new AppProxyNotFoundException(
					"Не удалось создать инстанцию обработчика событий " + className + ".");
		}
		catch(IllegalAccessException e)
		{
			throw new AppProxyNotFoundException(
					"Не удалось получить доступ к обработчику событий " + className + ".");
		}
		catch(ClassCastException e)
		{
			throw new AppProxyNotFoundException(
					"Неподходящий класс для обработки событий: " + className + ".");
		}
		return result;
	}


	private boolean terminated;
	private boolean[] keyPressed;
	private String errorMessage;
	private Hashtable playerHandlers;
	private QueueOfObjects threadTerminations;
	private QueueOfMalikAppEvents mainEvents;
	private StretchDrawDescriptor stretchDescriptor;
	private StringDrawDescriptor stringDescriptor;
	private ThreadTerminationListener threadProxy;
	private AppProxy eventsProxy;

	private Run()
	{
		this.terminated = false;
		this.keyPressed = new boolean[0x0100];
		this.errorMessage = "Ошибка приложения";
		this.playerHandlers = new Hashtable();
		this.threadTerminations = new QueueOfObjects();
		this.mainEvents = new QueueOfMalikAppEvents();
		this.stretchDescriptor = new StretchDrawDescriptor();
		this.stringDescriptor = new StringDrawDescriptor();
	}

	public void threadTerminated(Thread terminatedThread, Throwable exitThrowable)
	{
		if(exitThrowable == null)
		{
			return;
		}
		showMessage(errorMessage);
		exitThrowable.printRealStackTrace();
		terminate();
	}

	public void serviceImmediately(Object argument)
	{
		QueueOfMalikAppEvents events = mainEvents;
		Object monitor = keyPressed;
		for(Runnable event; ; )
		{
			synchronized(monitor)
			{
				event = events.findImmediatelyRunnable(argument);
			}
			if(event == null) break;
			try
			{
				event.run();
			}
			catch(RuntimeException e)
			{
				e.printRealStackTrace();
			}
		}
	}

	public void setPlayerPCMLoadBlockHandler(int playerPCMHandle,
			PlayerPCMLoadBlockHandler handler)
	{
		Object key = new Integer(playerPCMHandle);
		if(handler == null)
		{
			playerHandlers.remove(key);
			return;
		}
		playerHandlers.put(key, handler);
	}

	public void setPlayerMIDIEndTrackHandler(int playerMIDIHandle,
			PlayerMIDIEndTrackHandler handler)
	{
		Object key = new Integer(playerMIDIHandle);
		if(handler == null)
		{
			playerHandlers.remove(key);
			return;
		}
		playerHandlers.put(key, handler);
	}

	public void setThreadProxy(ThreadTerminationListener threadProxy)
	{
		this.threadProxy = threadProxy;
	}

	public void setEventsProxy(AppProxy eventsProxy)
	{
		this.eventsProxy = eventsProxy;
	}

	public void setEvent(Runnable event)
	{
		Object monitor;
		if(event == null)
		{
			return;
		}
		synchronized(monitor = keyPressed)
		{
			mainEvents.push(-1L, event);
			monitor.notify();
		}
	}

	public void showMessage(String message)
	{
		int i;
		int fontHandle;
		int fontHeight;
		int fontBaseline;
		int textWidth;
		int bkcolor;
		int top;
		int left;
		int width;
		int height;
		int x;
		int y;
		StretchDrawDescriptor s;
		StringDrawDescriptor t;
		if(message == null)
		{
			throw new NullPointerException("Run.showMessage: " +
					"параметр message равен нулевой ссылке.");
		}
		fontHandle = (int) MalikSystem.syscall(0L, 0x0028);
		fontHeight = ((i = (int) MalikSystem.syscall((long) fontHandle, 0x002a)) >> 8) & 0xff;
		fontBaseline = (i >> 16) & 0xff;
		bkcolor = 0xc0000000;
		s = stretchDescriptor;
		MalikSystem.syscall(((long) (t = stringDescriptor).getDescriptorAddress()) << 32, 0x0022);
		top = (865 * (t.sizes >>> 16)) >> 10;
		left = 0;
		width = t.sizes & 0xffff;
		height = fontHeight;
		s.dstBase = t.base;
		s.dstScan = t.scan;
		s.dstSizes = t.sizes;
		s.dstAlpha = 0;
		s.origins = (top << 16) | (left & 0xffff);
		s.sizes = (height << 16) | (width & 0xffff);
		s.srcBase = MalikSystem.getLocalVariableAddress(bkcolor);
		s.srcScan = 1;
		s.srcSizes = 0x00010001;
		s.srcAlpha = 1;
		s.transform = 0;
		MalikSystem.syscall((long) s.getDescriptorAddress(), 0x0027);
		t.alpha = 0;
		t.handle = fontHandle;
		t.style = Integer.MIN_VALUE;
		t.color = 0xffffffff;
		t.length = message.length();
		t.chars = Array.getFirstElementAddress(message);
		textWidth = (int) MalikSystem.syscall((long) t.getDescriptorAddress(), 0x002c);
		x = (width - textWidth) >> 1;
		y = top + fontHeight - fontBaseline;
		t.coords = (y << 16) | (x & 0xffff);
		MalikSystem.syscall((long) t.getDescriptorAddress(), 0x002d);
		MalikSystem.syscall(0L, 0x0020);
	}

	public void terminate()
	{
		Object monitor;
		synchronized(monitor = keyPressed)
		{
			terminated = true;
			mainEvents.push(((long) EVENT_APP_TERMINATE) << 60, null);
			monitor.notify();
		}
	}

	public boolean terminated()
	{
		return terminated;
	}

	public Runnable[] getImmediatelyServices(Object argument)
	{
		int len = 0;
		Runnable[] result = new ImmediatelyRunnable[0x0f];
		QueueOfMalikAppEvents events = mainEvents;
		Object monitor = keyPressed;
		for(Runnable event; ; )
		{
			synchronized(monitor)
			{
				event = events.findImmediatelyRunnable(argument);
			}
			if(event == null) break;
			if(len == result.length)
			{
				Array.copy(result, 0, result = new ImmediatelyRunnable[(len << 1) + 1], 0, len);
			}
			result[len++] = event;
		}
		if(len < result.length)
		{
			Array.copy(result, 0, result = new ImmediatelyRunnable[len], 0, len);
		}
		return result;
	}

	void setEvent(long event)
	{
		Object monitor;
		synchronized(monitor = keyPressed)
		{
			mainEvents.push(event, null);
			monitor.notify();
		}
	}

	void setEvent(Thread terminatedThread, Throwable exitThrowable)
	{
		Object monitor;
		synchronized(monitor = keyPressed)
		{
			threadTerminations.push(new ThreadTerminationRecord(this,
					terminatedThread, exitThrowable));
			monitor.notify();
		}
	}

	void handlePlayerPCMLoadBlock(int playerPCMHandle, int blockIndex)
	{
		((PlayerPCMLoadBlockHandler) playerHandlers.get(new Integer(playerPCMHandle))).
				loadPCMBlock(playerPCMHandle, blockIndex);
	}

	void handlePlayerMIDILoadBlock(int playerMIDIHandle)
	{
		((PlayerMIDIEndTrackHandler) playerHandlers.get(new Integer(playerMIDIHandle))).
				endMIDITrack(playerMIDIHandle);
	}

	private void execute()
	{
		Throwable exitThrowable;
		ThreadTerminationListener listener;
		try
		{
			ThrowableStackTrace.enable(true);
			showMessage("Пожалуйста, подождите…");
			findAppProxy();
			registerInterruptHandlers();
			lifecycle();
			exitThrowable = null;
		}
		catch(Throwable t)
		{
			exitThrowable = t;
		}
		MalikInterrupt.disable();
		try
		{
			((listener = threadProxy) != null ? listener : this).
					threadTerminated(Thread.currentThread(), exitThrowable);
		}
		catch(Throwable t)
		{
			/* проигнорировать исключение */
		}
		System.out.close();
		System.err.close();
	}

	private void lifecycle()
	{
		long eventFromUser;
		Runnable eventFromApp;
		ThreadTerminationRecord eventTermination;
		QueueOfObjects queueOfTerminations = threadTerminations;
		QueueOfMalikAppEvents queueOfEvents = mainEvents;
		Object monitor = keyPressed;
		do
		{
			waitEvent();
			try
			{
				if(!queueOfTerminations.isEmpty())
				{
					synchronized(monitor)
					{
						eventTermination = (ThreadTerminationRecord) queueOfTerminations.peek();
						queueOfTerminations.removeTailElement();
					}
					eventTermination.invokeListener(threadProxy);
					continue;
				}
				if(!queueOfEvents.isEmpty())
				{
					synchronized(monitor)
					{
						eventFromApp = queueOfEvents.peekEventFromApp();
						eventFromUser = queueOfEvents.peekEventFromUser();
						queueOfEvents.removeTailElement();
					}
					if(eventFromApp == null)
					{
						if(handleEventMustTerminated(eventFromUser))
						{
							break;
						}
					} else
					{
						eventFromApp.run();
					}
					continue;
				}
			}
			catch(RuntimeException e)
			{
				e.printRealStackTrace();
			}
		} while(true);
	}

	private void waitEvent()
	{
		Object monitor;
		synchronized(monitor = keyPressed)
		{
			do
			{
				try
				{
					monitor.wait();
					break;
				}
				catch(InterruptedException e)
				{
					e.printRealStackTrace();
				}
			} while(true);
		}
	}

	private void findAppProxy()
			throws AppProxyNotFoundException
	{
		eventsProxy = getStartAppProxyInstance();
		setEvent(((long) EVENT_APP_LAUNCH) << 60);
	}

	private void registerInterruptHandlers()
	{
		MalikInterrupt.register(0x10, new InterruptHandlerForKeyboard(this));
		MalikInterrupt.register(0x11, new InterruptHandlerForPointer(this));
		MalikInterrupt.register(0x12, new InterruptHandlerForWindowShowHide(this));
		MalikInterrupt.register(0x13, new InterruptHandlerForWindowResize(this));
		MalikInterrupt.register(0x1e, new InterruptHandlerForPlayerPCM(this));
		MalikInterrupt.register(0x1f, new InterruptHandlerForPlayerMIDI(this));
		Runtime.getRuntime().startInterruptHandling();
	}

	private boolean handleEventMustTerminated(long event)
	{
		int e = (int) (event >>> 60);
		int i;
		boolean[] k;
		AppProxy p;
		if((p = eventsProxy) == null)
		{
			return e == EVENT_APP_TERMINATE;
		}
		switch(e)
		{
		default:
			return false;
		case EVENT_APP_LAUNCH:
			p.appLaunch();
			return false;
		case EVENT_APP_TERMINATE:
			p.appTerminate();
			return true;
		case EVENT_KEY_PRESSED:
			if(!(k = keyPressed)[i = (int) event])
			{
				k[i] = true;
				p.keyPressed(i, ((int) (event >> 32)) & 0x00ffffff);
				return false;
			}
			p.keyRepeated(i, ((int) (event >> 32)) & 0x00ffffff);
			return false;
		case EVENT_KEY_RELEASED:
			keyPressed[i = (int) event] = false;
			p.keyReleased(i);
			return false;
		case EVENT_POINTER_PRESSED:
			p.pointerPressed((short) event, ((int) event) >> 16, ((int) (event >> 32)) & 0x07);
			return false;
		case EVENT_POINTER_DRAGGED:
			p.pointerDragged((short) event, ((int) event) >> 16);
			return false;
		case EVENT_POINTER_RELEASED:
			p.pointerReleased((short) event, ((int) event) >> 16, ((int) (event >> 32)) & 0x07);
			return false;
		case EVENT_WINDOW_RESIZE:
			p.windowResize((short) event, ((int) event) >> 16);
			return false;
		case EVENT_WINDOW_SHOW:
			p.windowShow();
			return false;
		case EVENT_WINDOW_HIDE:
			p.windowHide();
			return false;
		}
	}
}
