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

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import org.apache.qpid.protonj2.client.ReconnectLocation;

/**
 * Manages the list of available reconnect entries that are used to connect
 * and recover a connection.
 */
public class ReconnectLocationPool {

    private final LinkedList<ReconnectLocation> entries;

    /**
     * Creates an empty {@link ReconnectLocationPool}.
     */
    public ReconnectLocationPool() {
        this.entries = new LinkedList<ReconnectLocation>();
    }

    /**
     * Creates a new {@link ReconnectLocationPool} with the provided {@link ReconnectLocation} values.
     *
     * @param backups
     * 		a list of location where a reconnection attempt should be made.
     */
    public ReconnectLocationPool(List<ReconnectLocation> backups) {
        this.entries = new LinkedList<ReconnectLocation>();

        if (backups != null) {
            backups.forEach(entry -> add(entry));
        }
    }

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

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

    /**
     * Returns the next entry in the pool of entries.  The entry 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 entry that should be used for a connection attempt.
     */
    public ReconnectLocation getNext() {
        ReconnectLocation next = null;
        synchronized (entries) {
            if (!entries.isEmpty()) {
                next = entries.removeFirst();
                entries.addLast(next);
            }
        }

        return next;
    }

    /**
     * Randomizes the order of the list of entries contained within the pool.
     */
    public void shuffle() {
        synchronized (entries) {
            Collections.shuffle(entries);
        }
    }

    /**
     * Adds a new entry to the pool if not already contained within.
     *
     * @param entry
     *        The new {@link ReconnectLocation} to add to the pool.
     */
    public void add(ReconnectLocation entry) {
        if (entry != null) {
            synchronized (entries) {
                if (!contains(entry)) {
                    entries.add(entry);
                }
            }
        }
    }

    /**
     * Adds a list of new {@link ReconnectLocation} values to the pool if not already contained within.
     *
     * @param additions
     *        The new list of {@link ReconnectLocation} to add to the pool.
     */
    public void addAll(List<ReconnectLocation> additions) {
        if (additions != null && !additions.isEmpty()) {
            synchronized (entries) {
                additions.forEach(entry -> add(entry));
            }
        }
    }

    /**
     * Adds a new {@link ReconnectLocation} to the pool if not already contained within.
     *
     * The {@link ReconnectLocation} is added to the head of the pooled {@link ReconnectLocation} list and will be the
     * next value that is returned from the pool.
     *
     * @param entry
     *        The new {@link ReconnectLocation} to add to the pool.
     */
    public void addFirst(ReconnectLocation entry) {
        if (entry != null) {
            synchronized (entries) {
                if (!contains(entry)) {
                    entries.addFirst(entry);
                }
            }
        }
    }

    /**
     * Remove a {@link ReconnectLocation} from the pool if present, otherwise has no effect.
     *
     * @param entry
     *        The {@link ReconnectLocation} to attempt to remove from the pool.
     *
     * @return true if the given {@link ReconnectLocation} was removed from the pool.
     */
    public boolean remove(ReconnectLocation entry) {
        if (entry != null) {
            synchronized (entries) {
                for (ReconnectLocation candidate : entries) {
                    if (compareEntries(entry, candidate)) {
                        return entries.remove(candidate);
                    }
                }
            }
        }

        return false;
    }

    /**
     * Removes all currently configured {@link ReconnectLocation} from the pool, no new {@link ReconnectLocation} values
     * will be served from this pool until new ones are added.
     */
    public void removeAll() {
        synchronized (entries) {
            entries.clear();
        }
    }

    /**
     * Removes all currently configured {@link ReconnectLocation} values from the pool and replaces them with
     * the new set given.
     *
     * @param replacements
     * 		The new set of reconnect {@link ReconnectLocation} values to serve from this pool.
     */
    public void replaceAll(List<ReconnectLocation> replacements) {
        synchronized (entries) {
            entries.clear();
            addAll(replacements);
        }
    }

    /**
     * Gets the current list of {@link ReconnectLocation} values. The returned list is a copy.
     *
     * @return a copy of the current list of {@link ReconnectLocation} values in the pool.
     */
    public List<ReconnectLocation> getList() {
        synchronized (entries) {
            return new ArrayList<>(entries);
        }
    }

    @Override
    public String toString() {
        synchronized (entries) {
            return "ReconnectLocationPool { " + entries + " }";
        }
    }

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

    private boolean contains(ReconnectLocation newEntry) {
        boolean result = false;
        for (ReconnectLocation entry : entries) {
            if (compareEntries(newEntry, entry)) {
                result = true;
                break;
            }
        }

        return result;
    }

    private boolean compareEntries(final ReconnectLocation first, final ReconnectLocation second) {
        boolean result = false;

        if (first == null || second == null) {
            return result;
        } else if (first.getPort() == second.getPort()) {
            final String firstHost = first.getHost();
            final String secondHost = second.getHost();

            if (firstHost.equalsIgnoreCase(secondHost)) {
                result = true;
            }
        }

        return result;
    }
}
