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

using KeePass.Resources;
using KeePass.Util;
using KeePass.Util.XmlSerialization;

using KeePassLib;
using KeePassLib.Serialization;
using KeePassLib.Utility;

namespace KeePass.App.Configuration
{
	public static class AppConfigSerializer
	{
		private static string g_strBaseName = null; // Null allowed

		private static string g_strAppDataDir = null;
		private static string g_strAppDataLocalDir = null;
		private static string g_strEnforcedConfigFile = null;
		private static string g_strGlobalConfigFile = null;
		private static string g_strUserConfigFile = null;

		/// <summary>
		/// Get our folder in the application data directory.
		/// </summary>
		public static string AppDataDirectory
		{
			get
			{
				AppConfigSerializer.GetConfigPaths();
				return g_strAppDataDir;
			}
		}

		/// <summary>
		/// Get our folder in the local application data directory.
		/// </summary>
		public static string LocalAppDataDirectory
		{
			get
			{
				AppConfigSerializer.GetConfigPaths();
				return g_strAppDataLocalDir;
			}
		}

		/// <summary>
		/// Get/set the base name for the configuration. If this property is
		/// <c>null</c>, the class constructs names based on the current
		/// assembly and the product name.
		/// </summary>
		public static string BaseName
		{
			get { return g_strBaseName; }

			set
			{
				g_strBaseName = value;

				// Invalidate paths
				g_strAppDataDir = null;
				g_strAppDataLocalDir = null;
				g_strEnforcedConfigFile = null;
				g_strGlobalConfigFile = null;
				g_strUserConfigFile = null;
			}
		}

		private static XmlDocument m_xdEnforced = null;
		public static XmlDocument EnforcedConfigXml
		{
			get { return m_xdEnforced; }
		}

		private static void GetConfigPaths()
		{
			if(g_strGlobalConfigFile == null)
			{
				Assembly asm = Assembly.GetExecutingAssembly();
				if(asm == null) { Debug.Assert(false); return; }

#if !KeePassLibSD
				string strFile = null;

				try { strFile = asm.Location; }
				catch(Exception) { }

				if(string.IsNullOrEmpty(strFile))
					strFile = UrlUtil.FileUrlToPath(asm.GetName().CodeBase);
#else
				string strFile = UrlUtil.FileUrlToPath(asm.GetName().CodeBase);
#endif
				if(strFile == null) { Debug.Assert(false); return; }

				if(string.IsNullOrEmpty(g_strBaseName))
				{
					// Remove assembly extension
					if(strFile.EndsWith(".exe", StrUtil.CaseIgnoreCmp))
						strFile = strFile.Substring(0, strFile.Length - 4);
					else if(strFile.EndsWith(".dll", StrUtil.CaseIgnoreCmp))
						strFile = strFile.Substring(0, strFile.Length - 4);
				}
				else // Base name != null
					strFile = UrlUtil.GetFileDirectory(strFile, true, false) + g_strBaseName;

				g_strGlobalConfigFile = strFile + ".config.xml";
				g_strEnforcedConfigFile = strFile + ".config.enforced.xml";
			}

			if(g_strUserConfigFile == null)
			{
				string strBaseDirName = PwDefs.ShortProductName;
				if(!string.IsNullOrEmpty(g_strBaseName))
					strBaseDirName = g_strBaseName;

				string strUserDir;
				try
				{
					strUserDir = Environment.GetFolderPath(
						Environment.SpecialFolder.ApplicationData);
				}
				catch(Exception)
				{
					strUserDir = UrlUtil.GetFileDirectory(UrlUtil.FileUrlToPath(
						Assembly.GetExecutingAssembly().GetName().CodeBase), true, false);
				}
				strUserDir = UrlUtil.EnsureTerminatingSeparator(strUserDir, false);

				string strUserDirLocal;
				try
				{
					strUserDirLocal = Environment.GetFolderPath(
						Environment.SpecialFolder.LocalApplicationData);
				}
				catch(Exception) { strUserDirLocal = strUserDir; }
				strUserDirLocal = UrlUtil.EnsureTerminatingSeparator(strUserDirLocal, false);

				g_strAppDataDir = strUserDir + strBaseDirName;
				g_strAppDataLocalDir = strUserDirLocal + strBaseDirName;
				g_strUserConfigFile = UrlUtil.EnsureTerminatingSeparator(
					g_strAppDataDir, false) + strBaseDirName + ".config.xml";
			}

			string strLocalOvr = Program.CommandLineArgs[
				AppDefs.CommandLineOptions.ConfigPathLocal];
			if(!string.IsNullOrEmpty(strLocalOvr))
			{
				string strWD = UrlUtil.EnsureTerminatingSeparator(
					WinUtil.GetWorkingDirectory(), false);
				g_strUserConfigFile = UrlUtil.MakeAbsolutePath(strWD +
					"Sentinel.txt", strLocalOvr);
				// Do not change g_strAppDataDir, as it's returned from
				// the AppDataDirectory property
			}

			Debug.Assert(!string.IsNullOrEmpty(g_strAppDataDir));
		}

		private static void EnsureDirOfFileExists(string strFile)
		{
			if(string.IsNullOrEmpty(strFile)) { Debug.Assert(false); return; }

			try
			{
				string strDir = UrlUtil.GetFileDirectory(strFile, false, true);
				if(!Directory.Exists(strDir))
					Directory.CreateDirectory(strDir);
			}
			catch(Exception) { Debug.Assert(false); }
		}

		private static XmlDocument LoadEnforcedConfigFile()
		{
#if DEBUG
			Stopwatch sw = Stopwatch.StartNew();
#endif

			m_xdEnforced = null;
			try
			{
				// Performance optimization
				if(!File.Exists(g_strEnforcedConfigFile)) return null;

				XmlDocument xmlDoc = XmlUtilEx.CreateXmlDocument();
				xmlDoc.Load(g_strEnforcedConfigFile);

				m_xdEnforced = xmlDoc;
				return xmlDoc;
			}
			catch(Exception) { Debug.Assert(false); }
#if DEBUG
			finally
			{
				sw.Stop();
			}
#endif

			return null;
		}

		private static AppConfigEx LoadConfigFileEx(string strFilePath,
			XmlDocument xdEnforced)
		{
			if(string.IsNullOrEmpty(strFilePath)) return null;

			AppConfigEx tConfig = null;
			XmlSerializerEx xmlSerial = new XmlSerializerEx(typeof(AppConfigEx));

			if(xdEnforced == null)
			{
				try
				{
					using(FileStream fs = new FileStream(strFilePath,
						FileMode.Open, FileAccess.Read, FileShare.Read))
					{
						tConfig = (AppConfigEx)xmlSerial.Deserialize(fs);
					}
				}
				catch(Exception) { } // Do not assert
			}
			else // Enforced configuration
			{
				try
				{
					XmlDocument xd = XmlUtilEx.CreateXmlDocument();
					xd.Load(strFilePath);

					XmContext ctx = new XmContext(xd, AppConfigEx.GetNodeOptions,
						AppConfigEx.GetNodeKey);
					XmlUtil.MergeElements(xd.DocumentElement, xdEnforced.DocumentElement,
						"/" + xd.DocumentElement.Name, null, ctx);

					using(MemoryStream msAsm = new MemoryStream())
					{
						xd.Save(msAsm);

						using(MemoryStream msRead = new MemoryStream(
							msAsm.ToArray(), false))
						{
							tConfig = (AppConfigEx)xmlSerial.Deserialize(msRead);
						}
					}
				}
				catch(FileNotFoundException) { }
				catch(Exception) { Debug.Assert(false); }
			}

			if(tConfig != null) tConfig.OnLoad();

			return tConfig;
		}

		public static AppConfigEx Load()
		{
			AppConfigSerializer.GetConfigPaths();

			// AppConfigEx cfgEnf = LoadConfigFileEx(g_strEnforcedConfigFile);
			// if(cfgEnf != null)
			// {
			//	cfgEnf.Meta.IsEnforcedConfiguration = true;
			//	return cfgEnf;
			// }
			XmlDocument xdEnforced = LoadEnforcedConfigFile();

			AppConfigEx cfgGlobal = LoadConfigFileEx(g_strGlobalConfigFile, xdEnforced);
			AppConfigEx cfgUser = LoadConfigFileEx(g_strUserConfigFile, xdEnforced);

			if((cfgGlobal == null) && (cfgUser == null))
			{
				if(xdEnforced != null)
				{
					// Create an empty configuration file and merge it with
					// the enforced configuration; this ensures that merge
					// behaviors (like the node mode 'Remove') work as intended
					try
					{
						string strFile = Program.TempFilesPool.GetTempFileName("xml");
						File.WriteAllText(strFile, AppConfigEx.GetEmptyConfigXml(),
							StrUtil.Utf8);

						AppConfigEx cfg = LoadConfigFileEx(strFile, xdEnforced);
						if(cfg != null) return cfg;
						Debug.Assert(false);
					}
					catch(Exception) { Debug.Assert(false); }
				}

				AppConfigEx cfgNew = new AppConfigEx();
				cfgNew.OnLoad(); // Create defaults
				return cfgNew;
			}
			else if((cfgGlobal != null) && (cfgUser == null))
				return cfgGlobal;
			else if((cfgGlobal == null) && (cfgUser != null))
				return cfgUser;

			cfgUser.Meta.PreferUserConfiguration = cfgGlobal.Meta.PreferUserConfiguration;
			return (cfgGlobal.Meta.PreferUserConfiguration ? cfgUser : cfgGlobal);
		}

		private static bool SaveConfigFileEx(AppConfigEx tConfig,
			string strFilePath, bool bRemoveConfigPref)
		{
			tConfig.OnSavePre();

			// Temporarily remove user file preference (restore after saving)
			bool bConfigPref = tConfig.Meta.PreferUserConfiguration;
			if(bRemoveConfigPref) tConfig.Meta.PreferUserConfiguration = false;

			bool bResult = true;
			try
			{
				Debug.Assert(!string.IsNullOrEmpty(strFilePath));
				IOConnectionInfo iocPath = IOConnectionInfo.FromPath(strFilePath);

				using(FileTransactionEx ft = new FileTransactionEx(iocPath,
					Program.Config.Application.UseTransactedConfigWrites))
				{
					using(Stream s = ft.OpenWrite())
					{
						using(XmlWriter xw = XmlUtilEx.CreateXmlWriter(s))
						{
							XmlSerializerEx xs = new XmlSerializerEx(typeof(AppConfigEx));
							xs.Serialize(xw, tConfig);
						}
					}

					ft.CommitWrite();
				}
			}
			catch(Exception) { Debug.Assert(false); bResult = false; }

			if(bRemoveConfigPref) tConfig.Meta.PreferUserConfiguration = bConfigPref;

			AssertConfigPref(tConfig);

			tConfig.OnSavePost();
			return bResult;
		}

		public static bool Save(AppConfigEx tConfig)
		{
			Debug.Assert(tConfig != null);
			if(tConfig == null) throw new ArgumentNullException("tConfig");

			AppConfigSerializer.GetConfigPaths();

			XmlDocument xdEnforced = LoadEnforcedConfigFile();
			AppConfigEx cfgGlobal = LoadConfigFileEx(g_strGlobalConfigFile, xdEnforced);

			bool bPreferUser = (xdEnforced != null);
			if(cfgGlobal != null)
				bPreferUser = cfgGlobal.Meta.PreferUserConfiguration;
			else if(xdEnforced != null)
				GetConfigPref(xdEnforced, ref bPreferUser, null);

			if(bPreferUser)
			{
				EnsureDirOfFileExists(g_strUserConfigFile);
				if(SaveConfigFileEx(tConfig, g_strUserConfigFile, true)) return true;

				if(SaveConfigFileEx(tConfig, g_strGlobalConfigFile, false)) return true;
			}
			else // Don't prefer user -- use global first
			{
				if(SaveConfigFileEx(tConfig, g_strGlobalConfigFile, false)) return true;

				EnsureDirOfFileExists(g_strUserConfigFile);
				if(SaveConfigFileEx(tConfig, g_strUserConfigFile, true)) return true;
			}

#if !KeePassLibSD
			if(Program.MainForm != null)
				Program.MainForm.SetStatusEx(KPRes.ConfigSaveFailed);
#endif

			return false;
		}

		private static void GetConfigPref(XmlDocument d, ref bool bPrefUser,
			bool? obExpectedPref)
		{
			if(d == null) { Debug.Assert(false); return; }

			try
			{
				XmlElement eRoot = d.DocumentElement;
				if(eRoot == null) { Debug.Assert(false); return; }

				// obExpectedPref is for debugging only; an assertion
				// will be displayed if the XPath is incorrect

				XmlNode n = eRoot.SelectSingleNode("Meta/PreferUserConfiguration");
				if(n == null) { Debug.Assert(!obExpectedPref.HasValue); return; }

				bPrefUser = XmlConvert.ToBoolean(XmlUtil.SafeInnerText(n));
				if(obExpectedPref.HasValue) { Debug.Assert(bPrefUser == obExpectedPref.Value); }
			}
			catch(Exception) { Debug.Assert(false); }
		}

		[Conditional("DEBUG")]
		private static void AssertConfigPref(AppConfigEx t)
		{
#if DEBUG
			if(t == null) { Debug.Assert(false); return; }

			using(MemoryStream ms = new MemoryStream())
			{
				XmlUtilEx.Serialize<AppConfigEx>(ms, t);

				using(MemoryStream msRead = new MemoryStream(ms.ToArray(), false))
				{
					XmlDocument d = XmlUtilEx.CreateXmlDocument();
					d.Load(msRead);

					// Assert that the meta node XPath is correct
					bool bDummy = false;
					GetConfigPref(d, ref bDummy, t.Meta.PreferUserConfiguration);
				}
			}
#endif
		}
	}
}
