using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using NuGet.Versioning;
using Velopack.Logging;
namespace Velopack.Locators
{
    /// 
    /// A base class describing where Velopack can find key folders and files.
    /// 
    public abstract class VelopackLocator : IVelopackLocator
    {
        private static IVelopackLocator? _current;
        
        /// 
        /// The default log file name for Velopack.
        /// 
        protected const string DefaultLoggingFileName = "velopack.log";
        /// 
        /// Check if a VelopackLocator has been set for the current process.
        /// 
        public static bool IsCurrentSet => _current != null;
        /// 
        /// Get the current locator in use, this process-wide locator can be set/overridden during VelopackApp.Build().
        /// Alternatively, most methods which use locators also accept an IVelopackLocator as a parameter.
        /// 
        public static IVelopackLocator Current {
            get {
                if (_current == null)
                    throw new InvalidOperationException(
                        $"No VelopackLocator has been set. Either call {nameof(VelopackApp)}.{nameof(VelopackApp.Build)}() or provide {nameof(IVelopackLocator)} as a method parameter.");
                return _current;
            }
        }
        ///  Create a new default locator based on the current operating system. 
        public static IVelopackLocator CreateDefaultForPlatform(IProcessImpl? processImpl = null, IVelopackLogger? logger = null)
        {
            if (VelopackRuntimeInfo.IsWindows)
                return _current = new WindowsVelopackLocator(processImpl, logger);
            if (VelopackRuntimeInfo.IsOSX)
                return _current = new OsxVelopackLocator(processImpl, logger);
            if (VelopackRuntimeInfo.IsLinux)
                return _current = new LinuxVelopackLocator(processImpl, logger);
            throw new PlatformNotSupportedException($"OS platform '{VelopackRuntimeInfo.SystemOs.GetOsLongName()}' is not supported.");
        }
        internal static void SetCurrentLocator(IVelopackLocator locator)
        {
            _current = locator;
        }
        internal static IVelopackLocator GetCurrentOrCreateDefault(IProcessImpl? processImpl = null, IVelopackLogger? logger = null)
        {
            _current ??= CreateDefaultForPlatform(processImpl, logger);
            return _current;
        }
        /// 
        public abstract string? AppId { get; }
        /// 
        public abstract string? RootAppDir { get; }
        /// 
        public abstract string? PackagesDir { get; }
        /// 
        public virtual string? AppTempDir => CreateSubDirIfDoesNotExist(PackagesDir, "VelopackTemp");
        /// 
        public abstract string? UpdateExePath { get; }
        /// 
        public abstract string? AppContentDir { get; }
        /// 
        public abstract string? Channel { get; }
        /// 
        public abstract IVelopackLogger Log { get; }
        /// 
        public virtual bool IsPortable => false;
        /// 
        public abstract IProcessImpl Process { get; }
        /// 
        public virtual string? ThisExeRelativePath {
            get {
                if (AppContentDir == null) return null;
                var path = Process.GetCurrentProcessPath();
                if (path.StartsWith(AppContentDir, StringComparison.OrdinalIgnoreCase)) {
                    return path.Substring(AppContentDir.Length + 1);
                } else {
                    throw new InvalidOperationException(path + " is not contained in " + AppContentDir);
                }
            }
        }
        /// 
        public abstract SemanticVersion? CurrentlyInstalledVersion { get; }
        /// 
        public virtual List GetLocalPackages()
        {
            try {
                if (CurrentlyInstalledVersion == null)
                    return new List(0);
                var list = new List();
                if (PackagesDir is { } packagesDir) {
                    foreach (var pkg in Directory.EnumerateFiles(packagesDir, "*.nupkg")) {
                        try {
                            var asset = VelopackAsset.FromNupkg(pkg);
                            if (asset?.Version != null) {
                                list.Add(asset);
                            }
                        } catch (Exception ex) {
                            Log.Warn(ex, $"Error while reading local package '{pkg}'.");
                        }
                    }
                }
                return list;
            } catch (Exception ex) {
                Log.Error(ex, "Error while reading local packages.");
                return new List(0);
            }
        }
        /// 
        public virtual VelopackAsset? GetLatestLocalFullPackage()
        {
            return GetLocalPackages()
                .OrderByDescending(x => x.Version)
                .FirstOrDefault(a => a.Type == VelopackAssetType.Full);
        }
        /// 
        /// Given a base dir and a directory name, will create a new sub directory of that name.
        /// Will return null if baseDir is null, or if baseDir does not exist. 
        /// 
        protected static string? CreateSubDirIfDoesNotExist(string? baseDir, string? newDir)
        {
            if (String.IsNullOrEmpty(baseDir) || string.IsNullOrEmpty(newDir)) return null;
            var infoBase = new DirectoryInfo(baseDir);
            if (!infoBase.Exists) return null;
            var info = new DirectoryInfo(Path.Combine(baseDir, newDir));
            if (!info.Exists) info.Create();
            return info.FullName;
        }
        /// 
        public Guid? GetOrCreateStagedUserId()
        {
            if (PackagesDir == null) return null;
            var stagedUserIdFile = Path.Combine(PackagesDir, ".betaId");
            Guid ret;
            if (File.Exists(stagedUserIdFile)) {
                try {
                    if (!Guid.TryParse(File.ReadAllText(stagedUserIdFile, Encoding.UTF8), out ret)) {
                        throw new Exception("File was read but contents were invalid");
                    }
                    Log.Info($"Loaded existing staging userId: {ret} from {stagedUserIdFile}");
                    return ret;
                } catch (Exception ex) {
                    Log.Debug(ex, "Couldn't read staging userId, creating a new one");
                }
            } else {
                Log.Warn($"No staging userId in file '{stagedUserIdFile}', creating a new one.");
            }
            ret = Guid.NewGuid();
            try {
                File.WriteAllText(stagedUserIdFile, ret.ToString("N"), Encoding.UTF8);
                Log.Info($"Generated new staging userId: {ret}");
                return ret;
            } catch (Exception ex) {
                Log.Warn(ex, "Couldn't write out staging userId.");
                return null;
            }
        }
    }
}