/*******************************************************************************
 * Copyright (c) 2016 IBM Corp.
 *
 * Licensed 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 org.gameontext.sample.protocol;

import java.io.Closeable;
import java.io.IOException;
import java.util.logging.Level;

import javax.inject.Inject;
import javax.websocket.CloseReason;
import javax.websocket.CloseReason.CloseCodes;
import javax.websocket.EncodeException;
import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.RemoteEndpoint.Basic;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.gameontext.sample.Log;
import org.gameontext.sample.RoomImplementation;

import org.eclipse.microprofile.metrics.annotation.Timed;
import org.eclipse.microprofile.metrics.annotation.Metered;
import org.eclipse.microprofile.metrics.annotation.Counted;

/**
 * This is the WebSocket endpoint for a room. Java EE WebSockets
 * use simple annotations for event driven methods. An instance of this class
 * will be created for every connected client.
 * https://book.gameontext.org/microservices/WebSocketProtocol.html
 */
@ServerEndpoint(value = "/room", decoders = MessageDecoder.class, encoders = MessageEncoder.class)
public class RoomEndpoint {

    @Inject
    protected RoomImplementation roomImplementation;

    @Timed(name = "websocket_onOpen_timer",
        reusable = true,
        tags = "label=websocket")
    @Counted(name = "websocket_onOpen_count",
        monotonic = true,
        reusable = true,
        tags = "label=websocket")
    @Metered(name = "websocket_onOpen_meter",
        reusable = true,
        tags = "label=websocket")
    @OnOpen
    public void onOpen(Session session, EndpointConfig ec) {
        Log.log(Level.FINE, this, "A new connection has been made to the room.");

        // All we have to do in onOpen is send the acknowledgement
        sendMessage(session, Message.ACK_MSG);
    }

    @Timed(name = "websocket_onClose_timer",
        reusable = true,
        tags = "label=websocket")
    @Counted(name = "websocket_onClose_count",
        monotonic = true,
        reusable = true,
        tags = "label=websocket")
    @Metered(name = "websocket_onClose_meter",
        reusable = true,
        tags = "label=websocket")
    @OnClose
    public void onClose(Session session, CloseReason r) {
        Log.log(Level.FINE, this, "A connection to the room has been closed with reason " + r);
    }

    @Timed(name = "websocket_onError_timer",
        reusable = true,
        tags = "label=websocket")
    @Counted(name = "websocket_onError_count",
        monotonic = true,
        reusable = true,
        tags = "label=websocket")
    @Metered(name = "websocket_onError_meter",
        reusable = true,
        tags = "label=websocket")
    @OnError
    public void onError(Session session, Throwable t) {
        Log.log(Level.FINE, this, "A problem occurred on connection", t);

        // TODO: Careful with what might revealed about implementation details!!
        // We're opting for making debug easy..
        tryToClose(session,
                new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION,
                        trimReason(t.getClass().getName())));
    }

    /**
     * The hook into the interesting room stuff.
     * @param session
     * @param message
     * @throws IOException
     */
    @Timed(name = "websocket_onMessage_timer",
        reusable = true,
        tags = "label=websocket")
    @Counted(name = "websocket_onMessage_count",
        monotonic = true,
        reusable = true,
        tags = "label=websocket")
    @Metered(name = "websocket_onMessage_meter",
        reusable = true,
        tags = "label=websocket")
    @OnMessage
    public void receiveMessage(Session session, Message message) throws IOException {
        roomImplementation.handleMessage(session, message, this);
    }

    /**
     * Simple broadcast: loop over all mentioned sessions to send the message
     * <p>
     * We are effectively always broadcasting: a player could be connected
     * to more than one device, and that could correspond to more than one connected
     * session. Allow topic filtering on the receiving side (Mediator and browser)
     * to filter out and display messages.
     *
     * @param session Target session (used to find all related sessions)
     * @param message Message to send
     * @see #sendRemoteTextMessage(Session, Message)
     */
    @Timed(name = "websocket_sendMessage_timer",
        reusable = true,
        tags = "label=websocket")
    @Counted(name = "websocket_sendMessage_count",
        monotonic = true,
        reusable = true,
        tags = "label=websocket")
    @Metered(name = "websocket_sendMessage_meter",
        reusable = true,
        tags = "label=websocket")
    public void sendMessage(Session session, Message message) {
        for (Session s : session.getOpenSessions()) {
            sendMessageToSession(s, message);
        }
    }

    /**
     * Try sending the {@link Message} using
     * {@link Session#getBasicRemote()}, {@link Basic#sendObject(Object)}.
     *
     * @param session Session to send the message on
     * @param message Message to send
     * @return true if send was successful, or false if it failed
     */
    private boolean sendMessageToSession(Session session, Message message) {
        if (session.isOpen()) {
            try {
                session.getBasicRemote().sendObject(message);
                return true;
            } catch (EncodeException e) {
                // Something was wrong encoding this message, but the connection
                // is likely just fine.
                Log.log(Level.FINE, this, "Unexpected condition writing message", e);
            } catch (IOException ioe) {
                // An IOException, on the other hand, suggests the connection is
                // in a bad state.
                Log.log(Level.FINE, this, "Unexpected condition writing message", ioe);
                tryToClose(session, new CloseReason(CloseCodes.UNEXPECTED_CONDITION, trimReason(ioe.toString())));
            }
        }
        return false;
    }


    /**
     * @param message String to trim
     * @return a string no longer than 123 characters (limit of value length for {@code CloseReason})
     */
    private String trimReason(String message) {
        return message.length() > 123 ? message.substring(0, 123) : message;
    }

    /**
     * Try to close the WebSocket session and give a reason for doing so.
     *
     * @param s  Session to close
     * @param reason {@link CloseReason} the WebSocket is closing.
     */
    public void tryToClose(Session s, CloseReason reason) {
        try {
            s.close(reason);
        } catch (IOException e) {
            tryToClose(s);
        }
    }

    /**
     * Try to close a {@code Closeable} (usually once an error has already
     * occurred).
     *
     * @param c Closable to close
     */
    public void tryToClose(Closeable c) {
        if (c != null) {
            try {
                c.close();
            } catch (IOException e1) {
            }
        }
    }
}