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

import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.channel.Channel;
import org.apache.sshd.common.channel.ChannelListener;
import org.apache.sshd.common.util.logging.AbstractLoggingBean;

import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public class TestChannelListener extends AbstractLoggingBean implements ChannelListener, NamedResource {
    private final String name;
    private final Collection<Channel> activeChannels = new CopyOnWriteArraySet<>();
    private final Semaphore activeChannelsCounter = new Semaphore(0);
    private final Collection<Channel> openChannels = new CopyOnWriteArraySet<>();
    private final Semaphore openChannelsCounter = new Semaphore(0);
    private final Collection<Channel> failedChannels = new CopyOnWriteArraySet<>();
    private final Semaphore failedChannelsCounter = new Semaphore(0);
    private final Map<Channel, Collection<String>> channelStateHints = new ConcurrentHashMap<>();
    private final Semaphore chanelStateCounter = new Semaphore(0);
    private final Semaphore modificationsCounter = new Semaphore(0);
    private final Semaphore closedChannelsCounter = new Semaphore(0);

    public TestChannelListener(String discriminator) {
        super(discriminator);
        name = discriminator;
    }

    public boolean waitForModification(long timeout, TimeUnit unit) throws InterruptedException {
        return modificationsCounter.tryAcquire(timeout, unit);
    }

    @Override
    public String getName() {
        return name;
    }

    public Collection<Channel> getActiveChannels() {
        return activeChannels;
    }

    @Override
    public void channelInitialized(Channel channel) {
        assertTrue(activeChannels.add(channel), "Same channel instance re-initialized: " + channel);
        activeChannelsCounter.release();
        modificationsCounter.release();
        log.info("channelInitialized({})", channel);
    }

    public boolean waitForActiveChannelsChange(long timeout, TimeUnit unit) throws InterruptedException {
        return activeChannelsCounter.tryAcquire(timeout, unit);
    }

    public Collection<Channel> getOpenChannels() {
        return openChannels;
    }

    @Override
    public void channelOpenSuccess(Channel channel) {
        assertTrue(activeChannels.contains(channel), "Open channel not activated: " + channel);
        assertTrue(openChannels.add(channel), "Same channel instance re-opened: " + channel);
        openChannelsCounter.release();
        modificationsCounter.release();
        log.info("channelOpenSuccess({})", channel);
    }

    public boolean waitForOpenChannelsChange(long timeout, TimeUnit unit) throws InterruptedException {
        return openChannelsCounter.tryAcquire(timeout, unit);
    }

    public Collection<Channel> getFailedChannels() {
        return failedChannels;
    }

    @Override
    public void channelOpenFailure(Channel channel, Throwable reason) {
        assertTrue(activeChannels.contains(channel), "Failed channel not activated: " + channel);
        assertTrue(failedChannels.add(channel), "Same channel instance re-failed: " + channel);
        failedChannelsCounter.release();
        modificationsCounter.release();
        warn("channelOpenFailure({}) {} : {}", channel, reason.getClass().getSimpleName(), reason.getMessage(), reason);
    }

    public boolean waitForFailedChannelsChange(long timeout, TimeUnit unit) throws InterruptedException {
        return failedChannelsCounter.tryAcquire(timeout, unit);
    }

    @Override
    public void channelClosed(Channel channel, Throwable reason) {
        assertTrue(activeChannels.remove(channel), "Unknown closed channel instance: " + channel);
        activeChannelsCounter.release();
        closedChannelsCounter.release();
        modificationsCounter.release();
        log.info("channelClosed({})", channel);
    }

    public boolean waitForClosedChannelsChange(long timeout, TimeUnit unit) throws InterruptedException {
        return closedChannelsCounter.tryAcquire(timeout, unit);
    }

    public Map<Channel, Collection<String>> getChannelStateHints() {
        return channelStateHints;
    }

    @Override
    public void channelStateChanged(Channel channel, String hint) {
        Collection<String> hints;
        synchronized (channelStateHints) {
            hints = channelStateHints.get(channel);
            if (hints == null) {
                hints = new CopyOnWriteArrayList<>();
                channelStateHints.put(channel, hints);
            }
        }

        hints.add(hint);
        chanelStateCounter.release();
        modificationsCounter.release();
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "[" + getName() + "]";
    }
}
