/*
  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.IO;
using System.Text;
using System.Xml;

#if !ModernKeePassLib && !KeePassUAP
using System.Drawing;
#endif

using ModernKeePassLib;
using ModernKeePassLib.Collections;
using ModernKeePassLib.Cryptography;
using ModernKeePassLib.Cryptography.Cipher;
using ModernKeePassLib.Interfaces;
using ModernKeePassLib.Resources;
using ModernKeePassLib.Security;
using ModernKeePassLib.Utility;

namespace ModernKeePassLib.Serialization
{
	/// <summary>
	/// Serialization to KeePass KDBX files.
	/// </summary>
	public sealed partial class KdbxFile
	{
		private enum KdbContext
		{
			Null,
			KeePassFile,
			Meta,
			Root,
			MemoryProtection,
			CustomIcons,
			CustomIcon,
			Binaries,
			CustomData,
			CustomDataItem,
			RootDeletedObjects,
			DeletedObject,
			Group,
			GroupTimes,
			GroupCustomData,
			GroupCustomDataItem,
			Entry,
			EntryTimes,
			EntryString,
			EntryBinary,
			EntryAutoType,
			EntryAutoTypeItem,
			EntryHistory,
			EntryCustomData,
			EntryCustomDataItem
		}

		private bool m_bReadNextNode = true;
		private Stack<PwGroup> m_ctxGroups = new Stack<PwGroup>();
		private PwGroup m_ctxGroup = null;
		private PwEntry m_ctxEntry = null;
		private string m_ctxStringName = null;
		private ProtectedString m_ctxStringValue = null;
		private string m_ctxBinaryName = null;
		private ProtectedBinary m_ctxBinaryValue = null;
		private string m_ctxATName = null;
		private string m_ctxATSeq = null;
		private bool m_bEntryInHistory = false;
		private PwEntry m_ctxHistoryBase = null;
		private PwDeletedObject m_ctxDeletedObject = null;
		private PwUuid m_uuidCustomIconID = PwUuid.Zero;
		private byte[] m_pbCustomIconData = null;
		private string m_strCustomDataKey = null;
		private string m_strCustomDataValue = null;
		private string m_strGroupCustomDataKey = null;
		private string m_strGroupCustomDataValue = null;
		private string m_strEntryCustomDataKey = null;
		private string m_strEntryCustomDataValue = null;

		private void ReadXmlStreamed(Stream sXml, Stream sParent)
		{
			using(XmlReader xr = XmlUtilEx.CreateXmlReader(sXml))
			{
				ReadDocumentStreamed(xr, sParent);
			}
		}

		private void ReadDocumentStreamed(XmlReader xr, Stream sParentStream)
		{
			Debug.Assert(xr != null);
			if(xr == null) throw new ArgumentNullException("xr");

			m_ctxGroups.Clear();

			KdbContext ctx = KdbContext.Null;

			uint uTagCounter = 0;

			bool bSupportsStatus = (m_slLogger != null);
			long lStreamLength = 1;
			try
			{
				sParentStream.Position.ToString(); // Test Position support
				lStreamLength = sParentStream.Length;
			}
			catch(Exception) { bSupportsStatus = false; }
			if(lStreamLength <= 0) { Debug.Assert(false); lStreamLength = 1; }

			m_bReadNextNode = true;

			while(true)
			{
				if(m_bReadNextNode)
				{
					if(!xr.Read()) break;
				}
				else m_bReadNextNode = true;

				switch(xr.NodeType)
				{
					case XmlNodeType.Element:
						ctx = ReadXmlElement(ctx, xr);
						break;

					case XmlNodeType.EndElement:
						ctx = EndXmlElement(ctx, xr);
						break;

					case XmlNodeType.XmlDeclaration:
						break; // Ignore

					default:
						Debug.Assert(false);
						break;
				}

				++uTagCounter;
				if(((uTagCounter & 0xFFU) == 0) && bSupportsStatus)
				{
					Debug.Assert(lStreamLength == sParentStream.Length);
					uint uPct = (uint)((sParentStream.Position * 100) /
						lStreamLength);

					// Clip percent value in case the stream reports incorrect
					// position/length values (M120413)
					if(uPct > 100) { Debug.Assert(false); uPct = 100; }

					if(!m_slLogger.SetProgress(uPct))
						throw new OperationCanceledException();
				}
			}

			Debug.Assert(ctx == KdbContext.Null);
			if(ctx != KdbContext.Null) throw new FormatException();

			Debug.Assert(m_ctxGroups.Count == 0);
			if(m_ctxGroups.Count != 0) throw new FormatException();
		}

		private KdbContext ReadXmlElement(KdbContext ctx, XmlReader xr)
		{
			Debug.Assert(xr.NodeType == XmlNodeType.Element);

			switch(ctx)
			{
				case KdbContext.Null:
					if(xr.Name == ElemDocNode)
						return SwitchContext(ctx, KdbContext.KeePassFile, xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.KeePassFile:
					if(xr.Name == ElemMeta)
						return SwitchContext(ctx, KdbContext.Meta, xr);
					else if(xr.Name == ElemRoot)
						return SwitchContext(ctx, KdbContext.Root, xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.Meta:
					if(xr.Name == ElemGenerator)
						ReadString(xr); // Ignore
					else if(xr.Name == ElemHeaderHash)
					{
						// The header hash is typically only stored in
						// KDBX <= 3.1 files, not in KDBX >= 4 files
						// (here, the header is verified via a HMAC),
						// but we also support it for KDBX >= 4 files
						// (i.e. if it's present, we check it)

						string strHash = ReadString(xr);
						if(!string.IsNullOrEmpty(strHash) && (m_pbHashOfHeader != null) &&
							!m_bRepairMode)
						{
							Debug.Assert(m_uFileVersion < FileVersion32_4);

							byte[] pbHash = Convert.FromBase64String(strHash);
							if(!MemUtil.ArraysEqual(pbHash, m_pbHashOfHeader))
								throw new InvalidDataException(KLRes.FileCorrupted);
						}
					}
					else if(xr.Name == ElemSettingsChanged)
						m_pwDatabase.SettingsChanged = ReadTime(xr);
					else if(xr.Name == ElemDbName)
						m_pwDatabase.Name = ReadString(xr);
					else if(xr.Name == ElemDbNameChanged)
						m_pwDatabase.NameChanged = ReadTime(xr);
					else if(xr.Name == ElemDbDesc)
						m_pwDatabase.Description = ReadString(xr);
					else if(xr.Name == ElemDbDescChanged)
						m_pwDatabase.DescriptionChanged = ReadTime(xr);
					else if(xr.Name == ElemDbDefaultUser)
						m_pwDatabase.DefaultUserName = ReadString(xr);
					else if(xr.Name == ElemDbDefaultUserChanged)
						m_pwDatabase.DefaultUserNameChanged = ReadTime(xr);
					else if(xr.Name == ElemDbMntncHistoryDays)
						m_pwDatabase.MaintenanceHistoryDays = ReadUInt(xr, 365);
					else if(xr.Name == ElemDbColor)
					{
						string strColor = ReadString(xr);
						if(!string.IsNullOrEmpty(strColor))
							m_pwDatabase.Color = ColorTranslator.FromHtml(strColor);
					}
					else if(xr.Name == ElemDbKeyChanged)
						m_pwDatabase.MasterKeyChanged = ReadTime(xr);
					else if(xr.Name == ElemDbKeyChangeRec)
						m_pwDatabase.MasterKeyChangeRec = ReadLong(xr, -1);
					else if(xr.Name == ElemDbKeyChangeForce)
						m_pwDatabase.MasterKeyChangeForce = ReadLong(xr, -1);
					else if(xr.Name == ElemDbKeyChangeForceOnce)
						m_pwDatabase.MasterKeyChangeForceOnce = ReadBool(xr, false);
					else if(xr.Name == ElemMemoryProt)
						return SwitchContext(ctx, KdbContext.MemoryProtection, xr);
					else if(xr.Name == ElemCustomIcons)
						return SwitchContext(ctx, KdbContext.CustomIcons, xr);
					else if(xr.Name == ElemRecycleBinEnabled)
						m_pwDatabase.RecycleBinEnabled = ReadBool(xr, true);
					else if(xr.Name == ElemRecycleBinUuid)
						m_pwDatabase.RecycleBinUuid = ReadUuid(xr);
					else if(xr.Name == ElemRecycleBinChanged)
						m_pwDatabase.RecycleBinChanged = ReadTime(xr);
					else if(xr.Name == ElemEntryTemplatesGroup)
						m_pwDatabase.EntryTemplatesGroup = ReadUuid(xr);
					else if(xr.Name == ElemEntryTemplatesGroupChanged)
						m_pwDatabase.EntryTemplatesGroupChanged = ReadTime(xr);
					else if(xr.Name == ElemHistoryMaxItems)
						m_pwDatabase.HistoryMaxItems = ReadInt(xr, -1);
					else if(xr.Name == ElemHistoryMaxSize)
						m_pwDatabase.HistoryMaxSize = ReadLong(xr, -1);
					else if(xr.Name == ElemLastSelectedGroup)
						m_pwDatabase.LastSelectedGroup = ReadUuid(xr);
					else if(xr.Name == ElemLastTopVisibleGroup)
						m_pwDatabase.LastTopVisibleGroup = ReadUuid(xr);
					else if(xr.Name == ElemBinaries)
						return SwitchContext(ctx, KdbContext.Binaries, xr);
					else if(xr.Name == ElemCustomData)
						return SwitchContext(ctx, KdbContext.CustomData, xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.MemoryProtection:
					if(xr.Name == ElemProtTitle)
						m_pwDatabase.MemoryProtection.ProtectTitle = ReadBool(xr, false);
					else if(xr.Name == ElemProtUserName)
						m_pwDatabase.MemoryProtection.ProtectUserName = ReadBool(xr, false);
					else if(xr.Name == ElemProtPassword)
						m_pwDatabase.MemoryProtection.ProtectPassword = ReadBool(xr, true);
					else if(xr.Name == ElemProtUrl)
						m_pwDatabase.MemoryProtection.ProtectUrl = ReadBool(xr, false);
					else if(xr.Name == ElemProtNotes)
						m_pwDatabase.MemoryProtection.ProtectNotes = ReadBool(xr, false);
					// else if(xr.Name == ElemProtAutoHide)
					//	m_pwDatabase.MemoryProtection.AutoEnableVisualHiding = ReadBool(xr, true);
					else ReadUnknown(xr);
					break;

				case KdbContext.CustomIcons:
					if(xr.Name == ElemCustomIconItem)
						return SwitchContext(ctx, KdbContext.CustomIcon, xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.CustomIcon:
					if(xr.Name == ElemCustomIconItemID)
						m_uuidCustomIconID = ReadUuid(xr);
					else if(xr.Name == ElemCustomIconItemData)
					{
						string strData = ReadString(xr);
						if(!string.IsNullOrEmpty(strData))
							m_pbCustomIconData = Convert.FromBase64String(strData);
						else { Debug.Assert(false); }
					}
					else ReadUnknown(xr);
					break;

				case KdbContext.Binaries:
					if(xr.Name == ElemBinary)
					{
						if(xr.MoveToAttribute(AttrId))
						{
							string strKey = xr.Value;
							ProtectedBinary pbData = ReadProtectedBinary(xr);

							int iKey;
							if(!StrUtil.TryParseIntInvariant(strKey, out iKey))
								throw new FormatException();
							if(iKey < 0) throw new FormatException();

							Debug.Assert(m_pbsBinaries.Get(iKey) == null);
							Debug.Assert(m_pbsBinaries.Find(pbData) < 0);
							m_pbsBinaries.Set(iKey, pbData);
						}
						else ReadUnknown(xr);
					}
					else ReadUnknown(xr);
					break;

				case KdbContext.CustomData:
					if(xr.Name == ElemStringDictExItem)
						return SwitchContext(ctx, KdbContext.CustomDataItem, xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.CustomDataItem:
					if(xr.Name == ElemKey)
						m_strCustomDataKey = ReadString(xr);
					else if(xr.Name == ElemValue)
						m_strCustomDataValue = ReadString(xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.Root:
					if(xr.Name == ElemGroup)
					{
						Debug.Assert(m_ctxGroups.Count == 0);
						if(m_ctxGroups.Count != 0) throw new FormatException();

						m_pwDatabase.RootGroup = new PwGroup(false, false);
						m_ctxGroups.Push(m_pwDatabase.RootGroup);
						m_ctxGroup = m_ctxGroups.Peek();

						return SwitchContext(ctx, KdbContext.Group, xr);
					}
					else if(xr.Name == ElemDeletedObjects)
						return SwitchContext(ctx, KdbContext.RootDeletedObjects, xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.Group:
					if(xr.Name == ElemUuid)
						m_ctxGroup.Uuid = ReadUuid(xr);
					else if(xr.Name == ElemName)
						m_ctxGroup.Name = ReadString(xr);
					else if(xr.Name == ElemNotes)
						m_ctxGroup.Notes = ReadString(xr);
					else if(xr.Name == ElemIcon)
						m_ctxGroup.IconId = ReadIconId(xr, PwIcon.Folder);
					else if(xr.Name == ElemCustomIconID)
						m_ctxGroup.CustomIconUuid = ReadUuid(xr);
					else if(xr.Name == ElemTimes)
						return SwitchContext(ctx, KdbContext.GroupTimes, xr);
					else if(xr.Name == ElemIsExpanded)
						m_ctxGroup.IsExpanded = ReadBool(xr, true);
					else if(xr.Name == ElemGroupDefaultAutoTypeSeq)
						m_ctxGroup.DefaultAutoTypeSequence = ReadString(xr);
					else if(xr.Name == ElemEnableAutoType)
						m_ctxGroup.EnableAutoType = StrUtil.StringToBoolEx(ReadString(xr));
					else if(xr.Name == ElemEnableSearching)
						m_ctxGroup.EnableSearching = StrUtil.StringToBoolEx(ReadString(xr));
					else if(xr.Name == ElemLastTopVisibleEntry)
						m_ctxGroup.LastTopVisibleEntry = ReadUuid(xr);
					else if(xr.Name == ElemCustomData)
						return SwitchContext(ctx, KdbContext.GroupCustomData, xr);
					else if(xr.Name == ElemGroup)
					{
						m_ctxGroup = new PwGroup(false, false);
						m_ctxGroups.Peek().AddGroup(m_ctxGroup, true);

						m_ctxGroups.Push(m_ctxGroup);

						return SwitchContext(ctx, KdbContext.Group, xr);
					}
					else if(xr.Name == ElemEntry)
					{
						m_ctxEntry = new PwEntry(false, false);
						m_ctxGroup.AddEntry(m_ctxEntry, true);

						m_bEntryInHistory = false;
						return SwitchContext(ctx, KdbContext.Entry, xr);
					}
					else ReadUnknown(xr);
					break;

				case KdbContext.GroupCustomData:
					if(xr.Name == ElemStringDictExItem)
						return SwitchContext(ctx, KdbContext.GroupCustomDataItem, xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.GroupCustomDataItem:
					if(xr.Name == ElemKey)
						m_strGroupCustomDataKey = ReadString(xr);
					else if(xr.Name == ElemValue)
						m_strGroupCustomDataValue = ReadString(xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.Entry:
					if(xr.Name == ElemUuid)
						m_ctxEntry.Uuid = ReadUuid(xr);
					else if(xr.Name == ElemIcon)
						m_ctxEntry.IconId = ReadIconId(xr, PwIcon.Key);
					else if(xr.Name == ElemCustomIconID)
						m_ctxEntry.CustomIconUuid = ReadUuid(xr);
					else if(xr.Name == ElemFgColor)
					{
						string strColor = ReadString(xr);
						if(!string.IsNullOrEmpty(strColor))
							m_ctxEntry.ForegroundColor = ColorTranslator.FromHtml(strColor);
					}
					else if(xr.Name == ElemBgColor)
					{
						string strColor = ReadString(xr);
						if(!string.IsNullOrEmpty(strColor))
							m_ctxEntry.BackgroundColor = ColorTranslator.FromHtml(strColor);
					}
					else if(xr.Name == ElemOverrideUrl)
						m_ctxEntry.OverrideUrl = ReadString(xr);
					else if(xr.Name == ElemTags)
						m_ctxEntry.Tags = StrUtil.StringToTags(ReadString(xr));
					else if(xr.Name == ElemTimes)
						return SwitchContext(ctx, KdbContext.EntryTimes, xr);
					else if(xr.Name == ElemString)
						return SwitchContext(ctx, KdbContext.EntryString, xr);
					else if(xr.Name == ElemBinary)
						return SwitchContext(ctx, KdbContext.EntryBinary, xr);
					else if(xr.Name == ElemAutoType)
						return SwitchContext(ctx, KdbContext.EntryAutoType, xr);
					else if(xr.Name == ElemCustomData)
						return SwitchContext(ctx, KdbContext.EntryCustomData, xr);
					else if(xr.Name == ElemHistory)
					{
						Debug.Assert(m_bEntryInHistory == false);

						if(m_bEntryInHistory == false)
						{
							m_ctxHistoryBase = m_ctxEntry;
							return SwitchContext(ctx, KdbContext.EntryHistory, xr);
						}
						else ReadUnknown(xr);
					}
					else ReadUnknown(xr);
					break;

				case KdbContext.GroupTimes:
				case KdbContext.EntryTimes:
					ITimeLogger tl = ((ctx == KdbContext.GroupTimes) ?
						(ITimeLogger)m_ctxGroup : (ITimeLogger)m_ctxEntry);
					Debug.Assert(tl != null);

					if(xr.Name == ElemCreationTime)
						tl.CreationTime = ReadTime(xr);
					else if(xr.Name == ElemLastModTime)
						tl.LastModificationTime = ReadTime(xr);
					else if(xr.Name == ElemLastAccessTime)
						tl.LastAccessTime = ReadTime(xr);
					else if(xr.Name == ElemExpiryTime)
						tl.ExpiryTime = ReadTime(xr);
					else if(xr.Name == ElemExpires)
						tl.Expires = ReadBool(xr, false);
					else if(xr.Name == ElemUsageCount)
						tl.UsageCount = ReadULong(xr, 0);
					else if(xr.Name == ElemLocationChanged)
						tl.LocationChanged = ReadTime(xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.EntryString:
					if(xr.Name == ElemKey)
						m_ctxStringName = ReadString(xr);
					else if(xr.Name == ElemValue)
						m_ctxStringValue = ReadProtectedString(xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.EntryBinary:
					if(xr.Name == ElemKey)
						m_ctxBinaryName = ReadString(xr);
					else if(xr.Name == ElemValue)
						m_ctxBinaryValue = ReadProtectedBinary(xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.EntryAutoType:
					if(xr.Name == ElemAutoTypeEnabled)
						m_ctxEntry.AutoType.Enabled = ReadBool(xr, true);
					else if(xr.Name == ElemAutoTypeObfuscation)
						m_ctxEntry.AutoType.ObfuscationOptions =
							(AutoTypeObfuscationOptions)ReadInt(xr, 0);
					else if(xr.Name == ElemAutoTypeDefaultSeq)
						m_ctxEntry.AutoType.DefaultSequence = ReadString(xr);
					else if(xr.Name == ElemAutoTypeItem)
						return SwitchContext(ctx, KdbContext.EntryAutoTypeItem, xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.EntryAutoTypeItem:
					if(xr.Name == ElemWindow)
						m_ctxATName = ReadString(xr);
					else if(xr.Name == ElemKeystrokeSequence)
						m_ctxATSeq = ReadString(xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.EntryCustomData:
					if(xr.Name == ElemStringDictExItem)
						return SwitchContext(ctx, KdbContext.EntryCustomDataItem, xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.EntryCustomDataItem:
					if(xr.Name == ElemKey)
						m_strEntryCustomDataKey = ReadString(xr);
					else if(xr.Name == ElemValue)
						m_strEntryCustomDataValue = ReadString(xr);
					else ReadUnknown(xr);
					break;

				case KdbContext.EntryHistory:
					if(xr.Name == ElemEntry)
					{
						m_ctxEntry = new PwEntry(false, false);
						m_ctxHistoryBase.History.Add(m_ctxEntry);

						m_bEntryInHistory = true;
						return SwitchContext(ctx, KdbContext.Entry, xr);
					}
					else ReadUnknown(xr);
					break;

				case KdbContext.RootDeletedObjects:
					if(xr.Name == ElemDeletedObject)
					{
						m_ctxDeletedObject = new PwDeletedObject();
						m_pwDatabase.DeletedObjects.Add(m_ctxDeletedObject);

						return SwitchContext(ctx, KdbContext.DeletedObject, xr);
					}
					else ReadUnknown(xr);
					break;

				case KdbContext.DeletedObject:
					if(xr.Name == ElemUuid)
						m_ctxDeletedObject.Uuid = ReadUuid(xr);
					else if(xr.Name == ElemDeletionTime)
						m_ctxDeletedObject.DeletionTime = ReadTime(xr);
					else ReadUnknown(xr);
					break;

				default:
					ReadUnknown(xr);
					break;
			}

			return ctx;
		}

		private KdbContext EndXmlElement(KdbContext ctx, XmlReader xr)
		{
			Debug.Assert(xr.NodeType == XmlNodeType.EndElement);

			if((ctx == KdbContext.KeePassFile) && (xr.Name == ElemDocNode))
				return KdbContext.Null;
			else if((ctx == KdbContext.Meta) && (xr.Name == ElemMeta))
				return KdbContext.KeePassFile;
			else if((ctx == KdbContext.Root) && (xr.Name == ElemRoot))
				return KdbContext.KeePassFile;
			else if((ctx == KdbContext.MemoryProtection) && (xr.Name == ElemMemoryProt))
				return KdbContext.Meta;
			else if((ctx == KdbContext.CustomIcons) && (xr.Name == ElemCustomIcons))
				return KdbContext.Meta;
			else if((ctx == KdbContext.CustomIcon) && (xr.Name == ElemCustomIconItem))
			{
				if(!m_uuidCustomIconID.Equals(PwUuid.Zero) &&
					(m_pbCustomIconData != null))
					m_pwDatabase.CustomIcons.Add(new PwCustomIcon(
						m_uuidCustomIconID, m_pbCustomIconData));
				else { Debug.Assert(false); }

				m_uuidCustomIconID = PwUuid.Zero;
				m_pbCustomIconData = null;

				return KdbContext.CustomIcons;
			}
			else if((ctx == KdbContext.Binaries) && (xr.Name == ElemBinaries))
				return KdbContext.Meta;
			else if((ctx == KdbContext.CustomData) && (xr.Name == ElemCustomData))
				return KdbContext.Meta;
			else if((ctx == KdbContext.CustomDataItem) && (xr.Name == ElemStringDictExItem))
			{
				if((m_strCustomDataKey != null) && (m_strCustomDataValue != null))
					m_pwDatabase.CustomData.Set(m_strCustomDataKey, m_strCustomDataValue);
				else { Debug.Assert(false); }

				m_strCustomDataKey = null;
				m_strCustomDataValue = null;

				return KdbContext.CustomData;
			}
			else if((ctx == KdbContext.Group) && (xr.Name == ElemGroup))
			{
				if(PwUuid.Zero.Equals(m_ctxGroup.Uuid))
					m_ctxGroup.Uuid = new PwUuid(true); // No assert (import)

				m_ctxGroups.Pop();

				if(m_ctxGroups.Count == 0)
				{
					m_ctxGroup = null;
					return KdbContext.Root;
				}
				else
				{
					m_ctxGroup = m_ctxGroups.Peek();
					return KdbContext.Group;
				}
			}
			else if((ctx == KdbContext.GroupTimes) && (xr.Name == ElemTimes))
				return KdbContext.Group;
			else if((ctx == KdbContext.GroupCustomData) && (xr.Name == ElemCustomData))
				return KdbContext.Group;
			else if((ctx == KdbContext.GroupCustomDataItem) && (xr.Name == ElemStringDictExItem))
			{
				if((m_strGroupCustomDataKey != null) && (m_strGroupCustomDataValue != null))
					m_ctxGroup.CustomData.Set(m_strGroupCustomDataKey, m_strGroupCustomDataValue);
				else { Debug.Assert(false); }

				m_strGroupCustomDataKey = null;
				m_strGroupCustomDataValue = null;

				return KdbContext.GroupCustomData;
			}
			else if((ctx == KdbContext.Entry) && (xr.Name == ElemEntry))
			{
				// Create new UUID if absent
				if(PwUuid.Zero.Equals(m_ctxEntry.Uuid))
					m_ctxEntry.Uuid = new PwUuid(true); // No assert (import)

				if(m_bEntryInHistory)
				{
					m_ctxEntry = m_ctxHistoryBase;
					return KdbContext.EntryHistory;
				}

				return KdbContext.Group;
			}
			else if((ctx == KdbContext.EntryTimes) && (xr.Name == ElemTimes))
				return KdbContext.Entry;
			else if((ctx == KdbContext.EntryString) && (xr.Name == ElemString))
			{
				m_ctxEntry.Strings.Set(m_ctxStringName, m_ctxStringValue);
				m_ctxStringName = null;
				m_ctxStringValue = null;
				return KdbContext.Entry;
			}
			else if((ctx == KdbContext.EntryBinary) && (xr.Name == ElemBinary))
			{
				if(string.IsNullOrEmpty(m_strDetachBins))
					m_ctxEntry.Binaries.Set(m_ctxBinaryName, m_ctxBinaryValue);
				else
				{
					SaveBinary(m_ctxBinaryName, m_ctxBinaryValue, m_strDetachBins);

					m_ctxBinaryValue = null;
					GC.Collect();
				}

				m_ctxBinaryName = null;
				m_ctxBinaryValue = null;
				return KdbContext.Entry;
			}
			else if((ctx == KdbContext.EntryAutoType) && (xr.Name == ElemAutoType))
				return KdbContext.Entry;
			else if((ctx == KdbContext.EntryAutoTypeItem) && (xr.Name == ElemAutoTypeItem))
			{
				AutoTypeAssociation atAssoc = new AutoTypeAssociation(m_ctxATName,
					m_ctxATSeq);
				m_ctxEntry.AutoType.Add(atAssoc);
				m_ctxATName = null;
				m_ctxATSeq = null;
				return KdbContext.EntryAutoType;
			}
			else if((ctx == KdbContext.EntryCustomData) && (xr.Name == ElemCustomData))
				return KdbContext.Entry;
			else if((ctx == KdbContext.EntryCustomDataItem) && (xr.Name == ElemStringDictExItem))
			{
				if((m_strEntryCustomDataKey != null) && (m_strEntryCustomDataValue != null))
					m_ctxEntry.CustomData.Set(m_strEntryCustomDataKey, m_strEntryCustomDataValue);
				else { Debug.Assert(false); }

				m_strEntryCustomDataKey = null;
				m_strEntryCustomDataValue = null;

				return KdbContext.EntryCustomData;
			}
			else if((ctx == KdbContext.EntryHistory) && (xr.Name == ElemHistory))
			{
				m_bEntryInHistory = false;
				return KdbContext.Entry;
			}
			else if((ctx == KdbContext.RootDeletedObjects) && (xr.Name == ElemDeletedObjects))
				return KdbContext.Root;
			else if((ctx == KdbContext.DeletedObject) && (xr.Name == ElemDeletedObject))
			{
				m_ctxDeletedObject = null;
				return KdbContext.RootDeletedObjects;
			}
			else
			{
				Debug.Assert(false);
				throw new FormatException();
			}
		}

		private string ReadString(XmlReader xr)
		{
			XorredBuffer xb = ProcessNode(xr);
			if(xb != null)
			{
				Debug.Assert(false); // Protected data is unexpected here
				try
				{
					byte[] pb = xb.ReadPlainText();
					if(pb.Length == 0) return string.Empty;
					try { return StrUtil.Utf8.GetString(pb, 0, pb.Length); }
					finally { MemUtil.ZeroByteArray(pb); }
				}
				finally { xb.Dispose(); }
			}

			m_bReadNextNode = false; // ReadElementString skips end tag
#if ModernKeePassLib
			return xr.ReadElementContentAsString();
#else
			return xr.ReadElementString();
#endif
		}

		private string ReadStringRaw(XmlReader xr)
		{
			m_bReadNextNode = false; // ReadElementString skips end tag
#if ModernKeePassLib
			return xr.ReadElementContentAsString();
#else
			return xr.ReadElementString();
#endif
		}

		private byte[] ReadBase64(XmlReader xr, bool bRaw)
		{
			// if(bRaw) return ReadBase64RawInChunks(xr);

			string str = (bRaw ? ReadStringRaw(xr) : ReadString(xr));
			if(string.IsNullOrEmpty(str)) return MemUtil.EmptyByteArray;

			return Convert.FromBase64String(str);
		}

		/* private byte[] m_pbBase64ReadBuf = new byte[1024 * 1024 * 3];
		private byte[] ReadBase64RawInChunks(XmlReader xr)
		{
			xr.MoveToContent();

			List<byte[]> lParts = new List<byte[]>();
			byte[] pbBuf = m_pbBase64ReadBuf;
			while(true)
			{
				int cb = xr.ReadElementContentAsBase64(pbBuf, 0, pbBuf.Length);
				if(cb == 0) break;

				byte[] pb = new byte[cb];
				Array.Copy(pbBuf, 0, pb, 0, cb);
				lParts.Add(pb);

				// No break when cb < pbBuf.Length, because ReadElementContentAsBase64
				// moves to the next XML node only when returning 0
			}
			m_bReadNextNode = false;

			if(lParts.Count == 0) return MemUtil.EmptyByteArray;
			if(lParts.Count == 1) return lParts[0];

			long cbRes = 0;
			for(int i = 0; i < lParts.Count; ++i)
				cbRes += lParts[i].Length;

			byte[] pbRes = new byte[cbRes];
			int cbCur = 0;
			for(int i = 0; i < lParts.Count; ++i)
			{
				Array.Copy(lParts[i], 0, pbRes, cbCur, lParts[i].Length);
				cbCur += lParts[i].Length;
			}

			return pbRes;
		} */

		private bool ReadBool(XmlReader xr, bool bDefault)
		{
			string str = ReadString(xr);
			if(str == ValTrue) return true;
			else if(str == ValFalse) return false;

			Debug.Assert(false);
			return bDefault;
		}

		private PwUuid ReadUuid(XmlReader xr)
		{
			byte[] pb = ReadBase64(xr, false);
			if(pb.Length == 0) return PwUuid.Zero;
			return new PwUuid(pb);
		}

		private int ReadInt(XmlReader xr, int nDefault)
		{
			string str = ReadString(xr);

			int n;
			if(StrUtil.TryParseIntInvariant(str, out n)) return n;

			// Backward compatibility
			if(StrUtil.TryParseInt(str, out n)) return n;

			Debug.Assert(false);
			return nDefault;
		}

		private uint ReadUInt(XmlReader xr, uint uDefault)
		{
			string str = ReadString(xr);

			uint u;
			if(StrUtil.TryParseUIntInvariant(str, out u)) return u;

			// Backward compatibility
			if(StrUtil.TryParseUInt(str, out u)) return u;

			Debug.Assert(false);
			return uDefault;
		}

		private long ReadLong(XmlReader xr, long lDefault)
		{
			string str = ReadString(xr);

			long l;
			if(StrUtil.TryParseLongInvariant(str, out l)) return l;

			// Backward compatibility
			if(StrUtil.TryParseLong(str, out l)) return l;

			Debug.Assert(false);
			return lDefault;
		}

		private ulong ReadULong(XmlReader xr, ulong uDefault)
		{
			string str = ReadString(xr);

			ulong u;
			if(StrUtil.TryParseULongInvariant(str, out u)) return u;

			// Backward compatibility
			if(StrUtil.TryParseULong(str, out u)) return u;

			Debug.Assert(false);
			return uDefault;
		}

		private DateTime ReadTime(XmlReader xr)
		{
			// Cf. WriteObject(string, DateTime)
			if((m_format == KdbxFormat.Default) && (m_uFileVersion >= FileVersion32_4))
			{
				// long l = ReadLong(xr, -1);
				// if(l != -1) return DateTime.FromBinary(l);

				byte[] pb = ReadBase64(xr, false);
				if(pb.Length != 8)
				{
					Debug.Assert(false);
					byte[] pb8 = new byte[8];
					Array.Copy(pb, pb8, Math.Min(pb.Length, 8)); // Little-endian
					pb = pb8;
				}
				long lSec = MemUtil.BytesToInt64(pb);
				return new DateTime(lSec * TimeSpan.TicksPerSecond, DateTimeKind.Utc);
			}
			else
			{
				string str = ReadString(xr);

				DateTime dt;
				if(TimeUtil.TryDeserializeUtc(str, out dt)) return dt;
			}

			Debug.Assert(false);
			return m_dtNow;
		}

		private PwIcon ReadIconId(XmlReader xr, PwIcon icDefault)
		{
			int i = ReadInt(xr, (int)icDefault);
			if((i >= 0) && (i < (int)PwIcon.Count)) return (PwIcon)i;

			Debug.Assert(false);
			return icDefault;
		}

		private ProtectedString ReadProtectedString(XmlReader xr)
		{
			XorredBuffer xb = ProcessNode(xr);
			if(xb != null)
			{
				try { return new ProtectedString(true, xb); }
				finally { xb.Dispose(); }
			}

			bool bProtect = false;
			if(m_format == KdbxFormat.PlainXml)
			{
				if(xr.MoveToAttribute(AttrProtectedInMemPlainXml))
				{
					string strProtect = xr.Value;
					bProtect = ((strProtect != null) && (strProtect == ValTrue));
				}
			}

			return new ProtectedString(bProtect, ReadString(xr));
		}

		private ProtectedBinary ReadProtectedBinary(XmlReader xr)
		{
			if(xr.MoveToAttribute(AttrRef))
			{
				string strRef = xr.Value;
				if(!string.IsNullOrEmpty(strRef))
				{
					int iRef;
					if(StrUtil.TryParseIntInvariant(strRef, out iRef))
					{
						ProtectedBinary pb = m_pbsBinaries.Get(iRef);
						if(pb != null)
						{
							// https://sourceforge.net/p/keepass/feature-requests/2023/
							xr.MoveToElement();
#if DEBUG
							string strInner = ReadStringRaw(xr);
							Debug.Assert(string.IsNullOrEmpty(strInner));
#else
							ReadStringRaw(xr);
#endif

							return pb;
						}
						else { Debug.Assert(false); }
					}
					else { Debug.Assert(false); }
				}
				else { Debug.Assert(false); }
			}

			bool bCompressed = false;
			if(xr.MoveToAttribute(AttrCompressed))
				bCompressed = (xr.Value == ValTrue);

			XorredBuffer xb = ProcessNode(xr);
			if(xb != null)
			{
				Debug.Assert(!bCompressed); // See SubWriteValue(ProtectedBinary value)
				try { return new ProtectedBinary(true, xb); }
				finally { xb.Dispose(); }
			}

			byte[] pbData = ReadBase64(xr, true);
			if(pbData.Length == 0) return new ProtectedBinary();

			if(bCompressed) pbData = MemUtil.Decompress(pbData);
			return new ProtectedBinary(false, pbData);
		}

		private void ReadUnknown(XmlReader xr)
		{
			Debug.Assert(false); // Unknown node!
			Debug.Assert(xr.NodeType == XmlNodeType.Element);

			bool bRead = false;
			int cOpen = 0;

			do
			{
				if(bRead) xr.Read();
				bRead = true;

				if(xr.NodeType == XmlNodeType.EndElement) --cOpen;
				else if(xr.NodeType == XmlNodeType.Element)
				{
					if(!xr.IsEmptyElement)
					{
						XorredBuffer xb = ProcessNode(xr);
						if(xb != null) { xb.Dispose(); bRead = m_bReadNextNode; continue; }

						++cOpen;
					}
				}
			}
			while(cOpen > 0);

			m_bReadNextNode = bRead;
		}

		private XorredBuffer ProcessNode(XmlReader xr)
		{
			// Debug.Assert(xr.NodeType == XmlNodeType.Element);

			if(xr.HasAttributes)
			{
				if(xr.MoveToAttribute(AttrProtected))
				{
					if(xr.Value == ValTrue)
					{
						xr.MoveToElement();

						byte[] pbCT = ReadBase64(xr, true);
						byte[] pbPad = m_randomStream.GetRandomBytes((uint)pbCT.Length);

						return new XorredBuffer(pbCT, pbPad);
					}
				}
			}

			return null;
		}

		private static KdbContext SwitchContext(KdbContext ctxCurrent,
			KdbContext ctxNew, XmlReader xr)
		{
			if(xr.IsEmptyElement) return ctxCurrent;
			return ctxNew;
		}
	}
}
