﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Editor.TaskList;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.TaskList;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell.TableControl;
using Microsoft.VisualStudio.Shell.TableManager;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource
{
    internal sealed class VisualStudioTaskListTable : AbstractTable
    {
        internal const string IdentifierString = nameof(VisualStudioTaskListTable);

        private readonly TableDataSource _source;

        public VisualStudioTaskListTable(
            Workspace workspace,
            IThreadingContext threadingContext,
            ITableManagerProvider provider,
            ITaskListProvider taskProvider)
            : base(workspace, provider, StandardTables.TasksTable)
        {
            _source = new TableDataSource(workspace, threadingContext, taskProvider, IdentifierString);
            AddInitialTableSource(workspace.CurrentSolution, _source);
            ConnectWorkspaceEvents();
        }

        internal override ImmutableArray<string> Columns { get; } = ImmutableArray.Create(
            StandardTableColumnDefinitions.Priority,
            StandardTableColumnDefinitions.Text,
            StandardTableColumnDefinitions.ProjectName,
            StandardTableColumnDefinitions.DocumentName,
            StandardTableColumnDefinitions.Line,
            StandardTableColumnDefinitions.Column);

        protected override void AddTableSourceIfNecessary(Solution solution)
        {
            if (solution.ProjectIds.Count == 0 || this.TableManager.Sources.Any(s => s == _source))
            {
                return;
            }

            AddTableSource(_source);
        }

        protected override void RemoveTableSourceIfNecessary(Solution solution)
        {
            if (solution.ProjectIds.Count > 0 || !this.TableManager.Sources.Any(s => s == _source))
            {
                return;
            }

            this.TableManager.RemoveSource(_source);
        }

        protected override void ShutdownSource()
            => _source.Shutdown();

        private class TableDataSource : AbstractRoslynTableDataSource<TaskListTableItem, TaskListUpdatedArgs>
        {
            private readonly Workspace _workspace;
            private readonly string _identifier;
            private readonly ITaskListProvider _taskProvider;

            public TableDataSource(Workspace workspace, IThreadingContext threadingContext, ITaskListProvider taskProvider, string identifier)
                : base(workspace, threadingContext)
            {
                _workspace = workspace;
                _identifier = identifier;

                _taskProvider = taskProvider;
                _taskProvider.TaskListUpdated += OnTaskListUpdated;
            }

            public override string DisplayName => ServicesVSResources.CSharp_VB_Todo_List_Table_Data_Source;
            public override string SourceTypeIdentifier => StandardTableDataSources.CommentTableDataSource;
            public override string Identifier => _identifier;
            public override object GetItemKey(TaskListUpdatedArgs data) => data.DocumentId;

            protected override object GetOrUpdateAggregationKey(TaskListUpdatedArgs data)
            {
                var key = TryGetAggregateKey(data);
                if (key == null)
                {
                    key = CreateAggregationKey(data);
                    AddAggregateKey(data, key);
                    return key;
                }

                if (key is not ImmutableArray<DocumentId>)
                {
                    return key;
                }

                if (!CheckAggregateKey((ImmutableArray<DocumentId>)key, data))
                {
                    RemoveStaledData(data);

                    key = CreateAggregationKey(data);
                    AddAggregateKey(data, key);
                }

                return key;
            }

            private bool CheckAggregateKey(ImmutableArray<DocumentId> key, TaskListUpdatedArgs args)
            {
                Contract.ThrowIfNull(args.Solution);
                Contract.ThrowIfNull(args.DocumentId);
                var documents = GetDocumentsWithSameFilePath(args.Solution, args.DocumentId);
                return key == documents;
            }

            private object CreateAggregationKey(TaskListUpdatedArgs data)
            {
                Contract.ThrowIfNull(data.Solution);
                return GetDocumentsWithSameFilePath(data.Solution, data.DocumentId);
            }

            public override AbstractTableEntriesSnapshot<TaskListTableItem> CreateSnapshot(AbstractTableEntriesSource<TaskListTableItem> source, int version, ImmutableArray<TaskListTableItem> items, ImmutableArray<ITrackingPoint> trackingPoints)
                => new TableEntriesSnapshot(ThreadingContext, version, items, trackingPoints);

            public override IEqualityComparer<TaskListTableItem> GroupingComparer
                => TaskListTableItem.GroupingComparer.Instance;

            public override IEnumerable<TaskListTableItem> Order(IEnumerable<TaskListTableItem> groupedItems)
                => groupedItems.OrderBy(d => d.Data.Span.StartLinePosition);

            private void OnTaskListUpdated(object sender, TaskListUpdatedArgs e)
            {
                if (_workspace != e.Workspace)
                {
                    return;
                }

                Debug.Assert(e.DocumentId != null);

                if (e.TaskListItems.Length == 0)
                {
                    OnDataRemoved(e);
                    return;
                }

                OnDataAddedOrChanged(e);
            }

            public override AbstractTableEntriesSource<TaskListTableItem> CreateTableEntriesSource(object data)
            {
                var item = (TaskListUpdatedArgs)data;
                return new TableEntriesSource(this, item.Workspace, item.DocumentId);
            }

            private sealed class TableEntriesSource : AbstractTableEntriesSource<TaskListTableItem>
            {
                private readonly TableDataSource _source;
                private readonly Workspace _workspace;
                private readonly DocumentId _documentId;

                public TableEntriesSource(TableDataSource source, Workspace workspace, DocumentId documentId)
                {
                    _source = source;
                    _workspace = workspace;
                    _documentId = documentId;
                }

                public override object Key => _documentId;

                public override ImmutableArray<TaskListTableItem> GetItems()
                {
                    return _source._taskProvider.GetTaskListItems(_workspace, _documentId, CancellationToken.None)
                                   .Select(data => TaskListTableItem.Create(_workspace, data))
                                   .ToImmutableArray();
                }

                public override ImmutableArray<ITrackingPoint> GetTrackingPoints(ImmutableArray<TaskListTableItem> items)
                    => _workspace.CreateTrackingPoints(_documentId, items);
            }

            private sealed class TableEntriesSnapshot : AbstractTableEntriesSnapshot<TaskListTableItem>
            {
                public TableEntriesSnapshot(IThreadingContext threadingContext, int version, ImmutableArray<TaskListTableItem> items, ImmutableArray<ITrackingPoint> trackingPoints)
                    : base(threadingContext, version, items, trackingPoints)
                {
                }

                public override bool TryGetValue(int index, string columnName, [NotNullWhen(true)] out object? content)
                {
                    // REVIEW: this method is too-chatty to make async, but otherwise, how one can implement it async?
                    //         also, what is cancellation mechanism?
                    var item = GetItem(index);

                    if (item is not { Data: var data })
                    {
                        content = null;
                        return false;
                    }

                    switch (columnName)
                    {
                        case StandardTableKeyNames.Priority:
                            content = ValueTypeCache.GetOrCreate(data.Priority switch
                            {
                                TaskListItemPriority.Low => VSTASKPRIORITY.TP_LOW,
                                TaskListItemPriority.Medium => VSTASKPRIORITY.TP_NORMAL,
                                TaskListItemPriority.High => VSTASKPRIORITY.TP_HIGH,
                                _ => VSTASKPRIORITY.TP_NORMAL,
                            });
                            return content != null;
                        case StandardTableKeyNames.Text:
                            content = data.Message;
                            return content != null;
                        case StandardTableKeyNames.DocumentName:
                            content = data.MappedSpan.HasMappedPath ? data.MappedSpan.Path : data.Span.Path;
                            return content != null;
                        case StandardTableKeyNames.Line:
                            content = GetLineColumn(item).Line;
                            return true;
                        case StandardTableKeyNames.Column:
                            content = GetLineColumn(item).Character;
                            return true;
                        case StandardTableKeyNames.TaskCategory:
                            content = ValueTypeCache.GetOrCreate(VSTASKCATEGORY.CAT_COMMENTS);
                            return content != null;
                        case StandardTableKeyNames.ProjectName:
                            content = item.ProjectName;
                            return content != null;
                        case ProjectNames:
                            var names = item.ProjectNames;
                            content = names;
                            return names.Length > 0;
                        case StandardTableKeyNames.ProjectGuid:
                            content = ValueTypeCache.GetOrCreate(item.ProjectGuid);
                            return (Guid)content != Guid.Empty;
                        case ProjectGuids:
                            var guids = item.ProjectGuids;
                            content = guids;
                            return guids.Length > 0;
                        default:
                            content = null;
                            return false;
                    }
                }

                // TODO: Apply location mapping when creating the TODO item (https://github.com/dotnet/roslyn/issues/36217)
                private static LinePosition GetLineColumn(TaskListTableItem item)
                {
                    return VisualStudioVenusSpanMappingService.GetAdjustedLineColumn(
                        item.Data.DocumentId,
                        item.Data.Span.StartLinePosition.Line,
                        item.Data.Span.StartLinePosition.Character,
                        item.Data.MappedSpan.StartLinePosition.Line,
                        item.Data.MappedSpan.StartLinePosition.Character);
                }

                public override bool TryNavigateTo(int index, NavigationOptions options, CancellationToken cancellationToken)
                    => TryNavigateToItem(index, options, cancellationToken);
            }
        }
    }
}
