/**
 * Copyright 2011 John Sample
 * Copyright 2014 Andreas Schildbach
 *
 * Licensed 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.bitcoinj.net.discovery;

import org.bitcoinj.core.NetworkParameters;
import com.google.common.collect.Lists;
import org.bitcoinj.utils.DaemonThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.*;

/**
 * <p>Supports peer discovery through DNS.</p>
 *
 * <p>Failure to resolve individual host names will not cause an Exception to be thrown.
 * However, if all hosts passed fail to resolve a PeerDiscoveryException will be thrown during getPeers().
 * </p>
 *
 * <p>DNS seeds do not attempt to enumerate every peer on the network. {@link DnsDiscovery#getPeers(long, java.util.concurrent.TimeUnit)}
 * will return up to 30 random peers from the set of those returned within the timeout period. If you want more peers
 * to connect to, you need to discover them via other means (like addr broadcasts).</p>
 */
public class DnsDiscovery implements PeerDiscovery {
    private static final Logger log = LoggerFactory.getLogger(DnsDiscovery.class);

    private final String[] dnsSeeds;
    private final NetworkParameters netParams;

    /**
     * Supports finding peers through DNS A records. Community run DNS entry points will be used.
     *
     * @param netParams Network parameters to be used for port information.
     */
    public DnsDiscovery(NetworkParameters netParams) {
        this(netParams.getDnsSeeds(), netParams);
    }

    /**
     * Supports finding peers through DNS A records.
     *
     * @param dnsSeeds Host names to be examined for seed addresses.
     * @param netParams Network parameters to be used for port information.
     */
    public DnsDiscovery(String[] dnsSeeds, NetworkParameters netParams) {
        this.dnsSeeds = dnsSeeds;
        this.netParams = netParams;
    }

    @Override
    public InetSocketAddress[] getPeers(long timeoutValue, TimeUnit timeoutUnit) throws PeerDiscoveryException {
        if (dnsSeeds == null || dnsSeeds.length == 0)
            throw new PeerDiscoveryException("No DNS seeds configured; unable to find any peers");

        // Java doesn't have an async DNS API so we have to do all lookups in a thread pool, as sometimes seeds go
        // hard down and it takes ages to give up and move on.
        ExecutorService threadPool = Executors.newFixedThreadPool(dnsSeeds.length, new DaemonThreadFactory());
        try {
            List<Callable<InetAddress[]>> tasks = Lists.newArrayList();
            for (final String seed : dnsSeeds) {
                tasks.add(new Callable<InetAddress[]>() {
                    @Override
                    public InetAddress[] call() throws Exception {
                        return InetAddress.getAllByName(seed);
                    }
                });
            }
            final List<Future<InetAddress[]>> futures = threadPool.invokeAll(tasks, timeoutValue, timeoutUnit);
            ArrayList<InetSocketAddress> addrs = Lists.newArrayList();
            for (int i = 0; i < futures.size(); i++) {
                Future<InetAddress[]> future = futures.get(i);
                if (future.isCancelled()) {
                    log.warn("DNS seed {}: timed out", dnsSeeds[i]);
                    continue;  // Timed out.
                }
                final InetAddress[] inetAddresses;
                try {
                    inetAddresses = future.get();
                    log.info("DNS seed {}: got {} peers", dnsSeeds[i], inetAddresses.length);
                } catch (ExecutionException e) {
                    log.error("DNS seed {}: failed to look up: {}", dnsSeeds[i], e.getMessage());
                    continue;
                }
                for (InetAddress addr : inetAddresses) {
                    addrs.add(new InetSocketAddress(addr, netParams.getPort()));
                }
            }
            if (addrs.size() == 0)
                throw new PeerDiscoveryException("Unable to find any peers via DNS");
            Collections.shuffle(addrs);
            threadPool.shutdownNow();
            return addrs.toArray(new InetSocketAddress[addrs.size()]);
        } catch (InterruptedException e) {
            throw new PeerDiscoveryException(e);
        } finally {
            threadPool.shutdown();
        }
    }

    /** We don't have a way to abort a DNS lookup, so this does nothing */
    @Override
    public void shutdown() {
    }
}
