/*
 * 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 org.apache.nifi.websocket;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.nifi.processor.Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class WebSocketMessageRouter {
    private static final Logger logger = LoggerFactory.getLogger(WebSocketMessageRouter.class);
    private final String endpointId;
    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    private volatile Processor processor;

    public WebSocketMessageRouter(final String endpointId) {
        this.endpointId = endpointId;
    }

    public synchronized void registerProcessor(final Processor processor) throws WebSocketConfigurationException {
        if (this.processor != null) {
            throw new WebSocketConfigurationException("Processor " + this.processor + " is already assigned to this router.");
        }
        this.processor = processor;
    }

    public boolean isProcessorRegistered(final Processor processor) {
        return this.processor != null && this.processor.getIdentifier().equals(processor.getIdentifier());
    }

    public synchronized void deregisterProcessor(final Processor processor) {
        if (!isProcessorRegistered(processor)) {
            if (this.processor == null) {
                logger.info("Deregister processor {}, do nothing because this router doesn't have registered processor", processor);
            } else {
                logger.info("Deregister processor {}, do nothing because this router is assigned to different processor {}", processor, this.processor);
            }
            return;
        }

        this.processor = null;
        sessions.keySet().forEach(sessionId -> {
            try {
                disconnect(sessionId, "Processing has stopped.");
            } catch (IOException e) {
                logger.warn("Failed to disconnect session {} endpoint [{}] due to {}", sessionId, endpointId, e, e);
            }
        });
    }

    public void captureSession(final WebSocketSession session) {
        final String sessionId = session.getSessionId();
        sessions.put(sessionId, session);

        if (processor != null && processor instanceof ConnectedListener) {
            ((ConnectedListener) processor).connected(session);
        }
    }

    public void onWebSocketClose(final String sessionId, final int statusCode, final String reason) {
        if (processor instanceof ConnectedListener connectedListener) {
            connectedListener.disconnected(getSessionOrFail(sessionId));
        }
        sessions.remove(sessionId);
    }

    public void onWebSocketText(final String sessionId, final String message) {
        if (processor != null && processor instanceof TextMessageConsumer) {
            ((TextMessageConsumer) processor).consume(getSessionOrFail(sessionId), message);
        }
    }

    public void onWebSocketBinary(final String sessionId, final byte[] payload, final int offset, final int length) {
        if (processor != null && processor instanceof BinaryMessageConsumer) {
            ((BinaryMessageConsumer) processor).consume(getSessionOrFail(sessionId), payload, offset, length);
        }
    }

    private WebSocketSession getSessionOrFail(final String sessionId) {
        final WebSocketSession session = sessions.get(sessionId);
        if (session == null) {
            throw new IllegalStateException("Session was not found for the sessionId: " + sessionId);
        }
        return session;
    }

    public void sendMessage(final String sessionId, final SendMessage sendMessage) throws IOException {
        if (sessionId == null || sessionId.isEmpty()) {
            //The sessionID is not specified so broadcast the message to all connected client sessions.
            sessions.keySet().forEach(itrSessionId -> {
                try {
                    final WebSocketSession session = getSessionOrFail(itrSessionId);
                    sendMessage.send(session);
                } catch (IOException e) {
                    logger.warn("Failed to send message to session {} due to {}", itrSessionId, e, e);
                }
            });
        } else {
            final WebSocketSession session = getSessionOrFail(sessionId);
            sendMessage.send(session);
        }
    }

    public void disconnect(final String sessionId, final String reason) throws IOException {
        final WebSocketSession session = getSessionOrFail(sessionId);
        session.close(reason);
        sessions.remove(sessionId);
    }

    public boolean containsSession(final String sessionId) {
        return sessions.containsKey(sessionId);
    }

}
