/*
  KeePass Password Safe - The Open-Source Password Manager
  Copyright (C) 2003-2025 Dominik Reichl <dominik.reichl@t-online.de>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

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

using KeePassLib.Cryptography.PasswordGenerator;
using KeePassLib.Utility;

namespace KeePassLib.Cryptography
{
	/// <summary>
	/// A class that offers static functions to estimate the quality of
	/// passwords.
	/// </summary>
	public static class QualityEstimation
	{
		private static class PatternID
		{
			internal const char LowerAlpha = 'L';
			internal const char UpperAlpha = 'U';
			internal const char Digit = 'D';
			internal const char Special = 'S';
			internal const char Latin1S = 'H';
			internal const char Other = 'X';

			internal const char Dictionary = 'W';
			internal const char Repetition = 'R';
			internal const char Number = 'N';
			internal const char DiffSeq = 'C';

			internal const string All = "LUDSHXWRNC";
		}

		// private static class CharDistrib
		// {
		//	public static readonly ulong[] LowerAlpha = new ulong[26] {
		//		884, 211, 262, 249, 722, 98, 172, 234, 556, 124, 201, 447, 321,
		//		483, 518, 167, 18, 458, 416, 344, 231, 105, 80, 48, 238, 76
		//	};
		//	public static readonly ulong[] UpperAlpha = new ulong[26] {
		//		605, 188, 209, 200, 460, 81, 130, 163, 357, 122, 144, 332, 260,
		//		317, 330, 132, 18, 320, 315, 250, 137, 76, 60, 36, 161, 54
		//	};
		//	public static readonly ulong[] Digit = new ulong[10] {
		//		574, 673, 524, 377, 339, 336, 312, 310, 357, 386
		//	};
		// }

		private sealed class QeCharType
		{
			private readonly char m_chTypeID;
			public char TypeID { get { return m_chTypeID; } }

			private readonly string m_strAlph;
			public string Alphabet { get { return m_strAlph; } }

			private readonly int m_nChars;
			public int CharCount { get { return m_nChars; } }

			private readonly char m_chFirst;
			private readonly char m_chLast;

			private readonly double m_dblCharSize;
			public double CharSize { get { return m_dblCharSize; } }

			public QeCharType(char chTypeID, string strAlphabet, bool bIsConsecutive)
			{
				if(strAlphabet == null) throw new ArgumentNullException();
				if(strAlphabet.Length == 0) throw new ArgumentException();

				m_chTypeID = chTypeID;
				m_strAlph = strAlphabet;
				m_nChars = m_strAlph.Length;
				m_chFirst = (bIsConsecutive ? m_strAlph[0] : char.MinValue);
				m_chLast = (bIsConsecutive ? m_strAlph[m_nChars - 1] : char.MinValue);

				m_dblCharSize = Log2(m_nChars);

				Debug.Assert(((int)(m_chLast - m_chFirst) == (m_nChars - 1)) ||
					!bIsConsecutive);
			}

			public QeCharType(char chTypeID, int nChars) // Catch-none set
			{
				if(nChars <= 0) throw new ArgumentOutOfRangeException();

				m_chTypeID = chTypeID;
				m_strAlph = string.Empty;
				m_nChars = nChars;
				m_chFirst = char.MinValue;
				m_chLast = char.MinValue;

				m_dblCharSize = Log2(m_nChars);
			}

			public bool Contains(char ch)
			{
				if(m_chLast != char.MinValue)
					return ((ch >= m_chFirst) && (ch <= m_chLast));

				Debug.Assert(m_strAlph.Length > 0); // Don't call for catch-none set
				return (m_strAlph.IndexOf(ch) >= 0);
			}
		}

		private sealed class EntropyEncoder
		{
			private readonly string m_strAlph;
			private readonly Dictionary<char, ulong> m_dHisto = new Dictionary<char, ulong>();
			private readonly ulong m_uBaseWeight;
			private readonly ulong m_uCharWeight;
			private readonly ulong m_uOccExclThreshold;

			public EntropyEncoder(string strAlphabet, ulong uBaseWeight,
				ulong uCharWeight, ulong uOccExclThreshold)
			{
				if(strAlphabet == null) throw new ArgumentNullException();
				if(strAlphabet.Length == 0) throw new ArgumentException();

				m_strAlph = strAlphabet;
				m_uBaseWeight = uBaseWeight;
				m_uCharWeight = uCharWeight;
				m_uOccExclThreshold = uOccExclThreshold;

				Debug.Assert(m_strAlph.Distinct().Count() == m_strAlph.Length);
			}

			public void Reset()
			{
				m_dHisto.Clear();
			}

			public void Write(char ch)
			{
				Debug.Assert(m_strAlph.IndexOf(ch) >= 0);

				ulong uOcc;
				m_dHisto.TryGetValue(ch, out uOcc);
				Debug.Assert(m_dHisto.ContainsKey(ch) || (uOcc == 0));
				m_dHisto[ch] = uOcc + 1;
			}

			public double GetOutputSize()
			{
				ulong uTotalWeight = m_uBaseWeight * (ulong)m_strAlph.Length;
				foreach(ulong u in m_dHisto.Values)
				{
					Debug.Assert(u >= 1);
					if(u > m_uOccExclThreshold)
						uTotalWeight += (u - m_uOccExclThreshold) * m_uCharWeight;
				}

				double dSize = 0.0, dTotalWeight = (double)uTotalWeight;
				foreach(ulong u in m_dHisto.Values)
				{
					ulong uWeight = m_uBaseWeight;
					if(u > m_uOccExclThreshold)
						uWeight += (u - m_uOccExclThreshold) * m_uCharWeight;

					dSize -= (double)u * Log2((double)uWeight / dTotalWeight);
				}

				return dSize;
			}
		}

		private sealed class MultiEntropyEncoder
		{
			private readonly Dictionary<char, EntropyEncoder> m_dEncs =
				new Dictionary<char, EntropyEncoder>();

			public MultiEntropyEncoder()
			{
			}

			public void AddEncoder(char chTypeID, EntropyEncoder ec)
			{
				if(ec == null) { Debug.Assert(false); return; }

				Debug.Assert(!m_dEncs.ContainsKey(chTypeID));
				m_dEncs[chTypeID] = ec;
			}

			public void Reset()
			{
				foreach(EntropyEncoder ec in m_dEncs.Values) { ec.Reset(); }
			}

			public bool Write(char chTypeID, char chData)
			{
				EntropyEncoder ec;
				if(!m_dEncs.TryGetValue(chTypeID, out ec))
					return false;

				ec.Write(chData);
				return true;
			}

			public double GetOutputSize()
			{
				double d = 0.0;

				foreach(EntropyEncoder ec in m_dEncs.Values)
				{
					d += ec.GetOutputSize();
				}

				return d;
			}
		}

		private sealed class QePatternInstance
		{
			private readonly int m_iPos;
			public int Position { get { return m_iPos; } }

			private readonly int m_nLen;
			public int Length { get { return m_nLen; } }

			private readonly char m_chPatternID;
			public char PatternID { get { return m_chPatternID; } }

			private readonly double m_dblCost;
			public double Cost { get { return m_dblCost; } }

			private readonly QeCharType m_ctSingle;
			public QeCharType SingleCharType { get { return m_ctSingle; } }

			public QePatternInstance(int iPosition, int nLength, char chPatternID,
				double dblCost)
			{
				m_iPos = iPosition;
				m_nLen = nLength;
				m_chPatternID = chPatternID;
				m_dblCost = dblCost;
				m_ctSingle = null;
			}

			public QePatternInstance(int iPosition, int nLength, QeCharType ctSingle)
			{
				m_iPos = iPosition;
				m_nLen = nLength;
				m_chPatternID = ctSingle.TypeID;
				m_dblCost = ctSingle.CharSize;
				m_ctSingle = ctSingle;
			}
		}

		private sealed class QePathState
		{
			public readonly int Position;
			public readonly List<QePatternInstance> Path;

			public QePathState(int iPosition, List<QePatternInstance> lPath)
			{
				this.Position = iPosition;
				this.Path = lPath;
			}
		}

		private static readonly object m_objSyncInit = new object();
		private static List<QeCharType> m_lCharTypes = null;

		private static void EnsureInitialized()
		{
			lock(m_objSyncInit)
			{
				if(m_lCharTypes == null)
				{
					string strSpecial = PwCharSet.PrintableAsciiSpecial;
					if(strSpecial.IndexOf(' ') >= 0) { Debug.Assert(false); }
					else strSpecial += " ";

					int nSp = strSpecial.Length;
					int nL1S = PwCharSet.Latin1S.Length;

					m_lCharTypes = new List<QeCharType>()
					{
						new QeCharType(PatternID.LowerAlpha, PwCharSet.LowerCase, true),
						new QeCharType(PatternID.UpperAlpha, PwCharSet.UpperCase, true),
						new QeCharType(PatternID.Digit, PwCharSet.Digits, true),
						new QeCharType(PatternID.Special, strSpecial, false),
						new QeCharType(PatternID.Latin1S, PwCharSet.Latin1S, false),
						new QeCharType(PatternID.Other, 0x10000 - (2 * 26) - 10 - nSp - nL1S)
					};
				}
			}
		}

		/// <summary>
		/// Estimate the quality of a password.
		/// </summary>
		/// <param name="vPassword">Password to check.</param>
		/// <returns>Estimated bit-strength of the password.</returns>
		public static uint EstimatePasswordBits(char[] vPassword)
		{
			if(vPassword == null) { Debug.Assert(false); return 0; }
			if(vPassword.Length == 0) return 0;

			EnsureInitialized();

			int n = vPassword.Length;
			List<QePatternInstance>[] vPatterns = new List<QePatternInstance>[n];
			for(int i = 0; i < n; ++i)
			{
				vPatterns[i] = new List<QePatternInstance>();

				QePatternInstance piChar = new QePatternInstance(i, 1,
					GetCharType(vPassword[i]));
				vPatterns[i].Add(piChar);
			}

			FindRepetitions(vPassword, vPatterns);
			FindNumbers(vPassword, vPatterns);
			FindDiffSeqs(vPassword, vPatterns);
			FindPopularPasswords(vPassword, vPatterns);

			// Encoders must not be static, because the entropy estimation
			// may run concurrently in multiple threads and the encoders are
			// not read-only
			EntropyEncoder ecPattern = new EntropyEncoder(PatternID.All, 0, 1, 0);
			MultiEntropyEncoder mcData = new MultiEntropyEncoder();
			for(int i = 0; i < (m_lCharTypes.Count - 1); ++i)
			{
				// Let m be the alphabet size. In order to ensure that two same
				// characters cost at least as much as a single character, for
				// the probability p and weight w of the character it must hold:
				//     -log(1/m) >= -2*log(p)
				// <=> log(1/m) <= log(p^2) <=> 1/m <= p^2 <=> p >= sqrt(1/m);
				//     sqrt(1/m) = (1+w)/(m+w)
				// <=> m+w = (1+w)*sqrt(m) <=> m+w = sqrt(m) + w*sqrt(m)
				// <=> w*(1-sqrt(m)) = sqrt(m) - m <=> w = (sqrt(m)-m)/(1-sqrt(m))
				// <=> w = (sqrt(m)-m)*(1+sqrt(m))/(1-m)
				// <=> w = (sqrt(m)-m+m-m*sqrt(m))/(1-m) <=> w = sqrt(m)
				ulong uw = (ulong)Math.Sqrt((double)m_lCharTypes[i].CharCount);

				mcData.AddEncoder(m_lCharTypes[i].TypeID, new EntropyEncoder(
					m_lCharTypes[i].Alphabet, 1, uw, 1));
			}

			double dblMinCost = (double)int.MaxValue;
			int tStart = Environment.TickCount;

			Stack<QePathState> sRec = new Stack<QePathState>();
			sRec.Push(new QePathState(0, new List<QePatternInstance>()));
			while(sRec.Count > 0)
			{
				int tDiff = Environment.TickCount - tStart;
				if(tDiff > 500) break;

				QePathState s = sRec.Pop();

				if(s.Position >= n)
				{
					Debug.Assert(s.Position == n);

					double dblCost = ComputePathCost(s.Path, vPassword,
						ecPattern, mcData);
					if(dblCost < dblMinCost) dblMinCost = dblCost;
				}
				else
				{
					List<QePatternInstance> lSubs = vPatterns[s.Position];
					for(int i = lSubs.Count - 1; i >= 0; --i)
					{
						QePatternInstance pi = lSubs[i];
						Debug.Assert(pi.Position == s.Position);
						Debug.Assert(pi.Length >= 1);

						List<QePatternInstance> lNewPath =
							new List<QePatternInstance>(s.Path.Count + 1);
						lNewPath.AddRange(s.Path);
						lNewPath.Add(pi);
						Debug.Assert(lNewPath.Capacity == (s.Path.Count + 1));

						QePathState sNew = new QePathState(s.Position +
							pi.Length, lNewPath);
						sRec.Push(sNew);
					}
				}
			}

			return (uint)Math.Ceiling(dblMinCost);
		}

		/// <summary>
		/// Estimate the quality of a password.
		/// </summary>
		/// <param name="pbUnprotectedUtf8">Password to check, UTF-8 encoded.</param>
		/// <returns>Estimated bit-strength of the password.</returns>
		public static uint EstimatePasswordBits(byte[] pbUnprotectedUtf8)
		{
			if(pbUnprotectedUtf8 == null) { Debug.Assert(false); return 0; }

			char[] v = StrUtil.Utf8.GetChars(pbUnprotectedUtf8);
			uint r;
			try { r = EstimatePasswordBits(v); }
			finally { MemUtil.ZeroArray<char>(v); }

			return r;
		}

		private static QeCharType GetCharType(char ch)
		{
			int nTypes = m_lCharTypes.Count;
			Debug.Assert((nTypes > 0) && (m_lCharTypes[nTypes - 1].CharCount > 256));

			for(int i = 0; i < (nTypes - 1); ++i)
			{
				if(m_lCharTypes[i].Contains(ch))
					return m_lCharTypes[i];
			}

			return m_lCharTypes[nTypes - 1];
		}

		private static double ComputePathCost(List<QePatternInstance> l,
			char[] vPassword, EntropyEncoder ecPattern, MultiEntropyEncoder mcData)
		{
			ecPattern.Reset();
			for(int i = 0; i < l.Count; ++i)
				ecPattern.Write(l[i].PatternID);
			double dblPatternCost = ecPattern.GetOutputSize();

			mcData.Reset();
			double dblDataCost = 0.0;
			foreach(QePatternInstance pi in l)
			{
				QeCharType tChar = pi.SingleCharType;
				if(tChar != null)
				{
					char ch = vPassword[pi.Position];
					if(!mcData.Write(tChar.TypeID, ch))
						dblDataCost += pi.Cost;
				}
				else dblDataCost += pi.Cost;
			}
			dblDataCost += mcData.GetOutputSize();

			return (dblPatternCost + dblDataCost);
		}

		private static void FindPopularPasswords(char[] vPassword,
			List<QePatternInstance>[] vPatterns)
		{
			int n = vPassword.Length;

			char[] vLower = new char[n];
			char[] vLeet = new char[n];
			for(int i = 0; i < n; ++i)
			{
				char ch = vPassword[i];

				vLower[i] = char.ToLower(ch);
				vLeet[i] = char.ToLower(DecodeLeetChar(ch));
			}

			char chErased = default(char); // The value that Array.Clear uses
			Debug.Assert(chErased == char.MinValue);

			int nMaxLen = Math.Min(n, PopularPasswords.MaxLength);
			for(int nSubLen = nMaxLen; nSubLen >= 3; --nSubLen)
			{
				if(!PopularPasswords.ContainsLength(nSubLen)) continue;

				char[] vSub = new char[nSubLen];

				for(int i = 0; i <= (n - nSubLen); ++i)
				{
					if(Array.IndexOf<char>(vLower, chErased, i, nSubLen) >= 0)
						continue;

					Array.Copy(vLower, i, vSub, 0, nSubLen);
					if(!EvalAddPopularPasswordPattern(vPatterns, vPassword,
						i, vSub, 0.0))
					{
						Array.Copy(vLeet, i, vSub, 0, nSubLen);
						if(EvalAddPopularPasswordPattern(vPatterns, vPassword,
							i, vSub, 1.5))
						{
							Array.Clear(vLower, i, nSubLen); // Not vLeet
							Debug.Assert(vLower[i] == chErased);
						}
					}
					else
					{
						Array.Clear(vLower, i, nSubLen);
						Debug.Assert(vLower[i] == chErased);
					}
				}

				MemUtil.ZeroArray<char>(vSub);
			}

			MemUtil.ZeroArray<char>(vLower);
			MemUtil.ZeroArray<char>(vLeet);
		}

		private static bool EvalAddPopularPasswordPattern(List<QePatternInstance>[] vPatterns,
			char[] vPassword, int i, char[] vSub, double dblCostPerMod)
		{
			ulong uDictSize;
			if(!PopularPasswords.IsPopularPassword(vSub, out uDictSize))
				return false;

			int n = vSub.Length;
			int d = HammingDist(vSub, 0, vPassword, i, n);

			double dblCost = Log2((double)uDictSize);

			// dblCost += log2(n binom d)
			int k = Math.Min(d, n - d);
			for(int j = n; j > (n - k); --j)
				dblCost += Log2(j);
			for(int j = k; j >= 2; --j)
				dblCost -= Log2(j);

			dblCost += dblCostPerMod * (double)d;

			vPatterns[i].Add(new QePatternInstance(i, n, PatternID.Dictionary,
				dblCost));
			return true;
		}

		private static char DecodeLeetChar(char chLeet)
		{
			if((chLeet >= '\u00C0') && (chLeet <= '\u00C6')) return 'a';
			if((chLeet >= '\u00C8') && (chLeet <= '\u00CB')) return 'e';
			if((chLeet >= '\u00CC') && (chLeet <= '\u00CF')) return 'i';
			if((chLeet >= '\u00D2') && (chLeet <= '\u00D6')) return 'o';
			if((chLeet >= '\u00D9') && (chLeet <= '\u00DC')) return 'u';
			if((chLeet >= '\u00E0') && (chLeet <= '\u00E6')) return 'a';
			if((chLeet >= '\u00E8') && (chLeet <= '\u00EB')) return 'e';
			if((chLeet >= '\u00EC') && (chLeet <= '\u00EF')) return 'i';
			if((chLeet >= '\u00F2') && (chLeet <= '\u00F6')) return 'o';
			if((chLeet >= '\u00F9') && (chLeet <= '\u00FC')) return 'u';

			char ch;
			switch(chLeet)
			{
				case '4':
				case '@':
				case '?':
				case '^':
				case '\u00AA': ch = 'a'; break;
				case '8':
				case '\u00DF': ch = 'b'; break;
				case '(':
				case '{':
				case '[':
				case '<':
				case '\u00A2':
				case '\u00A9':
				case '\u00C7':
				case '\u00E7': ch = 'c'; break;
				case '\u00D0':
				case '\u00F0': ch = 'd'; break;
				case '3':
				case '\u20AC':
				case '&':
				case '\u00A3': ch = 'e'; break;
				case '6':
				case '9': ch = 'g'; break;
				case '#': ch = 'h'; break;
				case '1':
				case '!':
				case '|':
				case '\u00A1':
				case '\u00A6': ch = 'i'; break;
				case '\u00D1':
				case '\u00F1': ch = 'n'; break;
				case '0':
				case '*':
				case '\u00A4': // Currency
				case '\u00B0': // Degree
				case '\u00D8':
				case '\u00F8': ch = 'o'; break;
				case '\u00AE': ch = 'r'; break;
				case '$':
				case '5':
				case '\u00A7': ch = 's'; break;
				case '+':
				case '7': ch = 't'; break;
				case '\u00B5': ch = 'u'; break;
				case '%':
				case '\u00D7': ch = 'x'; break;
				case '\u00A5':
				case '\u00DD':
				case '\u00FD':
				case '\u00FF': ch = 'y'; break;
				case '2': ch = 'z'; break;
				default: ch = chLeet; break;
			}

			return ch;
		}

		private static int HammingDist(char[] v1, int iOffset1,
			char[] v2, int iOffset2, int nLength)
		{
			int nDist = 0;
			for(int i = 0; i < nLength; ++i)
			{
				if(v1[iOffset1 + i] != v2[iOffset2 + i]) ++nDist;
			}

			return nDist;
		}

		private static void FindRepetitions(char[] vPassword,
			List<QePatternInstance>[] vPatterns)
		{
			int n = vPassword.Length;
			char[] v = new char[n];
			Array.Copy(vPassword, v, n);

			char chErased = char.MaxValue;
			for(int m = (n / 2); m >= 3; --m)
			{
				for(int x1 = 0; x1 <= (n - (2 * m)); ++x1)
				{
					bool bFoundRep = false;

					for(int x2 = (x1 + m); x2 <= (n - m); ++x2)
					{
						if(PartsEqual(v, x1, x2, m))
						{
							double dblCost = Log2(x1 + 1) + Log2(m);
							vPatterns[x2].Add(new QePatternInstance(x2, m,
								PatternID.Repetition, dblCost));

							ErasePart(v, x2, m, ref chErased);
							bFoundRep = true;
						}
					}

					if(bFoundRep) ErasePart(v, x1, m, ref chErased);
				}
			}

			MemUtil.ZeroArray<char>(v);
		}

		private static bool PartsEqual(char[] v, int x1, int x2, int nLength)
		{
			for(int i = 0; i < nLength; ++i)
			{
				if(v[x1 + i] != v[x2 + i]) return false;
			}

			return true;
		}

		private static void ErasePart(char[] v, int i, int n, ref char chErased)
		{
			for(int j = 0; j < n; ++j)
			{
				v[i + j] = chErased;
				--chErased;
			}
		}

		private static void FindNumbers(char[] vPassword,
			List<QePatternInstance>[] vPatterns)
		{
			int n = vPassword.Length;
			StringBuilder sb = new StringBuilder();

			for(int i = 0; i < n; ++i)
			{
				char ch = vPassword[i];
				if((ch >= '0') && (ch <= '9')) sb.Append(ch);
				else
				{
					AddNumberPattern(vPatterns, sb, i - sb.Length);
					sb.Remove(0, sb.Length);
				}
			}
			AddNumberPattern(vPatterns, sb, n - sb.Length);
		}

		private static void AddNumberPattern(List<QePatternInstance>[] vPatterns,
			StringBuilder sb, int i)
		{
			if(sb.Length <= 2) return;
			string strNumber = sb.ToString();

			int nZeros = 0;
			for(int j = 0; j < strNumber.Length; ++j)
			{
				if(strNumber[j] != '0') break;
				++nZeros;
			}

			double dblCost = Log2(nZeros + 1);
			if(nZeros < strNumber.Length)
			{
				string strNonZero = strNumber.Substring(nZeros);
				double d;
				if(StrUtil.TryParseDoubleInvariant(strNonZero, out d))
					dblCost += Log2(d);
				else { Debug.Assert(false); return; }
			}

			vPatterns[i].Add(new QePatternInstance(i, strNumber.Length,
				PatternID.Number, dblCost));
		}

		private static void FindDiffSeqs(char[] vPassword,
			List<QePatternInstance>[] vPatterns)
		{
			int n = vPassword.Length;
			int d = int.MaxValue, p = 0;

			for(int i = 1; i <= n; ++i)
			{
				int dCur = ((i == n) ? int.MinValue :
					((int)vPassword[i] - (int)vPassword[i - 1]));
				if(dCur != d)
				{
					if((i - p) >= 3) // At least 3 chars involved
					{
						QeCharType ct = GetCharType(vPassword[p]);
						double dblCost = ct.CharSize + Log2(i - p - 1);

						vPatterns[p].Add(new QePatternInstance(p,
							i - p, PatternID.DiffSeq, dblCost));
					}

					d = dCur;
					p = i - 1;
				}
			}
		}

		private static double Log2(double dblValue)
		{
#if KeePassLibSD
			return (Math.Log(dblValue) / Math.Log(2.0));
#else
			return Math.Log(dblValue, 2.0);
#endif
		}
	}
}
