//
// ConfigurationBase.cs
//
// Author:
//       Carsten Sonne Larsen <cs@innolan.dk>
//
// Copyright (c) 2013-2016 Carsten Sonne Larsen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Ntp.Analyzer.Localize;

namespace Ntp.Config
{
    public abstract class ConfigurationBase : IEnumerable
    {
        protected ConfigurationBase(string configName, int indentLevel)
        {
            this.configName = configName;
            this.indentLevel = indentLevel;
        }

        protected const DateTimeKind DefaultTimeStamp = DateTimeKind.Utc;

        private readonly string configName;
        private readonly int indentLevel;

        /// <summary>
        /// Gets the name of this configuration config.
        /// </summary>
        /// <value>The name of the configuration.</value>
        public string ConfigName
        {
            get { return configName; }
        }

        protected int IndentLevel
        {
            get { return indentLevel; }
        }

        #region IEnumerable

        protected abstract IEnumerable<Object> Items { get; }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return Items.GetEnumerator();
        }

        #endregion

        #region Error Handling

        static ConfigurationBase()
        {
            ShowDefault = false;
        }

        private static StringBuilder errors = new StringBuilder();

        protected int IndentChars = 4;
        public static bool ShowDefault { get; set; }

        public static string Errors
        {
            get { return errors.ToString(); }
        }

        internal static void ClearErrors()
        {
            errors = new StringBuilder();
        }

        protected string ConfigText(string[] settings, IEnumerable<Object> values)
        {
            var valueList = new List<Object>(values);

            string indentHeader = String.Empty.PadLeft(IndentLevel*IndentChars, ' ');
            string indentContent = String.Empty.PadLeft((IndentLevel + 1)*IndentChars, ' ');

            var text = new StringBuilder();

            text.AppendLine();
            text.Append(indentHeader);
            text.AppendFormat("{0} {1}", settings[0], ConfigName);
            text.Append(ConfigName != String.Empty ? " {" : "{");
            text.AppendLine();

            for (int i = 1; i < settings.Length; i++)
            {
                if (valueList[i] == null || valueList[i].ToString() == String.Empty)
                    continue;

                if (valueList[i] is IList)
                {
                    var collection = valueList[i] as IEnumerable;

                    foreach (object item in collection)
                    {
                        text.Append(item);
                    }
                }
                else if (valueList[i] is ConfigurationBase)
                {
                    text.Append(valueList[i]);
                }
                else
                {
                    object value = valueList[i];
                    if (value is bool)
                    {
                        value = (bool) value ? 1 : 0;
                    }
                    text.Append(indentContent);
                    text.AppendFormat("{0} {1}", settings[i], value);
                    text.AppendLine();
                }
            }

            text.Append(indentHeader);
            text.AppendLine("}");

            return text.ToString();
        }

        protected static void ConfigError(string errorText)
        {
            errors.AppendLine(errorText);
        }

        #endregion

        protected static ConfigurationBlock FindSingleBlock(string name, ConfigurationBlock block)
        {
            return FindSingleBlock(name, block.Lines, block.IndentLevel + 1);
        }

        protected static ConfigurationBlock FindSingleBlock(string name, string[] lines, int indentLevel, int start = 0)
        {
            bool found = false;

            while (!found)
            {
                start = FindLine(start, lines, '{');

                if (start == -1)
                    return null;

                found = lines[start].Trim().StartsWith(name + " ");
            }

            int end = FindLine(start, lines, '}', '{');

            // No ending symbol
            if (end == -1)
            {
                ConfigError("End of block not found in " + name);
                return null;
            }

            var block = new List<string>(end - start + 1);

            for (int c = start; c < end + 1; c++)
                block.Add(lines[c]);

            return new ConfigurationBlock(start, block.ToArray(), indentLevel);
        }

        protected static ConfigurationBlock[] FindBlocks(string name, ConfigurationBlock block)
        {
            return FindBlocks(name, block.Lines, block.IndentLevel);
        }

        protected static ConfigurationBlock[] FindBlocks(string name, string[] lines, int indentLevel)
        {
            int start = 0;
            var blocks = new List<ConfigurationBlock>();

            ConfigurationBlock block;

            do
            {
                block = FindSingleBlock(name, lines, indentLevel + 1, start);

                if (block != null)
                {
                    blocks.Add(block);
                    start = block.Offset;
                }
            } while (block != null);

            return blocks.ToArray();
        }

        private static int FindLine(int startLine, string[] lines, char searchChar, char indentChar = (char) 0)
        {
            if (startLine >= lines.Length - 1)
                return -1;

            bool found = false;
            int i = startLine;
            int indent = 1;

            do
            {
                i++;

                if (indentChar != (char) 0 && lines[i].IndexOf(indentChar) != -1)
                    indent++;
                else if (lines[i].IndexOf(searchChar) != -1)
                {
                    indent--;
                    found = true;
                }
            } while ((!found || indent != 0) && i < lines.Length - 1);

            if (i == lines.Length - 1 && (!found || indent != 0))
                return -1;

            return i;
        }

        protected static OptionSet GetOptions(IEnumerable<String> options, IEnumerable<String> lines)
        {
            if (options == null || lines == null)
                return null;

            var result = new OptionSet();
            IList<string> optionList = options as IList<string> ?? options.ToList();

            foreach (string option in optionList)
                result.Add(option, String.Empty);

            foreach (string optionLine in lines)
            {
                string line = optionLine.Trim();

                if (line.StartsWith("#"))
                    continue;

                int pos = line.IndexOf(' ');

                if (pos == -1)
                    continue;

                string value = line.Substring(pos, line.Length - pos).Trim();

                foreach (string option in optionList)
                {
                    if (line.StartsWith(option + " "))
                    {
                        result[option] = value;
                        break;
                    }
                }
            }

            return result;
        }

        protected static IEnumerable<String> GetMultiOption(string option, ConfigurationBlock block)
        {
            return GetMultiOption(option, block.Lines);
        }

        protected static List<String> GetMultiOption(string option, string[] lines)
        {
            var result = new List<string>();

            foreach (string line in lines)
            {
                string currentLine = line.Trim();
                if (!currentLine.StartsWith(option))
                    continue;

                int pos = currentLine.IndexOf(' ');

                if (pos == -1)
                    continue;

                if (currentLine.Substring(0, pos).Trim() != option)
                    continue;

                string value = currentLine.Substring(pos, currentLine.Length - pos).Trim();
                result.Add(value);
            }

            return result;
        }

        protected static string GetOptionConfigName(
            Dictionary<String, String> options,
            string option,
            string configPath,
            bool mandatory,
            string defaultValue = null)
        {
            string value = GetOptionString(options, option, configPath, mandatory, String.Empty);
            return value.Replace("{", String.Empty).Trim();
        }

        protected static string GetOptionString(
            Dictionary<String, String> options,
            string option,
            string configPath)
        {
            return GetOptionString(options, option, configPath, true);
        }

        protected static string GetOptionString(
            Dictionary<String, String> options,
            string option,
            string configPath,
            bool mandatory,
            string defaultValue = null)
        {
            if (options.ContainsKey(option) && options[option] != String.Empty)
            {
                return options[option];
            }

            if (mandatory)
            {
                ConfigError(String.Format(
                    ConfigurationMessage.NoValueForSetting, option, configPath));
            }

            return defaultValue ?? String.Empty;
        }

        protected static Int32 GetOptionInteger(
            Dictionary<String, String> options,
            string option,
            string configPath,
            bool mandatory,
            int defaultValue = 0)
        {
            if (!options.ContainsKey(option) || options[option].Trim() == String.Empty)
            {
                if (mandatory)
                {
                    ConfigError(String.Format(
                        ConfigurationMessage.NoValueForSetting, option, configPath));
                }
                return defaultValue;
            }

            int ivalue;
            if (!Int32.TryParse(options[option].Trim(), out ivalue))
            {
                ivalue = defaultValue;
                ConfigError(String.Format(
                    ConfigurationMessage.InvalidIntegerValue, option, configPath));
            }

            return ivalue;
        }

        protected static Boolean GetOptionBoolean(
            Dictionary<String, String> options,
            string option,
            string configPath,
            bool mandatory,
            bool defaultValue)
        {
            if (!options.ContainsKey(option) || options[option].Trim() == String.Empty)
            {
                if (mandatory)
                {
                    ConfigError(String.Format(
                        ConfigurationMessage.NoValueForSetting, option, configPath));
                }
                return defaultValue;
            }

            string value = options[option].Trim();

            if (value.ToLower() == "on" ||
                value.ToLower() == "true" ||
                value.ToLower() == "enable" ||
                value == "1")
                return true;

            if (value.ToLower() == "off" ||
                value.ToLower() == "false" ||
                value.ToLower() == "disable" ||
                value == "0")
                return false;

            ConfigError(String.Format(
                ConfigurationMessage.InvalidBooleanValue, option, configPath));

            return false;
        }

        protected static Double GetOptionDouble(
            Dictionary<String, String> options,
            string option,
            string configPath,
            bool mandatory,
            double defaultValue = 0.0)
        {
            if (!options.ContainsKey(option) || options[option].Trim() == String.Empty)
            {
                if (mandatory)
                {
                    ConfigError(String.Format(
                        ConfigurationMessage.NoValueForSetting, option, configPath));
                }
                return defaultValue;
            }

            double dvalue;
            if (!Double.TryParse(options[option].Trim(), out dvalue))
            {
                dvalue = defaultValue;
                ConfigError(String.Format(
                    ConfigurationMessage.InvalidNumericValue, option, configPath));
            }

            return dvalue;
        }

        protected string GetTimezoneString(DateTimeKind timeStamp)
        {
            switch (timeStamp)
            {
                case DateTimeKind.Local:
                    return ConfigurationKeyword.TimeStampLocal;
                case DateTimeKind.Utc:
                    return ConfigurationKeyword.TimeStampUtc;
                default:
                    return ConfigurationKeyword.TypeUnknown;
            }
        }

        protected static DateTimeKind GetTimezone(
            Dictionary<String, String> options,
            string type,
            string configPath)
        {
            if (!options.ContainsKey(type) || options[type].Trim() == String.Empty)
            {
                return DefaultTimeStamp;
            }

            if (options[type].Trim().ToLower() == ConfigurationKeyword.TimeStampLocal.ToLower())
            {
                return DateTimeKind.Local;
            }

            if (options[type].Trim().ToLower() == ConfigurationKeyword.TimeStampUtc.ToLower())
            {
                return DateTimeKind.Utc;
            }

            ConfigError(String.Format(ConfigurationMessage.InvalidValue, type, configPath));
            return DateTimeKind.Unspecified;
        }
    }
}