// MIT license, https://github.com/jjxtra

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;

using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;

namespace BetterEmbeddedFileProviderNamespace
{
public class BetterEmbeddedFileProvider : IFileProvider, IChangeToken
{
	private static readonly string[] extensions = new string[]
	{
		".min.js",
		".min.css",
		".json",
		".js",
		".css",
		".xml",
		".htm",
		".html",
		".zip",
		".cshtml",
		".txt",
		".ico",
		".png",
		".jpg",
		".jpeg",
		".webp",
		".gif",
		".svg",
		".less",
		".scss"
	};

	private static readonly DummyDisposable dummyDisposable = new DummyDisposable();
	private class DummyDisposable : IDisposable
	{
		public void Dispose() { }
	}
	private class EmbeddedDirectory : IDirectoryContents
	{
		private static readonly List<EmbeddedFile> emptyList = new List<EmbeddedFile>(0);

		private readonly List<EmbeddedFile> files;

		public EmbeddedDirectory(List<EmbeddedFile> files)
		{
			this.files = files;
		}

		public bool Exists => true;

		public IEnumerator<IFileInfo> GetEnumerator()
		{
			return files.GetEnumerator();
		}

		IEnumerator IEnumerable.GetEnumerator()
		{
			return files.GetEnumerator();
		}
	}
	private class EmbeddedFile : IFileInfo
	{
		private static readonly DateTime lastModified = DateTime.UtcNow;

		private readonly Assembly assembly;
		private readonly string resourcePath;
		private readonly string filePath;
		private readonly string name;
		private readonly long length;
		private readonly bool isDirectory;
		private readonly bool exists;

		public EmbeddedFile(Assembly assembly, string resourcePath, string filePath, bool isDirectory)
		{
			this.assembly = assembly;
			this.resourcePath = resourcePath;
			this.filePath = filePath;
			this.isDirectory = isDirectory;
			if (resourcePath == null)
			{
				exists = false;
				name = "?";
			}
			else
			{
				exists = true;
				if (!isDirectory)
				{
					using (Stream s = assembly.GetManifestResourceStream(resourcePath))
					{
						length = s.Length;
					}
				}
				int pos = filePath.LastIndexOf('/');
				if (pos >= 0)
				{
					name = filePath.Substring(++pos);
				}
				else
				{
					name = filePath;
				}
			}
		}

		bool IFileInfo.Exists => exists;
		long IFileInfo.Length => length;
		string IFileInfo.PhysicalPath => filePath;
		string IFileInfo.Name => name;
		DateTimeOffset IFileInfo.LastModified => lastModified;
		bool IFileInfo.IsDirectory => isDirectory;
		Stream IFileInfo.CreateReadStream()
		{
			return assembly.GetManifestResourceStream(resourcePath);
		}
	}

	private readonly Assembly assembly;
	private readonly string contentRoot;
	private readonly Dictionary<string, List<EmbeddedFile>> folders = new Dictionary<string, List<EmbeddedFile>>(StringComparer.OrdinalIgnoreCase);
	private readonly Dictionary<string, EmbeddedFile> pathsToResources = new Dictionary<string, EmbeddedFile>(StringComparer.OrdinalIgnoreCase);
	private readonly EmbeddedFile notFoundFile;
	private readonly IDirectoryContents notFoundDirectory = NotFoundDirectoryContents.Singleton;

	bool IChangeToken.HasChanged => false;
	bool IChangeToken.ActiveChangeCallbacks => false;

	/// <summary>
	/// Constructor
	/// </summary>
	/// <param name="assembly">Assembly containing resources</param>
	/// <param name="ns">Namespace of the resources</param>
	/// <param name="contentRoot">Content root, i.e. empty string or www</param>
	public BetterEmbeddedFileProvider(Assembly assembly, string ns, string contentRoot = "")
	{
		this.assembly = assembly;
		this.contentRoot = (contentRoot.Length == 0 || contentRoot == "/" ? string.Empty : ("/" + contentRoot.Trim('/')));
		string[] allResources = assembly.GetManifestResourceNames();
		string path;
		string dir;
		string extension;
		int prefixLength = ns.Length + 1;
		int pos;
		EmbeddedFile file;
		foreach (string s in allResources)
		{
			extension = null;
			foreach (string ext in extensions)
			{
				if (s.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
				{
					extension = ext;
					break;
				}
			}
			if (extension == null)
			{
				throw new InvalidDataException("Invalid extension for embedded resource " + s);
			}
			path = "/" + s.Substring(prefixLength);
			path = path.Substring(0, path.Length - extension.Length);
			path = path.Replace('.', '/');
			path += extension;
			pos = path.LastIndexOf('/');
			pathsToResources[path] = file = new EmbeddedFile(assembly, s, path, false);
			if (pos >= 0)
			{
				dir = path.Substring(0, pos);
				if (!folders.TryGetValue(dir, out List<EmbeddedFile> files))
				{
					folders[dir] = files = new List<EmbeddedFile>();
				}
				files.Add(file);
			}
		}
		notFoundFile = new EmbeddedFile(assembly, null, null, false);
	}

	public IDirectoryContents GetDirectoryContents(string subPath)
	{
		string path = contentRoot + subPath;
		if (folders.TryGetValue(path, out List<EmbeddedFile> files))
		{
			return new EmbeddedDirectory(files);
		}
		return notFoundDirectory;
	}

	public IFileInfo GetFileInfo(string subPath)
	{
		pathsToResources.TryGetValue(contentRoot + subPath, out EmbeddedFile resource);
		return (resource ?? notFoundFile);
	}

	public IChangeToken Watch(string filter)
	{
		return this;
	}

	IDisposable IChangeToken.RegisterChangeCallback(Action<object> callback, object state)
	{
		return dummyDisposable;
	}
}
}