/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt VS Tools.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Framework;
using Microsoft.VisualStudio.ProjectSystem;
using Microsoft.VisualStudio.ProjectSystem.Properties;
using Microsoft.VisualStudio.TaskStatusCenter;
using Microsoft.VisualStudio.Threading;
using EnvDTE;

namespace QtVsTools.QtMsBuild
{
    using Core;
    using VisualStudio;
    using Thread = System.Threading.Thread;
    using SubscriberAction = ActionBlock<IProjectVersionedValue<IProjectSubscriptionUpdate>>;

    class QtProjectTracker : Concurrent<QtProjectTracker>
    {
        static ConcurrentDictionary<string, QtProjectTracker> _Instances;
        static ConcurrentDictionary<string, QtProjectTracker> Instances =>
            StaticThreadSafeInit(() => _Instances, () =>
                _Instances = new ConcurrentDictionary<string, QtProjectTracker>());

        static PunisherQueue<QtProjectTracker> _InitQueue;
        static PunisherQueue<QtProjectTracker> InitQueue =>
            StaticThreadSafeInit(() => _InitQueue, () =>
                _InitQueue = new PunisherQueue<QtProjectTracker>());

        static IVsTaskStatusCenterService _StatusCenter;
        static IVsTaskStatusCenterService StatusCenter => StaticThreadSafeInit(() => _StatusCenter,
                () => _StatusCenter = VsServiceProvider
                    .GetService<SVsTaskStatusCenterService, IVsTaskStatusCenterService>());

        static Task InitDispatcher { get; set; }
        static ITaskHandler2 InitStatus { get; set; }

        public static string SolutionPath { get; set; } = string.Empty;

        private QtProjectTracker()
        {
            Initialized = new EventWaitHandle(false, EventResetMode.ManualReset);
        }

        class Subscriber : IDisposable
        {
            public Subscriber(QtProjectTracker tracker, ConfiguredProject config)
            {
                Tracker = tracker;
                Config = config;
                Subscription = Config.Services.ProjectSubscription.JointRuleSource.SourceBlock
                    .LinkTo(new SubscriberAction(ProjectUpdateAsync),
                        ruleNames: new[]
                        {
                            "ClCompile",
                            "QtRule10_Settings",
                            "QtRule30_Moc",
                            "QtRule40_Rcc",
                            "QtRule60_Repc",
                            "QtRule50_Uic",
                            "QtRule_Translation",
                            "QtRule70_Deploy",
                        },
                        initialDataAsNew: false
                    );
            }

            QtProjectTracker Tracker { get; set; }
            ConfiguredProject Config { get; set; }
            IDisposable Subscription { get; set; }

            public void Dispose()
            {
                Subscription?.Dispose();
                Subscription = null;
            }

            async Task ProjectUpdateAsync(IProjectVersionedValue<IProjectSubscriptionUpdate> update)
            {
                await Tracker.OnProjectUpdateAsync(Config, update.Value);
            }
        }

        public EnvDTE.Project Project { get; private set; }
        public UnconfiguredProject UnconfiguredProject { get; private set; }
        public EventWaitHandle Initialized { get; private set; }
        List<Subscriber> Subscribers { get; set; }

        public static bool IsTracked(EnvDTE.Project project)
        {
            return Instances.ContainsKey(project.FullName);
        }

        public static void Add(EnvDTE.Project project)
        {
            if (!QtVsToolsPackage.Instance.Options.ProjectTracking)
                return;
            Get(project);
        }

        public static QtProjectTracker Get(EnvDTE.Project project)
        {
            lock (StaticCriticalSection) {
                QtProjectTracker tracker = null;
                if (Instances.TryGetValue(project.FullName, out tracker))
                    return tracker;
                tracker = new QtProjectTracker
                {
                    Project = project,
                };
                Instances[project.FullName] = tracker;
                InitQueue.Enqueue(tracker);
                if (InitDispatcher == null)
                    InitDispatcher = Task.Run(InitDispatcherLoopAsync);
                return tracker;
            }
        }

        public static void Reset()
        {
            lock (StaticCriticalSection) {
                Instances.Clear();
                InitQueue.Clear();
            }
        }

        static async Task InitDispatcherLoopAsync()
        {
            while (!QtVsToolsPackage.Instance.Zombied) {
                while (InitQueue.IsEmpty)
                    await Task.Delay(100);
                QtProjectTracker tracker;
                if (InitQueue.TryDequeue(out tracker)) {
                    if (InitStatus == null) {
                        await QtVsToolsPackage.Instance.JoinableTaskFactory.SwitchToMainThreadAsync();
                        tracker.BeginInitStatus();
                        await TaskScheduler.Default;
                    } else {
                        tracker.UpdateInitStatus(0);
                    }
                    await tracker.InitializeAsync();
                }
                if (InitStatus != null
                    && (InitQueue.IsEmpty
                    || InitStatus.UserCancellation.IsCancellationRequested)) {
                    if (InitStatus.UserCancellation.IsCancellationRequested) {
                        InitQueue.Clear();
                    }
                    tracker.EndInitStatus();
                }
            }
        }

        async Task InitializeAsync()
        {
            int p = 0;
            UpdateInitStatus(p += 10);

            await QtVsToolsPackage.Instance.JoinableTaskFactory.SwitchToMainThreadAsync();
            UpdateInitStatus(p += 10);

            var context = Project.Object as IVsBrowseObjectContext;
            if (context == null)
                return;
            UpdateInitStatus(p += 10);

            UnconfiguredProject = context.UnconfiguredProject;
            if (UnconfiguredProject == null
                || UnconfiguredProject.ProjectService == null
                || UnconfiguredProject.ProjectService.Services == null)
                return;
            await TaskScheduler.Default;
            UpdateInitStatus(p += 10);

            var configs = await UnconfiguredProject.Services
                .ProjectConfigurationsService.GetKnownProjectConfigurationsAsync();
            UpdateInitStatus(p += 10);

            Initialized.Set();

            Subscribers = new List<Subscriber>();
            int n = configs.Count;
            int d = (100 - p) / (n * 2);
            foreach (var config in configs) {
                var configProject = await UnconfiguredProject.LoadConfiguredProjectAsync(config);
                UpdateInitStatus(p += d);
                Subscribers.Add(new Subscriber(this, configProject));
                configProject.ProjectUnloading += OnProjectUnloading;
                if (QtVsToolsPackage.Instance.Options.BuildDebugInformation) {
                    Messages.Print(string.Format(
                        "{0:HH:mm:ss.FFF} QtProjectTracker({1}): Started tracking [{2}] {3}",
                        DateTime.Now, Thread.CurrentThread.ManagedThreadId,
                        config.Name,
                        UnconfiguredProject.FullPath));
                }
                UpdateInitStatus(p += d);
            }
        }


        async Task OnProjectUpdateAsync(ConfiguredProject config, IProjectSubscriptionUpdate update)
        {
            var changes = update.ProjectChanges.Values
                .Where(x => x.Difference.AnyChanges)
                .Select(x => x.Difference);
            var changesCount = changes
                .Select(x => x.AddedItems.Count
                    + x.ChangedItems.Count
                    + x.ChangedProperties.Count
                    + x.RemovedItems.Count
                    + x.RenamedItems.Count)
                .Sum();
            var changedProps = changes.SelectMany(x => x.ChangedProperties);
            if (changesCount == 0
                || (changesCount == 1
                    && changedProps.Count() == 1
                    && changedProps.First() == "QtLastBackgroundBuild")) {
                return;
            }

            if (QtVsToolsPackage.Instance.Options.BuildDebugInformation) {
                Messages.Print(string.Format(
                    "{0:HH:mm:ss.FFF} QtProjectTracker({1}): Changed [{2}] {3}",
                    DateTime.Now, Thread.CurrentThread.ManagedThreadId,
                    config.ProjectConfiguration.Name,
                    config.UnconfiguredProject.FullPath));
            }
            await QtProjectIntellisense.RefreshAsync(Project, config.ProjectConfiguration.Name);
        }

        async Task OnProjectUnloading(object sender, EventArgs args)
        {
            var project = sender as ConfiguredProject;
            if (project == null || project.Services == null)
                return;
            if (QtVsToolsPackage.Instance.Options.BuildDebugInformation) {
                Messages.Print(string.Format(
                    "{0:HH:mm:ss.FFF} QtProjectTracker: Stopped tracking [{1}] {2}",
                    DateTime.Now,
                    project.ProjectConfiguration.Name,
                    project.UnconfiguredProject.FullPath));
            }
            lock (CriticalSection) {
                if (Subscribers != null) {
                    Subscribers.ForEach(s => s.Dispose());
                    Subscribers.Clear();
                    Subscribers = null;
                }
                project.ProjectUnloading -= OnProjectUnloading;
                Instances.TryRemove(Project.FullName, out QtProjectTracker tracker);
            }
        }

        void BeginInitStatus()
        {
            lock (StaticCriticalSection) {
                if (InitStatus != null)
                    return;
                try {
                    InitStatus = StatusCenter.PreRegister(
                        new TaskHandlerOptions
                        {
                            Title = "Qt VS Tools: Setting up project tracking..."
                        },
                        new TaskProgressData
                        {
                            ProgressText = string.Format("{0} ({1} projects remaining)",
                                Project.Name, InitQueue.Count),
                            CanBeCanceled = true,
                            PercentComplete = 0
                        })
                        as ITaskHandler2;
                } catch (Exception e) {
                    Messages.Print(
                        e.Message + "\r\n\r\nStacktrace:\r\n" + e.StackTrace);
                }
                InitStatus.RegisterTask(new Task(() => throw new InvalidOperationException()));
            }
        }

        void UpdateInitStatus(int percentComplete)
        {
            lock (StaticCriticalSection) {
                if (InitStatus == null)
                    return;
                try {
                    InitStatus.Progress.Report(new TaskProgressData
                    {
                        ProgressText = string.Format("{0} ({1} project(s) remaining)",
                            Project.Name, InitQueue.Count),
                        CanBeCanceled = true,
                        PercentComplete = percentComplete
                    });
                } catch (Exception e) {
                    Messages.Print(
                        e.Message + "\r\n\r\nStacktrace:\r\n" + e.StackTrace);
                }
            }
        }

        void EndInitStatus()
        {
            lock (StaticCriticalSection) {
                if (InitStatus == null)
                    return;
                InitStatus.Dismiss();
                InitStatus = null;
            }
        }
    }
}