using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace Billing.Models
{
    public interface IModel
    {
        void OnXmlSerialize(XElement node);

        void OnXmlDeserialize(XElement node);
    }

    public abstract class BaseModel : IModel, IDisposable
    {
        protected const string ICON_DEFAULT = "ic_default";

        private bool disposed = false;

        public static T ParseXml<T>(string xml) where T : BaseModel, new()
        {
            XDocument doc = XDocument.Parse(xml);
            T model = new();
            model.OnXmlDeserialize(doc.Root);
            return model;
        }

        protected static string ToString(IFormattable v) => v.ToString(null, CultureInfo.InvariantCulture);
        protected static double ParseDouble(string s) => double.Parse(s, CultureInfo.InvariantCulture);
        protected static decimal ParseDecimal(string s) => decimal.Parse(s, CultureInfo.InvariantCulture);
        protected static int ParseInt(string s) => int.Parse(s, CultureInfo.InvariantCulture);
        protected static long ParseLong(string s) => long.Parse(s, CultureInfo.InvariantCulture);

        protected static bool IsTrue(string s) =>
            string.Equals(s, "true", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(s, "yes", StringComparison.OrdinalIgnoreCase) ||
            s == "1";

        #region Private Methods

        private static XElement WriteString(XElement parent, string name, string val, Action<XElement> action = null)
        {
            XElement ele;
            if (val == null)
            {
                ele = new XElement(name);
            }
            else
            {
                ele = new XElement(name, val);
            }
            action?.Invoke(ele);
            parent.Add(ele);
            return ele;
        }

        private static T ReadSubnode<T>(XElement node, string subname, Func<XElement, T> func)
        {
            var ele = node.Elements().FirstOrDefault(e => string.Equals(e.Name.ToString(), subname, StringComparison.OrdinalIgnoreCase));
            return func(ele);
        }

        private static T ReadObject<T>(XElement node) where T : IModel, new()
        {
            if (IsTrue(node.Attribute("null")?.Value))
            {
                return default;
            }
            T value = new();
            value.OnXmlDeserialize(node);
            return value;
        }

        private static T[] ReadArray<T>(XElement node) where T : IModel, new()
        {
            if (IsTrue(node.Attribute("null")?.Value))
            {
                return default;
            }
            int count = ParseInt(node.Attribute("count").Value);
            T[] array = new T[count];
            foreach (XElement ele in node.Elements("item"))
            {
                int index = ParseInt(ele.Attribute("index").Value);
                array[index] = ReadObject<T>(ele);
            }
            return array;
        }

        #endregion

        #region Read/Write Helper

        protected static XElement Write(XElement parent, string name, string val) => WriteString(parent, name, val);
        protected static XElement Write(XElement parent, string name, DateTime date) => WriteString(parent, name, ToString(date.Ticks));
        protected static XElement Write<T>(XElement parent, string name, T value) where T : IFormattable => WriteString(parent, name, ToString(value));
        protected static XElement WriteObject<T>(XElement parent, string name, T value) where T : IModel => WriteString(parent, name, null,
            action: ele =>
            {
                if (value == null)
                {
                    ele.Add(new XAttribute("null", 1));
                }
                else
                {
                    value.OnXmlSerialize(ele);
                }
            });
        protected static XElement WriteArray<T>(XElement parent, string name, T[] value) where T : IModel => WriteString(parent, name, null,
            action: ele =>
            {
                if (value == null)
                {
                    ele.Add(new XAttribute("null", 1));
                }
                else
                {
                    ele.Add(new XAttribute("count", value.Length));
                    for (var i = 0; i < value.Length; i++)
                    {
                        XElement item = WriteObject(ele, "item", value[i]);
                        item.Add(new XAttribute("index", i));
                    }
                }
            });

        protected static string Read(XElement node, string subname, string def) => ReadSubnode(node, subname, e => e?.Value ?? def);
        protected static int Read(XElement node, string subname, int def) => ReadSubnode(node, subname, e => e == null ? def : ParseInt(e.Value));
        protected static long Read(XElement node, string subname, long def) => ReadSubnode(node, subname, e => e == null ? def : ParseLong(e.Value));
        protected static double Read(XElement node, string subname, double def) => ReadSubnode(node, subname, e => e == null ? def : ParseDouble(e.Value));
        protected static decimal Read(XElement node, string subname, decimal def) => ReadSubnode(node, subname, e => e == null ? def : ParseDecimal(e.Value));
        protected static DateTime Read(XElement node, string subname, DateTime def) => ReadSubnode(node, subname, e => e == null ? def : new DateTime(ParseLong(e.Value)));
        protected static T ReadObject<T>(XElement node, string subname, T def = default) where T : IModel, new() => ReadSubnode(node, subname, e => e == null ? def : ReadObject<T>(e));
        protected static T[] ReadArray<T>(XElement node, string subname, T[] def = null) where T : IModel, new() => ReadSubnode(node, subname, e => e == null ? def : ReadArray<T>(e));

        #endregion

        public XDocument ToXml()
        {
            XDocument xdoc = new(new XDeclaration("1.0", "utf-8", "yes"), new XElement("root"));
            ToXml(xdoc.Root);
            return xdoc;
        }

        public void ToXml(XElement node)
        {
            OnXmlSerialize(node);
        }

        public void SaveToStream(Stream stream)
        {
            ToXml().Save(stream, SaveOptions.DisableFormatting);
        }

        public abstract void OnXmlSerialize(XElement node);
        public abstract void OnXmlDeserialize(XElement node);


        public override string ToString()
        {
            XDocument xdoc = ToXml();
            using MemoryStream ms = new();
            using StreamWriter writer = new(ms, Encoding.UTF8);
            xdoc.Save(writer, SaveOptions.DisableFormatting);
            writer.Flush();
            ms.Seek(0, SeekOrigin.Begin);
            using StreamReader reader = new(ms, Encoding.UTF8);
            return reader.ReadToEnd();
        }

        protected virtual void Dispose(bool dispose) { }

        public void Dispose()
        {
            if (!disposed)
            {
                Dispose(true);
                disposed = true;
            }
        }
    }
}