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

import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.StreamCorruptedException;

import org.apache.sshd.common.SshException;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.logging.AbstractLoggingBean;

/**
 * @param <T> Type of future
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public abstract class AbstractSshFuture<T extends SshFuture> extends AbstractLoggingBean implements SshFuture<T> {
    /**
     * A default value to indicate the future has been canceled
     */
    protected static final Object CANCELED = new Object();

    protected AbstractSshFuture() {
        super();
    }

    @Override
    public boolean await(long timeoutMillis) throws IOException {
        return await0(timeoutMillis, true) != null;
    }

    @Override
    public boolean awaitUninterruptibly(long timeoutMillis) {
        try {
            return await0(timeoutMillis, false) != null;
        } catch (InterruptedIOException e) {
            throw new InternalError("Unexpected interrupted exception wile awaitUninterruptibly "
                    + timeoutMillis + " msec.: " + e.getMessage(), e);
        }
    }

    /**
     * <P>Waits (interruptible) for the specified timeout (msec.) and then checks
     * the result:</P>
     * <UL>
     * <LI><P>
     * If result is {@code null} then timeout is assumed to have expired - throw
     * an appropriate {@link IOException}
     * </P></LI>
     *
     * <LI><P>
     * If the result is of the expected type, then cast and return it
     * </P></LI>
     *
     * <LI><P>
     * If the result is an {@link IOException} then re-throw it
     * </P></LI>
     *
     * <LI><P>
     * If the result is a {@link Throwable} then throw an {@link IOException}
     * whose cause is the original exception
     * </P></LI>
     *
     * <LI><P>
     * Otherwise (should never happen), throw a {@link StreamCorruptedException}
     * with the name of the result type
     * </P></LI>
     * </UL>
     *
     * @param <R>          The generic result type
     * @param expectedType The expected result type
     * @param timeout      The timeout (millis) to wait for a result
     * @return The (never {@code null}) result
     * @throws IOException If failed to retrieve the expected result on time
     */
    protected <R> R verifyResult(Class<? extends R> expectedType, long timeout) throws IOException {
        Object value = await0(timeout, true);
        if (value == null) {
            throw new SshException("Failed to get operation result within specified timeout: " + timeout);
        }

        Class<?> actualType = value.getClass();
        if (expectedType.isAssignableFrom(actualType)) {
            return expectedType.cast(value);
        }

        if (Throwable.class.isAssignableFrom(actualType)) {
            Throwable t = GenericUtils.peelException((Throwable) value);
            if (t != value) {
                value = t;
                actualType = value.getClass();
            }

            if (IOException.class.isAssignableFrom(actualType)) {
                throw (IOException) value;
            }

            throw new SshException("Failed (" + t.getClass().getSimpleName() + ") to execute: " + t.getMessage(), GenericUtils.resolveExceptionCause(t));
        } else {    // what else can it be ????
            throw new StreamCorruptedException("Unknown result type: " + actualType.getName());
        }
    }

    /**
     * Wait for the Future to be ready. If the requested delay is 0 or
     * negative, this method returns immediately.
     *
     * @param timeoutMillis The delay we will wait for the Future to be ready
     * @param interruptable Tells if the wait can be interrupted or not.
     *                      If {@code true} and the thread is interrupted then an {@link InterruptedIOException}
     *                      is thrown.
     * @return The non-{@code null} result object if the Future is ready,
     * {@code null} if the timeout expired and no result was received
     * @throws InterruptedIOException If the thread has been interrupted
     *                                when it's not allowed.
     */
    protected abstract Object await0(long timeoutMillis, boolean interruptable) throws InterruptedIOException;

    @SuppressWarnings("unchecked")
    protected SshFutureListener<T> asListener(Object o) {
        return (SshFutureListener<T>) o;
    }

    protected void notifyListener(SshFutureListener<T> l) {
        try {
            l.operationComplete(asT());
        } catch (Throwable t) {
            log.warn("notifyListener({}) failed ({}) to invoke {}: {}",
                     this, t.getClass().getSimpleName(), l, t.getMessage());
            if (log.isDebugEnabled()) {
                log.debug("notifyListener(" + this + ")[" + l + "] invocation failure details", t);
            }
        }
    }

    @SuppressWarnings("unchecked")
    protected T asT() {
        return (T) this;
    }
}
