using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor.TestTools.CodeCoverage.CommandLineParser;
using UnityEditor.TestTools.CodeCoverage.Utils;
using UnityEngine;

namespace UnityEditor.TestTools.CodeCoverage
{
    internal class CommandLineManager : CommandLineManagerImplementation
    {
        private static CommandLineManager s_Instance = null;

        public static CommandLineManager instance
        {
            get
            {
                if (s_Instance == null)
                    s_Instance = new CommandLineManager();

                return s_Instance;
            }
        }

        protected CommandLineManager() : base(Environment.GetCommandLineArgs())
        {
        }
    }

    internal class CommandLineManagerImplementation
    {
        public bool runFromCommandLine
        {
            get;
            private set;
        }

        public string coverageResultsPath
        {
            get;
            private set;
        }

        public string coverageHistoryPath
        {
            get;
            private set;
        }

        public bool generateAdditionalMetrics
        {
            get;
            private set;
        }

        public bool generateTestReferences
        {
            get;
            private set;
        }

        public bool generateHTMLReportHistory
        {
            get;
            private set;
        }

        public bool generateHTMLReport
        {
            get;
            private set;
        }

        public bool generateBadgeReport
        {
            get;
            private set;
        }

        public bool generateAdditionalReports
        {
            get;
            private set;
        }

        public bool useProjectSettings
        {
            get;
            private set;
        }

        public bool generateRootEmptyReport
        {
            get;
            private set;
        }

        public bool dontClear
        {
            get;
            private set;
        }

        public bool verbosityLevelSpecified
        {
            get;
            private set;
        }

        public bool assemblyFiltersSpecified
        {
            get;
            private set;
        }

        public bool assemblyFiltersFromFileSpecified
        {
            get;
            private set;
        }

        public bool pathFiltersSpecified
        {
            get;
            private set;
        }

        public bool pathFiltersFromFileSpecified
        {
            get;
            private set;
        }

        public bool pathReplacingSpecified
        {
            get;
            private set;
        }

        public string sourcePaths
        {
            get;
            private set;
        }

        public bool sourcePathsSpecified
        {
            get;
            private set;
        }

        public AssemblyFiltering assemblyFiltering
        {
            get;
            private set;
        }

        public PathFiltering pathFiltering
        {
            get;
            private set;
        }

        public PathReplacing pathReplacing
        {
            get;
            private set;
        }

        public bool runTests
        {
            get;
            private set;
        }

        public bool batchmode
        {
            get;
            private set;
        }

        public bool burstDisabled
        {
            get;
            private set;
        }

        private string m_CoverageOptionsArg;
        private string m_IncludeAssemblies;
        private string m_ExcludeAssemblies;
        private string m_IncludePaths;
        private string m_ExcludePaths;
        private string m_PathReplacePatterns;

        public CommandLineManagerImplementation(string[] commandLineArgs)
        {
            runFromCommandLine = false;
            coverageResultsPath = string.Empty;
            coverageHistoryPath = string.Empty;
            sourcePaths = string.Empty;
            generateAdditionalMetrics = false;
            generateTestReferences = false;
            generateHTMLReportHistory = false;
            generateHTMLReport = false;
            generateBadgeReport = false;
            generateAdditionalReports = false;
            useProjectSettings = false;
            generateRootEmptyReport = false;
            dontClear = false;
            verbosityLevelSpecified = false;
            assemblyFiltersSpecified = false;
            pathFiltersSpecified = false;
            pathReplacingSpecified = false;
            sourcePathsSpecified = false;
            pathFiltersFromFileSpecified = false;
            assemblyFiltering = new AssemblyFiltering();
            pathFiltering = new PathFiltering();
            pathReplacing = new PathReplacing();
            runTests = false;
            batchmode = false;
            burstDisabled = false;

            m_CoverageOptionsArg = string.Empty;
            m_IncludeAssemblies = string.Empty;
            m_ExcludeAssemblies = string.Empty;
            m_IncludePaths = string.Empty;
            m_ExcludePaths = string.Empty;
            m_PathReplacePatterns = string.Empty;

            CommandLineOptionSet optionSet = new CommandLineOptionSet(
                new CommandLineOption("enableCodeCoverage", () => { runFromCommandLine = true; }),
                new CommandLineOption("coverageResultsPath", filePathArg => { SetCoverageResultsPath(filePathArg); }),
                new CommandLineOption("coverageHistoryPath", filePathArg => { SetCoverageHistoryPath(filePathArg); }),
                new CommandLineOption("coverageOptions", optionsArg => { AddCoverageOptions(optionsArg); }),
                new CommandLineOption("runTests", () => { runTests = true; }),
                new CommandLineOption("batchmode", () => { batchmode = true; }),
                new CommandLineOption("-burst-disable-compilation", () => { burstDisabled = true; })
            );
            optionSet.Parse(commandLineArgs);

            ValidateCoverageResultsPath();
            ValidateCoverageHistoryPath();

            if (runFromCommandLine)
                ParseCoverageOptions();
        }

        private void SetCoverageResultsPath(string filePathArg)
        {
            if (coverageResultsPath != string.Empty)
            {
                ResultsLogger.Log(ResultID.Warning_MultipleResultsPaths, coverageResultsPath);
            }
            else
            {
                if (filePathArg != null)
                {
                    coverageResultsPath = CoverageUtils.NormaliseFolderSeparators(filePathArg);
                }
            }
        }

        private void ValidateCoverageResultsPath()
        {
            if (!CoverageUtils.EnsureFolderExists(coverageResultsPath))
                coverageResultsPath = string.Empty;
        }

        private void SetCoverageHistoryPath(string filePathArg)
        {
            if (coverageHistoryPath != string.Empty)
            {
                ResultsLogger.Log(ResultID.Warning_MultipleHistoryPaths, coverageHistoryPath);
            }
            else
            {
                if (filePathArg != null)
                {
                    coverageHistoryPath = CoverageUtils.NormaliseFolderSeparators(filePathArg);
                }
            }
        }

        private void ValidateCoverageHistoryPath()
        {
            if (!CoverageUtils.EnsureFolderExists(coverageHistoryPath))
                coverageHistoryPath = string.Empty;
        }

        private void AddCoverageOptions(string coverageOptionsArg)
        {
            if (coverageOptionsArg != null)
            {
                coverageOptionsArg = coverageOptionsArg.Trim('\'');

                if (coverageOptionsArg != string.Empty)
                {
                    if (m_CoverageOptionsArg == string.Empty)
                    {
                        m_CoverageOptionsArg = coverageOptionsArg;
                    }
                    else
                    {
                        m_CoverageOptionsArg += ";";
                        m_CoverageOptionsArg += coverageOptionsArg;
                    }
                }
            }
        }

        private void ParseCoverageOptions()
        {
            // Make sure there is no trailing quotes at the end of the options
            m_CoverageOptionsArg = m_CoverageOptionsArg.TrimEnd('"');

            // 'sourcePaths' option is moved at the beginning to ensure it is handled first,
            // since it may be needed for other options
            var options = m_CoverageOptionsArg.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries).ToList();
            if (options.Count > 1)
            {
                var sourcePath = options.FirstOrDefault(option => option.StartsWith("SOURCEPATHS:", StringComparison.InvariantCultureIgnoreCase));
                if (sourcePath != null)
                {
                    var sourcePathIndex = options.IndexOf(sourcePath);
                    var firstElement = options[0];
                    options[sourcePathIndex] = firstElement;
                    options[0] = sourcePath;
                }
            }

            string[] coverageOptions = options.ToArray();

            foreach (string optionArgsStr in coverageOptions)
            {
                if (optionArgsStr.Length == 0)
                    continue;

                string optionName = optionArgsStr;
                string optionArgs = string.Empty;

                int indexOfColon = optionArgsStr.IndexOf(':');
                if (indexOfColon > 0)
                {
                    optionName = optionArgsStr.Substring(0, indexOfColon);
                    optionArgs = optionArgsStr.Substring(indexOfColon+1);
                }

                switch (optionName.ToUpperInvariant())
                {
                    case "GENERATEADDITIONALMETRICS":
                        generateAdditionalMetrics = true;
                        break;

                    case "GENERATEHTMLREPORTHISTORY":
                        generateHTMLReportHistory = true;
                        break;

                    case "GENERATEHTMLREPORT":
                        generateHTMLReport = true;
                        break;

                    case "GENERATEBADGEREPORT":
                        generateBadgeReport = true;
                        break;

                    case "GENERATEADDITIONALREPORTS":
                        generateAdditionalReports = true;
                        break;

                    case "GENERATEROOTEMPTYREPORT":
                        generateRootEmptyReport = true;
                        break;

                    case "DONTCLEAR":
                        dontClear = true;
                        break;

                    case "GENERATETESTREFERENCES":
                        generateTestReferences = true;
                        break;

                    case "USEPROJECTSETTINGS":
                        if (batchmode)
                            useProjectSettings = true;
                        else
                            ResultsLogger.Log(ResultID.Warning_UseProjectSettingsNonBatchmode);
                        break;

                    case "VERBOSITY":
                        if (optionArgs.Length > 0)
                        {
                            verbosityLevelSpecified = true;

                            switch (optionArgs.ToUpperInvariant())
                            {
                                case "VERBOSE":
                                    ResultsLogger.VerbosityLevel = LogVerbosityLevel.Verbose;
                                    break;
                                case "INFO":
                                    ResultsLogger.VerbosityLevel = LogVerbosityLevel.Info;
                                    break;
                                case "WARNING":
                                    ResultsLogger.VerbosityLevel = LogVerbosityLevel.Warning;
                                    break;
                                case "ERROR":
                                    ResultsLogger.VerbosityLevel = LogVerbosityLevel.Error;
                                    break;
                                case "OFF":
                                    ResultsLogger.VerbosityLevel = LogVerbosityLevel.Off;
                                    break;
                            }
                        }
                        break;

                    case "ASSEMBLYFILTERS":
                        if (optionArgs.Length > 0)
                        {
                            assemblyFiltersSpecified = true;

                            string[] assemblyFilters = optionArgs.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

                            ParseAssemblyFilters(assemblyFilters);
                        }
                        break;

                    case "PATHFILTERSFROMFILE":
                        if (optionArgs.Length > 0)
                        {
                            pathFiltersFromFileSpecified = true;
                            ResultsLogger.Log(ResultID.Warning_PathFiltersFromFileDeprecation);

                            if (File.Exists(optionArgs))
                            {
                                try
                                {
                                    ParsePathFilters( GetPathFiltersFromFile(optionArgs) );
                                }
                                catch (Exception e)
                                {
                                    ResultsLogger.Log(ResultID.Warning_FailedToExtractPathFiltersFromFile, e.Message, optionArgs);
                                }
                            }
                        }
                        break;

                    case "PATHFILTERS":
                        if (optionArgs.Length > 0)
                        {
                            string[] pathFilters = optionArgs.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

                            ParsePathFilters(pathFilters);
                        }
                        break;

                    case "FILTERSFROMFILE":
                        if (optionArgs.Length > 0)
                        {
                            try
                            {
                                JsonFile jsonFile = GetFiltersFromFile(optionArgs);
                                if (jsonFile != null)
                                {
                                    string[] pathFilters = ConvertToFilterArray(jsonFile.pathsInclude, jsonFile.pathsExclude);
                                    if (pathFilters != null && pathFilters.Length > 0)
                                    {
                                        pathFiltersFromFileSpecified = true;
                                        ParsePathFilters(pathFilters);
                                    }

                                    string[] assemblyFilters = ConvertToFilterArray(jsonFile.assembliesInclude, jsonFile.assembliesExclude);
                                    if (assemblyFilters != null && assemblyFilters.Length > 0)
                                    {
                                        assemblyFiltersFromFileSpecified = true;
                                        ParseAssemblyFilters(assemblyFilters);
                                    }
                                }
                            }
                            catch (Exception e)
                            {
                                ResultsLogger.Log(ResultID.Warning_FailedToExtractFiltersFromFile, e.Message, optionArgs);
                            }
                        }
                            break;
                        

                    case "PATHREPLACEPATTERNS":
                        if (optionArgs.Length > 0)
                        {
                            pathReplacingSpecified = true;
                            m_PathReplacePatterns = optionArgs;
                        }
                        break;

                    case "SOURCEPATHS":
                        if (optionArgs.Length > 0)
                        {
                            string[] rawSourcePaths = optionArgs.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                            for (int i = 0; i < rawSourcePaths.Length; ++i)
                            {
                                if (sourcePaths.Length > 0)
                                    sourcePaths += ",";
                                sourcePaths += CoverageUtils.NormaliseFolderSeparators(rawSourcePaths[i]);
                            }

                            if (sourcePaths.Length > 0)
                                sourcePathsSpecified = true;
                        }
                        break;

                    default:
                        ResultsLogger.Log(ResultID.Warning_UnknownCoverageOptionProvided, optionArgsStr);
                        break;
                }
            }

            if (m_IncludeAssemblies.Length == 0)
            {
                // If there are no inlcudedAssemblies specified but there are includedPaths specified
                // then include all project assemblies so path filtering can take precedence over assembly filtering,
                // othewise if there are no includedPaths specified neither then inlcude just the user assemblies (found under the Assets folder)

                if (m_IncludePaths.Length > 0)
                    m_IncludeAssemblies = AssemblyFiltering.GetAllProjectAssembliesString();
                else
                    m_IncludeAssemblies = AssemblyFiltering.GetUserOnlyAssembliesString();
            }

            assemblyFiltering.Parse(m_IncludeAssemblies, m_ExcludeAssemblies);
            pathFiltering.Parse(m_IncludePaths, m_ExcludePaths);
            pathReplacing.Parse(m_PathReplacePatterns);
        }

        private void ParseAssemblyFilters(string[] assemblyFilters)
        {
            for (int i = 0; i < assemblyFilters.Length; ++i)
            {
                string filter = assemblyFilters[i];
                string filterBody = filter.Length > 1 ? filter.Substring(1) : string.Empty;

                if (filter.StartsWith("+", StringComparison.OrdinalIgnoreCase))
                {
                    if (m_IncludeAssemblies.Length > 0)
                        m_IncludeAssemblies += ",";

                    if (filterBody.StartsWith("<", StringComparison.OrdinalIgnoreCase))
                    {
                        if (string.Equals(filterBody, AssemblyFiltering.kAssetsAlias, StringComparison.OrdinalIgnoreCase))
                            m_IncludeAssemblies += AssemblyFiltering.GetUserOnlyAssembliesString();
                        else if (string.Equals(filterBody, AssemblyFiltering.kAllAlias, StringComparison.OrdinalIgnoreCase))
                            m_IncludeAssemblies += AssemblyFiltering.GetAllProjectAssembliesString();
                        else if (string.Equals(filterBody, AssemblyFiltering.kPackagesAlias, StringComparison.OrdinalIgnoreCase))
                            m_IncludeAssemblies += AssemblyFiltering.GetPackagesOnlyAssembliesString();
                        else if (string.Equals(filterBody, AssemblyFiltering.kCoreAlias, StringComparison.OrdinalIgnoreCase))
                            m_IncludeAssemblies += AssemblyFiltering.kCoreAssemblies;
                    }
                    else
                    {
                        m_IncludeAssemblies += filterBody;
                    }
                }
                else if (filter.StartsWith("-", StringComparison.OrdinalIgnoreCase))
                {
                    if (m_ExcludeAssemblies.Length > 0)
                        m_ExcludeAssemblies += ",";

                    if (filterBody.StartsWith("<", StringComparison.OrdinalIgnoreCase))
                    {
                        if (string.Equals(filterBody, AssemblyFiltering.kAssetsAlias, StringComparison.OrdinalIgnoreCase))
                            m_ExcludeAssemblies += AssemblyFiltering.GetUserOnlyAssembliesString();
                        else if (string.Equals(filterBody, AssemblyFiltering.kAllAlias, StringComparison.OrdinalIgnoreCase))
                            m_ExcludeAssemblies += AssemblyFiltering.GetAllProjectAssembliesString();
                        else if (string.Equals(filterBody, AssemblyFiltering.kPackagesAlias, StringComparison.OrdinalIgnoreCase))
                            m_ExcludeAssemblies += AssemblyFiltering.GetPackagesOnlyAssembliesString();
                        else if (string.Equals(filterBody, AssemblyFiltering.kCoreAlias, StringComparison.OrdinalIgnoreCase))
                            m_ExcludeAssemblies += AssemblyFiltering.kCoreAssemblies;
                    }
                    else
                    {
                        m_ExcludeAssemblies += filterBody;
                    }
                }
                else
                {
                    ResultsLogger.Log(ResultID.Warning_AssemblyFiltersNotPrefixed, filter);
                }
            }
        }

        private void ParsePathFilters(string[] pathFilters)
        {
            var sources = new string[0];
            if (sourcePathsSpecified)
                sources = sourcePaths.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

            for (int i = 0; i < pathFilters.Length; ++i)
            {
                string filter = pathFilters[i];
                string filterBody = filter.Length > 1 ? filter.Substring(1) : string.Empty;

                var isRelative = !filterBody.StartsWith("*") && !filterBody.StartsWith("?") && string.IsNullOrEmpty(Path.GetPathRoot(filterBody));
                //If current path is relative - expand it to an absolute path using specified source paths
                if (isRelative && sourcePathsSpecified)
                {
                    string expandedPaths = string.Empty;
                    foreach (var source in sources)
                    {
                        if (expandedPaths.Length > 0)
                            expandedPaths += ",";
                        expandedPaths += CoverageUtils.NormaliseFolderSeparators(Path.Combine(source, filterBody));
                    }
                    filterBody = expandedPaths;
                }

                if (filter.StartsWith("+", StringComparison.OrdinalIgnoreCase))
                {
                    if (m_IncludePaths.Length > 0)
                        m_IncludePaths += ",";
                    m_IncludePaths += filterBody;
                }
                else if (filter.StartsWith("-", StringComparison.OrdinalIgnoreCase))
                {
                    if (m_ExcludePaths.Length > 0)
                        m_ExcludePaths += ",";
                    m_ExcludePaths += filterBody;
                }
                else
                {
                    ResultsLogger.Log(ResultID.Warning_PathFiltersNotPrefixed, filter);
                }
            }

            if (m_IncludePaths.Length > 0 || m_ExcludePaths.Length > 0)
                pathFiltersSpecified = true;
        }

        internal string[] GetPathFiltersFromFile(string path)
        {
            var paths = new List<string>();

            foreach (var line in File.ReadAllLines(path))
            {
                var entry = line.Trim();
                paths.Add(CoverageUtils.NormaliseFolderSeparators(entry));
            }

            return paths.ToArray();
        }

        internal JsonFile GetFiltersFromFile(string path)
        {
            string jsonString = JsonUtils.CleanJsonString(File.ReadAllText(path));
            JsonUtils.ValidateJsonKeys(jsonString);
            JsonFile jsonFile = JsonUtility.FromJson<JsonFile>(jsonString);
            return jsonFile;
        }

        internal string[] ConvertToFilterArray(string[] include, string[] exclude)
        {
            var filtersList = new List<string>();

            if (include != null && include.Length > 0)
            {
                foreach (var filter in include)
                {
                    filtersList.Add($"+{filter}");
                }
            }
            if (exclude != null && exclude.Length > 0)
            {
                foreach (var filter in exclude)
                {
                    filtersList.Add($"-{filter}");
                }
            }
            return filtersList.ToArray();
        }
    }
}
