package core.framework.internal.web.websocket;

import core.framework.log.ActionLogContext;
import core.framework.util.Sets;
import core.framework.util.StopWatch;
import core.framework.web.websocket.Channel;
import io.undertow.websockets.core.WebSocketChannel;
import io.undertow.websockets.core.WebSockets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author neo
 */
public class ChannelImpl implements Channel, Channel.Context {
    private static final Logger LOGGER = LoggerFactory.getLogger(ChannelImpl.class);
    final String id = UUID.randomUUID().toString();
    final Set<String> rooms = Sets.newConcurrentHashSet();
    final ChannelHandler handler;
    private final WebSocketChannel channel;
    private final Map<String, Object> context = new ConcurrentHashMap<>();
    private final WebSocketContextImpl webSocketContext;
    String action;
    String clientIP;
    String refId;

    ChannelImpl(WebSocketChannel channel, WebSocketContextImpl webSocketContext, ChannelHandler handler) {
        this.channel = channel;
        this.webSocketContext = webSocketContext;
        this.handler = handler;
    }

    @Override
    public <T> void send(T message) {
        var watch = new StopWatch();
        String text = handler.toServerMessage(message);

        // refer to io.undertow.websockets.core.WebSocketChannel.send(WebSocketFrameType),
        // in concurrent env, one thread can still get hold of channel from context right before channel close listener removes it from context
        // this is to reduce chance of triggering WebSocketMessages.MESSAGES.channelClosed() exception
        // but in theory, there is still small possibility to cause channelClosed()
        if (channel.isCloseFrameSent() || channel.isCloseFrameReceived()) return;

        try {
            WebSockets.sendText(text, channel, ChannelCallback.INSTANCE);
        } finally {
            long elapsed = watch.elapsed();
            ActionLogContext.track("ws", elapsed, 0, 1);
            LOGGER.debug("send ws message, id={}, text={}, elapsed={}", id, text, elapsed);     // not mask, assume ws message not containing sensitive info, the text can be json or plain text
        }
    }

    @Override
    public void close() {
        var watch = new StopWatch();
        try {
            WebSockets.sendClose(WebSocketCloseCodes.NORMAL_CLOSURE, null, channel, ChannelCallback.INSTANCE);
        } finally {
            long elapsed = watch.elapsed();
            ActionLogContext.track("ws", elapsed, 0, 1);
            LOGGER.debug("close ws channel, id={}, elapsed={}", id, elapsed);
        }
    }

    @Override
    public void join(String room) {
        webSocketContext.join(this, room);
    }

    @Override
    public void leave(String room) {
        webSocketContext.leave(this, room);
    }

    @Override
    public Context context() {
        return this;
    }

    @Override
    public Object get(String key) {
        return context.get(key);
    }

    @Override
    public void put(String key, Object value) {
        if (value == null) context.remove(key);
        else context.put(key, value);
    }
}