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

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

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

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

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

package malik.emulator.application;

import java.io.*;
import java.util.*;
import malik.emulator.fileformats.*;
import malik.emulator.fileformats.text.mapped.*;
import malik.emulator.io.cloud.*;
import malik.emulator.media.graphics.*;
import malik.emulator.util.*;

public final class Run extends Requestable implements ThreadTerminationListener
{
    static final int ACTION_APP_LAUNCH = 0x10;
    static final int ACTION_APP_TERMINATE = 0x11;
    private static final int ACTION_VIRTUAL_KEYBOARD = 0x0020;

    public static final Run instance;

    static {
        instance = new Run();
    }

    public static long workingTimeMillis() {
        return MalikSystem.syscall(0L, 0x000d);
    }

    public static String getLocalizedApplicationError() {
        return "Ошибка приложения";
    }

    static void main() {
        instance.execute();
    }

    private static void loadFromFile(InputAdapter adapter, String fileName) {
        HandleInputStream stream;
        if(!(stream = new FileInputStream("/META-INF/MANIFEST.MF")).hasOpenError())
        {
            try
            {
                try
                {
                    adapter.loadFromInputStream(stream);
                }
                finally
                {
                    stream.close();
                }
            }
            catch(IOException e)
            {
                e.printRealStackTrace();
            }
        }
    }

    private static AppProxy getStartAppProxyInstance() {
        String typeName;
        AppProxy result = null;
        if((typeName = System.getSystemProperty("malik.emulator.application.app.proxy.class")) == null)
        {
            System.err.println("Run: отсутствует важное системное свойство malik.emulator.application.app.proxy.class. Проверьте файл config.properties и исходный код системных библиотек.");
            return null;
        }
        try
        {
            result = (AppProxy) Class.forName(typeName).newInstance();
        }
        catch(Exception e)
        {
            e.printRealStackTrace();
        }
        return result;
    }

    private AppProxy proxyActions;
    private ThreadTerminationListener proxyThreads;
    private KeyboardEvent eventKeyboard;
    private PointerEvent eventPointer;
    private final Hashtable playerListeners;
    private final RunnableQueue queueThreads;
    private final RequestableQueue queueActions;
    private final AttributedTextDecoder appManifest;
    private final InputDevice deviceKeyboardVirtual;
    private final InputDevice deviceKeyboardStandard;
    private final InputDevice devicePointingStandard;
    private final Paint fillScreenParameters;
    private final Paint drawMessageParameters;

    private Run() {
        int messageHeight;
        SystemFont font;
        AttributedTextDecoder manifest;
        messageHeight = (font = SystemFont.getDefault()).getHeight();
        loadFromFile(manifest = new ManifestDecoder(), "/META-INF/MANIFEST.MF");
        this.eventKeyboard = new KeyboardEvent();
        this.eventPointer = new PointerEvent();
        this.playerListeners = new Hashtable();
        this.queueThreads = new RunnableQueue();
        this.queueActions = new RequestableQueue();
        this.appManifest = manifest;
        this.deviceKeyboardVirtual = InputDevice.get(InputDevice.ID_KEYBOARD_VIRTUAL);
        this.deviceKeyboardStandard = InputDevice.get(InputDevice.ID_KEYBOARD_STANDARD);
        this.devicePointingStandard = InputDevice.get(InputDevice.ID_POINTING_STANDARD);
        this.fillScreenParameters = new Paint(RasterCanvas.MAX_SCREEN_WIDTH, RasterCanvas.MAX_SCREEN_HEIGHT);
        this.drawMessageParameters = new TextPaint(RasterCanvas.MAX_SCREEN_WIDTH, messageHeight, font, false, false);
    }

    public void request(int action, int param1, int param2, int param3) {
        RequestableQueue monitor;
        synchronized(monitor = queueActions)
        {
            monitor.addTailElement(null, (long) (action & 0xffff) | (long) (param1 & 0xffff) << 0x10 | (long) (param2 & 0xffff) << 0x20 | (long) (param3 & 0xffff) << 0x30);
            monitor.notify();
        }
    }

    public void request(Runnable action, long delay) {
        RunnableQueue monitor;
        if(action == null) return;
        if(delay > 0L)
        {
            Scheduler.schedule(new DelayedAction(action), delay, Scheduler.ACTION);
            return;
        }
        synchronized(monitor = queueActions)
        {
            monitor.addTailElement(action);
            monitor.notify();
        }
    }

    public void terminate() {
        RequestableQueue monitor;
        super.terminate();
        synchronized(monitor = queueActions)
        {
            monitor.addTailElement(null, (long) ACTION_APP_TERMINATE);
            monitor.notify();
        }
    }

    public void threadTerminated(Thread terminatedThread, Throwable exitThrowable) {
        if(exitThrowable != null)
        {
            drawMessage(getLocalizedApplicationError());
            exitThrowable.printRealStackTrace();
            terminate();
        }
    }

    public void requestVirtualKeyboardEvent(int action, int key, int charCode) {
        if(action != ACTION_KEY_REPEATED && action != ACTION_KEY_PRESSED && action != ACTION_KEY_RELEASED)
        {
            throw new IllegalArgumentException("Run.requestVirtualKeyboardEvent: аргумент action имеет недопустимое значение.");
        }
        request(action | ACTION_VIRTUAL_KEYBOARD, key, charCode & 0xffff, charCode >>> 0x10);
    }

    public void fillScreen(int colorRGB) {
        synchronized(appManifest)
        {
            Paint paint;
            RasterCanvas canvas = RasterCanvas.screen;
            (paint = fillScreenParameters).setColor(colorRGB, false);
            canvas.fillRectangle(0, 0, canvas.getWidth(), canvas.getHeight(), paint);
            canvas.updateScreen();
        }
    }

    public void drawMessage(String message) {
        synchronized(appManifest)
        {
            int w;
            int h;
            Paint paint;
            RasterCanvas canvas = RasterCanvas.screen;
            (paint = drawMessageParameters).setColor(0xc0404040, true);
            paint.translateTo(0, ((865 * canvas.getHeight()) >> 10) - ((h = paint.getHeight()) >> 1));
            paint.setClip(0, 0, w = canvas.getWidth(), h);
            canvas.fillRectangle(0, 0, w, h, paint);
            if(message != null)
            {
                SystemFont font = SystemFont.getDefault();
                paint.setColor(0xffffff, false);
                canvas.drawString(message, (w - font.stringWidth(message)) >> 1, font.getBaselinePosition(), paint);
            }
            canvas.updateScreen();
        }
    }

    public void setAppProxy(AppProxy proxy) {
        this.proxyActions = proxy;
    }

    public void setPointerEvent(PointerEvent event) {
        if(event == null)
        {
            throw new NullPointerException("Run.setPointerEvent: аргумент event равен нулевой ссылке.");
        }
        eventPointer = event;
    }

    public void setKeyboardEvent(KeyboardEvent event) {
        if(event == null)
        {
            throw new NullPointerException("Run.setKeyboardEvent: аргумент event равен нулевой ссылке.");
        }
        eventKeyboard = event;
    }

    public void setThreadTerminationListener(ThreadTerminationListener listener) {
        proxyThreads = listener;
    }

    public void setPCMPlayerListener(int playerHandle, PCMPlayerListener listener) {
        setPlayerListener(playerHandle, listener);
    }

    public void setMIDIPlayerListener(int playerHandle, MIDIPlayerListener listener) {
        setPlayerListener(playerHandle, listener);
    }

    public String[] getAppPropertyAttributes(String key) {
        return appManifest.getAttributes(key);
    }

    public String getAppProperty(String key) {
        return appManifest.get(key);
    }

    public PointerEvent getPointerEvent() {
        return eventPointer;
    }

    public KeyboardEvent getKeyboardEvent() {
        return eventKeyboard;
    }

    void requestThreadTerminated(Thread terminatedThread, Throwable exitThrowable) {
        Object monitor;
        synchronized(monitor = queueActions)
        {
            queueThreads.addTailElement(new ThreadTerminationRecord(terminatedThread, exitThrowable));
            monitor.notify();
        }
    }

    void notifyPlayerListener(int playerHandle, int blockIndexForLoad) {
        Object listener;
        if((listener = playerListeners.get(new Integer(playerHandle))) instanceof PCMPlayerListener)
        {
            ((PCMPlayerListener) listener).endOfBlock(playerHandle, blockIndexForLoad);
        }
        if(listener instanceof MIDIPlayerListener)
        {
            ((MIDIPlayerListener) listener).endOfTrack(playerHandle);
        }
    }

    ThreadTerminationListener getThreadTerminationListener() {
        ThreadTerminationListener result;
        return (result = proxyThreads) == null ? this : result;
    }

    private void execute() {
        Throwable exitThrowable;
        ThreadTerminationListener listener;
        ThreadTerminationListenerCollection.instance.addListener(listener = new ThreadTerminationRequest());
        try
        {
            exitThrowable = null;
            try
            {
                ThrowableStackTrace.enable();
                drawMessage("Пожалуйста, подождите…");
                if(findAppProxy())
                {
                    registerInterruptHandlers();
                    lifecycle();
                }
            }
            catch(Throwable e)
            {
                exitThrowable = e;
            }
        }
        finally
        {
            ThreadTerminationListenerCollection.instance.removeListener(listener);
        }
        MalikInterrupt.disable();
        try
        {
            getThreadTerminationListener().threadTerminated(Thread.currentThread(), exitThrowable);
        }
        catch(Throwable e)
        {
            /* проигнорировать исключение */
        }
        System.out.close();
        System.err.close();
    }

    private void lifecycle() {
        RunnableQueue queueThreads = this.queueThreads;
        RequestableQueue queueActions = this.queueActions;
        for(; ; )
        {
            try
            {
                boolean empty;
                long actionByUser;
                Runnable actionByApplication;
                waitAction();
                empty = false;
                actionByUser = 0L;
                actionByApplication = null;
                synchronized(queueActions)
                {
                    if(!queueThreads.isEmpty())
                    {
                        actionByApplication = queueThreads.peekHeadRunnable();
                        ((Queue) queueThreads).removeHeadElement();
                    }
                    else if(!queueActions.isEmpty())
                    {
                        actionByUser = queueActions.peekHeadLong();
                        actionByApplication = queueActions.peekHeadRunnable();
                        ((Queue) queueActions).removeHeadElement();
                    }
                    else
                    {
                        empty = true;
                    }
                }
                if(empty) continue;
                if(actionByApplication != null)
                {
                    actionByApplication.run();
                    continue;
                }
                if(handleActionMustTerminated(actionByUser)) break;
            }
            catch(RuntimeException e)
            {
                e.printRealStackTrace();
            }
        }
    }

    private void waitAction() {
        Object monitor;
        synchronized(monitor = queueActions)
        {
            for(; ; )
            {
                try
                {
                    monitor.wait();
                    break;
                }
                catch(InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }

    private void registerInterruptHandlers() {
        InterruptLong handler = new PlayerInterruptHandler();
        MalikInterrupt.register(0x10, new KeyboardInterruptHandler());
        MalikInterrupt.register(0x11, new PointerInterruptHandler());
        MalikInterrupt.register(0x12, new WindowShowHideInterruptHandler());
        MalikInterrupt.register(0x13, new WindowResizeInterruptHandler());
        MalikInterrupt.register(0x1e, handler);
        MalikInterrupt.register(0x1f, handler);
        Runtime.getRuntime().startInterruptHandling();
    }

    private void setPlayerListener(int playerHandle, Object listener) {
        Object key = new Integer(playerHandle);
        if(listener == null)
        {
            playerListeners.remove(key);
            return;
        }
        playerListeners.put(key, listener);
    }

    private boolean findAppProxy() {
        AppProxy proxy;
        if((proxy = getStartAppProxyInstance()) == null)
        {
            System.err.println("Реализация AppProxy не найдена: запуск программы невозможен.");
            drawMessage(getLocalizedApplicationError());
            return false;
        }
        proxyActions = proxy;
        request(ACTION_APP_LAUNCH, 0, 0, 0);
        return true;
    }

    private boolean handleActionMustTerminated(long code) {
        int action;
        int param1;
        int param2;
        int param3;
        AppProxy proxy;
        InputDevice device;
        PointerEvent ptrEvent;
        KeyboardEvent kbdEvent;
        if((proxy = proxyActions) == null) return (char) code == ACTION_APP_TERMINATE;
        action = (char) code;
        param1 = (char) (code >> 0x10);
        param2 = (char) (code >> 0x20);
        param3 = (char) (code >> 0x30);
        ptrEvent = eventPointer;
        kbdEvent = eventKeyboard;
        ptrEvent.translateReset();
        switch(action)
        {
        default:
            break;
        case ACTION_WINDOW_RESIZE:
            proxy.windowSizeChanged(param2, param3);
            break;
        case ACTION_WINDOW_SHOW:
            proxy.windowShow();
            break;
        case ACTION_WINDOW_HIDE:
            proxy.windowHide();
            break;
        case ACTION_KEY_REPEATED:
        case ACTION_KEY_PRESSED:
        case ACTION_KEY_RELEASED:
        case ACTION_KEY_REPEATED | ACTION_VIRTUAL_KEYBOARD:
        case ACTION_KEY_PRESSED | ACTION_VIRTUAL_KEYBOARD:
        case ACTION_KEY_RELEASED | ACTION_VIRTUAL_KEYBOARD:
            device = (action & ACTION_VIRTUAL_KEYBOARD) == 0 ? deviceKeyboardStandard : deviceKeyboardVirtual;
            kbdEvent.addStory(action & 0x0f, param1, param2 | param3 << 0x0010, device, InputDevice.SOURCE_KEYBOARD, 0L);
            proxy.keyboardEvent(kbdEvent);
            break;
        case ACTION_POINTER_DRAGGED:
        case ACTION_POINTER_PRESSED:
        case ACTION_POINTER_RELEASED:
        case ACTION_BUTTON_PRESSED:
        case ACTION_BUTTON_RELEASED:
            device = devicePointingStandard;
            ptrEvent.addStory(action, param1, param2, param3, device, InputDevice.SOURCE_MOUSE, 0L);
            proxy.pointerEvent(ptrEvent);
            break;
        case ACTION_APP_LAUNCH:
            proxy.appLaunch();
            break;
        case ACTION_APP_TERMINATE:
            proxy.appTerminate();
            return true;
        }
        return false;
    }
}

abstract class InputDeviceInterruptHandler extends Object implements Interrupt, InterruptLong
{
    protected InputDeviceInterruptHandler() {
    }

    public abstract void interrupt(long argument);
}

final class KeyboardInterruptHandler extends InputDeviceInterruptHandler
{
    public KeyboardInterruptHandler() {
    }

    public void interrupt(long argument) {
        int key = (int) argument & 0xff;
        int charCode = (int) (argument >> 32) & 0x00ffffff;
        int action = (argument >> 63) == 0L ? Requestable.ACTION_KEY_PRESSED : Requestable.ACTION_KEY_RELEASED;
        Run.instance.requestKeyboardEvent(action, key, charCode);
    }
}

final class PointerInterruptHandler extends InputDeviceInterruptHandler
{
    private static int hardwareButtonToPointerEventButton(int hardwareButton) {
        switch(hardwareButton)
        {
        default:
            return PointerEvent.BUTTON_MAIN;
        case 1:
            return PointerEvent.BUTTON_AUX_1;
        case 2:
            return PointerEvent.BUTTON_WHEEL;
        case 4:
            return PointerEvent.BUTTON_WHEEL_UP;
        case 5:
            return PointerEvent.BUTTON_WHEEL_DOWN;
        }
    }

    public PointerInterruptHandler() {
    }

    public void interrupt(long argument) {
        int e = (int) (argument >> 32) & 0x0f;
        int rawX = (short) argument;
        int rawY = (short) (argument >> 16);
        int button = hardwareButtonToPointerEventButton(e & 0x07);
        int action = e == 0x0f ? Requestable.ACTION_POINTER_DRAGGED : (e & 0x08) == 0 ? Requestable.ACTION_POINTER_PRESSED : Requestable.ACTION_POINTER_RELEASED;
        Run.instance.requestPointerEvent(action, button, rawX, rawY);
    }
}

final class WindowShowHideInterruptHandler extends InputDeviceInterruptHandler
{
    public WindowShowHideInterruptHandler() {
    }

    public void interrupt(long argument) {
        if(((int) (argument >> 32) & 0x01) == 0)
        {
            Run.instance.requestWindowShow();
            return;
        }
        Run.instance.requestWindowHide();
    }
}

final class WindowResizeInterruptHandler extends InputDeviceInterruptHandler
{
    public WindowResizeInterruptHandler() {
    }

    public void interrupt(long argument) {
        int width = (short) argument;
        int height = (short) (argument >> 16);
        Run.instance.requestWindowResize(width, height);
    }
}

final class PlayerInterruptHandler extends InputDeviceInterruptHandler
{
    public PlayerInterruptHandler() {
    }

    public void interrupt(long argument) {
        int playerHandle = (int) argument;
        int blockIndexForLoad = (int) (argument >> 32);
        Run.instance.notifyPlayerListener(playerHandle, blockIndexForLoad);
    }
}

final class DelayedAction extends Scheduler.Task
{
    private final Runnable action;

    public DelayedAction(Runnable action) {
        this.action = action;
    }

    public void run() {
        action.run();
    }
}

final class ThreadTerminationRequest extends Object implements ThreadTerminationListener
{
    public ThreadTerminationRequest() {
    }

    public void threadTerminated(Thread terminatedThread, Throwable exitThrowable) {
        Run.instance.requestThreadTerminated(terminatedThread, exitThrowable);
    }
}

final class ThreadTerminationRecord extends Object implements Runnable
{
    private final Thread terminatedThread;
    private final Throwable exitThrowable;

    public ThreadTerminationRecord(Thread terminatedThread, Throwable exitThrowable) {
        this.terminatedThread = terminatedThread;
        this.exitThrowable = exitThrowable;
    }

    public void run() {
        try
        {
            Run.instance.getThreadTerminationListener().threadTerminated(terminatedThread, exitThrowable);
        }
        catch(Throwable e)
        {
        }
    }
}
