import Dispatcher;
import Math;

namespace Game {

    class Experimental {
        // maps an ID to a game coordinate (as a list of two integers)
        static field touches = {};
        static field touchesRawData = [];

        static function getTouchState() {
            rd = Experimental.touchesRawData;
            rd.clear();
            t =  Experimental.touches;
            t.clear();
            $lib_game_getTouchState(rd);
            for (i = 0; i < rd.length; i += 3) {
                t[rd[i]] = [rd[i + 1], rd[i + 2]];
            }
            return t;
        }
    }

    enum PointerCanonicalizerMode {

        // All mouse and touch events will be presented separately in their original form.
        NONE = 0,

        // THIS IS DEFAULT.
        // The average of the touch points will be converted to mouse events.
        // The original touch events will still be propagated.
        CONVERT_TOUCH_TO_MOUSE = 1,


        // The left mouse click events will be converted into touch events.
        // The pointer ID will be 0.
        CONVERT_MOUSE_TO_TOUCH = 2,
    }

    // WARNING: not currently supported by all platforms yet
    enum WindowScaleMode {
        // Stretch the screen to cover the whole available window
        FILL = 0,

        // Stretch the window, but add black boxes to the sides to maintain the aspect ratio
        //FIT = 1,

        // The width and height will simply be the real pixels
        //SIZE_TO_WINDOW = 2,

        // The largest dimension can be set to a constant
        SIZE_TO_WINDOW_CONSTRAINED_MAX = 4,
    }

    @localized('en', 'GameWindow')
    @localized('es', 'JuegoVentana')
    @localized('jp', 'JPGameWindow')
    class GameWindow {

        static field instance;

        // TODO: in lieu of private fields, move these to native fields on the window object. They are only accessed by .getSize()
        field _w;
        field _h;
        field _scale_mode = WindowScaleMode.FILL;

        static field _reusable_list = [];

        // These are static and not associated with the actual GameWindow instance.
        // That means static initializers (such as the GamePad event pump) can register events without the
        // window being open yet. Also, becuase they are static, they can be guarded against unauthorized
        // access from user code (TODO: make that last statement true by adding the library access annotation)
        static field _eventPumpHandlers = [];
        static field _frameBeginHandlers = [];
        static field _frameEndHandlers = [];

        field _pointerMode = PointerCanonicalizerMode.CONVERT_TOUCH_TO_MOUSE;

        field fps;

        constructor(title, fps, width, height, screenWidth = null, screenHeight = null) {
            Core.assert(Core.isString(title), "Title must be a string.");
            t = " must be a number.";
            w = "width";
            h = "height";
            Core.assert(Core.isNumber(fps), "FPS" + t);
            Core.assert(Core.isNumber(width), "width" + t);
            Core.assert(Core.isNumber(height), "height" + t);
            Core.assert(screenWidth == null || Core.isNumber(screenWidth), "screen " + w + t);
            Core.assert(screenHeight == null || Core.isNumber(screenHeight), "screen " + h + t);

            Core.assert(GameWindow.instance == null, "Cannot create multiple GameWindows.");
            GameWindow.instance = this;

            this.fps = fps;

            screenWidth = screenWidth ?? width;
            screenHeight = screenHeight ?? height;

            width = Math.floor(width);
            height = Math.floor(height);
            screenWidth = Math.floor(screenWidth);
            screenHeight = Math.floor(screenHeight);

            // TODO: re-assess if this is necessary
            this._w = width;
            this._h = height;

            t = " must be positive.";
            Core.assert(width > 0, w + t);
            Core.assert(height > 0, h + t);
            Core.assert(screenWidth > 0, "screen " + w + t);
            Core.assert(screenHeight > 0, "screen " + h + t);
            Core.assert(fps > 0, "FPS" + t);

            Dispatcher.registerDispatcherFramework(this);

            $lib_game_initialize(fps);
            $lib_game_initialize_screen(width, height, screenWidth, screenHeight, Core.getCurrentExecutionToken());
            $lib_game_set_title(title);
            $lib_game_setInstance(this);
        }

        function getSize() {
            return [this._w, this._h];
        }

        function setWindowMode(mode, width = null, height = null) {
            // TODO: throw a nice error for bad types
            this._scale_mode = mode;
            return $lib_game_set_window_mode(mode, width ?? 0, height ?? 0);
        }

        function setPointerCanonicalizationMode(mode) {
            this._pointerMode = mode;
            return this;
        }


        field _timedCallbacks = [];
        function _setTimedCallback(deadline, cb) {
            this._timedCallbacks.add([deadline, cb]);
        }

        function _flushCallbacks() {
            Dispatcher.flush();

            if (this._timedCallbacks.length > 0) {
                now = Core.currentTime();
                for (i = this._timedCallbacks.length - 1; i >= 0; --i) {
                    if (this._timedCallbacks[i][0] <= now) {
                        cb = this._timedCallbacks[i][1];
                        this._timedCallbacks.remove(i);
                        cb();
                    }
                }
            }
        }

        function dispatcherCbComplete() { }

        // returns a list [isAspectRatioForced, screenWidth, screenHeight]
        // can be called before game is created
        // may possibly return 0 x 0 for the screen size if the underlying API can't tell
        // will never return [true, 0, 0]
        static function getScreenInfo() {
            return $lib_game_getScreenInfo([false, 0, 0]);
        }

        @localized('en', 'clockTick')
        @localized('es', 'ESclockTick')
        @localized('jp', 'JPclockTick')
        function clockTick() {
            for (t : Game.GameWindow._frameEndHandlers) {
                t();
            }

            this._flushCallbacks();

            $lib_game_clock_tick(Core.getCurrentExecutionToken());

            this._flushCallbacks();

            for (t : Game.GameWindow._frameBeginHandlers) {
                t();
            }
        }

        /*
            pointer ID

            Key is the pointer ID where 0 is the mouse
            Value is a list of the following:
                [0] -> pointerId (redundant with key)
                [1] -> x
                [2] -> y
                [3] -> pressed mask (1 for left, 2 for right, 3 for both, 0 for none)

        */
        field _pointers = {
            0: [0, 0, 0, 0]
        };

        function _getAveragePointerLocation() {
            total = 0;
            x = 0;
            y = 0;
            for (pointer : this._pointers.values()) {
                x += pointer[1];
                y += pointer[2];
                total++;
            }

            if (total == 0) return null;
            return [x / total, y / total];
        }

        @localized('en', 'pumpEvents')
        @localized('es', 'ESpumpEvents')
        @localized('jp', 'JPpumpEvents')
        function pumpEvents() {
            output = [];
            mouseState = this._pointers[0];
            t = $lib_game_pump_events(GameWindow._reusable_list);
            len = t.length;
            i = 0;
            while (i < len) {
                eventType = t[i++];
                switch (eventType) {
                    // for all mouse events: t[1] -> x, t[2] -> y
                    case EventType.MOUSE_MOVE:
                        x = t[i++];
                        y = t[i++];
                        output.add(new MouseMoveEvent(x, y));
                        if (this._pointerMode == PointerCanonicalizerMode.CONVERT_MOUSE_TO_TOUCH) {
                            output.add(new TouchMoveEvent(0, x, y));
                        }
                        break;
                    case EventType.MOUSE_LEFT_DOWN:
                    case EventType.MOUSE_LEFT_UP:
                        isDown = eventType == EventType.MOUSE_LEFT_DOWN;
                        x = t[i++];
                        y = t[i++];
                        output.add(new MouseClickEvent(eventType, EventButton.LEFT, isDown, x, y));
                        if (this._pointerMode == PointerCanonicalizerMode.CONVERT_MOUSE_TO_TOUCH) {
                            output.add(isDown ? new TouchStartEvent(0, x, y) : new TouchEndEvent(0, x, y));
                        }
                        break;
                    case EventType.MOUSE_RIGHT_DOWN:
                    case EventType.MOUSE_RIGHT_UP:
                        isDown = eventType == EventType.MOUSE_RIGHT_DOWN;
                        x = t[i++];
                        y = t[i++];
                        output.add(new MouseClickEvent(eventType, EventButton.RIGHT, isDown, x, y));
                        break;

                    case EventType.TOUCH_START:
                    case EventType.TOUCH_END:
                    case EventType.TOUCH_MOVE:
                        id = t[i++];
                        x = t[i++];
                        y = t[i++];
                        output.add(eventType == EventType.TOUCH_MOVE
                            ? new TouchMoveEvent(id, x, y)
                            : eventType == EventType.TOUCH_START
                                ? new TouchStartEvent(id, x, y)
                                : new TouchEndEvent(id, x, y));

                        if (this._pointerMode == PointerCanonicalizerMode.CONVERT_TOUCH_TO_MOUSE) {
                            switch (eventType) {
                                case EventType.TOUCH_START:
                                    avgPointerLoc = this._getAveragePointerLocation();

                                    // if there are no pointers masked to 1 as their button state, then
                                    // issue a new MouseClickEvent for MOUSE_LEFT_DOWN
                                    this._pointers[id] = [id, x, y, 1];
                                    if (avgPointerLoc == null) {
                                        output.add(new MouseClickEvent(EventType.MOUSE_LEFT_DOWN, EventButton.LEFT, true, x, y));
                                    } else {
                                        // if there are already pointers pressed, then incorporate this into
                                        // the average location and then treat this as a mouse move event
                                        avgPointerLoc = this._getAveragePointerLocation();
                                        output.add(new MouseMoveEvent(avgPointerLoc[0], avgPointerLoc[1]));
                                    }
                                    break;

                                case EventType.TOUCH_END:
                                    // safely pop the pointer
                                    if (this._pointers.get(id) != null) {
                                        this._pointers.pop(id);
                                    }
                                    avgPointerLoc = this._getAveragePointerLocation();
                                    if (avgPointerLoc == null) {
                                        output.add(new MouseClickEvent(EventType.MOUSE_LEFT_UP, EventButton.LEFT, false, x, y));
                                    } else {
                                        output.add(new MouseMoveEvent(avgPointerLoc[0], avgPointerLoc[1]));
                                    }
                                    break;

                                default:
                                    this._pointers[id] = [id, x, y, 1];
                                    avgPointerLoc = this._getAveragePointerLocation();
                                    output.add(new MouseMoveEvent(avgPointerLoc[0], avgPointerLoc[1]));
                                    break;
                            }
                        } else {
                            if (eventType == EventType.TOUCH_END) {
                                if (this._pointers.get(id) != null) {
                                    this._pointers.pop(id);
                                }
                            } else {
                                this._pointers[id] = [id, x, y, 1];
                            }
                        }
                        break;

                    // for mouse scroll events: t[3] -> scroll amount
                    case EventType.MOUSE_SCROLL:
                        output.add(new MouseScrollEvent(t[i++], t[i++], t[i++]));
                        break;

                    // for key events: t[1] -> key code (see enum)
                    case EventType.KEY_DOWN:
                    case EventType.KEY_UP:
                        output.add(new KeyEvent(t[i++], eventType == EventType.KEY_DOWN));
                        break;

                    // for quit events: t[1] -> quit subtype
                    case EventType.QUIT:
                        output.add(new QuitEvent(t[i++]));
                        break;

                    case EventType.WINDOW_SIZE_CHANGE:
                        w = t[i++];
                        h = t[i++];
                        switch (this._scale_mode) {
                            case WindowScaleMode.SIZE_TO_WINDOW_CONSTRAINED_MAX:
                                this._w = w;
                                this._h = h;
                                break;
                        }
                        break;

                    default: assert(false); break;
                }
            }

            // Things like gamepad events are added here
            if (GameWindow._eventPumpHandlers.length > 0) {
                for (t : GameWindow._eventPumpHandlers) {
                    t(output);
                }
            }

            return output;
        }

        function getTouchPoints() {
            output = [];
            for (pointer : this._pointers.values()) {
                if ((pointer[3] & 1) != 0) {
                    output.add(pointer[:3]);
                }
            }
            return output;
        }

        @localized('en', 'setTitle')
        @localized('es', 'ESsetTitle')
        @localized('jp', 'JPsetTitle')
        function setTitle(title) {
            title += '';
            if (title.length == 0) throw new Core.InvalidArgumentException("Title cannot be empty.");
            return $lib_game_set_title(title);
        }
    }
}
