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

#if KeePassLibSD
using KeePassLibSD;
#else
using System.IO.Compression;
#endif

using KeePassLib.Collections;
using KeePassLib.Cryptography;
using KeePassLib.Cryptography.Cipher;
using KeePassLib.Cryptography.KeyDerivation;
using KeePassLib.Delegates;
using KeePassLib.Interfaces;
using KeePassLib.Keys;
using KeePassLib.Resources;
using KeePassLib.Security;
using KeePassLib.Utility;

namespace KeePassLib.Serialization
{
	/// <summary>
	/// Serialization to KeePass KDBX files.
	/// </summary>
	public sealed partial class KdbxFile
	{
		// public void Save(string strFile, PwGroup pgDataSource, KdbxFormat fmt,
		//	IStatusLogger slLogger)
		// {
		//	bool bMadeUnhidden = UrlUtil.UnhideFile(strFile);
		//
		//	IOConnectionInfo ioc = IOConnectionInfo.FromPath(strFile);
		//	this.Save(IOConnection.OpenWrite(ioc), pgDataSource, format, slLogger);
		//
		//	if(bMadeUnhidden) UrlUtil.HideFile(strFile, true); // Hide again
		// }

		/// <summary>
		/// Save the contents of the current <c>PwDatabase</c> to a KDBX file.
		/// </summary>
		/// <param name="sSaveTo">Stream to write the KDBX file into.</param>
		/// <param name="pgDataSource">Group containing all groups and
		/// entries to write. If <c>null</c>, the complete database will
		/// be written.</param>
		/// <param name="fmt">Format of the file to create.</param>
		/// <param name="slLogger">Logger that receives status information.</param>
		public void Save(Stream sSaveTo, PwGroup pgDataSource, KdbxFormat fmt,
			IStatusLogger slLogger)
		{
			Debug.Assert(sSaveTo != null);
			if(sSaveTo == null) throw new ArgumentNullException("sSaveTo");

			if(m_bUsedOnce)
				throw new InvalidOperationException("Do not reuse KdbxFile objects!");
			m_bUsedOnce = true;

			m_format = fmt;
			m_slLogger = slLogger;
			m_xmlWriter = null;

			PwGroup pgRoot = (pgDataSource ?? m_pwDatabase.RootGroup);
			CryptoRandom cr = CryptoRandom.Instance;
			byte[] pbCipherKey = null;
			byte[] pbHmacKey64 = null;

			m_pbsBinaries = new ProtectedBinarySet(true);
			m_pbsBinaries.AddFrom(pgRoot);

			List<Stream> lStreams = new List<Stream> { sSaveTo };

			HashingStreamEx sHashing = new HashingStreamEx(sSaveTo, true, null);
			lStreams.Add(sHashing);

			try
			{
				// Fix history entries (should not be necessary; just for safety,
				// as e.g. XPath searches depend on correct history entry UUIDs)
				if(m_pwDatabase.MaintainBackups()) { Debug.Assert(false); }

				m_uFileVersion = GetMinKdbxVersion();

				int cbEncKey, cbEncIV;
				ICipherEngine iCipher = GetCipher(out cbEncKey, out cbEncIV);

				m_pbMasterSeed = cr.GetRandomBytes(32);
				m_pbEncryptionIV = cr.GetRandomBytes((uint)cbEncIV);

				// m_pbTransformSeed = cr.GetRandomBytes(32);
				PwUuid puKdf = m_pwDatabase.KdfParameters.KdfUuid;
				KdfEngine kdf = KdfPool.Get(puKdf);
				if(kdf == null)
					throw new Exception(KLRes.UnknownKdf + MessageService.NewParagraph +
						// KLRes.FileNewVerOrPlgReq + MessageService.NewParagraph +
						"UUID: " + puKdf.ToHexString() + ".");
				kdf.Randomize(m_pwDatabase.KdfParameters);

				if(m_format == KdbxFormat.Default)
				{
					if(m_uFileVersion < FileVersion32_4)
					{
						m_craInnerRandomStream = CrsAlgorithm.Salsa20;
						m_pbInnerRandomStreamKey = cr.GetRandomBytes(32);
					}
					else // KDBX >= 4
					{
						m_craInnerRandomStream = CrsAlgorithm.ChaCha20;
						m_pbInnerRandomStreamKey = cr.GetRandomBytes(64);
					}

					m_randomStream = new CryptoRandomStream(m_craInnerRandomStream,
						m_pbInnerRandomStreamKey);
				}

				if(m_uFileVersion < FileVersion32_4)
					m_pbStreamStartBytes = cr.GetRandomBytes(32);

				Stream sXml;
				if(m_format == KdbxFormat.Default)
				{
					byte[] pbHeader = GenerateHeader();
					m_pbHashOfHeader = CryptoUtil.HashSha256(pbHeader);

					MemUtil.Write(sHashing, pbHeader);
					sHashing.Flush();

					ComputeKeys(out pbCipherKey, cbEncKey, out pbHmacKey64);

					Stream sPlain;
					if(m_uFileVersion < FileVersion32_4)
					{
						Stream sEncrypted = EncryptStream(sHashing, iCipher,
							pbCipherKey, cbEncIV, true);
						if((sEncrypted == null) || (sEncrypted == sHashing))
							throw new SecurityException(KLRes.CryptoStreamFailed);
						lStreams.Add(sEncrypted);

						MemUtil.Write(sEncrypted, m_pbStreamStartBytes);

						sPlain = new HashedBlockStream(sEncrypted, true);
					}
					else // KDBX >= 4
					{
						// For integrity checking (without knowing the master key)
						MemUtil.Write(sHashing, m_pbHashOfHeader);

						byte[] pbHeaderHmac = ComputeHeaderHmac(pbHeader, pbHmacKey64);
						MemUtil.Write(sHashing, pbHeaderHmac);

						Stream sBlocks = new HmacBlockStream(sHashing, true,
							true, pbHmacKey64);
						lStreams.Add(sBlocks);

						sPlain = EncryptStream(sBlocks, iCipher, pbCipherKey,
							cbEncIV, true);
						if((sPlain == null) || (sPlain == sBlocks))
							throw new SecurityException(KLRes.CryptoStreamFailed);
					}
					lStreams.Add(sPlain);

					if(m_pwDatabase.Compression == PwCompressionAlgorithm.GZip)
					{
						sXml = new GZipStream(sPlain, CompressionMode.Compress);
						lStreams.Add(sXml);
					}
					else sXml = sPlain;

					if(m_uFileVersion >= FileVersion32_4)
						WriteInnerHeader(sXml); // Binary header before XML
				}
				else if(m_format == KdbxFormat.PlainXml)
					sXml = sHashing;
				else
				{
					Debug.Assert(false);
					throw new ArgumentOutOfRangeException("fmt");
				}

				m_xmlWriter = XmlUtilEx.CreateXmlWriter(sXml);

				WriteDocument(pgRoot);

				m_xmlWriter.Flush();
			}
			finally
			{
				CommonCleanUpWrite(lStreams, sHashing);

				if(pbCipherKey != null) MemUtil.ZeroByteArray(pbCipherKey);
				if(pbHmacKey64 != null) MemUtil.ZeroByteArray(pbHmacKey64);
			}
		}

		private void CommonCleanUpWrite(List<Stream> lStreams, HashingStreamEx sHashing)
		{
			if(m_xmlWriter != null) { m_xmlWriter.Close(); m_xmlWriter = null; }

			DisposeStreams(lStreams);

			Debug.Assert(lStreams.Contains(sHashing)); // sHashing must be closed
			m_pbHashOfFileOnDisk = sHashing.Hash;
			Debug.Assert(m_pbHashOfFileOnDisk != null);

			CleanUpInnerRandomStream();

			m_pbHashOfHeader = null;
		}

		private byte[] GenerateHeader()
		{
			byte[] pbHeader;
			using(MemoryStream ms = new MemoryStream())
			{
				MemUtil.Write(ms, MemUtil.UInt32ToBytes(FileSignature1));
				MemUtil.Write(ms, MemUtil.UInt32ToBytes(FileSignature2));
				MemUtil.Write(ms, MemUtil.UInt32ToBytes(m_uFileVersion));

				WriteHeaderField(ms, KdbxHeaderFieldID.CipherID,
					m_pwDatabase.DataCipherUuid.UuidBytes);
				WriteHeaderField(ms, KdbxHeaderFieldID.CompressionFlags,
					MemUtil.UInt32ToBytes((uint)m_pwDatabase.Compression));
				WriteHeaderField(ms, KdbxHeaderFieldID.MasterSeed, m_pbMasterSeed);

				if(m_uFileVersion < FileVersion32_4)
				{
					Debug.Assert(m_pwDatabase.KdfParameters.KdfUuid.Equals(
						(new AesKdf()).Uuid));
					WriteHeaderField(ms, KdbxHeaderFieldID.TransformSeed,
						m_pwDatabase.KdfParameters.GetByteArray(AesKdf.ParamSeed));
					WriteHeaderField(ms, KdbxHeaderFieldID.TransformRounds,
						MemUtil.UInt64ToBytes(m_pwDatabase.KdfParameters.GetUInt64(
						AesKdf.ParamRounds, PwDefs.DefaultKeyEncryptionRounds)));
				}
				else
					WriteHeaderField(ms, KdbxHeaderFieldID.KdfParameters,
						KdfParameters.SerializeExt(m_pwDatabase.KdfParameters));

				if(m_pbEncryptionIV.Length > 0)
					WriteHeaderField(ms, KdbxHeaderFieldID.EncryptionIV, m_pbEncryptionIV);

				if(m_uFileVersion < FileVersion32_4)
				{
					WriteHeaderField(ms, KdbxHeaderFieldID.InnerRandomStreamKey,
						m_pbInnerRandomStreamKey);
					WriteHeaderField(ms, KdbxHeaderFieldID.StreamStartBytes,
						m_pbStreamStartBytes);
					WriteHeaderField(ms, KdbxHeaderFieldID.InnerRandomStreamID,
						MemUtil.Int32ToBytes((int)m_craInnerRandomStream));
				}

				// Write public custom data only when there is at least one item,
				// because KDBX 3.1 didn't support this field yet
				if(m_pwDatabase.PublicCustomData.Count > 0)
					WriteHeaderField(ms, KdbxHeaderFieldID.PublicCustomData,
						VariantDictionary.Serialize(m_pwDatabase.PublicCustomData));

				WriteHeaderField(ms, KdbxHeaderFieldID.EndOfHeader, new byte[] {
					(byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' });

				pbHeader = ms.ToArray();
			}

			return pbHeader;
		}

		private void WriteHeaderField(Stream s, KdbxHeaderFieldID kdbID,
			byte[] pbData)
		{
			s.WriteByte((byte)kdbID);

			byte[] pb = (pbData ?? MemUtil.EmptyByteArray);
			int cb = pb.Length;
			if(cb < 0) { Debug.Assert(false); throw new OutOfMemoryException(); }

			Debug.Assert(m_uFileVersion > 0);
			if(m_uFileVersion < FileVersion32_4)
			{
				if(cb > (int)ushort.MaxValue)
				{
					Debug.Assert(false);
					throw new ArgumentOutOfRangeException("pbData");
				}

				MemUtil.Write(s, MemUtil.UInt16ToBytes((ushort)cb));
			}
			else MemUtil.Write(s, MemUtil.Int32ToBytes(cb));

			MemUtil.Write(s, pb);
		}

		private void WriteInnerHeader(Stream s)
		{
			WriteInnerHeaderField(s, KdbxInnerHeaderFieldID.InnerRandomStreamID,
				MemUtil.Int32ToBytes((int)m_craInnerRandomStream), null);
			WriteInnerHeaderField(s, KdbxInnerHeaderFieldID.InnerRandomStreamKey,
				m_pbInnerRandomStreamKey, null);

			ProtectedBinary[] vBin = m_pbsBinaries.ToArray();
			for(int i = 0; i < vBin.Length; ++i)
			{
				ProtectedBinary pb = vBin[i];
				if(pb == null) throw new InvalidOperationException();

				KdbxBinaryFlags f = KdbxBinaryFlags.None;
				if(pb.IsProtected) f |= KdbxBinaryFlags.Protected;

				byte[] pbFlags = new byte[1] { (byte)f };
				byte[] pbData = pb.ReadData();

				WriteInnerHeaderField(s, KdbxInnerHeaderFieldID.Binary,
					pbFlags, pbData);

				if(pb.IsProtected) MemUtil.ZeroByteArray(pbData);
			}

			WriteInnerHeaderField(s, KdbxInnerHeaderFieldID.EndOfHeader,
				null, null);
		}

		private void WriteInnerHeaderField(Stream s, KdbxInnerHeaderFieldID kdbID,
			byte[] pbData1, byte[] pbData2)
		{
			s.WriteByte((byte)kdbID);

			byte[] pb1 = (pbData1 ?? MemUtil.EmptyByteArray);
			byte[] pb2 = (pbData2 ?? MemUtil.EmptyByteArray);

			int cb = pb1.Length + pb2.Length;
			if(cb < 0) { Debug.Assert(false); throw new OutOfMemoryException(); }

			MemUtil.Write(s, MemUtil.Int32ToBytes(cb));
			MemUtil.Write(s, pb1);
			MemUtil.Write(s, pb2);
		}

		private void WriteDocument(PwGroup pgRoot)
		{
			Debug.Assert(m_xmlWriter != null);
			if(m_xmlWriter == null) throw new InvalidOperationException();

			uint uNumGroups, uNumEntries, uCurEntry = 0;
			pgRoot.GetCounts(true, out uNumGroups, out uNumEntries);

			m_xmlWriter.WriteStartDocument(true);
			m_xmlWriter.WriteStartElement(ElemDocNode);

			WriteMeta();

			m_xmlWriter.WriteStartElement(ElemRoot);
			StartGroup(pgRoot);

			Stack<PwGroup> groupStack = new Stack<PwGroup>();
			groupStack.Push(pgRoot);

			GroupHandler gh = delegate(PwGroup pg)
			{
				Debug.Assert(pg != null);
				if(pg == null) throw new ArgumentNullException("pg");

				while(true)
				{
					if(pg.ParentGroup == groupStack.Peek())
					{
						groupStack.Push(pg);
						StartGroup(pg);
						break;
					}
					else
					{
						groupStack.Pop();
						if(groupStack.Count <= 0) return false;

						EndGroup();
					}
				}

				return true;
			};

			EntryHandler eh = delegate(PwEntry pe)
			{
				Debug.Assert(pe != null);
				WriteEntry(pe, false);

				++uCurEntry;
				if(m_slLogger != null)
				{
					if(!m_slLogger.SetProgress((100 * uCurEntry) / uNumEntries))
						return false;
				}

				return true;
			};

			if(!pgRoot.TraverseTree(TraversalMethod.PreOrder, gh, eh))
				throw new OperationCanceledException();

			while(groupStack.Count > 1)
			{
				m_xmlWriter.WriteEndElement();
				groupStack.Pop();
			}

			EndGroup();

			WriteList(ElemDeletedObjects, m_pwDatabase.DeletedObjects);
			m_xmlWriter.WriteEndElement(); // Root

			m_xmlWriter.WriteEndElement(); // ElemDocNode
			m_xmlWriter.WriteEndDocument();
		}

		private void WriteMeta()
		{
			m_xmlWriter.WriteStartElement(ElemMeta);

			WriteObject(ElemGenerator, PwDatabase.LocalizedAppName, false);

			if((m_pbHashOfHeader != null) && (m_uFileVersion < FileVersion32_4))
				WriteObject(ElemHeaderHash, Convert.ToBase64String(
					m_pbHashOfHeader), false);

			if(m_uFileVersion >= FileVersion32_4)
				WriteObject(ElemSettingsChanged, m_pwDatabase.SettingsChanged);

			WriteObject(ElemDbName, m_pwDatabase.Name, true);
			WriteObject(ElemDbNameChanged, m_pwDatabase.NameChanged);
			WriteObject(ElemDbDesc, m_pwDatabase.Description, true);
			WriteObject(ElemDbDescChanged, m_pwDatabase.DescriptionChanged);
			WriteObject(ElemDbDefaultUser, m_pwDatabase.DefaultUserName, true);
			WriteObject(ElemDbDefaultUserChanged, m_pwDatabase.DefaultUserNameChanged);
			WriteObject(ElemDbMntncHistoryDays, m_pwDatabase.MaintenanceHistoryDays);
			WriteObject(ElemDbColor, StrUtil.ColorToUnnamedHtml(m_pwDatabase.Color, true), false);
			WriteObject(ElemDbKeyChanged, m_pwDatabase.MasterKeyChanged);
			WriteObject(ElemDbKeyChangeRec, m_pwDatabase.MasterKeyChangeRec);
			WriteObject(ElemDbKeyChangeForce, m_pwDatabase.MasterKeyChangeForce);
			if(m_pwDatabase.MasterKeyChangeForceOnce)
				WriteObject(ElemDbKeyChangeForceOnce, true);

			WriteList(ElemMemoryProt, m_pwDatabase.MemoryProtection);

			WriteCustomIconList();

			WriteObject(ElemRecycleBinEnabled, m_pwDatabase.RecycleBinEnabled);
			WriteObject(ElemRecycleBinUuid, m_pwDatabase.RecycleBinUuid);
			WriteObject(ElemRecycleBinChanged, m_pwDatabase.RecycleBinChanged);
			WriteObject(ElemEntryTemplatesGroup, m_pwDatabase.EntryTemplatesGroup);
			WriteObject(ElemEntryTemplatesGroupChanged, m_pwDatabase.EntryTemplatesGroupChanged);
			WriteObject(ElemHistoryMaxItems, m_pwDatabase.HistoryMaxItems);
			WriteObject(ElemHistoryMaxSize, m_pwDatabase.HistoryMaxSize);

			WriteObject(ElemLastSelectedGroup, m_pwDatabase.LastSelectedGroup);
			WriteObject(ElemLastTopVisibleGroup, m_pwDatabase.LastTopVisibleGroup);

			if((m_format != KdbxFormat.Default) || (m_uFileVersion < FileVersion32_4))
				WriteBinPool();

			WriteList(ElemCustomData, m_pwDatabase.CustomData);

			m_xmlWriter.WriteEndElement();
		}

		private void StartGroup(PwGroup pg)
		{
			m_xmlWriter.WriteStartElement(ElemGroup);
			WriteObject(ElemUuid, pg.Uuid);
			WriteObject(ElemName, pg.Name, true);
			WriteObject(ElemNotes, pg.Notes, true);
			WriteObject(ElemIcon, (int)pg.IconId);
			
			if(!pg.CustomIconUuid.IsZero)
				WriteObject(ElemCustomIconID, pg.CustomIconUuid);
			
			WriteList(ElemTimes, pg);
			WriteObject(ElemIsExpanded, pg.IsExpanded);
			WriteObject(ElemGroupDefaultAutoTypeSeq, pg.DefaultAutoTypeSequence, true);
			WriteObject(ElemEnableAutoType, pg.EnableAutoType);
			WriteObject(ElemEnableSearching, pg.EnableSearching);
			WriteObject(ElemLastTopVisibleEntry, pg.LastTopVisibleEntry);

			if(m_uFileVersion >= FileVersion32_4_1)
			{
				if(!pg.PreviousParentGroup.IsZero)
					WriteObject(ElemPreviousParentGroup, pg.PreviousParentGroup);

				List<string> lTags = pg.Tags;
				if(lTags.Count != 0)
					WriteObject(ElemTags, StrUtil.TagsToString(lTags, false), true);
			}

			if(pg.CustomData.Count > 0)
				WriteList(ElemCustomData, pg.CustomData);
		}

		private void EndGroup()
		{
			m_xmlWriter.WriteEndElement(); // Close group element
		}

		private void WriteEntry(PwEntry pe, bool bIsHistory)
		{
			Debug.Assert(pe != null); if(pe == null) throw new ArgumentNullException("pe");

			m_xmlWriter.WriteStartElement(ElemEntry);

			WriteObject(ElemUuid, pe.Uuid);
			WriteObject(ElemIcon, (int)pe.IconId);

			if(!pe.CustomIconUuid.IsZero)
				WriteObject(ElemCustomIconID, pe.CustomIconUuid);

			WriteObject(ElemFgColor, StrUtil.ColorToUnnamedHtml(pe.ForegroundColor, true), false);
			WriteObject(ElemBgColor, StrUtil.ColorToUnnamedHtml(pe.BackgroundColor, true), false);
			WriteObject(ElemOverrideUrl, pe.OverrideUrl, true);

			if((m_uFileVersion >= FileVersion32_4_1) && !pe.QualityCheck)
				WriteObject(ElemQualityCheck, false);

			WriteObject(ElemTags, StrUtil.TagsToString(pe.Tags, false), true);

			if((m_uFileVersion >= FileVersion32_4_1) && !pe.PreviousParentGroup.IsZero)
				WriteObject(ElemPreviousParentGroup, pe.PreviousParentGroup);

			WriteList(ElemTimes, pe);

			WriteList(pe.Strings, true);
			WriteList(pe.Binaries);
			WriteList(ElemAutoType, pe.AutoType);

			if(pe.CustomData.Count > 0)
				WriteList(ElemCustomData, pe.CustomData);

			if(!bIsHistory) WriteList(ElemHistory, pe.History, true);
			else { Debug.Assert(pe.History.UCount == 0); }

			m_xmlWriter.WriteEndElement();
		}

		private void WriteList(ProtectedStringDictionary dictStrings, bool bEntryStrings)
		{
			Debug.Assert(dictStrings != null);
			if(dictStrings == null) throw new ArgumentNullException("dictStrings");

			foreach(KeyValuePair<string, ProtectedString> kvp in dictStrings)
				WriteObject(kvp.Key, kvp.Value, bEntryStrings);
		}

		private void WriteList(ProtectedBinaryDictionary dictBinaries)
		{
			Debug.Assert(dictBinaries != null);
			if(dictBinaries == null) throw new ArgumentNullException("dictBinaries");

			foreach(KeyValuePair<string, ProtectedBinary> kvp in dictBinaries)
				WriteObject(kvp.Key, kvp.Value, true);
		}

		private void WriteList(string name, AutoTypeConfig cfgAutoType)
		{
			Debug.Assert(name != null);
			Debug.Assert(cfgAutoType != null);
			if(cfgAutoType == null) throw new ArgumentNullException("cfgAutoType");

			m_xmlWriter.WriteStartElement(name);

			WriteObject(ElemAutoTypeEnabled, cfgAutoType.Enabled);
			WriteObject(ElemAutoTypeObfuscation, (int)cfgAutoType.ObfuscationOptions);

			if(cfgAutoType.DefaultSequence.Length > 0)
				WriteObject(ElemAutoTypeDefaultSeq, cfgAutoType.DefaultSequence, true);

			foreach(AutoTypeAssociation a in cfgAutoType.Associations)
				WriteObject(ElemAutoTypeItem, ElemWindow, ElemKeystrokeSequence,
					new KeyValuePair<string, string>(a.WindowName, a.Sequence), null);

			m_xmlWriter.WriteEndElement();
		}

		private void WriteList(string name, ITimeLogger times)
		{
			Debug.Assert(name != null);
			Debug.Assert(times != null); if(times == null) throw new ArgumentNullException("times");

			m_xmlWriter.WriteStartElement(name);

			WriteObject(ElemCreationTime, times.CreationTime);
			WriteObject(ElemLastModTime, times.LastModificationTime);
			WriteObject(ElemLastAccessTime, times.LastAccessTime);
			WriteObject(ElemExpiryTime, times.ExpiryTime);
			WriteObject(ElemExpires, times.Expires);
			WriteObject(ElemUsageCount, times.UsageCount);
			WriteObject(ElemLocationChanged, times.LocationChanged);

			m_xmlWriter.WriteEndElement();
		}

		private void WriteList(string name, PwObjectList<PwEntry> value, bool bIsHistory)
		{
			Debug.Assert(name != null);
			Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value");

			m_xmlWriter.WriteStartElement(name);

			foreach(PwEntry pe in value)
				WriteEntry(pe, bIsHistory);

			m_xmlWriter.WriteEndElement();
		}

		private void WriteList(string name, PwObjectList<PwDeletedObject> value)
		{
			Debug.Assert(name != null);
			Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value");

			m_xmlWriter.WriteStartElement(name);

			foreach(PwDeletedObject pdo in value)
				WriteObject(ElemDeletedObject, pdo);

			m_xmlWriter.WriteEndElement();
		}

		private void WriteList(string name, MemoryProtectionConfig value)
		{
			Debug.Assert(name != null);
			Debug.Assert(value != null);

			m_xmlWriter.WriteStartElement(name);

			WriteObject(ElemProtTitle, value.ProtectTitle);
			WriteObject(ElemProtUserName, value.ProtectUserName);
			WriteObject(ElemProtPassword, value.ProtectPassword);
			WriteObject(ElemProtUrl, value.ProtectUrl);
			WriteObject(ElemProtNotes, value.ProtectNotes);
			// WriteObject(ElemProtAutoHide, value.AutoEnableVisualHiding);

			m_xmlWriter.WriteEndElement();
		}

		private void WriteList(string name, StringDictionaryEx value)
		{
			Debug.Assert(name != null);
			Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value");

			m_xmlWriter.WriteStartElement(name);

			foreach(KeyValuePair<string, string> kvp in value)
			{
				DateTime? odtLastMod = null;
				if(m_uFileVersion >= FileVersion32_4_1)
					odtLastMod = value.GetLastModificationTime(kvp.Key);

				WriteObject(ElemStringDictExItem, ElemKey, ElemValue, kvp, odtLastMod);
			}

			m_xmlWriter.WriteEndElement();
		}

		private void WriteCustomIconList()
		{
			if(m_pwDatabase.CustomIcons.Count == 0) return;

			m_xmlWriter.WriteStartElement(ElemCustomIcons);

			foreach(PwCustomIcon ci in m_pwDatabase.CustomIcons)
			{
				m_xmlWriter.WriteStartElement(ElemCustomIconItem);

				WriteObject(ElemCustomIconItemID, ci.Uuid);

				string strData = Convert.ToBase64String(ci.ImageDataPng);
				WriteObject(ElemCustomIconItemData, strData, false);

				if(m_uFileVersion >= FileVersion32_4_1)
				{
					if(ci.Name.Length != 0)
						WriteObject(ElemName, ci.Name, true);
					if(ci.LastModificationTime.HasValue)
						WriteObject(ElemLastModTime, ci.LastModificationTime.Value);
				}

				m_xmlWriter.WriteEndElement();
			}

			m_xmlWriter.WriteEndElement();
		}

		private void WriteObject(string name, string value,
			bool bFilterValueXmlChars)
		{
			Debug.Assert(name != null);
			Debug.Assert(value != null);

			m_xmlWriter.WriteStartElement(name);

			if(bFilterValueXmlChars)
				m_xmlWriter.WriteString(StrUtil.SafeXmlString(value));
			else m_xmlWriter.WriteString(value);

			m_xmlWriter.WriteEndElement();
		}

		private void WriteObject(string name, bool value)
		{
			WriteObject(name, (value ? ValTrue : ValFalse), false);
		}

		private void WriteObject(string name, bool? value)
		{
			if(!value.HasValue) WriteObject(name, ValNull, false);
			else WriteObject(name, (value.Value ? ValTrue : ValFalse), false);
		}

		private void WriteObject(string name, PwUuid value)
		{
			Debug.Assert(name != null);
			Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value");

			WriteObject(name, Convert.ToBase64String(value.UuidBytes), false);
		}

		private void WriteObject(string name, int value)
		{
			Debug.Assert(name != null);

			m_xmlWriter.WriteStartElement(name);
			m_xmlWriter.WriteString(value.ToString(NumberFormatInfo.InvariantInfo));
			m_xmlWriter.WriteEndElement();
		}

		private void WriteObject(string name, uint value)
		{
			Debug.Assert(name != null);

			m_xmlWriter.WriteStartElement(name);
			m_xmlWriter.WriteString(value.ToString(NumberFormatInfo.InvariantInfo));
			m_xmlWriter.WriteEndElement();
		}

		private void WriteObject(string name, long value)
		{
			Debug.Assert(name != null);

			m_xmlWriter.WriteStartElement(name);
			m_xmlWriter.WriteString(value.ToString(NumberFormatInfo.InvariantInfo));
			m_xmlWriter.WriteEndElement();
		}

		private void WriteObject(string name, ulong value)
		{
			Debug.Assert(name != null);

			m_xmlWriter.WriteStartElement(name);
			m_xmlWriter.WriteString(value.ToString(NumberFormatInfo.InvariantInfo));
			m_xmlWriter.WriteEndElement();
		}

		private void WriteObject(string name, DateTime value)
		{
			Debug.Assert(name != null);
			Debug.Assert(value.Kind == DateTimeKind.Utc);

			// Cf. ReadTime
			if((m_format == KdbxFormat.Default) && (m_uFileVersion >= FileVersion32_4))
			{
				DateTime dt = TimeUtil.ToUtc(value, false);

				// DateTime dtBase = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc);
				// dt -= new TimeSpan(dtBase.Ticks);

				// WriteObject(name, dt.ToBinary());

				// dt = TimeUtil.RoundToMultOf2PowLess1s(dt);
				// long lBin = dt.ToBinary();

				long lSec = dt.Ticks / TimeSpan.TicksPerSecond;
				// WriteObject(name, lSec);

				byte[] pb = MemUtil.Int64ToBytes(lSec);
				WriteObject(name, Convert.ToBase64String(pb), false);
			}
			else WriteObject(name, TimeUtil.SerializeUtc(value), false);
		}

		private void WriteObject(string name, string strKeyName, string strValueName,
			KeyValuePair<string, string> kvp, DateTime? odtLastMod)
		{
			m_xmlWriter.WriteStartElement(name);

			m_xmlWriter.WriteStartElement(strKeyName);
			m_xmlWriter.WriteString(StrUtil.SafeXmlString(kvp.Key));
			m_xmlWriter.WriteEndElement();

			m_xmlWriter.WriteStartElement(strValueName);
			m_xmlWriter.WriteString(StrUtil.SafeXmlString(kvp.Value));
			m_xmlWriter.WriteEndElement();

			if(odtLastMod.HasValue)
				WriteObject(ElemLastModTime, odtLastMod.Value);

			m_xmlWriter.WriteEndElement();
		}

		private void WriteObject(string name, ProtectedString value, bool bIsEntryString)
		{
			Debug.Assert(name != null);
			Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value");

			m_xmlWriter.WriteStartElement(ElemString);
			m_xmlWriter.WriteStartElement(ElemKey);
			m_xmlWriter.WriteString(StrUtil.SafeXmlString(name));
			m_xmlWriter.WriteEndElement();
			m_xmlWriter.WriteStartElement(ElemValue);

			bool bProtected = value.IsProtected;
			if(bIsEntryString)
			{
				// Adjust memory protection setting (which might be different
				// from the database default, e.g. due to an import which
				// didn't specify the correct setting)
				if(name == PwDefs.TitleField)
					bProtected = m_pwDatabase.MemoryProtection.ProtectTitle;
				else if(name == PwDefs.UserNameField)
					bProtected = m_pwDatabase.MemoryProtection.ProtectUserName;
				else if(name == PwDefs.PasswordField)
					bProtected = m_pwDatabase.MemoryProtection.ProtectPassword;
				else if(name == PwDefs.UrlField)
					bProtected = m_pwDatabase.MemoryProtection.ProtectUrl;
				else if(name == PwDefs.NotesField)
					bProtected = m_pwDatabase.MemoryProtection.ProtectNotes;
			}

			if(bProtected && (m_format == KdbxFormat.Default))
			{
				m_xmlWriter.WriteAttributeString(AttrProtected, ValTrue);

				byte[] pbEnc = value.ReadXorredString(m_randomStream);
				if(pbEnc.Length > 0)
					m_xmlWriter.WriteBase64(pbEnc, 0, pbEnc.Length);
			}
			else
			{
				string strValue = value.ReadString();

				// If names should be localized, we need to apply the language-dependent
				// string transformation here. By default, language-dependent conversions
				// should be applied, otherwise characters could be rendered incorrectly
				// (code page problems).
				if(g_bLocalizedNames)
				{
					StringBuilder sb = new StringBuilder();
					foreach(char ch in strValue)
					{
						char chMapped = ch;

						// Symbols and surrogates must be moved into the correct code
						// page area
						if(char.IsSymbol(ch) || char.IsSurrogate(ch))
						{
							UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory(ch);
							// Map character to correct position in code page
							chMapped = (char)((int)cat * 32 + ch);
						}
						else if(char.IsControl(ch))
						{
							if(ch >= 256) // Control character in high ANSI code page
							{
								// Some of the control characters map to corresponding ones
								// in the low ANSI range (up to 255) when calling
								// ToLower on them with invariant culture (see
								// http://lists.ximian.com/pipermail/mono-patches/2002-February/086106.html )
#if !KeePassLibSD
								chMapped = char.ToLowerInvariant(ch);
#else
								chMapped = char.ToLower(ch);
#endif
							}
						}

						sb.Append(chMapped);
					}

					strValue = sb.ToString(); // Correct string for current code page
				}

				if((m_format == KdbxFormat.PlainXml) && bProtected)
					m_xmlWriter.WriteAttributeString(AttrProtectedInMemPlainXml, ValTrue);

				m_xmlWriter.WriteString(StrUtil.SafeXmlString(strValue));
			}

			m_xmlWriter.WriteEndElement(); // ElemValue
			m_xmlWriter.WriteEndElement(); // ElemString
		}

		private void WriteObject(string name, ProtectedBinary value, bool bAllowRef)
		{
			Debug.Assert(name != null);
			Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value");

			m_xmlWriter.WriteStartElement(ElemBinary);
			m_xmlWriter.WriteStartElement(ElemKey);
			m_xmlWriter.WriteString(StrUtil.SafeXmlString(name));
			m_xmlWriter.WriteEndElement();
			m_xmlWriter.WriteStartElement(ElemValue);

			string strRef = null;
			if(bAllowRef)
			{
				int iRef = m_pbsBinaries.Find(value);
				if(iRef >= 0) strRef = iRef.ToString(NumberFormatInfo.InvariantInfo);
				else { Debug.Assert(false); }
			}
			if(strRef != null)
				m_xmlWriter.WriteAttributeString(AttrRef, strRef);
			else SubWriteValue(value);

			m_xmlWriter.WriteEndElement(); // ElemValue
			m_xmlWriter.WriteEndElement(); // ElemBinary
		}

		private void SubWriteValue(ProtectedBinary value)
		{
			if(value.IsProtected && (m_format == KdbxFormat.Default))
			{
				m_xmlWriter.WriteAttributeString(AttrProtected, ValTrue);

				byte[] pbEnc = value.ReadXorredData(m_randomStream);
				if(pbEnc.Length > 0)
					m_xmlWriter.WriteBase64(pbEnc, 0, pbEnc.Length);
			}
			else
			{
				if(m_pwDatabase.Compression != PwCompressionAlgorithm.None)
				{
					m_xmlWriter.WriteAttributeString(AttrCompressed, ValTrue);

					byte[] pbRaw = value.ReadData();
					byte[] pbCmp = MemUtil.Compress(pbRaw);
					m_xmlWriter.WriteBase64(pbCmp, 0, pbCmp.Length);

					if(value.IsProtected)
					{
						MemUtil.ZeroByteArray(pbRaw);
						MemUtil.ZeroByteArray(pbCmp);
					}
				}
				else
				{
					byte[] pbRaw = value.ReadData();
					m_xmlWriter.WriteBase64(pbRaw, 0, pbRaw.Length);

					if(value.IsProtected) MemUtil.ZeroByteArray(pbRaw);
				}
			}
		}

		private void WriteObject(string name, PwDeletedObject value)
		{
			Debug.Assert(name != null);
			Debug.Assert(value != null); if(value == null) throw new ArgumentNullException("value");

			m_xmlWriter.WriteStartElement(name);
			WriteObject(ElemUuid, value.Uuid);
			WriteObject(ElemDeletionTime, value.DeletionTime);
			m_xmlWriter.WriteEndElement();
		}

		private void WriteBinPool()
		{
			m_xmlWriter.WriteStartElement(ElemBinaries);

			ProtectedBinary[] v = m_pbsBinaries.ToArray();
			for(int i = 0; i < v.Length; ++i)
			{
				m_xmlWriter.WriteStartElement(ElemBinary);
				m_xmlWriter.WriteAttributeString(AttrId,
					i.ToString(NumberFormatInfo.InvariantInfo));
				SubWriteValue(v[i]);
				m_xmlWriter.WriteEndElement();
			}

			m_xmlWriter.WriteEndElement();
		}

		internal static void WriteGroup(Stream msOutput, PwDatabase pdContext,
			PwGroup pg)
		{
			if(msOutput == null) throw new ArgumentNullException("msOutput");
			// pdContext may be null
			if(pg == null) throw new ArgumentNullException("pg");

			PwDatabase pd = new PwDatabase();
			pd.New(new IOConnectionInfo(), new CompositeKey());

			pd.RootGroup = pg.CloneDeep();
			pd.RootGroup.ParentGroup = null;

			PwDatabase.CopyCustomIcons(pdContext, pd, pd.RootGroup, true);

			KdbxFile f = new KdbxFile(pd);
			f.Save(msOutput, null, KdbxFormat.PlainXml, null);
		}

		[Obsolete]
		public static bool WriteEntries(Stream msOutput, PwEntry[] vEntries)
		{
			return WriteEntries(msOutput, null, vEntries);
		}

		public static bool WriteEntries(Stream msOutput, PwDatabase pdContext,
			PwEntry[] vEntries)
		{
			if(msOutput == null) { Debug.Assert(false); return false; }
			// pdContext may be null
			if(vEntries == null) { Debug.Assert(false); return false; }

			PwGroup pg = new PwGroup(true, true);

			foreach(PwEntry pe in vEntries)
			{
				PwEntry peCopy = pe.CloneDeep();
				pg.AddEntry(peCopy, true);
			}

			WriteGroup(msOutput, pdContext, pg);
			return true;
		}
	}
}
