/*
 * 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.sshd.common.util;

import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EventListener;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

/**
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public final class EventListenerUtils {
    /**
     * A special &quot;comparator&quot; whose only purpose is to ensure
     * there are no same references in a listener's set - to be used
     * in conjunction with a {@code TreeSet} as its comparator
     */
    @SuppressWarnings("checkstyle:anoninnerlength")
    public static final Comparator<EventListener> LISTENER_INSTANCE_COMPARATOR = (l1, l2) -> {
        if (l1 == l2) {
            return 0;
        } else if (l1 == null) {
            return 1;
        } else if (l2 == null) {
            return -1;
        }

        Class<?> c1 = l1.getClass();
        Class<?> c2 = l2.getClass();
        boolean checkHashCodes = true;
        if (Proxy.isProxyClass(c1)) {
            if (Proxy.isProxyClass(c2)) {
                checkHashCodes = false; // cannot call hashCode on a proxy
            } else {
                return 1;
            }
        } else if (Proxy.isProxyClass(c2)) {
            return -1;
        }

        if (checkHashCodes) {
            int nRes = Integer.compare(l1.hashCode(), l2.hashCode());
            if (nRes != 0) {
                return nRes;
            }
        }

        int nRes = Integer.compare(System.identityHashCode(l1), System.identityHashCode(l2));
        if (nRes != 0) {
            return nRes;
        }

        if (c1 != c2) {
            return c1.getName().compareTo(c2.getName());
        }

        String s1 = Objects.toString(l1.toString(), "");
        String s2 = Objects.toString(l2.toString(), "");
        nRes = s1.compareTo(s2);
        if (nRes != 0) {
            return nRes;
        }
        throw new UnsupportedOperationException("Ran out of options to compare instance of " + s1 + " vs. " + s2);
    };

    private EventListenerUtils() {
        throw new UnsupportedOperationException("No instance");
    }

    /**
     * @param <L> Type of {@link SshdEventListener} contained in the set
     * @param listeners The listeners to pre-add to the create set - ignored
     * if (@code null}/empty
     * @return A (synchronized) {@link Set} for containing the listeners ensuring
     * that if same listener instance is added repeatedly only <U>one</U>
     * instance is actually contained
     */
    public static <L extends SshdEventListener> Set<L> synchronizedListenersSet(Collection<? extends L> listeners) {
        Set<L> s = EventListenerUtils.synchronizedListenersSet();
        if (GenericUtils.size(listeners) > 0) {
            s.addAll(listeners);
        }

        return s;
    }

    /**
     * @param <L> Type of {@link SshdEventListener} contained in the set
     * @return A (synchronized) {@link Set} for containing the listeners ensuring
     * that if same listener instance is added repeatedly only <U>one</U>
     * instance is actually contained
     * @see #LISTENER_INSTANCE_COMPARATOR
     */
    public static <L extends SshdEventListener> Set<L> synchronizedListenersSet() {
        return Collections.synchronizedSet(new TreeSet<L>(LISTENER_INSTANCE_COMPARATOR));
    }

    /**
     * Provides proxy wrapper around an {@link Iterable} container of listener
     * interface implementation. <b>Note:</b> a listener interface is one whose
     * invoked methods return <u>only</u> {@code void}.
     *
     * @param <T>          Generic listener type
     * @param listenerType The expected listener <u>interface</u>
     * @param listeners    An {@link Iterable} container of listeners to be invoked.
     *                     <p>
     *                     <b>Note(s):</b>
     *                     </p>
     *                     <ul>
     *                     <li><p>
     *                     The invocation order is same as the {@link Iterable} container
     *                     </p></li>
     *
     *                     <li><p>
     *                     If any of the invoked listener methods throws an exception, the
     *                     rest of the listener are <u>not</u> invoked and the exception is
     *                     propagated to the caller
     *                     </p></li>
     *
     *                     <li><p>
     *                     It is up to the <u>caller</u> to ensure that the container does
     *                     not change while the proxy is invoked
     *                     </p></li>
     *                     </ul>
     * @return A proxy wrapper implementing the same interface, but delegating
     * the calls to the container
     * @see #proxyWrapper(Class, ClassLoader, Iterable)
     */
    public static <T extends SshdEventListener> T proxyWrapper(Class<T> listenerType, Iterable<? extends T> listeners) {
        return proxyWrapper(listenerType, listenerType.getClassLoader(), listeners);
    }

    /**
     * Provides proxy wrapper around an {@link Iterable} container of listener
     * interface implementation. <b>Note:</b> a listener interface is one whose
     * invoked methods return <u>only</u> {@code void}.
     *
     * @param <T>          Generic {@link SshdEventListener} type
     * @param listenerType The expected listener <u>interface</u>
     * @param loader       The {@link ClassLoader} to use for the proxy
     * @param listeners    An {@link Iterable} container of listeners to be invoked.
     *                     <p>
     *                     <b>Note(s):</b>
     *                     </p>
     *                     <ul>
     *                     <li><p>
     *                     The invocation order is same as the {@link Iterable} container
     *                     </p></li>
     *
     *                     <li><p>
     *                     If any of the invoked listener methods throws an exception, the
     *                     rest of the listener are <u>not</u> invoked and the exception is
     *                     propagated to the caller
     *                     </p></li>
     *
     *                     <li><p>
     *                     It is up to the <u>caller</u> to ensure that the container does
     *                     not change while the proxy is invoked
     *                     </p></li>
     *                     </ul>
     * @return A proxy wrapper implementing the same interface, but delegating
     * the calls to the container
     * @throws IllegalArgumentException if <tt>listenerType</tt> is not an interface
     *                                  or a {@code null} container has been provided
     * @see #proxyWrapper(Class, ClassLoader, Iterable)
     */
    public static <T extends SshdEventListener> T proxyWrapper(Class<T> listenerType, ClassLoader loader, final Iterable<? extends T> listeners) {
        Objects.requireNonNull(listenerType, "No listener type specified");
        ValidateUtils.checkTrue(listenerType.isInterface(), "Target proxy is not an interface: %s", listenerType.getSimpleName());
        Objects.requireNonNull(listeners, "No listeners container provided");

        Object wrapper = Proxy.newProxyInstance(loader, new Class<?>[]{listenerType}, (proxy, method, args) -> {
            Throwable err = null;
            for (T l : listeners) {
                try {
                    method.invoke(l, args);
                } catch (Throwable t) {
                    Throwable e = GenericUtils.peelException(t);
                    err = GenericUtils.accumulateException(err, e);
                }
            }

            if (err != null) {
                throw err;
            }

            return null;    // we assume always void return value...
        });
        return listenerType.cast(wrapper);
    }
}
