// Created with MonoDevelop
//
//    Copyright (C) 2023 Hiperscan GmbH Dresden, Germany
//
//    This program 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.
//
//    This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Globalization;
using System.Collections.Generic;

using Hiperscan.Unix;

namespace Hiperscan.Common.ParameterFile
{
    public abstract partial class ParameterFile
    {
        public enum ParameterFileType
        {
            Normal,
            Primary,
            Secondary
        }

        private const char KEY_VALUE_SEPARATOR = '=';
        private const char COMMENTARY_CHAR = '#';
        private const char LIST_SEPARATOR = ',';
        public const string NONE_VALUE = "NONE";

        protected Dictionary<string, string> file_parameter_strings = new Dictionary<string, string>();
        protected List<string> file_comments = new List<string>();

        protected ParameterFile(string filename)
            : this(filename, CultureInfo.InvariantCulture)
        {

        }

        protected ParameterFile(string filename, CultureInfo culture_info)
        {
            this.Filename = filename;
            this.CultureInfo = culture_info;

            if (false == FileIO.ExistsFile(this.Filename))
                throw new FileNotFoundException(string.Format("Parameter file '{0}' not found.", this.Filename));

            using (StreamReader sr = new StreamReader(this.Filename))
            {
                int i = 0;
                while (sr.EndOfStream == false)
                {
                    string line = sr.ReadLine().Trim();
                    i++;

                    if (string.IsNullOrEmpty(line))
                        continue;

                    if (line.StartsWith(ParameterFile.COMMENTARY_CHAR.ToString(), StringComparison.InvariantCulture))
                    {
                        this.file_comments.Add(line.Substring(line.IndexOf(ParameterFile.COMMENTARY_CHAR)));
                        continue;
                    }

                    string[] line_pair = line.Split(ParameterFile.KEY_VALUE_SEPARATOR);

                    if (line_pair.Length != 2)
                        throw new FormatException(string.Format("Error in parameter file '{0}': Line {1} ({2}) is not a key/value pair.", 
                                                                this.Filename, i, line));

                    if (string.IsNullOrEmpty(line_pair[1]))
                        throw new FormatException(string.Format("Error in parameter file '{0}': Do not accept empty value in line {1} ({2}).",
                                                                this.Filename, i, line));
                            
                    file_parameter_strings.Add(line_pair[0].Trim(), line_pair[1].Trim());
                }
            }

            this.BindRequiredParameters();
        }

        private string ReadString(string key)
        {
            string value = this.file_parameter_strings[key];
            return value == ParameterFile.NONE_VALUE ? null : value;
        }

        private MemberInfo GetLocalParameterProperty(string parameter_key)
        {
            return this.LocalParameterProperties.First(x => x.GetCustomAttribute<ParameterKeyAttribute>().Key == parameter_key);
        }

        protected void WriteLocalParameter(string parameter_name, string value)
        {
            MemberInfo member = this.GetLocalParameterProperty(parameter_name);
            if (member == null)
                throw new MissingMemberException(string.Format("Cannot found local Parameter '{0}' in class '{1}'", parameter_name, this.GetType().Name));

            if (member.MemberType != MemberTypes.Property)
                throw new TypeAccessException(string.Format("Local Parameter '{0}' ({1}) is not from member type 'Property'. Member type is {2}.",
                                                            parameter_name, member.Name, member.MemberType));

            Type reflected_type = ((PropertyInfo)member).PropertyType;
            object value_object = value;

            if(reflected_type == typeof(Int32))
                value_object = Int32.Parse(value, this.CultureInfo);
            else if (reflected_type == typeof(Int32?))
                value_object = value == ParameterFile.NONE_VALUE ? (Int32?)null : Int32.Parse(value, this.CultureInfo);
            else if (reflected_type == typeof(string))
                value_object = value.ToString();
            else if (reflected_type == typeof(double))
                value_object = Double.Parse(value, this.CultureInfo);
            else if (reflected_type == typeof(double?))
                value_object = value == ParameterFile.NONE_VALUE ? (double?)null : Double.Parse(value, this.CultureInfo);
            else if (reflected_type == typeof(bool))
                value_object = bool.Parse(value);
            else if (reflected_type == typeof(bool?))
                value_object = value == ParameterFile.NONE_VALUE ? (bool?)null : bool.Parse(value);
            else if (reflected_type == typeof(List<string>))
                value_object = new List<string>(value.Split(ParameterFile.LIST_SEPARATOR));
            else
                throw new FormatException(string.Format("Value type '{0}' of '{1}' is not supported", reflected_type.Name, member.Name));

            ((PropertyInfo)member).SetValue(this, value_object);
        }

        public string ReadLocalParameter(string parameter_name)
        {
            return this.ReadLocalParameter(parameter_name, this.CultureInfo);
        }

        public string ReadLocalParameter(string parameter_name, System.Globalization.CultureInfo culture)
        {
            MemberInfo member = this.GetLocalParameterProperty(parameter_name);

            if (member.MemberType != MemberTypes.Property)
                throw new TypeAccessException(string.Format("Local Parameter '{0}' ({1}) is not from member type 'Property'. Member type is {2}.",
                                                            parameter_name, member.Name, member.MemberType));

            if (member == null)
                return null;

            Type reflected_type = ((PropertyInfo)member).PropertyType;

            object value_object = ((PropertyInfo)member).GetValue(this) ?? ParameterFile.NONE_VALUE;
            if (value_object.ToString() == ParameterFile.NONE_VALUE)
                return value_object.ToString();

            if (reflected_type == typeof(Int32) || reflected_type == typeof(Int32?))
                return ((Int32)value_object).ToString(culture);
            else if (reflected_type == typeof(string))
                return value_object.ToString();
            else if (reflected_type == typeof(double) || reflected_type == typeof(double?))
                return ((double)value_object).ToString(culture);
            else if (reflected_type == typeof(bool) || reflected_type == typeof(bool?))
                return ((bool)value_object).ToString(culture);
            else if (reflected_type == typeof(List<string>))
                return string.Join<string>(ParameterFile.LIST_SEPARATOR.ToString(), (List<string>)value_object);
            else
                throw new FormatException(string.Format("Value type '{0}' of '{1}' is not supported", reflected_type.Name, member.Name));
        }

        private ParameterInfo? GetParameterInfo(string parameter_key)
        {
            ParameterInfo parameter_info = new ParameterInfo();

            MemberInfo member = this.GetLocalParameterProperty(parameter_key);

            if (member.MemberType != MemberTypes.Property)
                throw new TypeAccessException(string.Format("Local Parameter '{0}' ({1}) is not from member type 'Property'. Member type is {2}.",
                                                            parameter_key, member.Name, member.MemberType));

            if (member == null)
                return null;
                                    
            parameter_info.ParameterName = parameter_key;
            parameter_info.ParameterValue = file_parameter_strings.ContainsKey(parameter_key) ? file_parameter_strings[parameter_key] : null;

            parameter_info.PropertyName = member.Name;
            parameter_info.PropertyValue = ((PropertyInfo)member).GetValue(this)?.ToString() ?? ParameterFile.NONE_VALUE;
            parameter_info.PropertyType = ((PropertyInfo)member).PropertyType;

            parameter_info.IsOverrideable = member.GetCustomAttribute<OverrideableAttribute>() != null;
            parameter_info.IsRequired = member.GetCustomAttribute<RequiredAttribute>() != null;
            parameter_info.IsPrimaryRequired = member.GetCustomAttribute<PrimaryRequiredAttribute>() != null;
            parameter_info.IsSecondaryRequired = member.GetCustomAttribute<SecondaryRequiredAttribute>() != null;
            parameter_info.IsPrimaryOptional = member.GetCustomAttribute<PrimaryOptionalAttribute>() != null;

            return parameter_info;
        }

        public bool IsParameterPrimaryRequired(string parameter_key)
        {
            return ((ParameterInfo)this.GetParameterInfo(parameter_key)).IsPrimaryRequired;
        }

        public bool IsParameterPrimaryOptional(string parameter_key)
        {
            return ((ParameterInfo)this.GetParameterInfo(parameter_key)).IsPrimaryOptional;
        }

        public bool IsParameterSecondaryRequired(string parameter_key)
        {
            return ((ParameterInfo)this.GetParameterInfo(parameter_key)).IsSecondaryRequired;
        }

        public bool IsParameterRequired(string parameter_key)
        {
            return ((ParameterInfo)this.GetParameterInfo(parameter_key)).IsRequired;
        }

        public bool IsParameterOvrerrideable(string parameter_key)
        {
            return ((ParameterInfo)this.GetParameterInfo(parameter_key)).IsOverrideable;
        }

        protected void BindRequiredParameters()
        {
            foreach (string key in this.RequiredParameterKeys)
            {
                this.WriteLocalParameter(key, this.file_parameter_strings[key]);
            }
        }

        protected void BindPrimaryRequiredParameters()
        {
            foreach (string key in this.PrimaryRequiredParameterKeys)
            {
                this.WriteLocalParameter(key, this.file_parameter_strings[key]);
            }
        }

        protected void BindSecondaryRequiredParameters()
        {
            foreach (string key in this.SecondaryRequiredParameterKeys)
            {
                this.WriteLocalParameter(key, this.file_parameter_strings[key]);
            }
        }

        protected void BindOverrideableParameters()
        {
            foreach (string key in this.OverrideableParameterKeys)
            {
                if (this.file_parameter_strings.ContainsKey(key))
                    this.WriteLocalParameter(key, this.file_parameter_strings[key]);
            }
        }

        protected void BindOptionalParameters()
        {
            foreach (string key in this.PrimaryOptionalParameterKeys)
            {
                if (this.file_parameter_strings.ContainsKey(key))
                    this.WriteLocalParameter(key, this.file_parameter_strings[key]);
                else
                    this.WriteLocalParameter(key, ParameterFile.NONE_VALUE);
            }
        }

        public void OverrideParameterFile(ParameterFile parameter_file)
        {
            if (this.FileType != ParameterFileType.Primary)
                throw new Exception(string.Format("File type '{0}' of primary file '{1}' is not from type primary", this.FileType, this.Filename));
            if (parameter_file.FileType != ParameterFileType.Secondary)
                throw new Exception(string.Format("File type '{0}' of secondary file '{1}' is not from type secondary", parameter_file.FileType, parameter_file.Filename));

            if (this.IsOverridden)
                throw new Exception("Parameter file is allready overridden.");

            foreach (string key in this.OverrideableParameterKeys)
            {
                if (parameter_file.FileParameterKeys.Contains(key))
                {
                    this.WriteLocalParameter(key, parameter_file.ReadLocalParameter(key));
                }
            }

            this.IsOverridden = true;
            this.SecondaryFilename = parameter_file.Filename;
        }


        public Dictionary<string, ParameterInfo> ParameterInfos
        {
            get
            {
                Dictionary<string, ParameterInfo> parameter_infos = new Dictionary<string, ParameterInfo>();

                foreach (string key in this.LocalParameterKeys)
                {
                    ParameterInfo? info = this.GetParameterInfo(key);

                    if(info != null)
                        parameter_infos.Add(key, (ParameterInfo)this.GetParameterInfo(key));
                }

                return parameter_infos;
            }
        }

        public string[] RequiredParameterKeys
        {
            get
            {
                return this.ParameterInfos.Select(x => x.Value).Where(x => x.IsRequired).Select(x => x.ParameterName).ToArray();
            }
        }

        public string[] OverrideableParameterKeys
        {
            get
            {
                return this.ParameterInfos.Select(x => x.Value).Where(x => x.IsOverrideable).Select(x => x.ParameterName).ToArray();
            }
        }

        public string[] OverrideableAndDefinedParameterKeys
        {
            get
            {
                return this.OverrideableParameterKeys.Where( x => this.file_parameter_strings.ContainsKey(x)).ToArray();
            }
        }

        public string[] PrimaryRequiredParameterKeys
        {
            get
            {
                return this.ParameterInfos.Select(x => x.Value).Where(x => x.IsPrimaryRequired).Select(x => x.ParameterName).ToArray();
            }
        }

        public string[] PrimaryOptionalParameterKeys
        {
            get
            {
                return this.ParameterInfos.Select(x => x.Value).Where(x => x.IsPrimaryOptional).Select(x => x.ParameterName).ToArray();
            }
        }

        public string[] SecondaryRequiredParameterKeys
        {
            get
            {
                return this.ParameterInfos.Select(x => x.Value).Where(x => x.IsSecondaryRequired).Select(x => x.ParameterName).ToArray();
            }
        }

        public string[] LocalParameterNames
        {
            get
            {
                return this.LocalParameterProperties.Select(x => x.Name).ToArray();
            }
        }

        public string[] LocalParameterKeys
        {
            get
            {
                return this.LocalParameterProperties
                            .Select(x => x.GetCustomAttribute<ParameterKeyAttribute>())
                            .Select(x => x.Key).ToArray();
            }
        }

        public string[] FileParameterKeys
        {
            get
            {
                return this.file_parameter_strings.Keys.ToArray();
            }
        }

        private IEnumerable<MemberInfo> LocalParameterProperties
        {
            get
            {
                return this.GetType().GetProperties()
                           .Where(x => x.GetCustomAttribute(typeof(ParameterKeyAttribute)) != null);
            }
        }

        public string Filename { get; private set; } = string.Empty;

        public string SecondaryFilename { get; private set; } = string.Empty;

        public bool IsOverridden { get; private set; } = false;

        public ParameterFileType FileType { get; protected set; } = ParameterFileType.Normal;

        public CultureInfo CultureInfo { get; private set; }
    }
}
