/*
 * 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.qpid.jms.provider.failover;

import java.io.IOException;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.qpid.jms.util.URISupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Manages the list of available failover URIs that are used to connect
 * and recover a connection.
 */
public class FailoverUriPool {

    private static final Logger LOG = LoggerFactory.getLogger(FailoverUriPool.class);

    public static final boolean DEFAULT_RANDOMIZE_ENABLED = false;

    private final LinkedList<URI> uris;
    private final Map<String, String> nestedOptions;
    private final AtomicBoolean randomize = new AtomicBoolean(DEFAULT_RANDOMIZE_ENABLED);

    public FailoverUriPool() {
        this.uris = new LinkedList<URI>();
        this.nestedOptions = Collections.emptyMap();
    }

    public FailoverUriPool(List<URI> uris, Map<String, String> nestedOptions) {
        this.uris = new LinkedList<URI>();
        if (nestedOptions != null) {
            this.nestedOptions = nestedOptions;
        } else {
            this.nestedOptions = Collections.emptyMap();
        }

        if (uris != null) {
            for (URI uri : uris) {
                this.add(uri);
            }
        }
    }

    /**
     * @return the current size of the URI pool.
     */
    public int size() {
        synchronized (uris) {
            return uris.size();
        }
    }

    /**
     * @return true if the URI pool is empty.
     */
    public boolean isEmpty() {
        synchronized (uris) {
            return uris.isEmpty();
        }
    }

    /**
     * Returns the next URI in the pool of URIs.  The URI will be shifted to the
     * end of the list and not be attempted again until the full list has been
     * returned once.
     *
     * @return the next URI that should be used for a connection attempt.
     */
    public URI getNext() {
        URI next = null;
        synchronized (uris) {
            if (!uris.isEmpty()) {
                next = uris.removeFirst();
                uris.addLast(next);
            }
        }

        return next;
    }

    /**
     * Reports that the Failover Provider connected to the last URI returned from
     * this pool.  If the Pool is set to randomize this will result in the Pool of
     * URIs being shuffled in preparation for the next connect cycle.
     */
    public void connected() {
        if (isRandomize()) {
            synchronized (uris) {
                Collections.shuffle(uris);
            }
        }
    }

    /**
     * @return true if this pool returns the URI values in random order.
     */
    public boolean isRandomize() {
        return randomize.get();
    }

    /**
     * Sets whether the URIs that are returned by this pool are returned in random
     * order or not.  If false the URIs are returned in FIFO order.
     *
     * @param random
     *        true to have the URIs returned in a random order.
     */
    public void setRandomize(boolean random) {
        if (randomize.compareAndSet(!random, random)) {
            if (random) {
                synchronized (uris) {
                    Collections.shuffle(uris);
                }
            }
        }
    }

    /**
     * Adds a new URI to the pool if not already contained within.  The URI will have
     * any nest options that have been configured added to its existing set of options.
     *
     * @param uri
     *        The new URI to add to the pool.
     */
    public void add(URI uri) {
        if (uri == null) {
            return;
        }

        synchronized (uris) {
            if (!contains(uri)) {
                if (!nestedOptions.isEmpty()) {
                    try {
                        uri = URISupport.applyParameters(uri, nestedOptions);
                    } catch (URISyntaxException e) {
                        LOG.debug("Failed to add nested options to uri: {}", uri);
                    }
                }

                uris.add(uri);
            }
        }
    }

    /**
     * Adds a new URI to the pool if not already contained within.  The URI will have
     * any nested options that have been configured added to its existing set of options.
     *
     * The URI is added to the head of the pooled URIs and will be the next value that
     * is returned from the pool.
     *
     * @param uri
     *        The new URI to add to the pool.
     */
    public void addFirst(URI uri) {
        if (uri == null) {
            return;
        }

        synchronized (uris) {
            if (!contains(uri)) {
                if (!nestedOptions.isEmpty()) {
                    try {
                        uri = URISupport.applyParameters(uri, nestedOptions);
                    } catch (URISyntaxException e) {
                        LOG.debug("Failed to add nested options to uri: {}", uri);
                    }
                }

                uris.addFirst(uri);
            }
        }
    }

    /**
     * Remove a URI from the pool if present, otherwise has no effect.
     *
     * @param uri
     *        The URI to attempt to remove from the pool.
     *
     * @return true if the given URI was removed from the pool.
     */
    public boolean remove(URI uri) {
        if (uri == null) {
            return false;
        }

        synchronized (uris) {
            for (URI candidate : uris) {
                if (compareURIs(uri, candidate)) {
                    return uris.remove(candidate);
                }
            }
        }

        return false;
    }

    /**
     * Returns the currently set value for nested options which will be added to each
     * URI that is returned from the pool.
     *
     * @return the Map instance containing the nest options which can be empty if none set.
     */
    public Map<String, String> getNestedOptions() {
        return nestedOptions;
    }

    //----- Internal methods that require the locks be held ------------------//

    private boolean contains(URI newURI) {
        boolean result = false;
        for (URI uri : uris) {
            if (compareURIs(newURI, uri)) {
                result = true;
                break;
            }
        }

        return result;
    }

    private boolean compareURIs(final URI first, final URI second) {
        boolean result = false;
        if (first == null || second == null) {
            return result;
        }

        if (first.getPort() == second.getPort()) {
            InetAddress firstAddr = null;
            InetAddress secondAddr = null;
            try {
                firstAddr = InetAddress.getByName(first.getHost());
                secondAddr = InetAddress.getByName(second.getHost());

                if (firstAddr.equals(secondAddr)) {
                    result = true;
                }
            } catch(IOException e) {
                if (firstAddr == null) {
                    LOG.error("Failed to Lookup INetAddress for URI[ " + first + " ] : " + e);
                } else {
                    LOG.error("Failed to Lookup INetAddress for URI[ " + second + " ] : " + e);
                }

                if (first.getHost().equalsIgnoreCase(second.getHost())) {
                    result = true;
                }
            }
        }

        return result;
    }
}
