// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;

namespace Microsoft.AspNetCore.Razor.Language
{
    /// <summary>
    /// An abstraction for working with a project containing Razor files.
    /// </summary>
    public abstract class RazorProject
    {
        /// <summary>
        /// Gets a sequence of <see cref="RazorProjectItem"/> under the specific path in the project.
        /// </summary>
        /// <param name="basePath">The base path.</param>
        /// <returns>The sequence of <see cref="RazorProjectItem"/>.</returns>
        /// <remarks>
        /// Project items returned by this method have inferred FileKinds from their corresponding file paths.
        /// </remarks>
        public abstract IEnumerable<RazorProjectItem> EnumerateItems(string basePath);

        /// <summary>
        /// Gets a <see cref="RazorProjectItem"/> for the specified path.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <returns>The <see cref="RazorProjectItem"/>.</returns>

        public abstract RazorProjectItem GetItem(string path);

        /// <summary>
        /// Gets a <see cref="RazorProjectItem"/> for the specified path.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <param name="fileKind">The file kind</param>
        /// <returns>The <see cref="RazorProjectItem"/>.</returns>
        public abstract RazorProjectItem GetItem(string path, string fileKind);

        /// <summary>
        /// Gets the sequence of files named <paramref name="fileName"/> that are applicable to the specified path.
        /// </summary>
        /// <param name="path">The path of a project item.</param>
        /// <param name="fileName">The file name to seek.</param>
        /// <returns>A sequence of applicable <see cref="RazorProjectItem"/> instances.</returns>
        /// <remarks>
        /// This method returns paths starting from the directory of <paramref name="path"/> and
        /// traverses to the project root.
        /// e.g.
        /// /Views/Home/View.cshtml -> [ /Views/Home/FileName.cshtml, /Views/FileName.cshtml, /FileName.cshtml ]
        ///
        /// Project items returned by this method have inferred FileKinds from their corresponding file paths.
        /// </remarks>
        public IEnumerable<RazorProjectItem> FindHierarchicalItems(string path, string fileName)
        {
            return FindHierarchicalItems(basePath: "/", path: path, fileName: fileName);
        }

        /// <summary>
        /// Gets the sequence of files named <paramref name="fileName"/> that are applicable to the specified path.
        /// </summary>
        /// <param name="basePath">The base path.</param>
        /// <param name="path">The path of a project item.</param>
        /// <param name="fileName">The file name to seek.</param>
        /// <returns>A sequence of applicable <see cref="RazorProjectItem"/> instances.</returns>
        /// <remarks>
        /// This method returns paths starting from the directory of <paramref name="path"/> and
        /// traverses to the <paramref name="basePath"/>.
        /// e.g.
        /// (/Views, /Views/Home/View.cshtml) -> [ /Views/Home/FileName.cshtml, /Views/FileName.cshtml ]
        ///
        /// Project items returned by this method have inferred FileKinds from their corresponding file paths.
        /// </remarks>
        public virtual IEnumerable<RazorProjectItem> FindHierarchicalItems(string basePath, string path, string fileName)
        {
            basePath = NormalizeAndEnsureValidPath(basePath);
            path = NormalizeAndEnsureValidPath(path);
            if (string.IsNullOrEmpty(fileName))
            {
                throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(fileName));
            }

            Debug.Assert(!string.IsNullOrEmpty(path));
            if (path.Length == 1)
            {
                yield break;
            }

            if (!path.StartsWith(basePath, StringComparison.OrdinalIgnoreCase))
            {
                yield break;
            }

            StringBuilder builder;
            var fileNameIndex = path.LastIndexOf('/');
            var length = path.Length;
            Debug.Assert(fileNameIndex != -1);
            if (string.Compare(path, fileNameIndex + 1, fileName, 0, fileName.Length, StringComparison.Ordinal) == 0)
            {
                // If the specified path is for the file hierarchy being constructed, then the first file that applies
                // to it is in a parent directory.
                builder = new StringBuilder(path, 0, fileNameIndex, fileNameIndex + fileName.Length);
                length = fileNameIndex;
            }
            else
            {
                builder = new StringBuilder(path);
            }

            var maxDepth = 255;
            var index = length;
            while (maxDepth-- > 0 && index > basePath.Length && (index = path.LastIndexOf('/', index - 1)) != -1)
            {
                builder.Length = index + 1;
                builder.Append(fileName);

                var itemPath = builder.ToString();
                yield return GetItem(itemPath, fileKind: null);
            }
        }

        /// <summary>
        /// Performs validation for paths passed to methods of <see cref="RazorProject"/>.
        /// </summary>
        /// <param name="path">The path to validate.</param>
        protected virtual string NormalizeAndEnsureValidPath(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(path));
            }

            if (path[0] != '/')
            {
                throw new ArgumentException(Resources.RazorProject_PathMustStartWithForwardSlash, nameof(path));
            }

            return path;
        }
    }
}
