using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

namespace Novacode
{
    /// <summary>
    /// Represents a List in a document.
    /// </summary>
    public class List : InsertBeforeOrAfter
    {
        /// <summary>
        /// This is a list of paragraphs that will be added to the document
        /// when the list is inserted into the document.
        /// The paragraph needs a numPr defined to be in this items collection.
        /// </summary>
        public List<Paragraph> Items { get; private set; }
        /// <summary>
        /// The numId used to reference the list settings in the numbering.xml
        /// </summary>
        public int NumId { get; private set; }
        /// <summary>
        /// The ListItemType (bullet or numbered) of the list.
        /// </summary>
        public ListItemType? ListType { get; private set; }

        internal List(DocX document, XElement xml)
            : base(document, xml)
        {
            Items = new List<Paragraph>();
            ListType = null;
        }

        /// <summary>
        /// Adds an item to the list.
        /// </summary>
        /// <param name="paragraph"></param>
        /// <exception cref="InvalidOperationException">
        /// Throws an InvalidOperationException if the item cannot be added to the list.
        /// </exception>
        public void AddItem(Paragraph paragraph)
        {
            if (paragraph.IsListItem)
            {
                var numIdNode = paragraph.Xml.Descendants().First(s => s.Name.LocalName == "numId");
                var numId = Int32.Parse(numIdNode.Attribute(DocX.w + "val").Value);

                if (CanAddListItem(paragraph))
                {
                    NumId = numId;
                    Items.Add(paragraph);
                }
                else
                    throw new InvalidOperationException("New list items can only be added to this list if they are have the same numId.");
            }
        }

        public void AddItemWithStartValue(Paragraph paragraph, int start)
        {
            //TODO: Update the numbering
            UpdateNumberingForLevelStartNumber(int.Parse(paragraph.IndentLevel.ToString()), start);
            if (ContainsLevel(start))
                throw new InvalidOperationException("Cannot add a paragraph with a start value if another element already exists in this list with that level.");
            AddItem(paragraph);
        }

        private void UpdateNumberingForLevelStartNumber(int iLevel, int start)
        {
            var abstractNum = GetAbstractNum(NumId);
            var level = abstractNum.Descendants().First(el => el.Name.LocalName == "lvl" && el.GetAttribute(DocX.w + "ilvl") == iLevel.ToString());
            level.Descendants().First(el => el.Name.LocalName == "start").SetAttributeValue(DocX.w + "val", start);
        }

        /// <summary>
        /// Determine if it is able to add the item to the list
        /// </summary>
        /// <param name="paragraph"></param>
        /// <returns>
        /// Return true if AddItem(...) will succeed with the given paragraph.
        /// </returns>
        public bool CanAddListItem(Paragraph paragraph)
        {
            if (paragraph.IsListItem)
            {
                //var lvlNode = paragraph.Xml.Descendants().First(s => s.Name.LocalName == "ilvl");
                var numIdNode = paragraph.Xml.Descendants().First(s => s.Name.LocalName == "numId");
                var numId = Int32.Parse(numIdNode.Attribute(DocX.w + "val").Value);

                //Level = Int32.Parse(lvlNode.Attribute(DocX.w + "val").Value);
                if (NumId == 0 || (numId == NumId && numId > 0))
                {
                    return true;
                }
            }
            return false;
        }

        public bool ContainsLevel(int ilvl)
        {
            return Items.Any(i => i.ParagraphNumberProperties.Descendants().First(el => el.Name.LocalName == "ilvl").Value == ilvl.ToString());
        }

        internal void CreateNewNumberingNumId(int level = 0, ListItemType listType = ListItemType.Numbered, int? startNumber = null, bool continueNumbering = false)
        {
            ValidateDocXNumberingPartExists();
            if (Document.numbering.Root == null)
            {
                throw new InvalidOperationException("Numbering section did not instantiate properly.");
            }

            ListType = listType;

            var numId = GetMaxNumId() + 1;
            var abstractNumId = GetMaxAbstractNumId() + 1;

            XDocument listTemplate;
            switch (listType)
            {
                case ListItemType.Bulleted:
                    listTemplate = HelperFunctions.DecompressXMLResource("Novacode.Resources.numbering.default_bullet_abstract.xml.gz");
                    break;
                case ListItemType.Numbered:
                    listTemplate = HelperFunctions.DecompressXMLResource("Novacode.Resources.numbering.default_decimal_abstract.xml.gz");
                    break;
                default:
                    throw new InvalidOperationException(string.Format("Unable to deal with ListItemType: {0}.", listType.ToString()));
            }

            var abstractNumTemplate = listTemplate.Descendants().Single(d => d.Name.LocalName == "abstractNum");
            abstractNumTemplate.SetAttributeValue(DocX.w + "abstractNumId", abstractNumId);
            
            //Fixing an issue where numbering would continue from previous numbered lists. Setting startOverride assures that a numbered list starts on the provided number.
            //The override needs only be on level 0 as this will cascade to the rest of the list.
            var abstractNumXml = GetAbstractNumXml(abstractNumId, numId, startNumber, continueNumbering);

            var abstractNumNode = Document.numbering.Root.Descendants().LastOrDefault(xElement => xElement.Name.LocalName == "abstractNum");
            var numXml = Document.numbering.Root.Descendants().LastOrDefault(xElement => xElement.Name.LocalName == "num");

            if (abstractNumNode == null || numXml == null)
            {
                Document.numbering.Root.Add(abstractNumTemplate);
                Document.numbering.Root.Add(abstractNumXml);
            }
            else
            {
                abstractNumNode.AddAfterSelf(abstractNumTemplate);
                numXml.AddAfterSelf(
                    abstractNumXml
                );
            }

            NumId = numId;
        }

        private XElement GetAbstractNumXml(int abstractNumId, int numId, int? startNumber, bool continueNumbering)
        {
            //Fixing an issue where numbering would continue from previous numbered lists. Setting startOverride assures that a numbered list starts on the provided number.
            //The override needs only be on level 0 as this will cascade to the rest of the list.
            var startOverride = new XElement(XName.Get("startOverride", DocX.w.NamespaceName), new XAttribute(DocX.w + "val", startNumber ?? 1));
            var lvlOverride = new XElement(XName.Get("lvlOverride", DocX.w.NamespaceName), new XAttribute(DocX.w + "ilvl", 0), startOverride);
            var abstractNumIdElement = new XElement(XName.Get("abstractNumId", DocX.w.NamespaceName), new XAttribute(DocX.w + "val", abstractNumId));
            return continueNumbering 
                ? new XElement(XName.Get("num", DocX.w.NamespaceName), new XAttribute(DocX.w + "numId", numId), abstractNumIdElement)
                : new XElement(XName.Get("num", DocX.w.NamespaceName), new XAttribute(DocX.w + "numId", numId), abstractNumIdElement, lvlOverride);
        }

        /// <summary>
        /// Method to determine the last numId for a list element. 
        /// Also useful for determining the next numId to use for inserting a new list element into the document.
        /// </summary>
        /// <returns>
        /// 0 if there are no elements in the list already.
        /// Increment the return for the next valid value of a new list element.
        /// </returns>
        private int GetMaxNumId()
        {
            const int defaultValue = 0;
            if (Document.numbering == null)
                return defaultValue;

            var numlist = Document.numbering.Descendants().Where(d => d.Name.LocalName == "num").ToList();
            if (numlist.Any())
                return numlist.Attributes(DocX.w + "numId").Max(e => int.Parse(e.Value));
            return defaultValue;
        }

        /// <summary>
        /// Method to determine the last abstractNumId for a list element.
        /// Also useful for determining the next abstractNumId to use for inserting a new list element into the document.
        /// </summary>
        /// <returns>
        /// -1 if there are no elements in the list already.
        /// Increment the return for the next valid value of a new list element.
        /// </returns>
        private int GetMaxAbstractNumId()
        {
            const int defaultValue = -1;

            if (Document.numbering == null)
                return defaultValue;

            var numlist = Document.numbering.Descendants().Where(d => d.Name.LocalName == "abstractNum").ToList();
            if (numlist.Any())
            {
                var maxAbstractNumId = numlist.Attributes(DocX.w + "abstractNumId").Max(e => int.Parse(e.Value));
                return maxAbstractNumId;
            }
            return defaultValue;
        }

        /// <summary>
        /// Get the abstractNum definition for the given numId
        /// </summary>
        /// <param name="numId">The numId on the pPr element</param>
        /// <returns>XElement representing the requested abstractNum</returns>
        internal XElement GetAbstractNum(int numId)
        {
            var num = Document.numbering.Descendants().First(d => d.Name.LocalName == "num" && d.GetAttribute(DocX.w + "numId").Equals(numId.ToString()));
            var abstractNumId = num.Descendants().First(d => d.Name.LocalName == "abstractNumId");
            return Document.numbering.Descendants().First(d => d.Name.LocalName == "abstractNum" && d.GetAttribute("abstractNumId").Equals(abstractNumId.Value));
        }

        private void ValidateDocXNumberingPartExists()
        {
            var numberingUri = new Uri("/word/numbering.xml", UriKind.Relative);

            // If the internal document contains no /word/numbering.xml create one.
            if (!Document.package.PartExists(numberingUri))
                Document.numbering = HelperFunctions.AddDefaultNumberingXml(Document.package);
        }
    }
}
