/*
 * Copyright(c) 2017-2018 Syroot
 * Licensed under the MIT license. See https://gitlab.com/Syroot/KnownFolders/-/blob/master/LICENSE for full license information.
 */

using System.Runtime.InteropServices;
using System.Security.Principal;

namespace BardMusicPlayer.Seer.Utilities.KnownFolder;

/// <summary>
/// Represents a special Windows directory and provides methods to retrieve information about it.
/// </summary>
internal sealed class KnownFolder
{
#pragma warning disable CA1416
    // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------

    /// <summary>
    /// Initializes a new instance of the <see cref="KnownFolder"/> class for the folder of the given type. It
    /// provides the values for the current user.
    /// </summary>
    /// <param name="type">The <see cref="KnownFolderType"/> of the known folder to represent.</param>
    internal KnownFolder(KnownFolderType type) : this(type, WindowsIdentity.GetCurrent()) { }
#pragma warning restore CA1416

    /// <summary>
    /// Initializes a new instance of the <see cref="KnownFolder"/> class for the folder of the given type. It
    /// provides the values for the given impersonated user.
    /// </summary>
    /// <param name="type">The <see cref="KnownFolderType"/> of the known folder to represent.</param>
    /// <param name="identity">The <see cref="WindowsIdentity"/> of the impersonated user which values will be
    /// provided.</param>
    internal KnownFolder(KnownFolderType type, WindowsIdentity identity)
    {
        Type     = type;
        Identity = identity;
    }

    // ---- PROPERTIES ---------------------------------------------------------------------------------------------

    /// <summary>
    /// Gets the type of the known folder which is represented.
    /// </summary>
    internal KnownFolderType Type { get; }

    /// <summary>
    /// Gets the <see cref="WindowsIdentity"/> of the user whose folder values are provided.
    /// </summary>
    internal WindowsIdentity Identity { get; }

    /// <summary>
    /// Gets the default path of the folder. This does not require the folder to be existent.
    /// </summary>
    /// <exception cref="ExternalException">The known folder could not be retrieved.</exception>
    internal string DefaultPath => GetPath(KnownFolderFlags.DontVerify | KnownFolderFlags.DefaultPath);

    /// <summary>
    /// Gets or sets the path as currently configured. This does not require the folder to be existent.
    /// </summary>
    /// <exception cref="ExternalException">The known folder could not be retrieved.</exception>
    internal string Path
    {
        get => GetPath(KnownFolderFlags.DontVerify);
        set => SetPath(KnownFolderFlags.None, value);
    }

    /// <summary>
    /// Gets or sets the path as currently configured, with all environment variables expanded.
    /// This does not require the folder to be existent.
    /// </summary>
    /// <exception cref="ExternalException">The known folder could not be retrieved.</exception>
    internal string ExpandedPath
    {
        get => GetPath(KnownFolderFlags.DontVerify | KnownFolderFlags.NoAlias);
        set => SetPath(KnownFolderFlags.DontUnexpand, value);
    }

    // ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------

    /// <summary>
    /// Creates the folder using its Desktop.ini settings.
    /// </summary>
    /// <exception cref="ExternalException">The known folder could not be retrieved.</exception>
    internal void Create() { GetPath(KnownFolderFlags.Init | KnownFolderFlags.Create); }

    // ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
#pragma warning disable CA1416
    private string GetPath(KnownFolderFlags flags)
    {
        var result = SHGetKnownFolderPath(Type.GetGuid(), (uint) flags, Identity.Token, out var outPath);
        if (result >= 0)
        {
            var path = Marshal.PtrToStringUni(outPath);
            Marshal.FreeCoTaskMem(outPath);
            return path;
        }

        throw new ExternalException("Cannot get the known folder path. It may not be available on this system.",
            result);
    }

    private void SetPath(KnownFolderFlags flags, string path)
    {
        var result = SHSetKnownFolderPath(Type.GetGuid(), (uint) flags, Identity.Token, path);
        if (result < 0)
        {
            throw new ExternalException("Cannot set the known folder path. It may not be available on this system.",
                result);
        }
    }
#pragma warning restore CA1416

    /// <summary>
    /// Retrieves the full path of a known folder identified by the folder's known folder ID.
    /// </summary>
    /// <param name="rfid">A known folder ID that identifies the folder.</param>
    /// <param name="dwFlags">Flags that specify special retrieval options. This value can be 0; otherwise, one or
    /// more of the <see cref="KnownFolderFlags"/> values.</param>
    /// <param name="hToken">An access token that represents a particular user. If this parameter is NULL, which is
    /// the most common usage, the function requests the known folder for the current user. Assigning a value of -1
    /// indicates the Default User. The default user profile is duplicated when any new user account is created.
    /// Note that access to the Default User folders requires administrator privileges.</param>
    /// <param name="ppszPath">When this method returns, contains the address of a string that specifies the path of
    /// the known folder. The returned path does not include a trailing backslash.</param>
    /// <returns>Returns S_OK if successful, or an error value otherwise.</returns>
    /// <msdn-id>bb762188</msdn-id>
    [DllImport("Shell32.dll")]
    private static extern int SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags,
        IntPtr hToken, out IntPtr ppszPath);

    /// <summary>
    /// Redirects a known folder to a new location.
    /// </summary>
    /// <param name="rfid">A <see cref="Guid"/> that identifies the known folder.</param>
    /// <param name="dwFlags">Either 0 or <see cref="KnownFolderFlags.DontUnexpand"/>.</param>
    /// <param name="hToken"></param>
    /// <param name="pszPath"></param>
    /// <returns></returns>
    /// <msdn-id>bb762249</msdn-id>
    [DllImport("Shell32.dll")]
    private static extern int SHSetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags,
        IntPtr hToken, [MarshalAs(UnmanagedType.LPWStr)] string pszPath);

    // ---- ENUMERATIONS -------------------------------------------------------------------------------------------

    /// <summary>
    /// Represents the retrieval options for known folders.
    /// </summary>
    /// <msdn-id>dd378447</msdn-id>
    [Flags]
    private enum KnownFolderFlags : uint
    {
        None = 0x00000000,
        SimpleIDList = 0x00000100,
        NotParentRelative = 0x00000200,
        DefaultPath = 0x00000400,
        Init = 0x00000800,
        NoAlias = 0x00001000,
        DontUnexpand = 0x00002000,
        DontVerify = 0x00004000,
        Create = 0x00008000,
        NoAppcontainerRedirection = 0x00010000,
        AliasOnly = 0x80000000
    }
}