/*
 * 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.cassandra.locator;

import java.io.InputStream;
import java.net.InetAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.gms.ApplicationState;
import org.apache.cassandra.gms.EndpointState;
import org.apache.cassandra.gms.Gossiper;
import org.apache.cassandra.gms.VersionedValue;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.ResourceWatcher;
import org.apache.cassandra.utils.WrappedRunnable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;

import com.google.common.base.Objects;
import com.google.common.net.InetAddresses;

/**
 * Network topology snitch that reads its configuration from a YAML file.
 * <p>
 * This snitch supports connections over preferred addresses, such as a data-center-local address, based on the
 * reconnection trick used in {@link Ec2MultiRegionSnitch}. The configuration file, {@code cassandra-topology.yaml}, is
 * checked periodically for updates.
 * </p>
 */
public class YamlFileNetworkTopologySnitch
        extends AbstractNetworkTopologySnitch
{

    private static final Logger logger = LoggerFactory.getLogger(YamlFileNetworkTopologySnitch.class);
    
    /**
     * How often to check the topology configuration file, in milliseconds; defaults to one minute.
     */
    private static final int CHECK_PERIOD_IN_MS = 60 * 1000;

    /** Default name for the topology configuration file. */
    private static final String DEFAULT_TOPOLOGY_CONFIG_FILENAME = "cassandra-topology.yaml";

    /** Node data map, keyed by broadcast address. */
    private volatile Map<InetAddress, NodeData> nodeDataMap;

    /** Node data for this node. */
    private volatile NodeData localNodeData;

    /** Node data to fall back to when there is no match. */
    private volatile NodeData defaultNodeData;

    /** Name of the topology configuration file. */
    private final String topologyConfigFilename;

    /** True if the gossiper has been initialized. */
    private volatile boolean gossiperInitialized = false;

    /**
     * Constructor.
     *
     * @throws ConfigurationException
     *             on failure
     */
    public YamlFileNetworkTopologySnitch() throws ConfigurationException
    {
        this(DEFAULT_TOPOLOGY_CONFIG_FILENAME);
    }

    /**
     * Constructor.
     *
     * @param topologyConfigFilename
     *            name of the topology configuration file
     * @throws ConfigurationException
     *             on failure
     */
    YamlFileNetworkTopologySnitch(final String topologyConfigFilename)
            throws ConfigurationException
    {
        this.topologyConfigFilename = topologyConfigFilename;
        loadTopologyConfiguration();

        try
        {
            /*
             * Check if the topology configuration file is a plain file.
             */
            FBUtilities.resourceToFile(topologyConfigFilename);

            final Runnable runnable = new WrappedRunnable()
            {
                /**
                 * Loads the topology.
                 */
                protected void runMayThrow() throws ConfigurationException
                {
                    loadTopologyConfiguration();
                }
            };
            ResourceWatcher.watch(topologyConfigFilename, runnable,
                    CHECK_PERIOD_IN_MS);
        }
        catch (final ConfigurationException e)
        {
            logger.debug(
                    "{} found, but does not look like a plain file. Will not watch it for changes",
                    topologyConfigFilename);
        }
    }

    /**
     * Returns the name of the rack for the endpoint, or {@code UNKNOWN} if not known.
     *
     * @return the name of the data center for the endpoint, or {@code UNKNOWN} if not known
     */
    @Override
    public String getRack(final InetAddress endpoint)
    {
        final NodeData nodeData = nodeDataMap.get(endpoint);
        return nodeData != null ? nodeData.rack : defaultNodeData.rack;
    }

    /**
     * Returns the name of the data center for the endpoint, or {@code UNKNOWN} if not known.
     *
     * @return the name of the data center for the endpoint, or {@code UNKNOWN} if not known
     */
    @Override
    public String getDatacenter(final InetAddress endpoint)
    {
        final NodeData nodeData = nodeDataMap.get(endpoint);
        return nodeData != null ? nodeData.datacenter
                : defaultNodeData.datacenter;
    }

    /**
     * Root object type for the YAML topology configuration.
     */
    public static class TopologyConfig
    {
        public List<Datacenter> topology;
        public String default_dc_name = "UNKNOWN";
        public String default_rack_name = "UNKNOWN";
    }

    /**
     * Data center object type for the YAML topology configuration.
     */
    public static class Datacenter
    {
        public String dc_name;
        public List<Rack> racks = Collections.emptyList();
    }

    /**
     * Rack object type for the YAML topology configuration.
     */
    public static class Rack
    {
        public String rack_name;
        public List<Node> nodes = Collections.emptyList();
    }

    /**
     * Node object type for the YAML topology configuration.
     */
    public static class Node
    {
        public String broadcast_address;
        public String dc_local_address;
    }

    /**
     * Loads the topology configuration file.
     *
     * @throws ConfigurationException
     *             on failure
     */
    private synchronized void loadTopologyConfiguration()
            throws ConfigurationException
    {
        logger.debug("Loading topology configuration from {}",
                topologyConfigFilename);

        final TypeDescription topologyConfigTypeDescription = new TypeDescription(
                TopologyConfig.class);
        topologyConfigTypeDescription.putListPropertyType("topology",
                Datacenter.class);

        final TypeDescription topologyTypeDescription = new TypeDescription(
                Datacenter.class);
        topologyTypeDescription.putListPropertyType("racks", Rack.class);

        final TypeDescription rackTypeDescription = new TypeDescription(
                Rack.class);
        rackTypeDescription.putListPropertyType("nodes", Node.class);

        final Constructor configConstructor = new Constructor(
                TopologyConfig.class);
        configConstructor.addTypeDescription(topologyConfigTypeDescription);
        configConstructor.addTypeDescription(topologyTypeDescription);
        configConstructor.addTypeDescription(rackTypeDescription);

        final InputStream configFileInputStream = getClass().getClassLoader()
                .getResourceAsStream(topologyConfigFilename);
        if (configFileInputStream == null)
        {
            throw new ConfigurationException(
                    "Could not read topology config file "
                            + topologyConfigFilename);
        }
        Yaml yaml;
        TopologyConfig topologyConfig;
        try
        {
            yaml = new Yaml(configConstructor);
            topologyConfig = (TopologyConfig) yaml.load(configFileInputStream);
        }
        finally
        {
            FileUtils.closeQuietly(configFileInputStream);
        }
        final Map<InetAddress, NodeData> nodeDataMap = new HashMap<InetAddress, NodeData>();

        if (topologyConfig.topology == null)
        {
            throw new ConfigurationException(
                    "Topology configuration file is missing the topology section");
        }

        for (final Datacenter datacenter : topologyConfig.topology)
        {
            if (datacenter.dc_name == null)
            {
                throw new ConfigurationException(
                        "Topology configuration file is missing a data center name for some data center");
            }

            for (final Rack rack : datacenter.racks)
            {
                if (rack.rack_name == null)
                {
                    throw new ConfigurationException(
                            String.format(
                                    "Topology configuration file is missing a rack name for some rack under data center '%s'",
                                    datacenter.dc_name));
                }

                for (final Node node : rack.nodes)
                {
                    if (node.broadcast_address == null)
                    {
                        throw new ConfigurationException(
                                String.format(
                                        "Topology configuration file is missing a broadcast address for some node under data center '%s' rack '%s'",
                                        datacenter.dc_name, rack.rack_name));
                    }

                    final InetAddress endpoint = InetAddresses
                            .forString(node.broadcast_address);
                    final InetAddress dcLocalAddress = node.dc_local_address == null ? null
                            : InetAddresses.forString(node.dc_local_address);

                    final NodeData nodeData = new NodeData();
                    nodeData.datacenter = datacenter.dc_name;
                    nodeData.rack = rack.rack_name;
                    nodeData.dcLocalAddress = dcLocalAddress;

                    if (nodeDataMap.put(endpoint, nodeData) != null)
                    {
                        throw new ConfigurationException(
                                String.format(
                                        "IP address '%s' appears more than once in the topology configuration file",
                                        endpoint));
                    }

                    if (dcLocalAddress != null
                            && nodeDataMap.put(dcLocalAddress, nodeData) != null)
                    {
                        throw new ConfigurationException(
                                String.format(
                                        "IP address '%s' appears more than once in the topology configuration file",
                                        dcLocalAddress));
                    }
                }
            }
        }

        final NodeData localNodeData = nodeDataMap.get(FBUtilities
                .getBroadcastAddress());
        if (localNodeData == null)
        {
            throw new ConfigurationException(
                    "Topology configuration missing information for the local node");
        }

        final NodeData defaultNodeData = new NodeData();

        if (topologyConfig.default_dc_name == null)
        {
            throw new ConfigurationException(
                    "default_dc_name must be specified");
        }
        if (topologyConfig.default_rack_name == null)
        {
            throw new ConfigurationException(
                    "default_rack_name must be specified");
        }

        defaultNodeData.datacenter = topologyConfig.default_dc_name;
        defaultNodeData.rack = topologyConfig.default_rack_name;

        // YAML configuration looks good; now make the changes

        this.nodeDataMap = nodeDataMap;
        this.localNodeData = localNodeData;
        this.defaultNodeData = defaultNodeData;
        maybeSetApplicationState();

        if (logger.isDebugEnabled())
        {
            logger.debug(
                    "Built topology map from config file: localNodeData={}, nodeDataMap={}",
                    localNodeData, nodeDataMap);
        }

        if (gossiperInitialized)
        {
            StorageService.instance.gossipSnitchInfo();
        }
    }

    /**
     * be careful about just blindly updating ApplicationState.INTERNAL_IP everytime we read the yaml file,
     * as that can cause connections to get unnecessarily reset (via IESCS.onChange()).
     */
    private void maybeSetApplicationState()
    {
        if (localNodeData.dcLocalAddress == null)
            return;
        final EndpointState es = Gossiper.instance.getEndpointStateForEndpoint(FBUtilities.getBroadcastAddress());
        if (es == null)
            return;
        final VersionedValue vv = es.getApplicationState(ApplicationState.INTERNAL_IP);
        if ((vv != null && !vv.value.equals(localNodeData.dcLocalAddress.toString()))
            || vv == null)
        {
            Gossiper.instance.addLocalApplicationState(ApplicationState.INTERNAL_IP,
                StorageService.instance.valueFactory.internalIP(localNodeData.dcLocalAddress.toString()));
        }
    }

    /**
     * Topology data for a node.
     */
    private class NodeData
    {
        /** Data center name. */
        public String datacenter;
        /** Rack name. */
        public String rack;
        /** Data-center-local address. */
        public InetAddress dcLocalAddress;

        /**
         * Returns a simple key-value string representation of this node's data.
         *
         * @return a simple key-value string representation of this node's data
         */
        public String toString()
        {
            return Objects.toStringHelper(this).add("datacenter", datacenter)
                    .add("rack", rack).add("dcLocalAddress", dcLocalAddress)
                    .toString();
        }
    }

    /**
     * Called in preparation for the initiation of the gossip loop.
     */
    @Override
    public synchronized void gossiperStarting()
    {
        gossiperInitialized = true;
        StorageService.instance.gossipSnitchInfo();
        Gossiper.instance.register(new ReconnectableSnitchHelper(this, localNodeData.datacenter, true));
    }

}
