/*
 * 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.ignite.mesos;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * The class defines cluster configuration. This configuration created from properties file
 * that passed on startup or environment variables.
 * <p>
 * If Mesos cluster working in Intranet or behind NAT then an access to local resources can be set
 * by {@link #ignitePackagePath()} or {@link #ignitePackageUrl()} which should be available from nodes.
 */
public class ClusterProperties {
    /** */
    private static final Logger log = Logger.getLogger(ClusterProperties.class.getSimpleName());

    /** Unlimited. */
    public static final double UNLIMITED = Double.MAX_VALUE;

    /** */
    public static final String MESOS_MASTER_URL = "MESOS_MASTER_URL";

    /** */
    public static final String DEFAULT_MESOS_MASTER_URL = "zk://localhost:2181/mesos";

    /** Mesos master url. */
    private String mesosUrl = DEFAULT_MESOS_MASTER_URL;

    /** */
    public static final String IGNITE_JVM_OPTS = "IGNITE_JVM_OPTS";

    /** JVM options. */
    private String jvmOpts = "";

    /** */
    public static final String IGNITE_CLUSTER_NAME = "IGNITE_CLUSTER_NAME";

    /** */
    public static final String DEFAULT_CLUSTER_NAME = "ignite-cluster";

    /** Mesos master url. */
    private String clusterName = DEFAULT_CLUSTER_NAME;

    /** */
    public static final String IGNITE_HTTP_SERVER_HOST = "IGNITE_HTTP_SERVER_HOST";

    /** Http server host. */
    private String httpSrvHost = null;

    /** */
    public static final String IGNITE_HTTP_SERVER_PORT = "IGNITE_HTTP_SERVER_PORT";

    /** */
    public static final String DEFAULT_HTTP_SERVER_PORT = "48610";

    /** Http server host. */
    private int httpSrvPort = Integer.valueOf(DEFAULT_HTTP_SERVER_PORT);

    /** */
    public static final String IGNITE_TOTAL_CPU = "IGNITE_TOTAL_CPU";

    /** CPU limit. */
    private double cpu = UNLIMITED;

    /** */
    public static final String IGNITE_RUN_CPU_PER_NODE = "IGNITE_RUN_CPU_PER_NODE";

    /** CPU limit. */
    private double cpuPerNode = UNLIMITED;

    /** */
    public static final String IGNITE_TOTAL_MEMORY = "IGNITE_TOTAL_MEMORY";

    /** Memory limit. */
    private double mem = UNLIMITED;

    /** */
    public static final String IGNITE_MEMORY_PER_NODE = "IGNITE_MEMORY_PER_NODE";

    /** Memory limit. */
    private double memPerNode = UNLIMITED;

    /** */
    public static final String IGNITE_TOTAL_DISK_SPACE = "IGNITE_TOTAL_DISK_SPACE";

    /** Disk space limit. */
    private double disk = UNLIMITED;

    /** */
    public static final String IGNITE_DISK_SPACE_PER_NODE = "IGNITE_DISK_SPACE_PER_NODE";

    /** Disk space limit. */
    private double diskPerNode = UNLIMITED;

    /** */
    public static final String IGNITE_NODE_COUNT = "IGNITE_NODE_COUNT";

    /** Node count limit. */
    private double nodeCnt = UNLIMITED;

    /** */
    public static final String IGNITE_MIN_CPU_PER_NODE = "IGNITE_MIN_CPU_PER_NODE";

    /** */
    public static final double DEFAULT_RESOURCE_MIN_CPU = 1;

    /** Min memory per node. */
    private double minCpu = DEFAULT_RESOURCE_MIN_CPU;

    /** */
    public static final String IGNITE_MIN_MEMORY_PER_NODE = "IGNITE_MIN_MEMORY_PER_NODE";

    /** */
    public static final double DEFAULT_RESOURCE_MIN_MEM = 256;

    /** Min memory per node. */
    private double minMemory = DEFAULT_RESOURCE_MIN_MEM;

    /** */
    public static final String IGNITE_VERSION = "IGNITE_VERSION";

    /** */
    public static final String DEFAULT_IGNITE_VERSION = "latest";

    /** Ignite version. */
    private String igniteVer = DEFAULT_IGNITE_VERSION;

    /** */
    public static final String IGNITE_PACKAGE_URL = "IGNITE_PACKAGE_URL";

    /** Ignite package url. */
    private String ignitePkgUrl;

    /** */
    public static final String IGNITE_PACKAGE_PATH = "IGNITE_PACKAGE_PATH";

    /** Ignite package path. */
    private String ignitePkgPath;

    /** */
    public static final String IGNITE_WORK_DIR = "IGNITE_WORK_DIR";

    /** */
    public static final String DEFAULT_IGNITE_WORK_DIR = "ignite-releases/";

    /** Ignite version. */
    private String igniteWorkDir = DEFAULT_IGNITE_WORK_DIR;

    /** */
    public static final String IGNITE_USERS_LIBS = "IGNITE_USERS_LIBS";

    /** Path to users libs. */
    private String userLibs = null;

    /** */
    public static final String IGNITE_USERS_LIBS_URL = "IGNITE_USERS_LIBS_URL";

    /** URL to users libs. */
    private String userLibsUrl = null;

    /** */
    public static final String LICENCE_URL = "LICENCE_URL";

    /** Licence url. */
    private String licenceUrl = null;

    /** */
    public static final String IGNITE_CONFIG_XML = "IGNITE_XML_CONFIG";

    /** Ignite config. */
    private String igniteCfg = null;

    /** */
    public static final String IGNITE_CONFIG_XML_URL = "IGNITE_CONFIG_XML_URL";

    /** */
    public static final String IGNITE_HTTP_SERVER_IDLE_TIMEOUT = "IGNITE_HTTP_SERVER_IDLE_TIMEOUT";

    /** */
    public static final long IGNITE_HTTP_SERVER_IDLE_TIMEOUT_DEFAULT = 30000L;

    /** Jetty idle timeout. */
    private long idleTimeout = IGNITE_HTTP_SERVER_IDLE_TIMEOUT_DEFAULT;

    /** Url to ignite config. */
    private String igniteCfgUrl = null;

    /** */
    public static final String IGNITE_HOSTNAME_CONSTRAINT = "IGNITE_HOSTNAME_CONSTRAINT";

    /** Url to ignite config. */
    private Pattern hostnameConstraint = null;

    /** */
    public ClusterProperties() {
        // No-op.
    }

    /**
     * @return Cluster name.
     */
    public String clusterName() {
        return clusterName;
    }

    /**
     * @return CPU count limit.
     */
    public double cpus() {
        return cpu;
    }

    /**
     * Sets CPU count limit.
     */
    public void cpus(double cpu) {
        this.cpu = cpu;
    }

    /**
     * @return CPU count limit.
     */
    public double cpusPerNode() {
        return cpuPerNode;
    }

    /**
     * Sets CPU count limit.
     */
    public void cpusPerNode(double cpu) {
        this.cpuPerNode = cpu;
    }

    /**
     * @return mem limit.
     */
    public double memory() {
        return mem;
    }

    /**
     * Sets mem limit.
     *
     * @param mem Memory.
     */
    public void memory(double mem) {
        this.mem = mem;
    }

    /**
     * @return mem limit.
     */
    public double memoryPerNode() {
        return memPerNode;
    }

    /**
     * Sets mem limit.
     *
     * @param mem Memory.
     */
    public void memoryPerNode(double mem) {
         this.memPerNode = mem;
    }

    /**
     * @return JVM opts for ignite.
     */
    public String jmvOpts() {
        return this.jvmOpts;
    }

    /**
     * @return disk limit.
     */
    public double disk() {
        return disk;
    }

    /**
     * @return disk limit per node.
     */
    public double diskPerNode() {
        return diskPerNode;
    }

    /**
     * @return instance count limit.
     */
    public double instances() {
        return nodeCnt;
    }

    /**
     * @return min memory per node.
     */
    public double minMemoryPerNode() {
        return minMemory;
    }

    /**
     * Sets min memory.
     *
     * @param minMemory Min memory.
     */
    public void minMemoryPerNode(double minMemory) {
        this.minMemory = minMemory;
    }

    /**
     * Sets hostname constraint.
     *
     * @param ptrn Hostname pattern.
     */
    public void hostnameConstraint(Pattern ptrn) {
        this.hostnameConstraint = ptrn;
    }

    /**
     * @return min cpu count per node.
     */
    public double minCpuPerNode() {
        return minCpu;
    }

    /**
     * Sets min cpu count per node.
     *
     * @param minCpu min cpu count per node.
     */
    public void minCpuPerNode(double minCpu) {
        this.minCpu = minCpu;
    }

    /**
     * @return Ignite version.
     */
    public String igniteVer() {
        return igniteVer;
    }

    /**
     * @return Working directory.
     */
    public String igniteWorkDir() {
        return igniteWorkDir;
    }

    /**
     * @return User's libs.
     */
    public String userLibs() {
        return userLibs;
    }

    /**
     * @return Ignite configuration.
     */
    public String igniteCfg() {
        return igniteCfg;
    }

    /**
     * @return Master url.
     */
    public String masterUrl() {
        return mesosUrl;
    }

    /**
     * @return Http server host.
     */
    public String httpServerHost() {
        return httpSrvHost;
    }

    /**
     * @return Http server port.
     */
    public int httpServerPort() {
        return httpSrvPort;
    }

    /**
     * Sets the maximum Idle time for a http connection, which will be used for
     * jetty server. The server provides resources for ignite mesos framework such as
     * ignite archive, user's libs, configurations and etc.
     *
     * @return Http server idle timeout.
     */
    public long idleTimeout() {
        return idleTimeout;
    }

    /**
     * URL to ignite package. The URL should to point at valid apache ignite archive.
     * This property can be useful if using own apache ignite build.
     *
     * @return Url to ignite package.
     */
    public String ignitePackageUrl() {
        return ignitePkgUrl;
    }

    /**
     * Path on local file system to ignite archive. That can be useful when
     * Mesos working in Intranet or behind NAT.
     *
     * @return Path on local host to ignite package.
     */
    public String ignitePackagePath() {
        return ignitePkgPath;
    }

    /**
     * @return Url to ignite configuration.
     */
    public String igniteConfigUrl() {
        return igniteCfgUrl;
    }

    /**
     * @return Url to users libs configuration.
     */
    public String usersLibsUrl() {
        return userLibsUrl;
    }

    /**
     * @return Url to licence.
     */
    public String licenceUrl() {
        return licenceUrl;
    }

    /**
     * @return Host name constraint.
     */
    public Pattern hostnameConstraint() {
        return hostnameConstraint;
    }

    /**
     * @param cfg path to config file.
     * @return Cluster configuration.
     */
    public static ClusterProperties from(String cfg) {
        try {
            Properties props = null;

            if (cfg != null) {
                props = new Properties();

                try (FileInputStream in = new FileInputStream(cfg)) {
                    props.load(in);
                }
            }

            ClusterProperties prop = new ClusterProperties();

            prop.mesosUrl = getStringProperty(MESOS_MASTER_URL, props, DEFAULT_MESOS_MASTER_URL);

            prop.httpSrvHost = getStringProperty(IGNITE_HTTP_SERVER_HOST, props, getNonLoopbackAddress());

            String port = System.getProperty("PORT0");

            if (port != null && !port.isEmpty())
                prop.httpSrvPort = Integer.valueOf(port);
            else
                prop.httpSrvPort = Integer.valueOf(getStringProperty(IGNITE_HTTP_SERVER_PORT, props,
                    DEFAULT_HTTP_SERVER_PORT));

            prop.clusterName = getStringProperty(IGNITE_CLUSTER_NAME, props, DEFAULT_CLUSTER_NAME);

            prop.userLibsUrl = getStringProperty(IGNITE_USERS_LIBS_URL, props, null);
            prop.ignitePkgUrl = getStringProperty(IGNITE_PACKAGE_URL, props, null);
            prop.ignitePkgPath = getStringProperty(IGNITE_PACKAGE_PATH, props, null);
            prop.licenceUrl = getStringProperty(LICENCE_URL, props, null);
            prop.igniteCfgUrl = getStringProperty(IGNITE_CONFIG_XML_URL, props, null);

            prop.cpu = getDoubleProperty(IGNITE_TOTAL_CPU, props, UNLIMITED);
            prop.cpuPerNode = getDoubleProperty(IGNITE_RUN_CPU_PER_NODE, props, UNLIMITED);
            prop.mem = getDoubleProperty(IGNITE_TOTAL_MEMORY, props, UNLIMITED);
            prop.memPerNode = getDoubleProperty(IGNITE_MEMORY_PER_NODE, props, UNLIMITED);
            prop.disk = getDoubleProperty(IGNITE_TOTAL_DISK_SPACE, props, UNLIMITED);
            prop.diskPerNode = getDoubleProperty(IGNITE_DISK_SPACE_PER_NODE, props, 1024.0);
            prop.nodeCnt = getDoubleProperty(IGNITE_NODE_COUNT, props, UNLIMITED);
            prop.minCpu = getDoubleProperty(IGNITE_MIN_CPU_PER_NODE, props, DEFAULT_RESOURCE_MIN_CPU);
            prop.minMemory = getDoubleProperty(IGNITE_MIN_MEMORY_PER_NODE, props, DEFAULT_RESOURCE_MIN_MEM);

            prop.jvmOpts = getStringProperty(IGNITE_JVM_OPTS, props, "");

            prop.igniteVer = getStringProperty(IGNITE_VERSION, props, DEFAULT_IGNITE_VERSION);
            prop.igniteWorkDir = getStringProperty(IGNITE_WORK_DIR, props, DEFAULT_IGNITE_WORK_DIR);
            prop.igniteCfg = getStringProperty(IGNITE_CONFIG_XML, props, null);
            prop.userLibs = getStringProperty(IGNITE_USERS_LIBS, props, null);

            String ptrn = getStringProperty(IGNITE_HOSTNAME_CONSTRAINT, props, null);

            prop.idleTimeout = getLongProperty(IGNITE_HTTP_SERVER_IDLE_TIMEOUT, props, IGNITE_HTTP_SERVER_IDLE_TIMEOUT_DEFAULT);

            if (ptrn != null) {
                try {
                    prop.hostnameConstraint = Pattern.compile(ptrn);
                }
                catch (PatternSyntaxException e) {
                    log.log(Level.WARNING, "IGNITE_HOSTNAME_CONSTRAINT has invalid pattern. It will be ignore.", e);
                }
            }

            return prop;
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @param name Property name.
     * @param fileProps Property file.
     * @return Property value.
     */
    private static double getDoubleProperty(String name, Properties fileProps, Double dfltVal) {
        if (fileProps != null && fileProps.containsKey(name))
            return Double.valueOf(fileProps.getProperty(name));

        String prop = System.getProperty(name);

        if (prop == null)
            prop = System.getenv(name);

        return prop == null ? dfltVal : Double.valueOf(prop);
    }

    /**
     * @param name Property name.
     * @param fileProps Property file.
     * @return Property value.
     */
    private static long getLongProperty(String name, Properties fileProps, Long dfltVal) {
        if (fileProps != null && fileProps.containsKey(name))
            return Long.valueOf(fileProps.getProperty(name));

        String prop = System.getProperty(name);

        if (prop == null)
            prop = System.getenv(name);

        return prop == null ? dfltVal : Long.valueOf(prop);
    }

    /**
     * @param name Property name.
     * @param fileProps Property file.
     * @return Property value.
     */
    private static String getStringProperty(String name, Properties fileProps, String dfltVal) {
        if (fileProps != null && fileProps.containsKey(name))
            return fileProps.getProperty(name);

        String prop = System.getProperty(name);

        if (prop == null)
            prop = System.getenv(name);

        return prop == null ? dfltVal : prop;
    }

    /**
     * Finds a local, non-loopback, IPv4 address
     *
     * @return The first non-loopback IPv4 address found, or <code>null</code> if no such addresses found
     * @throws java.net.SocketException If there was a problem querying the network interfaces
     */
    public static String getNonLoopbackAddress() throws SocketException {
        Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces();

        while (ifaces.hasMoreElements()) {
            NetworkInterface iface = ifaces.nextElement();

            Enumeration<InetAddress> addrs = iface.getInetAddresses();

            while (addrs.hasMoreElements()) {
                InetAddress addr = addrs.nextElement();

                if (addr instanceof Inet4Address && !addr.isLoopbackAddress())
                    return addr.getHostAddress();
            }
        }

        throw new RuntimeException("Failed. Could not find non-loopback address");
    }
}
