﻿// 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.

#nullable disable

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
using Microsoft.VisualStudio.Shell.Interop;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.Preview
{
    internal class TopLevelChange : AbstractChange
    {
        private readonly string _name;
        private readonly Solution _newSolution;
        private readonly Glyph _glyph;

        public TopLevelChange(
            string name,
            Glyph glyph,
            Solution newSolution,
            PreviewEngine engine)
            : base(engine)
        {
            _name = name;
            _glyph = glyph;
            _newSolution = newSolution;
        }

        public override int GetText(out VSTREETEXTOPTIONS tto, out string pbstrText)
        {
            pbstrText = _name;
            tto = VSTREETEXTOPTIONS.TTO_DEFAULT;
            return VSConstants.S_OK;
        }

        public override int GetTipText(out VSTREETOOLTIPTYPE eTipType, out string pbstrText)
            => throw new NotImplementedException();

        public override int OnRequestSource(object pIUnknownTextView)
        {
            var firstChild = Children.Changes.OfType<FileChange>().First();
            return firstChild.OnRequestSource(pIUnknownTextView);
        }

        public override void UpdatePreview()
        {
            var firstChild = Children.Changes.OfType<FileChange>().First();
            firstChild.UpdatePreview();
        }

        public Solution GetUpdatedSolution(bool applyingChanges)
        {
            var solution = ApplyFileChanges(_newSolution, Children.Changes.OfType<FileChange>(), applyingChanges);
            if (applyingChanges)
            {
                solution = ApplyReferenceChanges(solution, Children.Changes.OfType<ReferenceChange>());
            }

            return solution;
        }

        private static Solution ApplyFileChanges(Solution solution, IEnumerable<FileChange> fileChanges, bool applyingChanges)
        {
            foreach (var fileChange in fileChanges)
            {
                var oldTextDocument = fileChange.GetOldDocument();
                var updatedTextDocument = fileChange.GetUpdatedDocument();
                var updatedDocumentTextOpt = updatedTextDocument?.GetTextSynchronously(CancellationToken.None);

                // Apply file change to document.
                ApplyFileChangesCore(oldTextDocument, updatedTextDocument?.Id, updatedDocumentTextOpt,
                    fileChange.CheckState, fileChange.ChangedDocumentKind);

                // Now apply file change to linked documents.
                if (oldTextDocument is Document oldDocument)
                {
                    foreach (var linkedDocumentId in oldDocument.GetLinkedDocumentIds())
                    {
                        var oldLinkedDocument = oldDocument.Project.Solution.GetDocument(linkedDocumentId);

                        // Ensure that we account for document removal, i.e. updatedDocumentTextOpt == null.
                        var newLinkedDocumentIdOpt = updatedDocumentTextOpt != null ? oldLinkedDocument.Id : null;

                        ApplyFileChangesCore(oldLinkedDocument, newLinkedDocumentIdOpt, updatedDocumentTextOpt,
                            fileChange.CheckState, fileChange.ChangedDocumentKind);
                    }
                }
                else if (updatedTextDocument is Document updatedDocument)
                {
                    foreach (var newLinkedDocumentId in updatedDocument.GetLinkedDocumentIds())
                    {
                        ApplyFileChangesCore(oldTextDocument, newLinkedDocumentId, updatedDocumentTextOpt,
                            fileChange.CheckState, fileChange.ChangedDocumentKind);
                    }
                }
            }

            return solution;

            // Local functions.
            void ApplyFileChangesCore(
                TextDocument oldDocument,
                DocumentId updatedDocumentIdOpt,
                SourceText updateDocumentTextOpt,
                __PREVIEWCHANGESITEMCHECKSTATE checkState,
                TextDocumentKind changedDocumentKind)
            {
                Debug.Assert(oldDocument != null || updatedDocumentIdOpt != null);
                Debug.Assert((updatedDocumentIdOpt != null) == (updateDocumentTextOpt != null));

                if (oldDocument == null)
                {
                    // Added document to new solution.
                    // If unchecked, then remove this added document from new solution.
                    if (applyingChanges && checkState == __PREVIEWCHANGESITEMCHECKSTATE.PCCS_Unchecked)
                    {
                        switch (changedDocumentKind)
                        {
                            case TextDocumentKind.Document:
                                solution = solution.RemoveDocument(updatedDocumentIdOpt);
                                break;

                            case TextDocumentKind.AnalyzerConfigDocument:
                                solution = solution.RemoveAnalyzerConfigDocument(updatedDocumentIdOpt);
                                break;

                            case TextDocumentKind.AdditionalDocument:
                                solution = solution.RemoveAdditionalDocument(updatedDocumentIdOpt);
                                break;

                            default:
                                throw ExceptionUtilities.UnexpectedValue(changedDocumentKind);
                        }
                    }
                }
                else if (updatedDocumentIdOpt == null)
                {
                    // Removed document from old solution.
                    // If unchecked, then add back this removed document to new solution.
                    if (applyingChanges && checkState == __PREVIEWCHANGESITEMCHECKSTATE.PCCS_Unchecked)
                    {
                        var oldText = oldDocument.GetTextSynchronously(CancellationToken.None).ToString();

                        switch (changedDocumentKind)
                        {
                            case TextDocumentKind.Document:
                                solution = solution.AddDocument(oldDocument.Id, oldDocument.Name, oldText, oldDocument.Folders, oldDocument.FilePath);
                                break;

                            case TextDocumentKind.AnalyzerConfigDocument:
                                solution = solution.AddAnalyzerConfigDocument(oldDocument.Id, oldDocument.Name, SourceText.From(oldText), oldDocument.Folders, oldDocument.FilePath);
                                break;

                            case TextDocumentKind.AdditionalDocument:
                                solution = solution.AddAdditionalDocument(oldDocument.Id, oldDocument.Name, oldText, oldDocument.Folders, oldDocument.FilePath);
                                break;

                            default:
                                throw ExceptionUtilities.UnexpectedValue(changedDocumentKind);
                        }
                    }
                }
                else
                {
                    Debug.Assert(oldDocument.Id == updatedDocumentIdOpt);

                    // Changed document.
                    solution = solution.WithTextDocumentText(updatedDocumentIdOpt, updateDocumentTextOpt, mode: PreservationMode.PreserveValue);
                }
            }
        }

        private static Solution ApplyReferenceChanges(Solution solution, IEnumerable<ReferenceChange> referenceChanges)
        {
            foreach (var referenceChange in referenceChanges)
            {
                if (referenceChange.IsAddedReference)
                {
                    // Added reference to new solution.
                    // If unchecked, then remove this added reference from new solution.
                    if (referenceChange.CheckState == __PREVIEWCHANGESITEMCHECKSTATE.PCCS_Unchecked)
                    {
                        solution = referenceChange.RemoveFromSolution(solution);
                    }
                }
                else
                {
                    // Removed reference from old solution.
                    // If unchecked, then add back this removed reference to new solution.
                    if (referenceChange.CheckState == __PREVIEWCHANGESITEMCHECKSTATE.PCCS_Unchecked)
                    {
                        solution = referenceChange.AddToSolution(solution);
                    }
                }
            }

            return solution;
        }

        internal override void GetDisplayData(VSTREEDISPLAYDATA[] pData)
            => pData[0].Image = pData[0].SelectedImage = (ushort)_glyph.GetStandardGlyphGroup();
    }
}
