// 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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
using Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel;
using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList;
using Microsoft.VisualStudio.LanguageServices.ProjectSystem;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.CPS;

internal sealed partial class CPSProject : IWorkspaceProjectContext
{
    private readonly ProjectSystemProject _projectSystemProject;

    /// <summary>
    /// The <see cref="ProjectSystemProjectOptionsProcessor"/> we're using to parse command line options. Null if we don't
    /// have the ability to parse command line options.
    /// </summary>
    private readonly ProjectSystemProjectOptionsProcessor? _projectSystemProjectOptionsProcessor;

    private readonly VisualStudioWorkspaceImpl _visualStudioWorkspace;
    private readonly IProjectCodeModel _projectCodeModel;
    private readonly Lazy<ProjectExternalErrorReporter?> _externalErrorReporter;

    public string DisplayName
    {
        get => _projectSystemProject.DisplayName;
        set => _projectSystemProject.DisplayName = value;
    }

    public string? ProjectFilePath
    {
        get => _projectSystemProject.FilePath;
        set => _projectSystemProject.FilePath = value;
    }

    public bool IsPrimary
    {
        get => _projectSystemProject.IsPrimary;
        set => _projectSystemProject.IsPrimary = value;
    }

    public Guid Guid
    {
        get;
        set; // VisualStudioProject doesn't allow GUID to be changed after creation
    }

    public bool LastDesignTimeBuildSucceeded
    {
        get => _projectSystemProject.HasAllInformation;
        set => _projectSystemProject.HasAllInformation = value;
    }

    public CPSProject(ProjectSystemProject projectSystemProject, VisualStudioWorkspaceImpl visualStudioWorkspace, IProjectCodeModelFactory projectCodeModelFactory, Guid projectGuid)
    {
        _projectSystemProject = projectSystemProject;
        _visualStudioWorkspace = visualStudioWorkspace;

        _externalErrorReporter = new Lazy<ProjectExternalErrorReporter?>(() =>
        {
            var prefix = projectSystemProject.Language switch
            {
                LanguageNames.CSharp => "CS",
                LanguageNames.VisualBasic => "BC",
                LanguageNames.FSharp => "FS",
                _ => null
            };

            return (prefix != null) ? new ProjectExternalErrorReporter(projectSystemProject.Id, projectGuid, prefix, projectSystemProject.Language, visualStudioWorkspace) : null;
        });

        _projectCodeModel = projectCodeModelFactory.CreateProjectCodeModel(projectSystemProject.Id, new CPSCodeModelInstanceFactory(this));

        // If we have a command line parser service for this language, also set up our ability to process options if they come in
        if (visualStudioWorkspace.Services.GetLanguageServices(projectSystemProject.Language).GetService<ICommandLineParserService>() != null)
        {
            _projectSystemProjectOptionsProcessor = new ProjectSystemProjectOptionsProcessor(_projectSystemProject, visualStudioWorkspace.Services.SolutionServices);
            _visualStudioWorkspace.AddProjectRuleSetFileToInternalMaps(
                projectSystemProject,
                () => _projectSystemProjectOptionsProcessor.EffectiveRuleSetFilePath);
        }

        Guid = projectGuid;
    }

    public string? BinOutputPath
    {
        get => _projectSystemProject.OutputFilePath;
        set
        {
            // If we don't have a path, always set it to null
            if (string.IsNullOrEmpty(value))
            {
                _projectSystemProject.OutputFilePath = null;
                return;
            }

            // If we only have a non-rooted path, make it full. This is apparently working around cases
            // where CPS pushes us a temporary path when they're loading. It's possible this hack
            // can be removed now, but we still have tests asserting it.
            if (!PathUtilities.IsAbsolute(value))
            {
                var rootDirectory = _projectSystemProject.FilePath != null
                                    ? Path.GetDirectoryName(_projectSystemProject.FilePath)
                                    : Path.GetTempPath();

                _projectSystemProject.OutputFilePath = Path.Combine(rootDirectory, value);
            }
            else
            {
                _projectSystemProject.OutputFilePath = value;
            }
        }
    }

    internal string? CompilationOutputAssemblyFilePath
    {
        get => _projectSystemProject.CompilationOutputAssemblyFilePath;
        set => _projectSystemProject.CompilationOutputAssemblyFilePath = value;
    }

    public ProjectId Id => _projectSystemProject.Id;

    public void SetOptions(string commandLineForOptions)
        => _projectSystemProjectOptionsProcessor?.SetCommandLine(commandLineForOptions);

    public void SetOptions(ImmutableArray<string> arguments)
        => _projectSystemProjectOptionsProcessor?.SetCommandLine(arguments);

    public void SetProperty(string name, string? value)
    {
        if (name == BuildPropertyNames.RootNamespace)
        {
            // Right now VB doesn't have the concept of "default namespace". But we conjure one in workspace 
            // by assigning the value of the project's root namespace to it. So various feature can choose to 
            // use it for their own purpose.
            // In the future, we might consider officially exposing "default namespace" for VB project 
            // (e.g. through a <defaultnamespace> msbuild property)
            _projectSystemProject.DefaultNamespace = value;
        }
        else if (name == BuildPropertyNames.MaxSupportedLangVersion)
        {
            _projectSystemProject.MaxLangVersion = value;
        }
        else if (name == BuildPropertyNames.RunAnalyzers)
        {
            var boolValue = bool.TryParse(value, out var parsedBoolValue) ? parsedBoolValue : (bool?)null;
            _projectSystemProject.RunAnalyzers = boolValue;
        }
        else if (name == BuildPropertyNames.RunAnalyzersDuringLiveAnalysis)
        {
            var boolValue = bool.TryParse(value, out var parsedBoolValue) ? parsedBoolValue : (bool?)null;
            _projectSystemProject.RunAnalyzersDuringLiveAnalysis = boolValue;
        }
        else if (name == BuildPropertyNames.TemporaryDependencyNodeTargetIdentifier && !RoslynString.IsNullOrEmpty(value))
        {
            _projectSystemProject.DependencyNodeTargetIdentifier = value;
        }
        else if (name == BuildPropertyNames.TargetRefPath)
        {
            _projectSystemProject.OutputRefFilePath = GetAbsolutePath(value);
        }
        else if (name == BuildPropertyNames.CompilerGeneratedFilesOutputPath)
        {
            _projectSystemProject.GeneratedFilesOutputDirectory = GetAbsolutePath(value);
        }

        string? GetAbsolutePath(string? value)
        {
            if (string.IsNullOrEmpty(value))
            {
                return null;
            }

            if (PathUtilities.IsAbsolute(value))
            {
                return value;
            }

            var rootDirectory = _projectSystemProject.FilePath != null
                ? Path.GetDirectoryName(_projectSystemProject.FilePath)
                : Path.GetTempPath();

            return Path.Combine(rootDirectory, value);
        }
    }

    public void AddMetadataReference(string referencePath, MetadataReferenceProperties properties)
    {
        referencePath = FileUtilities.NormalizeAbsolutePath(referencePath);
        _projectSystemProject.AddMetadataReference(referencePath, properties);
    }

    public void RemoveMetadataReference(string referencePath)
    {
        referencePath = FileUtilities.NormalizeAbsolutePath(referencePath);
        _projectSystemProject.RemoveMetadataReference(referencePath, _projectSystemProject.GetPropertiesForMetadataReference(referencePath).Single());
    }

    public void AddProjectReference(IWorkspaceProjectContext project, MetadataReferenceProperties properties)
    {
        var otherProjectId = ((CPSProject)project)._projectSystemProject.Id;
        _projectSystemProject.AddProjectReference(new ProjectReference(otherProjectId, properties.Aliases, properties.EmbedInteropTypes));
    }

    public void RemoveProjectReference(IWorkspaceProjectContext project)
    {
        var otherProjectId = ((CPSProject)project)._projectSystemProject.Id;
        var otherProjectReference = _projectSystemProject.GetProjectReferences().Single(pr => pr.ProjectId == otherProjectId);
        _projectSystemProject.RemoveProjectReference(otherProjectReference);
    }

    public void AddSourceFile(string filePath, bool isInCurrentContext = true, IEnumerable<string>? folderNames = null, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular)
        => _projectSystemProject.AddSourceFile(filePath, sourceCodeKind, folderNames.AsImmutableOrNull());

    public void RemoveSourceFile(string filePath)
    {
        _projectSystemProject.RemoveSourceFile(filePath);
        _projectCodeModel.OnSourceFileRemoved(filePath);
    }

    public void AddAdditionalFile(string filePath, bool isInCurrentContext = true)
        => _projectSystemProject.AddAdditionalFile(filePath);

    public void AddAdditionalFile(string filePath, IEnumerable<string> folderNames, bool isInCurrentContext = true)
        => _projectSystemProject.AddAdditionalFile(filePath, folders: [.. folderNames]);

    public void Dispose()
    {
        _projectCodeModel?.OnProjectClosed();
        _projectSystemProjectOptionsProcessor?.Dispose();
        _projectSystemProject.RemoveFromWorkspace();
    }

    public void AddAnalyzerReference(string referencePath)
        => _projectSystemProject.AddAnalyzerReference(referencePath);

    public void RemoveAnalyzerReference(string referencePath)
        => _projectSystemProject.RemoveAnalyzerReference(referencePath);

    public void RemoveAdditionalFile(string filePath)
        => _projectSystemProject.RemoveAdditionalFile(filePath);

    public void AddDynamicFile(string filePath, IEnumerable<string>? folderNames = null)
        => _projectSystemProject.AddDynamicSourceFile(filePath, folderNames.ToImmutableArrayOrEmpty());

    public void RemoveDynamicFile(string filePath)
        => _projectSystemProject.RemoveDynamicSourceFile(filePath);

    public void ReorderSourceFiles(IEnumerable<string>? filePaths)
        => _projectSystemProject.ReorderSourceFiles(filePaths.ToImmutableArrayOrEmpty());

    internal ProjectSystemProject GetProject_TestOnly()
        => _projectSystemProject;

    public void AddAnalyzerConfigFile(string filePath)
        => _projectSystemProject.AddAnalyzerConfigFile(filePath);

    public void RemoveAnalyzerConfigFile(string filePath)
        => _projectSystemProject.RemoveAnalyzerConfigFile(filePath);

    public async ValueTask<IAsyncDisposable> CreateBatchScopeAsync(CancellationToken cancellationToken)
        => await _projectSystemProject.CreateBatchScopeAsync(cancellationToken).ConfigureAwait(false);
}
