/*
 * 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.IO;
using System.Reflection;
using System.Threading;
using System.Xml;
using log4net;
using Nini.Config;
using OpenSim.Framework;

namespace OpenSim
{
    /// <summary>
    /// Loads the Configuration files into nIni
    /// </summary>
    public class ConfigurationLoader
    {
        private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
        
        /// <summary>
        /// Various Config settings the region needs to start
        /// Physics Engine, Mesh Engine, GridMode, PhysicsPrim allowed, Neighbor, 
        /// StorageDLL, Storage Connection String, Estate connection String, Client Stack
        /// Standalone settings.
        /// </summary>
        protected ConfigSettings m_configSettings;

        /// <summary>
        /// A source of Configuration data
        /// </summary>
        protected OpenSimConfigSource m_config;

        /// <summary>
        /// Grid Service Information.  This refers to classes and addresses of the grid service
        /// </summary>
        protected NetworkServersInfo m_networkServersInfo;

        /// <summary>
        /// Loads the region configuration
        /// </summary>
        /// <param name="argvSource">Parameters passed into the process when started</param>
        /// <param name="configSettings"></param>
        /// <param name="networkInfo"></param>
        /// <returns>A configuration that gets passed to modules</returns>
        public OpenSimConfigSource LoadConfigSettings(
                IConfigSource argvSource, EnvConfigSource envConfigSource, out ConfigSettings configSettings,
                out NetworkServersInfo networkInfo)
        {
            m_configSettings = configSettings = new ConfigSettings();
            m_networkServersInfo = networkInfo = new NetworkServersInfo();

            bool iniFileExists = false;

            IConfig startupConfig = argvSource.Configs["Startup"];

            List<string> sources = new List<string>();

            string masterFileName =
                    startupConfig.GetString("inimaster", "OpenSimDefaults.ini");

            if (masterFileName == "none")
                masterFileName = String.Empty;

            if (IsUri(masterFileName))
            {
                if (!sources.Contains(masterFileName))
                    sources.Add(masterFileName);
            }
            else
            {
                string masterFilePath = Path.GetFullPath(
                        Path.Combine(Util.configDir(), masterFileName));

                if (masterFileName != String.Empty)
                {
                    if (File.Exists(masterFilePath))
                    {
                        if (!sources.Contains(masterFilePath))
                            sources.Add(masterFilePath);
                    }
                    else
                    {
                        m_log.ErrorFormat("Master ini file {0} not found", Path.GetFullPath(masterFilePath));
                        Environment.Exit(1);
                    }
                }
            }

            string iniFileName = startupConfig.GetString("inifile", "OpenSim.ini");

            if (IsUri(iniFileName))
            {
                if (!sources.Contains(iniFileName))
                    sources.Add(iniFileName);
                Application.iniFilePath = iniFileName;
            }
            else
            {
                Application.iniFilePath = Path.GetFullPath(
                    Path.Combine(Util.configDir(), iniFileName));

                if (!File.Exists(Application.iniFilePath))
                {
                    iniFileName = "OpenSim.xml";
                    Application.iniFilePath = Path.GetFullPath(Path.Combine(Util.configDir(), iniFileName));
                }

                if (File.Exists(Application.iniFilePath))
                {
                    if (!sources.Contains(Application.iniFilePath))
                        sources.Add(Application.iniFilePath);
                }
            }

            m_config = new OpenSimConfigSource();
            m_config.Source = new IniConfigSource();
            m_config.Source.Merge(DefaultConfig());

            m_log.Info("[CONFIG]: Reading configuration settings");

            for (int i = 0 ; i < sources.Count ; i++)
            {
                if (ReadConfig(m_config, sources[i]))
                {
                    iniFileExists = true;
                    AddIncludes(m_config, sources);
                }
            }

            // Override distro settings with contents of inidirectory
            string iniDirName = startupConfig.GetString("inidirectory", "config");
            string iniDirPath = Path.Combine(Util.configDir(), iniDirName);

            if (Directory.Exists(iniDirPath))
            {
                m_log.InfoFormat("[CONFIG]: Searching folder {0} for config ini files", iniDirPath);
                List<string> overrideSources = new List<string>();

                string[] fileEntries = Directory.GetFiles(iniDirName);
                foreach (string filePath in fileEntries)
                {
                    if (Path.GetExtension(filePath).ToLower() == ".ini")
                    {
                        if (!sources.Contains(Path.GetFullPath(filePath)))
                        {
                            overrideSources.Add(Path.GetFullPath(filePath));
                            // put it in sources too, to avoid circularity
                            sources.Add(Path.GetFullPath(filePath));
                        }
                    }
                }


                if (overrideSources.Count > 0)
                {
                    OpenSimConfigSource overrideConfig = new OpenSimConfigSource();
                    overrideConfig.Source = new IniConfigSource();

                    for (int i = 0 ; i < overrideSources.Count ; i++)
                    {
                        if (ReadConfig(overrideConfig, overrideSources[i]))
                        {
                            iniFileExists = true;
                            AddIncludes(overrideConfig, overrideSources);
                        } 
                    }
                    m_config.Source.Merge(overrideConfig.Source);
                }
            }

            if (sources.Count == 0)
            {
                m_log.FatalFormat("[CONFIG]: Could not load any configuration");
                Environment.Exit(1);
            } 
            else if (!iniFileExists)
            {
                m_log.FatalFormat("[CONFIG]: Could not load any configuration");
                m_log.FatalFormat("[CONFIG]: Configuration exists, but there was an error loading it!");
                Environment.Exit(1);
            }

            // Make sure command line options take precedence
            m_config.Source.Merge(argvSource);

            IConfig enVars = m_config.Source.Configs["Environment"];

            if( enVars != null )
            {
                string[] env_keys = enVars.GetKeys();

                foreach ( string key in env_keys )
                {
                    envConfigSource.AddEnv(key, string.Empty);
                }

                envConfigSource.LoadEnv();
                m_config.Source.Merge(envConfigSource);
            }

            m_config.Source.ExpandKeyValues();

            ReadConfigSettings();

            return m_config;
        }

        /// <summary>
        /// Adds the included files as ini configuration files
        /// </summary>
        /// <param name="sources">List of URL strings or filename strings</param>
        private void AddIncludes(OpenSimConfigSource configSource, List<string> sources)
        {
            //loop over config sources
            foreach (IConfig config in configSource.Source.Configs)
            {
                // Look for Include-* in the key name
                string[] keys = config.GetKeys();
                foreach (string k in keys)
                {
                    if (k.StartsWith("Include-"))
                    {
                        // read the config file to be included.
                        string file = config.GetString(k);
                        if (IsUri(file))
                        {
                            if (!sources.Contains(file))
                                sources.Add(file);
                        }
                        else
                        {
                            string basepath = Path.GetFullPath(Util.configDir());
                            // Resolve relative paths with wildcards
                            string chunkWithoutWildcards = file;
                            string chunkWithWildcards = string.Empty;
                            int wildcardIndex = file.IndexOfAny(new char[] { '*', '?' });
                            if (wildcardIndex != -1)
                            {
                                chunkWithoutWildcards = file.Substring(0, wildcardIndex);
                                chunkWithWildcards = file.Substring(wildcardIndex);
                            }
                            string path = Path.Combine(basepath, chunkWithoutWildcards);
                            path = Path.GetFullPath(path) + chunkWithWildcards;
                            string[] paths = Util.Glob(path);
                            
                            // If the include path contains no wildcards, then warn the user that it wasn't found.
                            if (wildcardIndex == -1 && paths.Length == 0)
                            {
                                m_log.WarnFormat("[CONFIG]: Could not find include file {0}", path);
                            }
                            else
                            {                            
                                foreach (string p in paths)
                                {
                                    if (!sources.Contains(p))
                                        sources.Add(p);
                                }
                            }
                        }
                    }
                }
            }
        }
        /// <summary>
        /// Check if we can convert the string to a URI
        /// </summary>
        /// <param name="file">String uri to the remote resource</param>
        /// <returns>true if we can convert the string to a Uri object</returns>
        bool IsUri(string file)
        {
            Uri configUri;

            return Uri.TryCreate(file, UriKind.Absolute,
                    out configUri) && configUri.Scheme == Uri.UriSchemeHttp;
        }

        /// <summary>
        /// Provide same ini loader functionality for standard ini and master ini - file system or XML over http
        /// </summary>
        /// <param name="iniPath">Full path to the ini</param>
        /// <returns></returns>
        private bool ReadConfig(OpenSimConfigSource configSource, string iniPath)
        {
            bool success = false;

            if (!IsUri(iniPath))
            {
                m_log.InfoFormat("[CONFIG]: Reading configuration file {0}", Path.GetFullPath(iniPath));

                configSource.Source.Merge(new IniConfigSource(iniPath));
                success = true;
            }
            else
            {
                m_log.InfoFormat("[CONFIG]: {0} is a http:// URI, fetching ...", iniPath);

                // The ini file path is a http URI
                // Try to read it
                try
                {
                    XmlReader r = XmlReader.Create(iniPath);
                    XmlConfigSource cs = new XmlConfigSource(r);
                    configSource.Source.Merge(cs);

                    success = true;
                }
                catch (Exception e)
                {
                    m_log.FatalFormat("[CONFIG]: Exception reading config from URI {0}\n" + e.ToString(), iniPath);
                    Environment.Exit(1);
                }
            }
            return success;
        }

        /// <summary>
        /// Setup a default config values in case they aren't present in the ini file
        /// </summary>
        /// <returns>A Configuration source containing the default configuration</returns>
        private static IConfigSource DefaultConfig()
        {
            IConfigSource defaultConfig = new IniConfigSource();

            {
                IConfig config = defaultConfig.Configs["Startup"];

                if (null == config)
                    config = defaultConfig.AddConfig("Startup");

                config.Set("region_info_source", "filesystem");

                config.Set("physics", "OpenDynamicsEngine");
                config.Set("meshing", "Meshmerizer");
                config.Set("physical_prim", true);
                config.Set("serverside_object_permissions", true);
                config.Set("storage_prim_inventories", true);
                config.Set("startup_console_commands_file", String.Empty);
                config.Set("shutdown_console_commands_file", String.Empty);
                config.Set("DefaultScriptEngine", "XEngine");
                config.Set("clientstack_plugin", "OpenSim.Region.ClientStack.LindenUDP.dll");
                // life doesn't really work without this
                config.Set("EventQueue", true);
            }

            {
                IConfig config = defaultConfig.Configs["Network"];

                if (null == config)
                    config = defaultConfig.AddConfig("Network");

                config.Set("http_listener_port", ConfigSettings.DefaultRegionHttpPort);
            }

            return defaultConfig;
        }

        /// <summary>
        /// Read initial region settings from the ConfigSource
        /// </summary>
        protected virtual void ReadConfigSettings()
        {
            IConfig startupConfig = m_config.Source.Configs["Startup"];
            if (startupConfig != null)
            {
                m_configSettings.PhysicsEngine = startupConfig.GetString("physics");
                m_configSettings.MeshEngineName = startupConfig.GetString("meshing");

                m_configSettings.ClientstackDll 
                    = startupConfig.GetString("clientstack_plugin", "OpenSim.Region.ClientStack.LindenUDP.dll");
            }

            m_networkServersInfo.loadFromConfiguration(m_config.Source);
        }
    }
}