/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package websocket.drawboard;

import java.io.EOFException;
import java.io.IOException;

import jakarta.websocket.CloseReason;
import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.MessageHandler;
import jakarta.websocket.Session;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

import websocket.drawboard.DrawMessage.ParseException;
import websocket.drawboard.wsmessages.StringWebsocketMessage;


public final class DrawboardEndpoint extends Endpoint {

    private static final Log log =
            LogFactory.getLog(DrawboardEndpoint.class);


    /**
     * Our room where players can join.
     */
    private static volatile Room room = null;
    private static final Object roomLock = new Object();

    public static Room getRoom(boolean create) {
        if (create) {
            if (room == null) {
                synchronized (roomLock) {
                    if (room == null) {
                        room = new Room();
                    }
                }
            }
            return room;
        } else {
            return room;
        }
    }

    /**
     * The player that is associated with this Endpoint and the current room.
     * Note that this variable is only accessed from the Room Thread.<br><br>
     *
     * TODO: Currently, Tomcat uses an Endpoint instance once - however
     * the java doc of endpoint says:
     * "Each instance of a websocket endpoint is guaranteed not to be called by
     * more than one thread at a time per active connection."
     * This could mean that after calling onClose(), the instance
     * could be reused for another connection so onOpen() will get called
     * (possibly from another thread).<br>
     * If this is the case, we would need a variable holder for the variables
     * that are accessed by the Room thread, and read the reference to the holder
     * at the beginning of onOpen, onMessage, onClose methods to ensure the room
     * thread always gets the correct instance of the variable holder.
     */
    private Room.Player player;


    @Override
    public void onOpen(Session session, EndpointConfig config) {
        // Set maximum messages size to 10.000 bytes.
        session.setMaxTextMessageBufferSize(10000);
        session.addMessageHandler(stringHandler);
        final Client client = new Client(session);

        final Room room = getRoom(true);
        room.invokeAndWait(new Runnable() {
            @Override
            public void run() {
                try {

                    // Create a new Player and add it to the room.
                    try {
                        player = room.createAndAddPlayer(client);
                    } catch (IllegalStateException ex) {
                        // Probably the max. number of players has been
                        // reached.
                        client.sendMessage(new StringWebsocketMessage(
                                "0" + ex.getLocalizedMessage()));
                        // Close the connection.
                        client.close();
                    }

                } catch (RuntimeException ex) {
                    log.error("Unexpected exception: " + ex.toString(), ex);
                }
            }
        });

    }


    @Override
    public void onClose(Session session, CloseReason closeReason) {
        Room room = getRoom(false);
        if (room != null) {
            room.invokeAndWait(new Runnable() {
                @Override
                public void run() {
                    try {
                        // Player can be null if it couldn't enter the room
                        if (player != null) {
                            // Remove this player from the room.
                            player.removeFromRoom();

                            // Set player to null to prevent NPEs when onMessage events
                            // are processed (from other threads) after onClose has been
                            // called from different thread which closed the Websocket session.
                            player = null;
                        }
                    } catch (RuntimeException ex) {
                        log.error("Unexpected exception: " + ex.toString(), ex);
                    }
                }
            });
        }
    }



    @Override
    public void onError(Session session, Throwable t) {
        // Most likely cause is a user closing their browser. Check to see if
        // the root cause is EOF and if it is ignore it.
        // Protect against infinite loops.
        int count = 0;
        Throwable root = t;
        while (root.getCause() != null && count < 20) {
            root = root.getCause();
            count ++;
        }
        if (root instanceof EOFException) {
            // Assume this is triggered by the user closing their browser and
            // ignore it.
        } else if (!session.isOpen() && root instanceof IOException) {
            // IOException after close. Assume this is a variation of the user
            // closing their browser (or refreshing very quickly) and ignore it.
        } else {
            log.error("onError: " + t.toString(), t);
        }
    }



    private final MessageHandler.Whole<String> stringHandler =
            new MessageHandler.Whole<>() {

        @Override
        public void onMessage(final String message) {
            // Invoke handling of the message in the room.
            room.invokeAndWait(new Runnable() {
                @Override
                public void run() {
                    try {

                        // Currently, the only types of messages the client will send
                        // are draw messages prefixed by a Message ID
                        // (starting with char '1'), and pong messages (starting
                        // with char '0').
                        // Draw messages should look like this:
                        // ID|type,colR,colB,colG,colA,thickness,x1,y1,x2,y2,lastInChain

                        boolean dontSwallowException = false;
                        try {
                            char messageType = message.charAt(0);
                            String messageContent = message.substring(1);
                            switch (messageType) {
                            case '0':
                                // Pong message.
                                // Do nothing.
                                break;

                            case '1':
                                // Draw message
                                int indexOfChar = messageContent.indexOf('|');
                                long msgId = Long.parseLong(
                                        messageContent.substring(0, indexOfChar));

                                DrawMessage msg = DrawMessage.parseFromString(
                                        messageContent.substring(indexOfChar + 1));

                                // Don't ignore RuntimeExceptions thrown by
                                // this method
                                // TODO: Find a better solution than this variable
                                dontSwallowException = true;
                                if (player != null) {
                                    player.handleDrawMessage(msg, msgId);
                                }
                                dontSwallowException = false;

                                break;
                            }
                        } catch (ParseException e) {
                            // Client sent invalid data
                            // Ignore, TODO: maybe close connection
                        } catch (RuntimeException e) {
                            // Client sent invalid data.
                            // Ignore, TODO: maybe close connection
                            if (dontSwallowException) {
                                throw e;
                            }
                        }

                    } catch (RuntimeException ex) {
                        log.error("Unexpected exception: " + ex.toString(), ex);
                    }
                }
            });

        }
    };


}
