/* 
 * You may amend and distribute as you like, but don't remove this header!
 * 
 * ExcelPackage provides server-side generation of Excel 2007 spreadsheets.
 * See http://www.codeplex.com/ExcelPackage for details.
 * 
 * Copyright 2007 © Dr John Tunnicliffe 
 * mailto:dr.john.tunnicliffe@btinternet.com
 * All rights reserved.
 * 
 * ExcelPackage is an Open Source project provided under the 
 * GNU General Public License (GPL) as published by the 
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 * 
 * The GNU General Public License can be viewed at http://www.opensource.org/licenses/gpl-license.php
 * If you unfamiliar with this license or have questions about it, here is an http://www.gnu.org/licenses/gpl-faq.html
 * 
 * The code for this project may be used and redistributed by any means PROVIDING it is 
 * not sold for profit without the author's written consent, and providing that this notice 
 * and the author's name and all copyright notices remain intact.
 * 
 * All code and executables are provided "as is" with no warranty either express or implied. 
 * The author accepts no liability for any damage or loss of business that this product may cause.
 */

/*
 * Code change notes:
 * 
 * Author							Change						Date
 * ******************************************************************************
 * John Tunnicliffe		Initial Release		01-Jan-2007
 * ******************************************************************************
 */

using System;
using System.IO;
using System.IO.Packaging;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;

namespace OfficeOpenXml.Core.ExcelPackage
{
	/// <summary>
	/// Provides access to the properties bag of any office document (i.e. Word, Excel etc.)
	/// </summary>
	public class OfficeProperties
	{
		#region Private Properties
		
		private const string schemaCore = @"http://schemas.openxmlformats.org/package/2006/metadata/core-properties";
		private const string schemeExtended = @"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties";
		private const string schemaCustom = @"http://schemas.openxmlformats.org/officeDocument/2006/custom-properties";
		private const string schemaDc = @"http://purl.org/dc/elements/1.1/";
		private const string schemaDcTerms = @"http://purl.org/dc/terms/";
		private const string schemaDcmiType = @"http://purl.org/dc/dcmitype/";
		private const string schemaXsi = @"http://www.w3.org/2001/XMLSchema-instance";
		private const string schemaVt = @"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes";

		private Uri _uriPropertiesCore = new Uri("/docProps/core.xml", UriKind.Relative);
		private Uri _uriPropertiesExtended = new Uri("/docProps/app.xml", UriKind.Relative);
		private Uri _uriPropertiesCustom = new Uri("/docProps/custom.xml", UriKind.Relative);

		private XDocument _xmlPropertiesCore;
		private XDocument _xmlPropertiesExtended;
		private XDocument _xmlPropertiesCustom;
		private ExcelPackage _xlPackage;
		private XmlNamespaceManager _nsManager;
		#endregion 

		#region ExcelProperties Constructor
		/// <summary>
		/// Provides access to all the office document properties.
		/// </summary>
		/// <param name="xlPackage"></param>
		public OfficeProperties(ExcelPackage xlPackage)
		{
			_xlPackage = xlPackage;
			//  Create a NamespaceManager to handle the default namespace, 
			//  and create a prefix for the default namespace:
			NameTable nt = new NameTable();
			_nsManager = new XmlNamespaceManager(nt);
			// default namespace
			_nsManager.AddNamespace("d", ExcelPackage.schemaMain.NamespaceName);
			_nsManager.AddNamespace("vt", schemaVt);
			// extended properties (app.xml)
			_nsManager.AddNamespace("xp", schemeExtended);
			// custom properties
			_nsManager.AddNamespace("ctp", schemaCustom);
			// core properties
			_nsManager.AddNamespace("cp", schemaCore);
			// core property namespaces
			_nsManager.AddNamespace("dc", schemaDc);
			_nsManager.AddNamespace("dcterms", schemaDcTerms);
			_nsManager.AddNamespace("dcmitype", schemaDcmiType);
			_nsManager.AddNamespace("xsi", schemaXsi);
		}
		#endregion

		#region Protected Internal Properties
		/// <summary>
		/// The URI to the core properties component (core.xml)
		/// </summary>
		protected internal Uri CorePropertiesUri { get { return (_uriPropertiesCore); } }
		/// <summary>
		/// The URI to the extended properties component (app.xml)
		/// </summary>
		protected internal Uri ExtendedPropertiesUri { get { return (_uriPropertiesExtended); } }
		/// <summary>
		/// The URI to the custom properties component (custom.xml)
		/// </summary>
		protected internal Uri CustomPropertiesUri { get { return (_uriPropertiesCustom); } }
		#endregion

		#region Core Properties

		#region CorePropertiesXml
		/// <summary>
		/// Provides access to the XML document that holds all the code 
		/// document properties.
		/// </summary>
		public XDocument CorePropertiesXml
		{
			get
			{
				if (_xmlPropertiesCore == null)
				{
					if (_xlPackage.Package.PartExists(CorePropertiesUri))
						_xmlPropertiesCore = _xlPackage.GetXmlFromUri(CorePropertiesUri);
					else
					{
						// create a new document properties part and add to the package
                        PackagePart partCore = _xlPackage.Package.CreatePart(CorePropertiesUri, @"application/vnd.openxmlformats-package.core-properties+xml", CompressionOption.Normal);

						// create the document properties XML (with no entries in it)
						_xmlPropertiesCore = new XDocument(
							new XElement("cp:coreProperties")
							.AddSchemaAttribute(schemaCore, "cp")
							.AddSchemaAttribute(schemaDc, "dc")
							.AddSchemaAttribute(schemaDcTerms, "dcterms")
							.AddSchemaAttribute(schemaDcmiType, "dcmitype")
							.AddSchemaAttribute(schemaXsi, "xsi")
							);

						// save it to the package
						StreamWriter streamCore = new StreamWriter(partCore.GetStream(FileMode.Create, FileAccess.Write));
						_xmlPropertiesCore.Save(streamCore);
						streamCore.Dispose();
						_xlPackage.Package.Flush();

						// create the relationship between the workbook and the new shared strings part
						_xlPackage.Package.CreateRelationship(CorePropertiesUri, TargetMode.Internal, @"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties");
						_xlPackage.Package.Flush();
					}
				}
				return (_xmlPropertiesCore);
			}
		}
		#endregion

		/// <summary>
		/// Gets/sets the title property of the document (core property)
		/// </summary>
		public string Title
		{
			get	{	return GetCorePropertyValue("dc", "title"); }
			set { SetCorePropertyValue("dc", "title", value); }
		}

		/// <summary>
		/// Gets/sets the subject property of the document (core property)
		/// </summary>
		public string Subject
		{
			get { return GetCorePropertyValue("dc", "subject"); }
			set { SetCorePropertyValue("dc", "subject", value); }
		}

		/// <summary>
		/// Gets/sets the author property of the document (core property)
		/// </summary>
		public string Author
		{
			get { return GetCorePropertyValue("dc", "creator"); }
			set { SetCorePropertyValue("dc", "creator", value); }
		}

		/// <summary>
		/// Gets/sets the comments property of the document (core property)
		/// </summary>
		public string Comments
		{
			get { return GetCorePropertyValue("dc", "description"); }
			set { SetCorePropertyValue("dc", "description", value); }
		}

		/// <summary>
		/// Gets/sets the keywords property of the document (core property)
		/// </summary>
		public string Keywords
		{
			get { return GetCorePropertyValue("cp", "keywords"); }
			set { SetCorePropertyValue("cp", "keywords", value); }
		}

		/// <summary>
		/// Gets/sets the lastModifiedBy property of the document (core property)
		/// </summary>
		public string LastModifiedBy
		{
			get { return GetCorePropertyValue("cp", "lastModifiedBy"); }
			set { SetCorePropertyValue("cp", "lastModifiedBy", value); }
		}

		/// <summary>
		/// Gets/sets the lastPrinted property of the document (core property)
		/// </summary>
		public string LastPrinted
		{
			get { return GetCorePropertyValue("cp", "lastPrinted"); }
			set { SetCorePropertyValue("cp", "lastPrinted", value); }
		}

		/// <summary>
		/// Gets/sets the category property of the document (core property)
		/// </summary>
		public string Category
		{
			get { return GetCorePropertyValue("cp", "category"); }
			set { SetCorePropertyValue("cp", "category", value); }
		}

		/// <summary>
		/// Gets/sets the status property of the document (core property)
		/// </summary>
		public string Status
		{
			get { return GetCorePropertyValue("cp", "contentStatus"); }
			set { SetCorePropertyValue("cp", "contentStatus", value); }
		}
		
		#region Get and Set Core Properties
		/// <summary>
		/// Gets the value of a core property
		/// Private method, for internal use only!
		/// </summary>
		/// <param name="nameSpace">The namespace of the property</param>
		/// <param name="propertyName">The property name</param>
		/// <returns>The current value of the property</returns>
		private string GetCorePropertyValue(string nameSpace, string propertyName)
		{
			string retValue = null;
			string searchString = string.Format("//cp:coreProperties/{0}:{1}", nameSpace, propertyName);
			var node = CorePropertiesXml.XPathSelectElement(searchString, _nsManager);
			if (node != null)
			{
				retValue = node.Value;
			}
			return retValue;
		}

		/// <summary>
		/// Sets a core property value.
		/// Private method, for internal use only!
		/// </summary>
		/// <param name="nameSpace">The property's namespace</param>
		/// <param name="propertyName">The name of the property</param>
		/// <param name="propValue">The value of the property</param>
		private void SetCorePropertyValue(string nameSpace, string propertyName, string propValue)
		{
			string searchString = string.Format("//cp:coreProperties/{0}:{1}", nameSpace, propertyName);
			var node = CorePropertiesXml.XPathSelectElement(searchString, _nsManager);
			if (node == null)
			{
				// the property does not exist, so create the XML node
				string schema = schemaCore;
				switch (nameSpace)
				{
					case "cp": schema = schemaCore;	break;
					case "dc": schema = schemaDc;	break;
					case "dcterms": schema = schemaDcTerms;	break;
					case "dcmitype": schema = schemaDcmiType;	break;
					case "xsi": schema = schemaXsi;	break;
				}
				node = new XElement(propertyName)
					.AddSchemaAttribute(schema, nameSpace);
				CorePropertiesXml.Document.Add(node);
			}
			node.Value = propValue;
		}
		#endregion

		#endregion

		#region Extended Properties
		#region ExtendedPropertiesXml
		/// <summary>
		/// Provides access to the XML document that holds the extended properties of the document (app.xml)
		/// </summary>
		public XDocument ExtendedPropertiesXml
		{
			get
			{
				if (_xmlPropertiesExtended == null)
				{
					if (_xlPackage.Package.PartExists(ExtendedPropertiesUri))
						_xmlPropertiesExtended = _xlPackage.GetXmlFromUri(ExtendedPropertiesUri);
					else
					{
						// create a new extended properties part and add to the package
                        PackagePart partExtended = _xlPackage.Package.CreatePart(ExtendedPropertiesUri, @"application/vnd.openxmlformats-officedocument.extended-properties+xml", CompressionOption.Normal);

						// create the extended properties XML (with no entries in it)
						_xmlPropertiesExtended = new XDocument(
							new XElement("Properties")
							.AddSchemaAttribute(XNamespace.Xmlns, schemeExtended)
							.AddSchemaAttribute(schemaVt, "vt"));

						// save it to the package
						StreamWriter streamExtended = new StreamWriter(partExtended.GetStream(FileMode.Create, FileAccess.Write));
						_xmlPropertiesExtended.Save(streamExtended);
						streamExtended.Dispose();
						_xlPackage.Package.Flush();

						// create the relationship between the workbook and the new shared strings part
						_xlPackage.Package.CreateRelationship(ExtendedPropertiesUri, TargetMode.Internal, @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties");
						_xlPackage.Package.Flush();
					}
				}
				return (_xmlPropertiesExtended);
			}
		}
		#endregion

		/// <summary>
		/// Gets the Application property of the document (extended property)
		/// </summary>
		public string Application
		{
			get { return GetExtendedPropertyValue("Application"); }
		}

		/// <summary>
		/// Gets/sets the HyperlinkBase property of the document (extended property)
		/// </summary>
		public Uri HyperlinkBase
		{
			get { return new Uri(GetExtendedPropertyValue("HyperlinkBase")); }
			set	{	SetExtendedPropertyValue("HyperlinkBase", value.ToString());	}
		}

		/// <summary>
		/// Gets the AppVersion property of the document (extended property)
		/// </summary>
		public string AppVersion
		{
			get { return GetExtendedPropertyValue("AppVersion"); }
		}

		/// <summary>
		/// Gets/sets the Company property of the document (extended property)
		/// </summary>
		public string Company
		{
			get { return GetExtendedPropertyValue("Company"); }
			set { SetExtendedPropertyValue("Company", value); }
		}

		/// <summary>
		/// Gets/sets the Manager property of the document (extended property)
		/// </summary>
		public string Manager
		{
			get { return GetExtendedPropertyValue("Manager"); }
			set { SetExtendedPropertyValue("Manager", value); }
		}

		#region Get and Set Extended Properties
		private string GetExtendedPropertyValue(string propertyName)
		{
			string retValue = null;
			string searchString = string.Format("//xp:Properties/xp:{0}", propertyName);
			var node = ExtendedPropertiesXml.XPathSelectElement(searchString, _nsManager);
			if (node != null)
			{
				retValue = node.Value;
			}
			return retValue;
		}

		private void SetExtendedPropertyValue(string propertyName, string propValue)
		{
			string searchString = string.Format("//xp:Properties/xp:{0}", propertyName);
			var node = ExtendedPropertiesXml.XPathSelectElement(searchString, _nsManager);
			if (node == null)
			{
				// the property does not exist, so create the XML node
				node = 
					new XElement(propertyName)
					.AddSchemaAttribute(XNamespace.Xmlns, schemeExtended);
				ExtendedPropertiesXml.Document.Add(node);
			}
			node.Value = propValue;
		}
		#endregion
		#endregion

		#region Custom Properties

		#region CustomPropertiesXml
		/// <summary>
		/// Provides access to the XML document which holds the document's custom properties
		/// </summary>
		public XDocument CustomPropertiesXml
		{
			get
			{
				if (_xmlPropertiesCustom == null)
				{
					if (_xlPackage.Package.PartExists(CustomPropertiesUri))
						_xmlPropertiesCustom = _xlPackage.GetXmlFromUri(CustomPropertiesUri);
					else
					{
						// create a new extended properties part and add to the package
                        PackagePart partCustom = _xlPackage.Package.CreatePart(CustomPropertiesUri, @"application/vnd.openxmlformats-officedocument.custom-properties+xml", CompressionOption.Normal);

						// create the extended properties XML (with no entries in it)
						_xmlPropertiesCustom = new XDocument(
							new XElement("Properties")
							.AddSchemaAttribute(XNamespace.Xmlns, schemaCustom)
							.AddSchemaAttribute(schemaVt, "vt"));

						// save it to the package
						StreamWriter streamCustom = new StreamWriter(partCustom.GetStream(FileMode.Create, FileAccess.Write));
						_xmlPropertiesCustom.Save(streamCustom);
						streamCustom.Dispose();
						_xlPackage.Package.Flush();

						// create the relationship between the workbook and the new shared strings part
						_xlPackage.Package.CreateRelationship(CustomPropertiesUri, TargetMode.Internal, @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties");
						_xlPackage.Package.Flush();
					}
				}
				return (_xmlPropertiesCustom);
			}
		}
		#endregion

		#region Get and Set Custom Properties
		/// <summary>
		/// Gets the value of a custom property
		/// </summary>
		/// <param name="propertyName">The name of the property</param>
		/// <returns>The current value of the property</returns>
		public string GetCustomPropertyValue(string propertyName)
		{
			string retValue = null;
			string searchString = string.Format("//ctp:Properties/ctp:property/@name[.='{0}']", propertyName);
			var node = CustomPropertiesXml.XPathSelectElement(searchString, _nsManager);
			if (node != null)
			{
				retValue = (node.LastNode as XElement).Value;
			}
			return retValue;
		}

		/// <summary>
		/// Allows you to set the value of a current custom property or create 
		/// your own custom property.  
		/// Currently only supports string values.
		/// </summary>
		/// <param name="propertyName">The name of the property</param>
		/// <param name="propValue">The value of the property</param>
		public void SetCustomPropertyValue(string propertyName, string propValue)
		{
			// TODO:  provide support for other custom property data types
			string searchString = @"//ctp:Properties";
			var allProps = CustomPropertiesXml.XPathSelectElement(searchString, _nsManager);

			searchString = string.Format("//ctp:Properties/ctp:property/@ctp:name[.='{0}']", propertyName);
			var node = CustomPropertiesXml.XPathSelectElement(searchString, _nsManager);
			if (node == null)
			{
				// the property does not exist, so first find the max PID
				int pid = 4;
				foreach (var prop in allProps.Nodes())
				{
					var attr = ((XElement)prop).Attribute("pid");
					if (attr != null)
					{
						int attrValue = int.Parse(attr.Value);
						if (attrValue > pid)
							pid = attrValue;
					}
				}
				pid++;
				// the property does not exist, so create the XML node
				var element = new XElement("property")
					.AddSchemaAttribute(XNamespace.Xmlns, schemaCustom);				

				element.SetAttribute("fmtid", "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}");
				element.SetAttribute("pid", pid.ToString());  // custom property pid
				element.SetAttribute("name", propertyName);

				XElement valueElement = new XElement("vt").AddSchemaAttribute("lpwstr", schemaVt);
				valueElement.Value = propValue;
				element.Add(valueElement);

				CustomPropertiesXml.Document.Add(element);
			}
			else
			{
				(node.LastNode as XElement).Value = propValue;
			}
			
		}
		#endregion
		#endregion

		#region Save  // OfficeProperties save
		/// <summary>
		/// Saves the office document properties back to the package (if they exist!).
		/// </summary>
		protected internal void Save()
		{
			if (_xmlPropertiesCore != null)
			{
				_xlPackage.WriteDebugFile(_xmlPropertiesCore, "docProps", "core.xml");
				_xlPackage.SavePart(CorePropertiesUri, _xmlPropertiesCore);
			}
			if (_xmlPropertiesExtended != null)
			{
				_xlPackage.WriteDebugFile(_xmlPropertiesExtended, "docProps", "app.xml");
				_xlPackage.SavePart(ExtendedPropertiesUri, _xmlPropertiesExtended);
			}
			if (_xmlPropertiesCustom != null)
			{
				_xlPackage.WriteDebugFile(_xmlPropertiesCustom, "docProps", "custom.xml");
				_xlPackage.SavePart(CustomPropertiesUri, _xmlPropertiesCustom);
			}

		}
		#endregion

	}
}
