/*
  KeePass Password Safe - The Open-Source Password Manager
  Copyright (C) 2003-2020 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.Drawing;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;

using KeePassLib.Utility;

namespace KeePass.UI
{
	public sealed class RichTextBuilder
	{
		private StringBuilder m_sb = new StringBuilder();

		private static List<RtfbTag> g_vTags = null;
		private static bool? g_obURtf = null;

		private Font m_fDefault = null;
		public Font DefaultFont
		{
			get { return m_fDefault; }
			set { m_fDefault = value; }
		}

		private sealed class RtfbTag
		{
			public string IdCode { get; private set; }
			public string RtfCode { get; private set; }
			public bool StartTag { get; private set; }
			public FontStyle Style { get; private set; }

			public RtfbTag(string strId, string strRtf, bool bStartTag, FontStyle fs)
			{
				if(string.IsNullOrEmpty(strId)) strId = GenerateRandomIdCode();
				this.IdCode = strId;

				this.RtfCode = strRtf;
				this.StartTag = bStartTag;
				this.Style = fs;
			}

			internal static string GenerateRandomIdCode()
			{
				StringBuilder sb = new StringBuilder(15); // 1 + 12 + 2
				Random r = Program.GlobalRandom;

				// On Hebrew systems, IDs starting with digits break
				// the RTF generation; for safety, we do not use
				// digits at all
				sb.Append((char)('A' + r.Next(26)));
				for(int i = 0; i < 12; ++i)
				{
					int n = r.Next(52); // 62
					if(n < 26) sb.Append((char)('A' + n));
					// else if(n < 52)
					else sb.Append((char)('a' + (n - 26)));
					// else sb.Append((char)('0' + (n - 52)));
				}

				return sb.ToString();
			}

#if DEBUG
			// For debugger display
			public override string ToString()
			{
				return (this.IdCode + " => " + (this.RtfCode ?? string.Empty));
			}
#endif
		}

		public RichTextBuilder()
		{
			EnsureInitializedStatic();
		}

		private static void EnsureInitializedStatic()
		{
			if(g_vTags != null) return;

			// When running under Mono, replace bold and italic text
			// by underlined text, which is rendered correctly (in
			// contrast to bold and italic text)
			string strOvrS = null, strOvrE = null;
			if(MonoWorkarounds.IsRequired(1632))
			{
				strOvrS = "\\ul ";
				strOvrE = "\\ul0 ";
			}

			List<RtfbTag> l = new List<RtfbTag>();
			l.Add(new RtfbTag(null, (strOvrS ?? "\\b "), true, FontStyle.Bold));
			l.Add(new RtfbTag(null, (strOvrE ?? "\\b0 "), false, FontStyle.Bold));
			l.Add(new RtfbTag(null, (strOvrS ?? "\\i "), true, FontStyle.Italic));
			l.Add(new RtfbTag(null, (strOvrE ?? "\\i0 "), false, FontStyle.Italic));
			l.Add(new RtfbTag(null, "\\ul ", true, FontStyle.Underline));
			l.Add(new RtfbTag(null, "\\ul0 ", false, FontStyle.Underline));
			l.Add(new RtfbTag(null, "\\strike ", true, FontStyle.Strikeout));
			l.Add(new RtfbTag(null, "\\strike0 ", false, FontStyle.Strikeout));
			g_vTags = l;
		}

		public static KeyValuePair<string, string> GetStyleIdCodes(FontStyle fs)
		{
			string strL = null, strR = null;

			foreach(RtfbTag rTag in g_vTags)
			{
				if(rTag.Style == fs)
				{
					if(rTag.StartTag) strL = rTag.IdCode;
					else strR = rTag.IdCode;
				}
			}

			return new KeyValuePair<string, string>(strL, strR);
		}

		public void Append(string str)
		{
			m_sb.Append(str);
		}

		public void AppendLine()
		{
			m_sb.AppendLine();
		}

		public void AppendLine(string str)
		{
			m_sb.AppendLine(str);
		}

		public void Append(string str, FontStyle fs)
		{
			Append(str, fs, null, null, null, null);
		}

		public void Append(string str, FontStyle fs, string strOuterPrefix,
			string strInnerPrefix, string strInnerSuffix, string strOuterSuffix)
		{
			KeyValuePair<string, string> kvpTags = GetStyleIdCodes(fs);

			if(!string.IsNullOrEmpty(strOuterPrefix)) m_sb.Append(strOuterPrefix);

			if(!string.IsNullOrEmpty(kvpTags.Key)) m_sb.Append(kvpTags.Key);
			if(!string.IsNullOrEmpty(strInnerPrefix)) m_sb.Append(strInnerPrefix);
			m_sb.Append(str);
			if(!string.IsNullOrEmpty(strInnerSuffix)) m_sb.Append(strInnerSuffix);
			if(!string.IsNullOrEmpty(kvpTags.Value)) m_sb.Append(kvpTags.Value);

			if(!string.IsNullOrEmpty(strOuterSuffix)) m_sb.Append(strOuterSuffix);
		}

		public void AppendLine(string str, FontStyle fs, string strOuterPrefix,
			string strInnerPrefix, string strInnerSuffix, string strOuterSuffix)
		{
			Append(str, fs, strOuterPrefix, strInnerPrefix, strInnerSuffix, strOuterSuffix);
			m_sb.AppendLine();
		}

		public void AppendLine(string str, FontStyle fs)
		{
			Append(str, fs);
			m_sb.AppendLine();
		}

		private static RichTextBox CreateOpRtb(RichTextBox rtbUI)
		{
			RichTextBox rtbOp = new RichTextBox();
			rtbOp.Visible = false;
			rtbOp.DetectUrls = false;
			rtbOp.HideSelection = true;
			rtbOp.Multiline = true;
			rtbOp.WordWrap = false;

			// rtbOp.BorderStyle = rtbUI.BorderStyle;
			// rtbOp.ScrollBars = rtbUI.ScrollBars;
			rtbOp.Size = rtbUI.Size;

			if(rtbUI.RightToLeft == RightToLeft.Yes)
			{
				rtbOp.RightToLeft = RightToLeft.Yes;
				// rtbOp.SelectionAlignment = HorizontalAlignment.Right;
			}

			return rtbOp;
		}

		private static bool MayGetURtf(RichTextBox rtbUI)
		{
			if(!g_obURtf.HasValue)
			{
				using(RichTextBox rtb = CreateOpRtb(rtbUI))
				{
					// Ensure Rtf != null and encourage Unicode
					rtb.Text = "\u0413\u043E\u0434\r\n\u0397\u03C4\u03B1\r\n" +
						"\u30CE\u30FC\u30C8\r\n\u2211\u2026\u20AC";

					g_obURtf = StrUtil.RtfIsURtf(rtb.Rtf);
				}
			}

			return g_obURtf.Value;
		}

		public void Build(RichTextBox rtb)
		{
			Build(rtb, false);
		}

		internal void Build(RichTextBox rtb, bool bBracesBalanced)
		{
			string strRtf = null;
			Build(rtb, bBracesBalanced, ref strRtf);
		}

		internal bool Build(RichTextBox rtb, bool bBracesBalanced,
			ref string strLastRtf)
		{
			if(rtb == null) throw new ArgumentNullException("rtb");

			string strText = m_sb.ToString();
			bool bURtf = MayGetURtf(rtb);

			// Workaround for encoding bugs in Windows and Mono;
			// Windows: https://sourceforge.net/p/keepass/bugs/1780/
			// Mono: https://bugzilla.novell.com/show_bug.cgi?id=586901
			Dictionary<char, string> dEnc = new Dictionary<char, string>();
			if(bURtf || MonoWorkarounds.IsRequired(586901))
			{
				StringBuilder sbEnc = new StringBuilder();

				for(int i = 0; i < strText.Length; ++i)
				{
					char ch = strText[i];

					if(ch <= '\u00FF') sbEnc.Append(ch);
					else
					{
						string strCharEnc;
						if(!dEnc.TryGetValue(ch, out strCharEnc))
						{
							strCharEnc = RtfbTag.GenerateRandomIdCode();
							dEnc[ch] = strCharEnc;
						}

						sbEnc.Append(strCharEnc);
					}
				}

				strText = sbEnc.ToString();
			}

			string strRtf;
			using(RichTextBox rtbOp = CreateOpRtb(rtb))
			{
				string strFlt = StrUtil.RtfFilterText(strText);
				rtbOp.Text = strFlt;
				Debug.Assert(rtbOp.Text == strFlt); // Test committed

				if(m_fDefault != null)
				{
					rtbOp.SelectAll();
					rtbOp.SelectionFont = m_fDefault;
				}

				strRtf = rtbOp.Rtf;
			}

			foreach(KeyValuePair<char, string> kvpEnc in dEnc)
			{
				strRtf = strRtf.Replace(kvpEnc.Value,
					StrUtil.RtfEncodeChar(kvpEnc.Key));
			}
			foreach(RtfbTag rTag in g_vTags)
			{
				strRtf = strRtf.Replace(rTag.IdCode, rTag.RtfCode);
			}

			if(bBracesBalanced && MonoWorkarounds.IsRequired(2449941153U))
				strRtf = Regex.Replace(strRtf,
					@"(\\)(\{[\u0020-\u005B\u005D-z\w\s]*?)(\})", "$1$2$1$3");

			strRtf = StrUtil.RtfFix(strRtf);

			if(strRtf == strLastRtf) return false;
			rtb.Rtf = strRtf;
			strLastRtf = strRtf;
			return true;
		}
	}
}
