using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Umbraco.Core.Cache;
using Umbraco.Core.Events;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.UnitOfWork;
using File = System.IO.File;

namespace Umbraco.Core.Services
{
    internal class SectionService : ISectionService
    {
        private readonly IUserService _userService;
        private IEnumerable<Section> _allAvailableSections;
        private readonly IApplicationTreeService _applicationTreeService;
        private readonly IScopeUnitOfWorkProvider _uowProvider;
        private readonly CacheHelper _cache;
        internal const string AppConfigFileName = "applications.config";
        private static string _appConfig;
        private volatile bool _isInitialized = false;
        private static readonly object Locker = new object();

        public SectionService(
            IUserService userService,
            IApplicationTreeService applicationTreeService,
            IScopeUnitOfWorkProvider uowProvider,
            CacheHelper cache)
        {
            if (applicationTreeService == null) throw new ArgumentNullException("applicationTreeService");
            if (cache == null) throw new ArgumentNullException("cache");

            _userService = userService;
            _applicationTreeService = applicationTreeService;
            _uowProvider = uowProvider;
            _cache = cache;
        }

        

        /// <summary>
        /// gets/sets the application.config file path
        /// </summary>
        /// <remarks>
        /// The setter is generally only going to be used in unit tests, otherwise it will attempt to resolve it using the IOHelper.MapPath
        /// </remarks>
        internal static string AppConfigFilePath
        {
            get
            {
                if (string.IsNullOrWhiteSpace(_appConfig))
                {
                    _appConfig = IOHelper.MapPath(SystemDirectories.Config + "/" + AppConfigFileName);
                }
                return _appConfig;
            }
            set { _appConfig = value; }
        }

        /// <summary>
        /// Initializes the service with all available application plugins
        /// </summary>
        /// <param name="allAvailableSections">
        /// All application plugins found in assemblies
        /// </param>
        /// <remarks>
        /// This is used to populate the app.config file with any applications declared in plugins that don't exist in the file
        /// </remarks>
        public void Initialize(IEnumerable<Section> allAvailableSections)
        {
            _allAvailableSections = allAvailableSections;            
        }

        /// <summary>
        /// The cache storage for all applications
        /// </summary>
        public IEnumerable<Section> GetSections()
        {
            return _cache.RuntimeCache.GetCacheItem<IEnumerable<Section>>(
                CacheKeys.ApplicationsCacheKey,
                () =>
                    {
                        ////used for unit tests
                        //if (_testApps != null)
                        //    return _testApps;

                        var list = ReadFromXmlAndSort();

                        //On first access we need to do some initialization
                        if (_isInitialized == false)
                        {
                            lock (Locker)
                            {
                                if (_isInitialized == false)
                                {
                                    //now we can check the non-volatile flag
                                    if (_allAvailableSections != null)
                                    {
                                        var hasChanges = false;

                                        LoadXml(doc =>
                                        {
                                            //Now, load in the xml structure and update it with anything that is not declared there and save the file.

                                            //NOTE: On the first iteration here, it will lazily scan all apps, etc... this is because this ienumerable is lazy
                                            // based on the ApplicationRegistrar - and as noted there this is not an ideal way to do things but were stuck like this
                                            // currently because of the legacy assemblies and types not in the Core.

                                            //Get all the trees not registered in the config
                                            var unregistered = _allAvailableSections
                                                .Where(x => list.Any(l => l.Alias == x.Alias) == false)
                                                .ToArray();

                                            hasChanges = unregistered.Any();

                                            var count = 0;
                                            foreach (var attr in unregistered)
                                            {
                                                doc.Root.Add(new XElement("add",
                                                    new XAttribute("alias", attr.Alias),
                                                    new XAttribute("name", attr.Name),
                                                    new XAttribute("icon", attr.Icon),
                                                    new XAttribute("sortOrder", attr.SortOrder)));
                                                count++;
                                            }

                                            //don't save if there's no changes
                                            return count > 0;
                                        }, true);

                                        if (hasChanges)
                                        {
                                            //If there were changes, we need to re-read the structures from the XML
                                            list = ReadFromXmlAndSort();
                                        }
                                    }
                                }

                                _isInitialized = true;
                            }
                        }

                        return list;

                    }, new TimeSpan(0, 10, 0));
        }

        internal void LoadXml(Func<XDocument, bool> callback, bool saveAfterCallbackIfChanged)
        {
            lock (Locker)
            {
                var doc = File.Exists(AppConfigFilePath)
                    ? XDocument.Load(AppConfigFilePath)
                    : XDocument.Parse("<?xml version=\"1.0\"?><applications />");

                if (doc.Root != null)
                {
                    var changed = callback.Invoke(doc);

                    if (saveAfterCallbackIfChanged && changed)
                    {
                        //ensure the folder is created!
                        Directory.CreateDirectory(Path.GetDirectoryName(AppConfigFilePath));

                        doc.Save(AppConfigFilePath);

                        //remove the cache so it gets re-read ... SD: I'm leaving this here even though it
                        // is taken care of by events as well, I think unit tests may rely on it being cleared here.
                        _cache.RuntimeCache.ClearCacheItem(CacheKeys.ApplicationsCacheKey);
                    }
                }
            }
        }

        /// <summary>
        /// Get the user's allowed sections
        /// </summary>
        /// <param name="userId"></param>
        /// <returns></returns>
        public IEnumerable<Section> GetAllowedSections(int userId)
        {
            var user = _userService.GetUserById(userId);
            if (user == null)
            {
                throw new InvalidOperationException("No user found with id " + userId);
            }

            return GetSections().Where(x => user.AllowedSections.Contains(x.Alias));
        }

        /// <summary>
        /// Gets the application by its alias.
        /// </summary>
        /// <param name="appAlias">The application alias.</param>
        /// <returns></returns>
        public Section GetByAlias(string appAlias)
        {
            return GetSections().FirstOrDefault(t => t.Alias == appAlias);
        }

        /// <summary>
        /// Creates a new applcation if no application with the specified alias is found.
        /// </summary>
        /// <param name="name">The application name.</param>
        /// <param name="alias">The application alias.</param>
        /// <param name="icon">The application icon, which has to be located in umbraco/images/tray folder.</param>
        public void MakeNew(string name, string alias, string icon)
        {
            var sections = GetSections();
            var nextSortOrder = sections.Any() ? sections.Max(x => x.SortOrder) + 1 : 1;
            MakeNew(name, alias, icon, nextSortOrder);
        }

        /// <summary>
        /// Makes the new.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="alias">The alias.</param>
        /// <param name="icon">The icon.</param>
        /// <param name="sortOrder">The sort order.</param>
        public void MakeNew(string name, string alias, string icon, int sortOrder)
        {            
            if (GetSections().All(x => x.Alias != alias))
            {
                LoadXml(doc =>
                {
                    doc.Root.Add(new XElement("add",
                        new XAttribute("alias", alias),
                        new XAttribute("name", name),
                        new XAttribute("icon", icon),
                        new XAttribute("sortOrder", sortOrder)));
                    return true;
                }, true);

                //raise event
                OnNew(new Section(name, alias, icon, sortOrder), new EventArgs());
            }
        }

        /// <summary>
        /// Deletes the section
        /// </summary>        
        public void DeleteSection(Section section)
        {
            lock (Locker)
            {
                //delete the assigned applications
                using (var uow = _uowProvider.GetUnitOfWork())
                {
                    uow.Database.Execute(
                        "delete from umbracoUserGroup2App where app = @appAlias",
                        new { appAlias = section.Alias });
                    uow.Commit();
                }

                //delete the assigned trees
                var trees = _applicationTreeService.GetApplicationTrees(section.Alias);
                foreach (var t in trees)
                {
                    _applicationTreeService.DeleteTree(t);
                }

                LoadXml(doc =>
                {
                    doc.Root.Elements("add").Where(x => x.Attribute("alias") != null && x.Attribute("alias").Value == section.Alias)
                        .Remove();

                    return true;
                }, true);

                //raise event
                OnDeleted(section, new EventArgs());   
            }            
        }

        private List<Section> ReadFromXmlAndSort()
        {
            var tmp = new List<Section>();

            LoadXml(doc =>
            {
                foreach (var addElement in doc.Root.Elements("add").OrderBy(x =>
                {
                    var sortOrderAttr = x.Attribute("sortOrder");
                    return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0;
                }))
                {
                    var sortOrderAttr = addElement.Attribute("sortOrder");
                    tmp.Add(new Section(addElement.Attribute("name").Value,
                                        addElement.Attribute("alias").Value,
                                        addElement.Attribute("icon").Value,
                                        sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0));
                }
                return false;
            }, false);

            return tmp;
        } 

        internal static event TypedEventHandler<Section, EventArgs> Deleted;
        private static void OnDeleted(Section app, EventArgs args)
        {
            if (Deleted != null)
            {
                Deleted(app, args);
            }
        }

        internal static event TypedEventHandler<Section, EventArgs> New;
        private static void OnNew(Section app, EventArgs args)
        {
            if (New != null)
            {
                New(app, args);
            }
        }

    }
}
