/*
 * Copyright (c) Contributors, http://opensimulator.org/
 * See CONTRIBUTORS.TXT for a full list of copyright holders.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the OpenSimulator Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Text;

namespace OpenSim.Framework.Monitoring
{
    /// <summary>
    /// Singleton used to provide access to statistics reporters
    /// </summary>
    public class StatsManager
    {
        // Subcommand used to list other stats.
        public const string AllSubCommand = "all";

        // Subcommand used to list other stats.
        public const string ListSubCommand = "list";

        // All subcommands
        public static HashSet<string> SubCommands = new HashSet<string> { AllSubCommand, ListSubCommand };

        /// <summary>
        /// Registered stats categorized by category/container/shortname
        /// </summary>
        /// <remarks>
        /// Do not add or remove directly from this dictionary.
        /// </remarks>
        public static SortedDictionary<string, SortedDictionary<string, SortedDictionary<string, Stat>>> RegisteredStats
            = new SortedDictionary<string, SortedDictionary<string, SortedDictionary<string, Stat>>>();

        private static AssetStatsCollector assetStats;
        private static UserStatsCollector userStats;
        private static SimExtraStatsCollector simExtraStats = new SimExtraStatsCollector();

        public static AssetStatsCollector AssetStats { get { return assetStats; } }
        public static UserStatsCollector UserStats { get { return userStats; } }
        public static SimExtraStatsCollector SimExtraStats { get { return simExtraStats; } }

        public static void RegisterConsoleCommands(ICommandConsole console)
        {
            console.Commands.AddCommand(
                "General",
                false,
                "show stats",
                "show stats [list|all|<category>]",
                "Show statistical information for this server",
                "If no final argument is specified then legacy statistics information is currently shown.\n"
                    + "If list is specified then statistic categories are shown.\n"
                    + "If all is specified then all registered statistics are shown.\n"
                    + "If a category name is specified then only statistics from that category are shown.\n"
                    + "THIS STATS FACILITY IS EXPERIMENTAL AND DOES NOT YET CONTAIN ALL STATS",
                HandleShowStatsCommand);
        }

        public static void HandleShowStatsCommand(string module, string[] cmd)
        {
            ICommandConsole con = MainConsole.Instance;

            if (cmd.Length > 2)
            {
                var categoryName = cmd[2];
                var containerName = cmd.Length > 3 ? cmd[3] : String.Empty;

                if (categoryName == AllSubCommand)
                {
                    foreach (var category in RegisteredStats.Values)
                    {
                        OutputCategoryStatsToConsole(con, category);
                    }
                }
                else if (categoryName == ListSubCommand)
                {
                    con.Output("Statistic categories available are:");
                    foreach (string category in RegisteredStats.Keys)
                        con.OutputFormat("  {0}", category);
                }
                else
                {
                    SortedDictionary<string, SortedDictionary<string, Stat>> category;
                    if (!RegisteredStats.TryGetValue(categoryName, out category))
                    {
                        con.OutputFormat("No such category as {0}", categoryName);
                    }
                    else
                    {
                        if (String.IsNullOrEmpty(containerName))
                            OutputCategoryStatsToConsole(con, category);
                        else
                        {
                            SortedDictionary<string, Stat> container;
                            if (category.TryGetValue(containerName, out container))
                            {
                                OutputContainerStatsToConsole(con, container);
                            }
                            else
                            {
                                con.OutputFormat("No such container {0} in category {1}", containerName, categoryName);
                            }
                        }
                    }
                }
            }
            else
            {
                // Legacy
                con.Output(SimExtraStats.Report());
            }
        }

        private static void OutputCategoryStatsToConsole(
            ICommandConsole con, SortedDictionary<string, SortedDictionary<string, Stat>> category)
        {
            foreach (var container in category.Values)
            {
                OutputContainerStatsToConsole(con, container);
            }
        }

        private static void OutputContainerStatsToConsole( ICommandConsole con, SortedDictionary<string, Stat> container)
        {
            foreach (Stat stat in container.Values)
            {
                con.Output(stat.ToConsoleString());
            }
        }

        /// <summary>
        /// Start collecting statistics related to assets.
        /// Should only be called once.
        /// </summary>
        public static AssetStatsCollector StartCollectingAssetStats()
        {
            assetStats = new AssetStatsCollector();

            return assetStats;
        }

        /// <summary>
        /// Start collecting statistics related to users.
        /// Should only be called once.
        /// </summary>
        public static UserStatsCollector StartCollectingUserStats()
        {
            userStats = new UserStatsCollector();

            return userStats;
        }

        /// <summary>
        /// Registers a statistic.
        /// </summary>
        /// <param name='stat'></param>
        /// <returns></returns>
        public static bool RegisterStat(Stat stat)
        {
            SortedDictionary<string, SortedDictionary<string, Stat>> category = null, newCategory;
            SortedDictionary<string, Stat> container = null, newContainer;

            lock (RegisteredStats)
            {
                // Stat name is not unique across category/container/shortname key.
                // XXX: For now just return false.  This is to avoid problems in regression tests where all tests
                // in a class are run in the same instance of the VM.
                if (TryGetStat(stat, out category, out container))
                    return false;

                // We take a copy-on-write approach here of replacing dictionaries when keys are added or removed.
                // This means that we don't need to lock or copy them on iteration, which will be a much more
                // common operation after startup.
                if (container != null)
                    newContainer = new SortedDictionary<string, Stat>(container);
                else
                    newContainer = new SortedDictionary<string, Stat>();

                if (category != null)
                    newCategory = new SortedDictionary<string, SortedDictionary<string, Stat>>(category);
                else
                    newCategory = new SortedDictionary<string, SortedDictionary<string, Stat>>();

                newContainer[stat.ShortName] = stat;
                newCategory[stat.Container] = newContainer;
                RegisteredStats[stat.Category] = newCategory;
            }

            return true;
        }

        /// <summary>
        /// Deregister a statistic
        /// </summary>>
        /// <param name='stat'></param>
        /// <returns></returns>
        public static bool DeregisterStat(Stat stat)
        {
            SortedDictionary<string, SortedDictionary<string, Stat>> category = null, newCategory;
            SortedDictionary<string, Stat> container = null, newContainer;

            lock (RegisteredStats)
            {
                if (!TryGetStat(stat, out category, out container))
                    return false;

                newContainer = new SortedDictionary<string, Stat>(container);
                newContainer.Remove(stat.ShortName);

                newCategory = new SortedDictionary<string, SortedDictionary<string, Stat>>(category);
                newCategory.Remove(stat.Container);

                newCategory[stat.Container] = newContainer;
                RegisteredStats[stat.Category] = newCategory;

                return true;
            }
        }

        public static bool TryGetStats(string category, out SortedDictionary<string, SortedDictionary<string, Stat>> stats)
        {
            return RegisteredStats.TryGetValue(category, out stats);
        }

        public static bool TryGetStat(
            Stat stat,
            out SortedDictionary<string, SortedDictionary<string, Stat>> category,
            out SortedDictionary<string, Stat> container)
        {
            category = null;
            container = null;

            lock (RegisteredStats)
            {
                if (RegisteredStats.TryGetValue(stat.Category, out category))
                {
                    if (category.TryGetValue(stat.Container, out container))
                    {
                        if (container.ContainsKey(stat.ShortName))
                            return true;
                    }
                }
            }

            return false;
        }

        public static void RecordStats()
        {
            lock (RegisteredStats)
            {
                foreach (SortedDictionary<string, SortedDictionary<string, Stat>> category in RegisteredStats.Values)
                {
                    foreach (SortedDictionary<string, Stat> container in category.Values)
                    {
                        foreach (Stat stat in container.Values)
                        {
                            if (stat.MeasuresOfInterest != MeasuresOfInterest.None)
                                stat.RecordValue();
                        }
                    }
                }
            }
        }
    }

    /// <summary>
    /// Stat type.
    /// </summary>
    /// <remarks>
    /// A push stat is one which is continually updated and so it's value can simply by read.
    /// A pull stat is one where reading the value triggers a collection method - the stat is not continually updated.
    /// </remarks>
    public enum StatType
    {
        Push,
        Pull
    }

    /// <summary>
    /// Measures of interest for this stat.
    /// </summary>
    [Flags]
    public enum MeasuresOfInterest
    {
        None,
        AverageChangeOverTime
    }

    /// <summary>
    /// Verbosity of stat.
    /// </summary>
    /// <remarks>
    /// Info will always be displayed.
    /// </remarks>
    public enum StatVerbosity
    {
        Debug,
        Info
    }
}