/*
 * Copyright (c) 2013, 2021 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.tyrus.core;

import java.io.InputStream;
import java.io.Reader;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jakarta.websocket.Decoder;
import jakarta.websocket.MessageHandler;
import jakarta.websocket.PongMessage;

import org.glassfish.tyrus.core.coder.CoderWrapper;
import org.glassfish.tyrus.core.l10n.LocalizationMessages;

/**
 * Manages registered {@link MessageHandler}s and checks whether the new ones may be registered.
 *
 * @author Stepan Kopriva
 * @author Pavel Bucek
 * @see MessageHandler
 * @see jakarta.websocket.OnMessage
 */
public class MessageHandlerManager {

    private static final List<Class<?>> WHOLE_TEXT_HANDLER_TYPES = Arrays.<Class<?>>asList(String.class, Reader.class);
    private static final Class<?> PARTIAL_TEXT_HANDLER_TYPE = String.class;
    private static final List<Class<?>> WHOLE_BINARY_HANDLER_TYPES = Arrays.<Class<?>>asList(
            ByteBuffer.class, InputStream.class, byte[].class);
    private static final List<Class<?>> PARTIAL_BINARY_HANDLER_TYPES = Arrays.<Class<?>>asList(
            ByteBuffer.class, byte[].class);
    private static final Class<?> PONG_HANDLER_TYPE = PongMessage.class;

    private boolean textHandlerPresent;
    private boolean textWholeHandlerPresent;
    private boolean binaryHandlerPresent;
    private boolean binaryWholeHandlerPresent;
    private boolean pongHandlerPresent;
    private boolean readerHandlerPresent;
    private boolean inputStreamHandlerPresent;
    private final Map<Class<?>, MessageHandler> registeredHandlers = new HashMap<Class<?>, MessageHandler>();
    private final List<Class<? extends Decoder>> decoders;

    private Set<MessageHandler> messageHandlerCache;

    /**
     * Construct manager with no decoders.
     */
    public MessageHandlerManager() {
        this(Collections.<Class<? extends Decoder>>emptyList());
    }

    /**
     * Construct manager.
     *
     * @param decoders registered {@link Decoder}s.
     */
    MessageHandlerManager(List<Class<? extends Decoder>> decoders) {
        this.decoders = decoders;
    }

    /**
     * Construct manager.
     *
     * @param decoders registered {@link Decoder}s.
     */
    static MessageHandlerManager fromDecoderInstances(List<Decoder> decoders) {
        List<Class<? extends Decoder>> decoderList = new ArrayList<Class<? extends Decoder>>();
        for (Decoder decoder : decoders) {
            if (decoder instanceof CoderWrapper) {
                decoderList.add(((CoderWrapper<? extends Decoder>) decoder).getCoderClass());
            } else {
                decoderList.add(decoder.getClass());
            }
        }

        return new MessageHandlerManager(decoderList);
    }

    /**
     * Construct manager.
     *
     * @param decoderClasses registered {@link Decoder}s.
     * @return constructed message handler manager.
     */
    public static MessageHandlerManager fromDecoderClasses(List<Class<? extends Decoder>> decoderClasses) {
        return new MessageHandlerManager(decoderClasses);
    }

    /**
     * Add {@link MessageHandler} to the manager.
     *
     * @param handler {@link MessageHandler} to be added to the manager.
     */
    public void addMessageHandler(MessageHandler handler) throws IllegalStateException {

        final Class<?> handlerClass = getHandlerType(handler);

        if (handler instanceof MessageHandler.Whole) { //WHOLE MESSAGE HANDLER
            addMessageHandler(handlerClass, (MessageHandler.Whole) handler);
        } else if (handler instanceof MessageHandler.Partial) { // PARTIAL MESSAGE HANDLER
            addMessageHandler(handlerClass, (MessageHandler.Partial) handler);
        } else {
            throwException(LocalizationMessages.MESSAGE_HANDLER_WHOLE_OR_PARTIAL());
        }
    }

    /**
     * Add {@link MessageHandler.Whole} to the manager.
     *
     * @param clazz   type handled by {@link MessageHandler}.
     * @param handler {@link MessageHandler} to be added.
     * @param <T>     type of message to be handled.
     * @throws IllegalStateException when the message handler cannot be added.
     */
    public <T> void addMessageHandler(Class<T> clazz, MessageHandler.Whole<T> handler) throws IllegalStateException {
        if (WHOLE_TEXT_HANDLER_TYPES.contains(clazz)) { // text
            if (textHandlerPresent) {
                throwException(LocalizationMessages.MESSAGE_HANDLER_ALREADY_REGISTERED_TEXT());
            } else {
                if (Reader.class.isAssignableFrom(clazz)) {
                    readerHandlerPresent = true;
                }
                textHandlerPresent = true;
                textWholeHandlerPresent = true;
            }
        } else if (WHOLE_BINARY_HANDLER_TYPES.contains(clazz)) { // binary
            if (binaryHandlerPresent) {
                throwException(LocalizationMessages.MESSAGE_HANDLER_ALREADY_REGISTERED_BINARY());
            } else {
                if (InputStream.class.isAssignableFrom(clazz)) {
                    inputStreamHandlerPresent = true;
                }
                binaryHandlerPresent = true;
                binaryWholeHandlerPresent = true;
            }
        } else if (PONG_HANDLER_TYPE.equals(clazz)) { // pong
            if (pongHandlerPresent) {
                throwException(LocalizationMessages.MESSAGE_HANDLER_ALREADY_REGISTERED_PONG());
            } else {
                pongHandlerPresent = true;
            }
        } else {
            boolean viable = false;

            if (checkTextDecoders(clazz)) { //decodable text
                if (textHandlerPresent) {
                    throwException(LocalizationMessages.MESSAGE_HANDLER_ALREADY_REGISTERED_TEXT());
                } else {
                    textHandlerPresent = true;
                    textWholeHandlerPresent = true;
                    viable = true;
                }
            }

            if (checkBinaryDecoders(clazz)) { //decodable binary
                if (binaryHandlerPresent) {
                    throwException(LocalizationMessages.MESSAGE_HANDLER_ALREADY_REGISTERED_BINARY());
                } else {
                    binaryHandlerPresent = true;
                    binaryWholeHandlerPresent = true;
                    viable = true;
                }
            }

            if (!viable) {
                throwException(LocalizationMessages.MESSAGE_HANDLER_DECODER_NOT_REGISTERED(clazz));
            }
        }

        registerMessageHandler(clazz, handler);
    }

    /**
     * Add {@link MessageHandler.Partial} to the manager.
     *
     * @param clazz   type handled by {@link MessageHandler}.
     * @param handler {@link MessageHandler} to be added.
     * @param <T>     type of message to be handled.
     * @throws IllegalStateException when the message handler cannot be added.
     */
    public <T> void addMessageHandler(Class<T> clazz, MessageHandler.Partial<T> handler) throws IllegalStateException {
        boolean viable = false;

        if (PARTIAL_TEXT_HANDLER_TYPE.equals(clazz)) { // text
            if (textHandlerPresent) {
                throwException(LocalizationMessages.MESSAGE_HANDLER_ALREADY_REGISTERED_TEXT());
            } else {
                textHandlerPresent = true;
                viable = true;
            }
        }

        if (PARTIAL_BINARY_HANDLER_TYPES.contains(clazz)) { // binary
            if (binaryHandlerPresent) {
                throwException(LocalizationMessages.MESSAGE_HANDLER_ALREADY_REGISTERED_BINARY());
            } else {
                binaryHandlerPresent = true;
                viable = true;
            }
        }

        if (!viable) {
            throwException(LocalizationMessages.MESSAGE_HANDLER_PARTIAL_INVALID_TYPE(clazz.getName()));
        }

        registerMessageHandler(clazz, handler);
    }

    private <T> void registerMessageHandler(Class<T> clazz, MessageHandler handler) {
        // map of all registered handlers
        if (registeredHandlers.containsKey(clazz)) {
            throwException(LocalizationMessages.MESSAGE_HANDLER_ALREADY_REGISTERED_TYPE(clazz));
        } else {
            registeredHandlers.put(clazz, handler);
        }

        messageHandlerCache = null;
    }

    private void throwException(String text) throws IllegalStateException {
        throw new IllegalStateException(text);
    }

    /**
     * Remove {@link MessageHandler} from the manager.
     *
     * @param handler handler which will be removed.
     */
    public void removeMessageHandler(MessageHandler handler) {
        Iterator<Map.Entry<Class<?>, MessageHandler>> iterator = registeredHandlers.entrySet().iterator();
        Class<?> handlerClass = null;

        while (iterator.hasNext()) {
            final Map.Entry<Class<?>, MessageHandler> next = iterator.next();
            if (next.getValue().equals(handler)) {
                handlerClass = next.getKey();
                iterator.remove();
                messageHandlerCache = null;
                break;
            }
        }

        if (handlerClass == null) {
            return;
        }

        if (handler instanceof MessageHandler.Whole) { //WHOLE MESSAGE HANDLER
            if (WHOLE_TEXT_HANDLER_TYPES.contains(handlerClass)) { // text
                textHandlerPresent = false;
                textWholeHandlerPresent = false;

            } else if (WHOLE_BINARY_HANDLER_TYPES.contains(handlerClass)) { // binary
                binaryHandlerPresent = false;
                binaryWholeHandlerPresent = false;

            } else if (PONG_HANDLER_TYPE.equals(handlerClass)) { // pong
                pongHandlerPresent = false;
            } else {
                if (checkTextDecoders(handlerClass)) { //decodable text
                    textHandlerPresent = false;
                    textWholeHandlerPresent = false;

                } else if (checkBinaryDecoders(handlerClass)) { //decodable binary
                    binaryHandlerPresent = false;
                    binaryWholeHandlerPresent = false;
                }
            }
        } else { // PARTIAL MESSAGE HANDLER
            if (PARTIAL_TEXT_HANDLER_TYPE.equals(handlerClass)) { // text
                textHandlerPresent = false;

            } else if (PARTIAL_BINARY_HANDLER_TYPES.contains(handlerClass)) { // binary
                binaryHandlerPresent = false;
            }
        }
    }

    Map<Class<?>, MessageHandler> getRegisteredHandlers() {
        return new HashMap<>(registeredHandlers);
    }

    /**
     * Get all successfully registered {@link MessageHandler}s.
     *
     * @return unmodifiable {@link Set} of registered {@link MessageHandler}s.
     */
    public Set<MessageHandler> getMessageHandlers() {
        if (messageHandlerCache == null) {
            messageHandlerCache = Collections.unmodifiableSet(new HashSet<>(registeredHandlers.values()));
        }

        return messageHandlerCache;
    }

    public List<Map.Entry<Class<?>, MessageHandler>> getOrderedWholeMessageHandlers() {
        List<Map.Entry<Class<?>, MessageHandler>> result = new ArrayList<>();
        for (final Map.Entry<Class<?>, MessageHandler> entry : registeredHandlers.entrySet()) {
            if (entry.getValue() instanceof MessageHandler.Whole) {
                result.add(entry);
            }
        }
        Collections.sort(result, new MessageHandlerComparator());
        return result;
    }

    private static Class<?> getHandlerType(MessageHandler handler) {
        Class<?> root;
        if (handler instanceof AsyncMessageHandler) {
            return ((AsyncMessageHandler) handler).getType();
        } else if (handler instanceof BasicMessageHandler) {
            return ((BasicMessageHandler) handler).getType();
        } else if (handler instanceof MessageHandler.Partial) {
            root = MessageHandler.Partial.class;
        } else if (handler instanceof MessageHandler.Whole) {
            root = MessageHandler.Whole.class;
        } else {
            throw new IllegalArgumentException(LocalizationMessages.MESSAGE_HANDLER_ILLEGAL_ARGUMENT(handler));
        }
        Class<?> result = ReflectionHelper.getClassType(handler.getClass(), root);
        return result == null ? Object.class : result;
    }

    private boolean checkTextDecoders(Class<?> requiredType) {
        for (Class<? extends Decoder> decoderClass : decoders) {
            if (isTextDecoder(decoderClass)
                    && requiredType.isAssignableFrom(AnnotatedEndpoint.getDecoderClassType(decoderClass))) {
                return true;
            }
        }

        return false;
    }

    private boolean checkBinaryDecoders(Class<?> requiredType) {
        for (Class<? extends Decoder> decoderClass : decoders) {
            if (isBinaryDecoder(decoderClass)
                    && requiredType.isAssignableFrom(AnnotatedEndpoint.getDecoderClassType(decoderClass))) {
                return true;
            }
        }

        return false;
    }

    private boolean isTextDecoder(Class<? extends Decoder> decoderClass) {
        return Decoder.Text.class.isAssignableFrom(decoderClass)
                || Decoder.TextStream.class.isAssignableFrom(decoderClass);
    }

    private boolean isBinaryDecoder(Class<? extends Decoder> decoderClass) {
        return Decoder.Binary.class.isAssignableFrom(decoderClass)
                || Decoder.BinaryStream.class.isAssignableFrom(decoderClass);
    }

    boolean isWholeTextHandlerPresent() {
        return textWholeHandlerPresent;
    }

    boolean isWholeBinaryHandlerPresent() {
        return binaryWholeHandlerPresent;
    }

    boolean isPartialTextHandlerPresent() {
        return textHandlerPresent && !textWholeHandlerPresent;
    }

    boolean isPartialBinaryHandlerPresent() {
        return binaryHandlerPresent && !binaryWholeHandlerPresent;
    }

    public boolean isReaderHandlerPresent() {
        return readerHandlerPresent;
    }

    public boolean isInputStreamHandlerPresent() {
        return inputStreamHandlerPresent;
    }

    boolean isPongHandlerPresent() {
        return pongHandlerPresent;
    }

    private static class MessageHandlerComparator implements Comparator<Map.Entry<Class<?>, MessageHandler>>,
            Serializable {

        private static final long serialVersionUID = -5136634876439146784L;

        @Override
        public int compare(Map.Entry<Class<?>, MessageHandler> o1, Map.Entry<Class<?>, MessageHandler> o2) {
            if (o1.getKey().isAssignableFrom(o2.getKey())) {
                return 1;
            } else if (o2.getKey().isAssignableFrom(o1.getKey())) {
                return -1;
            } else {
                return 0;
            }
        }
    }
}
