context: // Copyright (C) 2004  Kevin Downs
  // Parts Copyright (c) 2002 Jean-Claude Manoli
  //
  // 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  
  using System;
  using System.IO;
  using System.Collections;
  using System.Xml;
  using System.Reflection;
  using Microsoft.Win32;
  using System.Diagnostics;
  
  namespace NDoc3.Core.Reflection {
  	/// <summary>
  	/// Caches XML summaries.
  	/// </summary>
  	internal class ExternalXmlSummaryCache {
  		private Hashtable cachedDocs;
  		private Hashtable summaries;
  		private readonly string localizationLanguage;
  
  		/// <summary>
  		/// Initializes a new instance of the <see cref="ExternalXmlSummaryCache" />
  		/// class.
  		/// </summary>
  		public ExternalXmlSummaryCache(string localizationLanguage) {
  			Flush();
  			this.localizationLanguage = localizationLanguage;
  		}
  
  		/// <summary>
  		/// Flushes the <see cref="ExternalXmlSummaryCache" />.
  		/// </summary>
  		public void Flush() {
  			cachedDocs = new Hashtable();
  			summaries = new Hashtable();
  		}
  
  		/// <summary>
  		/// Adds given XML document to the summary cache.
  		/// </summary>
  		/// <param name="xmlFileName">The filename of XML document to cache.</param>
  		public void AddXmlDoc(string xmlFileName) {
  			int start = Environment.TickCount;
  
  			XmlTextReader reader = null;
  			try {
  				reader = new XmlTextReader(xmlFileName);
  				CacheSummaries(reader);
  			} finally {
  				if (reader != null) reader.Close();
  			}
  			Debug.WriteLine("Cached doc : " + Path.GetFileName(xmlFileName) + " (" + ((Environment.TickCount - start) / 1000.0) + " sec.)");
  		}
  
  		/// <summary>
  		/// Gets the xml documentation for the assembly of the specified type.
  		/// </summary>
  		public void GetXmlFor(Type type) {
  			if (type.Assembly.FullName == null) throw new InvalidOperationException("type.Assembly.FullName are null");
  			string searchedDoc = (string)cachedDocs[type.Assembly.FullName];
  
  			if (searchedDoc == null) {
  				//Debug.WriteLine("Attempting to locate XML docs for " + type.Assembly.FullName);
  				Type t = Type.GetType(type.AssemblyQualifiedName);
  				if (t != null) {
  					Assembly a = t.Assembly;
  					string assemblyPath = a.Location;
  
  					if (!String.IsNullOrEmpty(assemblyPath)) {
  						string docPath = Path.ChangeExtension(assemblyPath, ".xml");
  
  						//if not found, try loading __AssemblyInfo__.ini
  						if (!File.Exists(docPath)) {
  							string infoPath = Path.Combine(
  								Path.GetDirectoryName(docPath), "__AssemblyInfo__.ini");
  							docPath = null;
  
  							if (File.Exists(infoPath)) {
  								//Debug.WriteLine("Loading __AssemblyInfo__.ini.");
  								TextReader reader = new StreamReader(infoPath);
  								string line;
  								try {
  									while ((line = reader.ReadLine()) != null) {
  										if (line.StartsWith("URL=file:///")) {
  											docPath = Path.ChangeExtension(line.Substring(12), ".xml");
  											break;
  										}
  									}
  								} finally {
  									reader.Close();
  								}
  							}
  						}
  
  #if (!MONO)
  						//TODO: search in the mono lib folder, if they ever give us the xml documentation
  						// If still not found, try locating the assembly in the Framework folder
  						if ((docPath == null) || (!File.Exists(docPath))) {
  							string frameworkPath = GetDotnetFrameworkPath(a.ImageRuntimeVersion);
  							if (frameworkPath != null) {
  								string localizedFrameworkPath = Path.Combine(frameworkPath, localizationLanguage);
  								if (Directory.Exists(localizedFrameworkPath)) {
  									docPath = Path.Combine(localizedFrameworkPath, a.GetName().Name + ".xml");
  								} else {
  									localizedFrameworkPath = Path.Combine(frameworkPath, localizationLanguage.Substring(0, 2));
  									if (Directory.Exists(localizedFrameworkPath))
  										docPath = Path.Combine(localizedFrameworkPath, a.GetName().Name + ".xml");
  								}
  								if ((docPath == null) || (!File.Exists(docPath))) {
  									docPath = Path.Combine(frameworkPath, a.GetName().Name + ".xml");
  								}
  							}
  						}
  #endif
  
  						if ((docPath != null) && (File.Exists(docPath))) {
  							Debug.WriteLine("Docs found : " + docPath);
  							AddXmlDoc(docPath);
  							searchedDoc = docPath;
  						}
  					}
  				}
  
  
  				//if the doc was still not found, create an empty document filename
  				if (searchedDoc == null) {
  					Trace.WriteLine("XML Doc not found for " + type.Assembly.FullName);
  					searchedDoc = "";
  				}
  				//cache the document path
  				cachedDocs.Add(type.Assembly.FullName, searchedDoc);
  			}
  		}
  
  		/// <summary>
  		/// Caches summaries for all members in XML documentation file.
  		/// </summary>
  		/// <param name="reader">XmlTextReader for XML Documentation</param>
  		/// <remarks>If a member does not have a summary, a zero-length string is stored instead.</remarks>
  		private void CacheSummaries(XmlTextReader reader) {
  			object oMember = reader.NameTable.Add("member");
  			object oSummary = reader.NameTable.Add("summary");
  
  			reader.MoveToContent();
  
  			string MemberID = "";
  			string Summary = "";
  			while (reader.Read()) {
  				switch (reader.NodeType) {
  					case XmlNodeType.Element:
  
  						if (reader.Name.Equals(oMember)) {
  							MemberID = reader.GetAttribute("name");
  							Summary = "";
  						}
  						if (reader.Name.Equals(oSummary)) {
  							Summary = reader.ReadInnerXml();
  							Summary = PreprocessDoc(MemberID, Summary);
  						}
  						break;
  
  					case XmlNodeType.EndElement:
  
  						if (reader.Name.Equals(oMember)) {
  							if (!summaries.ContainsKey(MemberID)) {
  								summaries.Add(MemberID, Summary);
  							}
  						}
  						break;
  
  					default:
  						break;
  				}
  			}
  		}
  
  		/// <summary>
  		/// Preprocess documentation before placing it in the cache.
  		/// </summary>
  		/// <param name="id">Member name 'id' to which the docs belong</param>
  		/// <param name="doc">A string containing the members documentation</param>
  		/// <returns>processed doc string</returns>
  		private static string PreprocessDoc(string id, string doc) {
  			//create an XmlDocument containg the memeber's documentation
  			XmlTextReader reader = new XmlTextReader(new StringReader("<root>" + doc + "</root>"));
  			reader.WhitespaceHandling = WhitespaceHandling.All;
  
  			XmlDocument xmldoc = new XmlDocument();
  			xmldoc.PreserveWhitespace = true;
  			xmldoc.Load(reader);
  
  			if (xmldoc.DocumentElement == null) throw new InvalidOperationException("xmldoc.DocumentElement are null");
  			
  			CleanupNodes(xmldoc.DocumentElement.ChildNodes);
  			
  			ProcessSeeLinks(id, xmldoc.DocumentElement.ChildNodes);
  			return xmldoc.DocumentElement.InnerXml;
  		}
  
  		/// <summary>
  		/// strip out redundant newlines and spaces from documentation.
  		/// </summary>
  		/// <param name="nodes">list of nodes</param>
  		private static void CleanupNodes(XmlNodeList nodes) {
  			foreach (XmlNode node in nodes) {
  				if (node.NodeType == XmlNodeType.Element) {
  					CleanupNodes(node.ChildNodes);
  
  					// Trim attribute values...
  					foreach (XmlNode attr in node.Attributes) {
  						attr.Value = attr.Value.Trim();
  					}
  				}
  				if (node.NodeType == XmlNodeType.Text) {
  					node.Value = node.Value.Replace("\t", "    ").Replace("\n", " ").Replace("\r", " ").Replace("        ", " ").Replace("    ", " ").Replace("   ", " ").Replace("  ", " ");
  				}
  			}
  		}
  
  		/// <summary>
  		/// Add 'nolink' attributes to self referencing or duplicate see tags.
  		/// </summary>
  		/// <param name="id">current member name 'id'</param>
  		/// <param name="nodes">list of top-level nodes</param>
  		/// <remarks>
  		/// </remarks>
  		private static void ProcessSeeLinks(string id, XmlNodeList nodes) {
  			foreach (XmlNode node in nodes) {
  				if (node.NodeType == XmlNodeType.Element) {
  					Hashtable linkTable = null;
  					MarkupSeeLinks(ref linkTable, id, node);
  				}
  			}
  		}
  
  		/// <summary>
  		/// Search tags for duplicate or self-referencing see links.
  		/// </summary>
  		/// <param name="linkTable">A table of previous links.</param>
  		/// <param name="id">current member name 'id'</param>
  		/// <param name="node">an Xml Node containing a doc tag</param>
  		private static void MarkupSeeLinks(ref Hashtable linkTable, string id, XmlNode node) {
  			if (node.LocalName == "see") {
  				//we will only do this for crefs
  				XmlAttribute cref = node.Attributes["cref"];
  				if (cref != null) {
  					if (cref.Value == id) //self referencing tag
  					{
  						XmlAttribute dup = node.OwnerDocument.CreateAttribute("nolink");
  						dup.Value = "true";
  						node.Attributes.Append(dup);
  					} else {
  						if (linkTable == null) {
  							//assume an resonable initial table size,
  							//so we don't have to resize often.
  							linkTable = new Hashtable(16);
  						}
  						if (linkTable.ContainsKey(cref.Value)) {
  							XmlAttribute dup = node.OwnerDocument.CreateAttribute("nolink");
  							dup.Value = "true";
  							node.Attributes.Append(dup);
  						} else {
  							linkTable.Add(cref.Value, null);
  						}
  					}
  				}
  			}
  
  			//search this tags' children
  			foreach (XmlNode childnode in node.ChildNodes) {
  				if (childnode.NodeType == XmlNodeType.Element) {
  					MarkupSeeLinks(ref linkTable, id, childnode);
  				}
  			}
  		}
  
  
  		/// <summary>
  		/// Returns the original summary for a member inherited from a specified type. 
  		/// </summary>
  		/// <param name="memberID">The member ID to lookup.</param>
  		/// <param name="declaringType">The type that declares that member.</param>
  		/// <returns>The summary xml.  If not found, returns an zero length string.</returns>
  		public string GetSummary(string memberID, Type declaringType) {
  			//extract member type (T:, P:, etc.)
  			string memberType = memberID.Substring(0, 2);
  
  			//extract member name
  			int i = memberID.IndexOf('(');
  			string memberName = i > -1 ? memberID.Substring(memberID.LastIndexOf('.', i) + 1)
  				: memberID.Substring(memberID.LastIndexOf('.') + 1);
  
  			//the member id in the declaring assembly
  			string key = memberType + declaringType.FullName.Replace("+", ".") + "." + memberName;
  
  			//check the summaries cache first
  			string summary = (string)summaries[key];
  
  			if (summary == null) {
  				//lookup the xml document
  				GetXmlFor(declaringType);
  
  				//the summary should now be cached (if it exists!)
  				//so lets have another go at getting it...
  				summary = (string)summaries[key];
  
  				//if no summary was not found, create an blank one
  
gt: 				if (summary == null) {