/*
 *  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 java.nio.channels.spi;

import java.io.IOException;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.IllegalBlockingModeException;
import java.nio.channels.IllegalSelectorException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.ArrayList;
import java.util.List;

/**
 * {@code AbstractSelectableChannel} is the base implementation class for
 * selectable channels. It declares methods for registering, unregistering and
 * closing selectable channels. It is thread-safe.
 */
public abstract class AbstractSelectableChannel extends SelectableChannel {

    private final SelectorProvider provider;

    /*
     * The collection of key.
     */
    private List<SelectionKey> keyList = new ArrayList<SelectionKey>();

    // Marker class so lock type shows up in profilers
    static private class BlockingLock {
    }

    private final Object blockingLock = new BlockingLock();

    boolean isBlocking = true;

    /**
     * Constructs a new {@code AbstractSelectableChannel}.
     *
     * @param selectorProvider
     *            the selector provider that creates this channel.
     */
    protected AbstractSelectableChannel(SelectorProvider selectorProvider) {
        super();
        provider = selectorProvider;
    }

    /**
     * Returns the selector provider that has created this channel.
     *
     * @see java.nio.channels.SelectableChannel#provider()
     * @return this channel's selector provider.
     */
    @Override
    public final SelectorProvider provider() {
        return provider;
    }

    /**
     * Indicates whether this channel is registered with one or more selectors.
     *
     * @return {@code true} if this channel is registered with a selector,
     *         {@code false} otherwise.
     */
    @Override
    synchronized public final boolean isRegistered() {
        return !keyList.isEmpty();
    }

    /**
     * Gets this channel's selection key for the specified selector.
     *
     * @param selector
     *            the selector with which this channel has been registered.
     * @return the selection key for the channel or {@code null} if this channel
     *         has not been registered with {@code selector}.
     */
    @Override
    synchronized public final SelectionKey keyFor(Selector selector) {
        for (int i = 0; i < keyList.size(); i++) {
            SelectionKey key = keyList.get(i);
            if (null != key && key.selector() == selector) {
                return key;
            }
        }
        return null;
    }

    /**
     * Registers this channel with the specified selector for the specified
     * interest set. If the channel is already registered with the selector, the
     * {@link SelectionKey interest set} is updated to {@code interestSet} and
     * the corresponding selection key is returned. If the channel is not yet
     * registered, this method calls the {@code register} method of
     * {@code selector} and adds the selection key to this channel's key set.
     *
     * @param selector
     *            the selector with which to register this channel.
     * @param interestSet
     *            this channel's {@link SelectionKey interest set}.
     * @param attachment
     *            the object to attach, can be {@code null}.
     * @return the selection key for this registration.
     * @throws CancelledKeyException
     *             if this channel is registered but its key has been canceled.
     * @throws ClosedChannelException
     *             if this channel is closed.
     * @throws IllegalArgumentException
     *             if {@code interestSet} is not supported by this channel.
     * @throws IllegalBlockingModeException
     *             if this channel is in blocking mode.
     * @throws IllegalSelectorException
     *             if this channel does not have the same provider as the given
     *             selector.
     */
    @Override
    public final SelectionKey register(Selector selector, int interestSet,
            Object attachment) throws ClosedChannelException {
        if (!isOpen()) {
            throw new ClosedChannelException();
        }
        if (!((interestSet & ~validOps()) == 0)) {
            throw new IllegalArgumentException();
        }

        synchronized (blockingLock) {
            if (isBlocking) {
                throw new IllegalBlockingModeException();
            }
            if (!selector.isOpen()) {
                if (0 == interestSet) {
                    // throw ISE exactly to keep consistency
                    throw new IllegalSelectorException();
                }
                // throw NPE exactly to keep consistency
                throw new NullPointerException();
            }
            SelectionKey key = keyFor(selector);
            if (null == key) {
                key = ((AbstractSelector) selector).register(this, interestSet,
                        attachment);
                keyList.add(key);
            } else {
                if (!key.isValid()) {
                    throw new CancelledKeyException();
                }
                key.interestOps(interestSet);
                key.attach(attachment);
            }
            return key;
        }
    }

    /**
     * Implements the channel closing behavior. Calls
     * {@code implCloseSelectableChannel()} first, then loops through the list
     * of selection keys and cancels them, which unregisters this channel from
     * all selectors it is registered with.
     *
     * @throws IOException
     *             if a problem occurs while closing the channel.
     */
    @Override
    synchronized protected final void implCloseChannel() throws IOException {
        implCloseSelectableChannel();
        for (int i = 0; i < keyList.size(); i++) {
            SelectionKey key = keyList.get(i);
            if (null != key) {
                key.cancel();
            }
        }
    }

    /**
     * Implements the closing function of the SelectableChannel. This method is
     * called from {@code implCloseChannel()}.
     *
     * @throws IOException
     *             if an I/O exception occurs.
     */
    protected abstract void implCloseSelectableChannel() throws IOException;

    /**
     * Indicates whether this channel is in blocking mode.
     *
     * @return {@code true} if this channel is blocking, {@code false}
     *         otherwise.
     */
    @Override
    public final boolean isBlocking() {
        synchronized (blockingLock) {
            return isBlocking;
        }
    }

    /**
     * Gets the object used for the synchronization of {@code register} and
     * {@code configureBlocking}.
     *
     * @return the synchronization object.
     */
    @Override
    public final Object blockingLock() {
        return blockingLock;
    }

    /**
     * Sets the blocking mode of this channel. A call to this method blocks if
     * other calls to this method or to {@code register} are executing. The
     * actual setting of the mode is done by calling
     * {@code implConfigureBlocking(boolean)}.
     *
     * @see java.nio.channels.SelectableChannel#configureBlocking(boolean)
     * @param blockingMode
     *            {@code true} for setting this channel's mode to blocking,
     *            {@code false} to set it to non-blocking.
     * @return this channel.
     * @throws ClosedChannelException
     *             if this channel is closed.
     * @throws IllegalBlockingModeException
     *             if {@code block} is {@code true} and this channel has been
     *             registered with at least one selector.
     * @throws IOException
     *             if an I/O error occurs.
     */
    @Override
    public final SelectableChannel configureBlocking(boolean blockingMode) throws IOException {
        if (isOpen()) {
            synchronized (blockingLock) {
                if (isBlocking == blockingMode) {
                    return this;
                }
                if (blockingMode && containsValidKeys()) {
                    throw new IllegalBlockingModeException();
                }
                implConfigureBlocking(blockingMode);
                isBlocking = blockingMode;
            }
            return this;
        }
        throw new ClosedChannelException();
    }

    /**
     * Implements the setting of the blocking mode.
     *
     * @param blockingMode
     *            {@code true} for setting this channel's mode to blocking,
     *            {@code false} to set it to non-blocking.
     * @throws IOException
     *             if an I/O error occurs.
     */
    protected abstract void implConfigureBlocking(boolean blockingMode)
            throws IOException;

    /*
     * package private for deregister method in AbstractSelector.
     */
    synchronized void deregister(SelectionKey k) {
        if (null != keyList) {
            keyList.remove(k);
        }
    }

    /**
     * Returns true if the keyList contains at least 1 valid key and false
     * otherwise.
     */
    private synchronized boolean containsValidKeys() {
        for (int i = 0; i < keyList.size(); i++) {
            SelectionKey key = keyList.get(i);
            if (key != null && key.isValid()) {
                return true;
            }
        }
        return false;
    }
}
