/*
 
    Copyright IBM Corp. 2010, 2016
    This file is part of Anomaly Detection Engine for Linux Logs (ADE).

    ADE is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    ADE is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with ADE.  If not, see <http://www.gnu.org/licenses/>.
 
*/
package org.openmainframe.ade.scoringApi;

import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;

import org.openmainframe.ade.exceptions.AdeInternalException;
import org.openmainframe.ade.exceptions.AdeUsageException;

/** An object of storing a collection of String and double statistics */
public class StatisticsChart {

    /** Map of double statistics: name->value */
    private SortedMap<String, Double> m_doubleStats = null;

    /** Map of String statistics: name->value */
    private SortedMap<String, String> m_stringStats = null;

    /** Set of all statistics names */
    private Set<String> m_allKeys = null;

    /** Add a double statistic.
     *  Duplicates are not allowed (not even between String and double)
     * @param name Name of statistic
     * @param val Value of statistic
     * @throws AdeInternalException
     */
    public void setStat(String name, double val) throws AdeInternalException {
        if (m_doubleStats == null) {
            m_doubleStats = new TreeMap<String, Double>();
        }
        verifyLegal(name);
        m_doubleStats.put(name, val);
    }

    /** Add a String statistic.
     *  Duplicates are not allowed (not even between String and double)
     * @param name Name of statistic
     * @param val Value of statistic
     * @throws AdeInternalException
     */
    public void setStat(String name, String val) throws AdeInternalException {
        if (val == null) {
            throw new AdeInternalException("Null values not allowed");
        }
        if (m_stringStats == null) {
            m_stringStats = new TreeMap<String, String>();
        }
        verifyLegal(name);
        m_stringStats.put(name, val);
    }

    /**
     * returns a collection of all entries for double statistics in this {@link StatisticsChart}
     * @return a collection of all entries for double statistics in this {@link StatisticsChart} or null if no double 
     * stats are available for this {@link StatisticsChart}
     */
    public Set<Entry<String, Double>> getDoubleStats() {
        if (m_doubleStats != null) {
            return m_doubleStats.entrySet();
        } else {
            return Collections.emptySet();
        }
    }

    /**
     * returns a collection of all entries for string statistics in this {@link StatisticsChart}
     * @return a collection of all entries for string statistics in this {@link StatisticsChart} or null if no string 
     * stats are available for this {@link StatisticsChart}
     */
    public Set<Entry<String, String>> getStringStats() {
        if (m_stringStats != null) {
            return m_stringStats.entrySet();
        } else {
            return Collections.emptySet();
        }
    }

    /** @return the given double statistic or throws an exception if it does not exist */
    public double getDoubleStatOrThrow(String name) throws AdeInternalException {
        final Double res = getDoubleStat(name);
        if (res == null) {
            throw new AdeInternalException("Missing double statistic " + name + "\nAvailable stats: " + this);
        }
        return res;
    }

    /** @return true if a double statistic with the given name exists */
    public boolean hasDoubleStat(String name) {
        return getDoubleStat(name) != null;
    }

    /** @return the given double statistic or null if it does not exist */
    public Double getDoubleStat(String name) {
        if (m_doubleStats == null) {
            return null;
        }
        return m_doubleStats.get(name);
    }

    /** @return the given String statistic or throws an exception if it does not exist */
    public String getStringStatOrThrow(String name) throws AdeInternalException {
        final String res = getStringStat(name);
        if (res == null) {
            throw new AdeInternalException("Missing string statistic " + name);
        }
        return res;
    }

    /** @return true if a String statistic with the given name exists */
    public boolean hasStringStat(String name) {
        return getStringStat(name) != null;
    }

    /** @return the given double statistic or null if it does not exist */
    public String getStringStat(String name) {
        if (m_stringStats == null) {
            return null;
        }
        return m_stringStats.get(name);
    }

    /** @return a map with all statistics sorted by name.
     * Double values are converted to strings using String.valueOf()
     */
    public SortedMap<String, String> getAllStatisticsSorted() {
        final SortedMap<String, String> res = new TreeMap<String, String>();

        if (m_stringStats != null) {
            res.putAll(m_stringStats);
        }
        if (m_doubleStats != null) {
            for (Map.Entry<String, Double> val : m_doubleStats.entrySet()) {
                res.put(val.getKey(), String.valueOf(val.getValue()));
            }
        }
        return res;
    }

    /** @return a map with all statistics sorted by name.
     * The Object values can be either Double or String
     */
    public SortedMap<String, Object> getAllStatisticsAsObjectsSorted() {
        final SortedMap<String, Object> res = new TreeMap<String, Object>();

        if (m_stringStats != null) {
            res.putAll(m_stringStats);
        }
        if (m_doubleStats != null) {
            res.putAll(m_doubleStats);
        }
        return res;
    }

    /** Verify if the given statistic name does not already exists */
    private void verifyLegal(String name) throws AdeInternalException {
        if (m_allKeys == null) {
            m_allKeys = new TreeSet<String>();
        }
        if (m_allKeys.contains(name)) {
            throw new AdeInternalException("Duplicate statistic " + name);
        }
        m_allKeys.add(name);
    }

    /** Adds all the statistics in another StatisticsChart object to this object,
     * with their names prefixed by the given name 
     */
    public void add(String name, StatisticsChart other) throws AdeInternalException {
        if (other.m_doubleStats != null) {
            for (Map.Entry<String, Double> val : other.m_doubleStats.entrySet()) {
                setStat(name + "." + val.getKey(), val.getValue());
            }
        }
        if (other.m_stringStats != null) {
            for (Map.Entry<String, String> val : other.m_stringStats.entrySet()) {
                setStat(name + "." + val.getKey(), val.getValue());
            }
        }
    }

    /** Adds all the statistics in another StatisticsChart object to this object,
     * The names of the other statistics or copies as is. 
     */
    public void add(StatisticsChart other) throws AdeInternalException {
        if (other.m_doubleStats != null) {
            for (Map.Entry<String, Double> val : other.m_doubleStats.entrySet()) {
                setStat(val.getKey(), val.getValue());
            }
        }
        if (other.m_stringStats != null) {
            for (Map.Entry<String, String> val : other.m_stringStats.entrySet()) {
                setStat(val.getKey(), val.getValue());
            }
        }
    }

    /** Remove the given statistic from the object.
     * @param key
     * @throws AdeInternalException if statistic does not exist
     */
    public void removeStat(String key) throws AdeInternalException {
        if (!m_allKeys.remove(key)) {
            throw new AdeInternalException("No statistic named " + key);
        }
        if (m_doubleStats.remove(key) == null && m_stringStats.remove(key) == null) {
            throw new AdeInternalException("Internal consistency problem: no key found");
        }

    }

    public String toString() {
        StringBuilder bldres = new StringBuilder("");
        if (m_doubleStats != null) {
            for (Entry<String, Double> dStat : m_doubleStats.entrySet()) {
                bldres.append(String.format("%-15s: %10.6f\n", dStat.getKey(), dStat.getValue()));
            }
        }
        if (m_stringStats != null) {
            for (Entry<String, String> sStat : m_stringStats.entrySet()) {
                bldres.append(String.format("%-15s: %s\n", sStat.getKey(), sStat.getValue()));
            }
        }
        return bldres.toString();
    }

    /** Replaces the statistics names according to the given map.
     *  A statistic that does not exist in the map is removed.
     * @throws AdeUsageException if map contains statistics that do not exists in this object.
     */
    public void applyResultMapping(SortedMap<String, String> mapping) throws AdeUsageException {
        checkValidity(mapping);
        m_doubleStats = applyResultMapping(m_doubleStats, mapping);
        m_stringStats = applyResultMapping(m_stringStats, mapping);
    }

    /** Verify all the names that are keys in the given map are contained as statistics in this object    *
     *  
     * @throws AdeUsageException with the details of the missing statistics if there are any
     */
    private void checkValidity(SortedMap<String, String> mapping) throws AdeUsageException {
        StringBuilder bldinvalidMaps = null;
        for (String key : mapping.keySet()) {
            if (!m_allKeys.contains(key)) {
                if (bldinvalidMaps == null) {
                    bldinvalidMaps = new StringBuilder(key);
                } else {
                    bldinvalidMaps.append(", " + key);
                }
            }
        }
        if (bldinvalidMaps != null && bldinvalidMaps.toString() != null) {
            throw new AdeUsageException("The following result mapping were not found: " + bldinvalidMaps.toString() + ". Valid staistics are " + m_allKeys);
        }
    }

    /** Apply mapping on a given map.
     * Map keys are renamed, and those missing are removed
     * 
     * @param src Map to be processed.
     * @param mapping Mapping according to which to process src
     * @return
     */
    static <T> TreeMap<String, T> applyResultMapping(SortedMap<String, T> src, SortedMap<String, String> mapping) {
        if (src == null) {
            return null;
        }
        final TreeMap<String, T> result = new TreeMap<String, T>();
        for (SortedMap.Entry<String, T> entry : src.entrySet()) {
            final String newName = mapping.get(entry.getKey());
            if (newName != null) {
                result.put(newName, entry.getValue());
            }
        }
        return result;
    }
}