/*
 * 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.dubbo.registry.dns;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.NamedThreadFactory;
import org.apache.dubbo.registry.client.DefaultServiceInstance;
import org.apache.dubbo.registry.client.ReflectionBasedServiceDiscovery;
import org.apache.dubbo.registry.client.ServiceInstance;
import org.apache.dubbo.registry.client.event.listener.ServiceInstancesChangedListener;
import org.apache.dubbo.registry.dns.util.DNSClientConst;
import org.apache.dubbo.registry.dns.util.DNSResolver;
import org.apache.dubbo.registry.dns.util.ResolveResult;
import org.apache.dubbo.rpc.model.ApplicationModel;
import org.apache.dubbo.rpc.model.ScopeModelUtil;

import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

public class DNSServiceDiscovery extends ReflectionBasedServiceDiscovery {
    private static final Logger logger = LoggerFactory.getLogger(DNSServiceDiscovery.class);

    /**
     * DNS properties
     */

    private final String addressPrefix;
    private final String addressSuffix;
    private final long pollingCycle;
    private DNSResolver dnsResolver;

    /**
     * Polling task ScheduledFuture, used to stop task when destroy
     */
    private final ConcurrentHashMap<String, ScheduledFuture<?>> pollingExecutorMap = new ConcurrentHashMap<>();

    /**
     * Polling check provider ExecutorService
     */
    private final ScheduledExecutorService pollingExecutorService;

    public DNSServiceDiscovery(ApplicationModel applicationModel, URL registryURL) {
        super(applicationModel, registryURL);
        this.addressPrefix = registryURL.getParameter(DNSClientConst.ADDRESS_PREFIX, "");
        this.addressSuffix = registryURL.getParameter(DNSClientConst.ADDRESS_SUFFIX, "");
        this.pollingCycle = registryURL.getParameter(DNSClientConst.DNS_POLLING_CYCLE, DNSClientConst.DEFAULT_DNS_POLLING_CYCLE);

        String nameserver = registryURL.getHost();
        int port = registryURL.getPort();
        int maxQueriesPerResolve = registryURL.getParameter(DNSClientConst.MAX_QUERIES_PER_RESOLVE, 10);
        this.dnsResolver = new DNSResolver(nameserver, port, maxQueriesPerResolve);


        int scheduledThreadPoolSize = registryURL.getParameter(DNSClientConst.DNS_POLLING_POOL_SIZE_KEY, DNSClientConst.DEFAULT_DNS_POLLING_POOL_SIZE);

        // polling task may take a lot of time, create a new ScheduledThreadPool
        pollingExecutorService = Executors.newScheduledThreadPool(scheduledThreadPoolSize, new NamedThreadFactory("Dubbo-DNS-Poll"));

    }

    @Override
    public void doDestroy() throws Exception {
        super.doDestroy();
        dnsResolver.destroy();
        pollingExecutorMap.forEach((serviceName, scheduledFuture) -> scheduledFuture.cancel(true));
        pollingExecutorMap.clear();
        pollingExecutorService.shutdown();
    }

    @Override
    public Set<String> getServices() {
        // it is impossible for dns to discover service names
        return Collections.singleton("Unsupported Method");
    }

    @Override
    public List<ServiceInstance> getInstances(String serviceName) throws NullPointerException {

        String serviceAddress = addressPrefix + serviceName + addressSuffix;

        ResolveResult resolveResult = dnsResolver.resolve(serviceAddress);

        return toServiceInstance(serviceName, resolveResult);
    }

    @Override
    public void addServiceInstancesChangedListener(ServiceInstancesChangedListener listener) throws NullPointerException, IllegalArgumentException {
        listener.getServiceNames().forEach(serviceName -> {
            ScheduledFuture<?> scheduledFuture = pollingExecutorService.scheduleAtFixedRate(() -> {
                    List<ServiceInstance> instances = getInstances(serviceName);
                    instances.sort(Comparator.comparingInt(ServiceInstance::hashCode));
                    notifyListener(serviceName, listener, instances);
                },
                pollingCycle, pollingCycle, TimeUnit.MILLISECONDS);

            pollingExecutorMap.put(serviceName, scheduledFuture);
        });
    }

    /**
     * UT used only
     */
    @Deprecated
    public void setDnsResolver(DNSResolver dnsResolver) {
        this.dnsResolver = dnsResolver;
    }

    private List<ServiceInstance> toServiceInstance(String serviceName, ResolveResult resolveResult) {

        int port;

        if (resolveResult.getPort().size() > 0) {
            // use first as default
            port = resolveResult.getPort().get(0);
        } else {
            // not support SRV record
            port = 20880;
        }

        List<ServiceInstance> instanceList = new LinkedList<>();

        for (String host : resolveResult.getHostnameList()) {
            DefaultServiceInstance serviceInstance = new DefaultServiceInstance(serviceName, host, port, ScopeModelUtil.getApplicationModel(getUrl().getScopeModel()));
            fillServiceInstance(serviceInstance);
            instanceList.add(serviceInstance);
        }

        return instanceList;
    }
}
