/**
 *
 * Copyright 2018-2021 Florian Schmaus
 *
 * 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.jivesoftware.smack.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;

import javax.xml.namespace.QName;

import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.StandardExtensionElement;
import org.jivesoftware.smack.packet.XmlElement;
import org.jivesoftware.smack.provider.ProviderManager;

import org.jxmpp.util.cache.LruCache;

public class XmppElementUtil {

    private static final LruCache<Class<? extends XmlElement>, QName> CLASS_TO_QNAME_CACHE = new LruCache<>(512);

    public static final Logger LOGGER = Logger.getLogger(XmppElementUtil.class.getName());

    public static QName getQNameFor(Class<? extends XmlElement> fullyQualifiedElement) {
        QName qname = CLASS_TO_QNAME_CACHE.get(fullyQualifiedElement);
        if (qname != null) {
            return qname;
        }

        try {
            Object qnameObject = fullyQualifiedElement.getField("QNAME").get(null);
            if (QName.class.isAssignableFrom(qnameObject.getClass())) {
                qname = (QName) qnameObject;
                CLASS_TO_QNAME_CACHE.put(fullyQualifiedElement, qname);
                return qname;
            }
            LOGGER.warning("The QNAME field of " + fullyQualifiedElement + " is not of type QNAME.");
        } catch (NoSuchFieldException e) {
            LOGGER.finer("The " + fullyQualifiedElement + " has no static QNAME field. Consider adding one.");
            // Proceed to fallback strategy.
        } catch (IllegalArgumentException | IllegalAccessException | SecurityException e) {
            throw new IllegalArgumentException(e);
        }

        String element, namespace;
        try {
            element = (String) fullyQualifiedElement.getField("ELEMENT").get(null);
            namespace = (String) fullyQualifiedElement.getField("NAMESPACE").get(null);
        }
        catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
            throw new IllegalArgumentException("The " + fullyQualifiedElement + " has no ELEMENT, NAMESPACE or QNAME member. Consider adding QNAME", e);
        }

        qname = new QName(namespace, element);
        CLASS_TO_QNAME_CACHE.put(fullyQualifiedElement, qname);
        return qname;
    }

    public static <E extends ExtensionElement> List<E> getElementsFrom(
                    MultiMap<QName, XmlElement> elementMap, Class<E> extensionElementClass) {
        QName qname = XmppElementUtil.getQNameFor(extensionElementClass);

        List<XmlElement> extensionElements = elementMap.getAll(qname);

        if (extensionElements.isEmpty()) {
            return Collections.emptyList();
        }

        List<E> res = new ArrayList<>(extensionElements.size());
        for (XmlElement extensionElement : extensionElements) {
            E e = castOrThrow(extensionElement, extensionElementClass);
            res.add(e);
        }
        return res;
    }

    public static <E extends ExtensionElement> E castOrThrow(XmlElement extensionElement, Class<E> extensionElementClass) {
        if (!extensionElementClass.isInstance(extensionElement)) {
            final QName qname = getQNameFor(extensionElementClass);

            final String detailMessage;
            if (extensionElement instanceof StandardExtensionElement) {
                detailMessage = "because there is no according extension element provider registered with ProviderManager for "
                                + qname
                                + ". WARNING: This indicates a serious problem with your Smack setup, probably causing Smack not being able to properly initialize itself.";
            } else {
                Object provider = ProviderManager.getExtensionProvider(qname);
                detailMessage = "because there is an inconsistency with the provider registered with ProviderManager: the active provider for "
                                + qname + " '" + provider.getClass()
                                + "' does not return instances of type " + extensionElementClass
                                + ", but instead returns instances of type " + extensionElement.getClass() + ".";
            }

            String message = "Extension element is not of expected class '" + extensionElementClass.getName() + "', "
                            + detailMessage;
            throw new IllegalStateException(message);
        }

        return extensionElementClass.cast(extensionElement);
    }

}
