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

import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * Cluster settings.
 */
public class ClusterProperties {
    /** */
    private static final Logger log = Logger.getLogger(ClusterProperties.class.getSimpleName());

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

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

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

    /** */
    public static final double DEFAULT_CPU_PER_NODE = 2;

    /** */
    public static final double DEFAULT_MEM_PER_NODE = 2048;
    
    /**
     * The minimum memory overhead: overhead is by default 0.1* MEMORY_PER_NODE,
     * with a minimum of DEFAULT_MINIMUM_MEM_OVERHEAD_PER_NODE.
     */
    public static final double DEFAULT_MINIMUM_MEM_OVERHEAD_PER_NODE = 384;

    /** Cluster name. */
    private String clusterName = DEFAULT_CLUSTER_NAME;

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

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

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

    /** Memory limit. */
    private double memPerNode = DEFAULT_MEM_PER_NODE;
    
    /** */
    public static final String IGNITE_MEMORY_OVERHEAD_PER_NODE = "IGNITE_MEMORY_OVERHEAD_PER_NODE";

    /** Memory over head to request yarn. */
    private double memOverHeadPerNode = 0;

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

    /** */
    public static final double DEFAULT_IGNITE_NODE_COUNT = 3;

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

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

    /** Ignite version. */
    private String igniteUrl = null;

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

    /** */
    public static final String DEFAULT_IGNITE_WORK_DIR = "/ignite/workdir/";

    /** Ignite work directory. */
    private String igniteWorkDir = DEFAULT_IGNITE_WORK_DIR;

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

    /** Ignite path. */
    private String ignitePath = null;

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

    /** Licence path. */
    private String licencePath = null;

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

    /** JVM opts. */
    private String jvmOpts = null;

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

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

    /** Ignite local work directory. */
    private String igniteLocalWorkDir = DEFAULT_IGNITE_LOCAL_WORK_DIR;

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

    /** */
    public static final String DEFAULT_IGNITE_RELEASES_DIR = "/ignite/releases/";

    /** Ignite local work directory. */
    private String igniteReleasesDir = DEFAULT_IGNITE_RELEASES_DIR;

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

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

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

    /** Ignite config. */
    private String igniteCfg = 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 cpusPerNode() {
        return cpuPerNode;
    }

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

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

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

    /**
     * @return Memory overhead for requested memory.
     */
    public double memoryOverHeadPerNode() {
		return memOverHeadPerNode;
	}

    /**
     * Sets memory overhead requested to YARN.
     *
     * @param memOverHeadPerNode Memory over head per node.
     */
	public void memoryOverHeadPerNode(double memOverHeadPerNode) {
		this.memOverHeadPerNode = memOverHeadPerNode;
	}
	
	/**
	 * @return Provide the total memory requested to ResourceManagers (memoryPerNode + memoryOverheadPerNode).
	 */
	public double totalMemoryPerNode(){
		return memoryPerNode() + memoryOverHeadPerNode();
	}

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

    /**
     * Sets instance count limit.
     */
    public void instances(int nodeCnt) {
        this.nodeCnt = nodeCnt;
    }

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

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

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

    /**
     * @return Local working directory.
     */
    public String igniteLocalWorkDir() {
        return igniteLocalWorkDir;
    }

    /**
     * @return Ignite releases dir.
     */
    public String igniteReleasesDir() {
        return igniteReleasesDir;
    }

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

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

    /**
     * @return Licence path.
     */
    public String licencePath() {
        return licencePath;
    }

    /**
     * @return Ignite hdfs path.
     */
    public String ignitePath() {
        return ignitePath;
    }

    /**
     * @return Jvm opts.
     */
    public String jvmOpts() {
        return jvmOpts;
    }

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

    /**
     * Instantiate a ClusterProperties from a set of properties.
     *
     * @param props If {@code null} will be used system properties.
     * @return Cluster properties.
     */
    private static ClusterProperties fromProperties(Properties props) {
    	ClusterProperties prop = new ClusterProperties();

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

        prop.cpuPerNode = getDoubleProperty(IGNITE_RUN_CPU_PER_NODE, props, DEFAULT_CPU_PER_NODE);
        prop.memPerNode = getDoubleProperty(IGNITE_MEMORY_PER_NODE, props, DEFAULT_MEM_PER_NODE);
        // The minimum memory overhead: overhead is by default 0.1* MEMORY_PER_NODE,
        // with a minimum of DEFAULT_MINIMUM_MEM_OVERHEAD_PER_NODE
        prop.memOverHeadPerNode = getDoubleProperty(IGNITE_MEMORY_OVERHEAD_PER_NODE, props,
            Math.max( 0.1 * prop.memPerNode, DEFAULT_MINIMUM_MEM_OVERHEAD_PER_NODE));
        prop.nodeCnt = getDoubleProperty(IGNITE_NODE_COUNT, props, DEFAULT_IGNITE_NODE_COUNT);

        prop.igniteUrl = getStringProperty(IGNITE_URL, props, null);
        prop.ignitePath = getStringProperty(IGNITE_PATH, props, null);
        prop.licencePath = getStringProperty(LICENCE_PATH, props, null);
        prop.jvmOpts = getStringProperty(IGNITE_JVM_OPTS, props, null);
        prop.igniteWorkDir = getStringProperty(IGNITE_WORKING_DIR, props, DEFAULT_IGNITE_WORK_DIR);
        prop.igniteLocalWorkDir = getStringProperty(IGNITE_LOCAL_WORK_DIR, props, DEFAULT_IGNITE_LOCAL_WORK_DIR);
        prop.igniteReleasesDir = getStringProperty(IGNITE_RELEASES_DIR, props, DEFAULT_IGNITE_RELEASES_DIR);
        prop.igniteCfg = getStringProperty(IGNITE_CONFIG_XML, props, null);
        prop.userLibs = getStringProperty(IGNITE_USERS_LIBS, props, null);

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

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

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

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

                props.load(new FileInputStream(config));
            }

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

    /**
     * @return Cluster configuration.
     */
    public static ClusterProperties from() {
        return fromProperties(null);
    }

    /**
     * Convert to properties to map.
     *
     * @return Key-value map.
     */
    public Map<String, String> toEnvs() {
        Map<String, String> envs = new HashMap<>();

        envs.put(IGNITE_CLUSTER_NAME, toEnvVal(clusterName));

        envs.put(IGNITE_RUN_CPU_PER_NODE, toEnvVal(cpuPerNode));
        envs.put(IGNITE_MEMORY_PER_NODE, toEnvVal(memPerNode));
        envs.put(IGNITE_MEMORY_OVERHEAD_PER_NODE, toEnvVal(memOverHeadPerNode));
        envs.put(IGNITE_NODE_COUNT, toEnvVal(nodeCnt));

        envs.put(IGNITE_URL, toEnvVal(igniteUrl));
        envs.put(IGNITE_PATH, toEnvVal(ignitePath));
        envs.put(LICENCE_PATH, toEnvVal(licencePath));
        envs.put(IGNITE_JVM_OPTS, toEnvVal(jvmOpts));
        envs.put(IGNITE_WORKING_DIR, toEnvVal(igniteWorkDir));
        envs.put(IGNITE_LOCAL_WORK_DIR, toEnvVal(igniteLocalWorkDir));
        envs.put(IGNITE_RELEASES_DIR, toEnvVal(igniteReleasesDir));
        envs.put(IGNITE_CONFIG_XML, toEnvVal(igniteCfg));
        envs.put(IGNITE_USERS_LIBS, toEnvVal(userLibs));

        if (hostnameConstraint != null)
            envs.put(IGNITE_HOSTNAME_CONSTRAINT, toEnvVal(hostnameConstraint.pattern()));

        return envs;
    }

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

        String property = System.getProperty(name);

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

        return property == null || property.isEmpty() ? defaultVal : Double.valueOf(property);
    }

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

        String property = System.getProperty(name);

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

        return property == null || property.isEmpty() ? defaultVal : property;
    }

    /**
     * @param val Value.
     * @return If val is null {@code EMPTY_STRING} else to string.
     */
    private String toEnvVal(Object val) {
        return val == null ? EMPTY_STRING : val.toString();
    }
}