// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Media;
using System.Xml;
using System.Xml.Schema;

using ICSharpCode.AvalonEdit.Utils;

namespace ICSharpCode.AvalonEdit.Highlighting.Xshd
{
	/// <summary>
	/// Loads .xshd files, version 2.0.
	/// Version 2.0 files are recognized by the namespace.
	/// </summary>
	static class V2Loader
	{
		public const string Namespace = "http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008";
		
		static XmlSchemaSet schemaSet;
		
		static XmlSchemaSet SchemaSet {
			get {
				if (schemaSet == null) {
					schemaSet = HighlightingLoader.LoadSchemaSet(new XmlTextReader(
						Resources.OpenStream("ModeV2.xsd")));
				}
				return schemaSet;
			}
		}
		
		public static XshdSyntaxDefinition LoadDefinition(XmlReader reader, bool skipValidation)
		{
			reader = HighlightingLoader.GetValidatingReader(reader, true, skipValidation ? null : SchemaSet);
			reader.Read();
			return ParseDefinition(reader);
		}
		
		static XshdSyntaxDefinition ParseDefinition(XmlReader reader)
		{
			Debug.Assert(reader.LocalName == "SyntaxDefinition");
			XshdSyntaxDefinition def = new XshdSyntaxDefinition();
			def.Name = reader.GetAttribute("name");
			string extensions = reader.GetAttribute("extensions");
			if (extensions != null)
				def.Extensions.AddRange(extensions.Split(';'));
			ParseElements(def.Elements, reader);
			Debug.Assert(reader.NodeType == XmlNodeType.EndElement);
			Debug.Assert(reader.LocalName == "SyntaxDefinition");
			return def;
		}
		
		static void ParseElements(ICollection<XshdElement> c, XmlReader reader)
		{
			if (reader.IsEmptyElement)
				return;
			while (reader.Read() && reader.NodeType != XmlNodeType.EndElement) {
				Debug.Assert(reader.NodeType == XmlNodeType.Element);
				switch (reader.Name) {
					case "RuleSet":
						c.Add(ParseRuleSet(reader));
						break;
					case "Property":
						c.Add(ParseProperty(reader));
						break;
					case "Color":
						c.Add(ParseNamedColor(reader));
						break;
					case "Keywords":
						c.Add(ParseKeywords(reader));
						break;
					case "Span":
						c.Add(ParseSpan(reader));
						break;
					case "Import":
						c.Add(ParseImport(reader));
						break;
					case "Rule":
						c.Add(ParseRule(reader));
						break;
					default:
						throw new NotSupportedException("Unknown element " + reader.Name);
				}
			}
		}
		
		static XshdElement ParseProperty(XmlReader reader)
		{
			XshdProperty property = new XshdProperty();
			SetPosition(property, reader);
			property.Name = reader.GetAttribute("name");
			property.Value = reader.GetAttribute("value");
			return property;
		}
		
		static XshdRuleSet ParseRuleSet(XmlReader reader)
		{
			XshdRuleSet ruleSet = new XshdRuleSet();
			SetPosition(ruleSet, reader);
			ruleSet.Name = reader.GetAttribute("name");
			ruleSet.IgnoreCase = reader.GetBoolAttribute("ignoreCase");
			
			CheckElementName(reader, ruleSet.Name);
			ParseElements(ruleSet.Elements, reader);
			return ruleSet;
		}
		
		static XshdRule ParseRule(XmlReader reader)
		{
			XshdRule rule = new XshdRule();
			SetPosition(rule, reader);
			rule.ColorReference = ParseColorReference(reader);
			if (!reader.IsEmptyElement) {
				reader.Read();
				if (reader.NodeType == XmlNodeType.Text) {
					rule.Regex = reader.ReadContentAsString();
					rule.RegexType = XshdRegexType.IgnorePatternWhitespace;
				}
			}
			return rule;
		}
		
		static XshdKeywords ParseKeywords(XmlReader reader)
		{
			XshdKeywords keywords = new XshdKeywords();
			SetPosition(keywords, reader);
			keywords.ColorReference = ParseColorReference(reader);
			reader.Read();
			while (reader.NodeType != XmlNodeType.EndElement) {
				Debug.Assert(reader.NodeType == XmlNodeType.Element);
				keywords.Words.Add(reader.ReadElementString());
			}
			return keywords;
		}
		
		static XshdImport ParseImport(XmlReader reader)
		{
			XshdImport import = new XshdImport();
			SetPosition(import, reader);
			import.RuleSetReference = ParseRuleSetReference(reader);
			if (!reader.IsEmptyElement)
				reader.Skip();
			return import;
		}
		
		static XshdSpan ParseSpan(XmlReader reader)
		{
			XshdSpan span = new XshdSpan();
			SetPosition(span, reader);
			span.BeginRegex = reader.GetAttribute("begin");
			span.EndRegex = reader.GetAttribute("end");
			span.Multiline = reader.GetBoolAttribute("multiline") ?? false;
			span.SpanColorReference = ParseColorReference(reader);
			span.RuleSetReference = ParseRuleSetReference(reader);
			if (!reader.IsEmptyElement) {
				reader.Read();
				while (reader.NodeType != XmlNodeType.EndElement) {
					Debug.Assert(reader.NodeType == XmlNodeType.Element);
					switch (reader.Name) {
						case "Begin":
							if (span.BeginRegex != null)
								throw Error(reader, "Duplicate Begin regex");
							span.BeginColorReference = ParseColorReference(reader);
							span.BeginRegex = reader.ReadElementString();
							span.BeginRegexType = XshdRegexType.IgnorePatternWhitespace;
							break;
						case "End":
							if (span.EndRegex != null)
								throw Error(reader, "Duplicate End regex");
							span.EndColorReference = ParseColorReference(reader);
							span.EndRegex = reader.ReadElementString();
							span.EndRegexType = XshdRegexType.IgnorePatternWhitespace;
							break;
						case "RuleSet":
							if (span.RuleSetReference.ReferencedElement != null)
								throw Error(reader, "Cannot specify both inline RuleSet and RuleSet reference");
							span.RuleSetReference = new XshdReference<XshdRuleSet>(ParseRuleSet(reader));
							reader.Read();
							break;
						default:
							throw new NotSupportedException("Unknown element " + reader.Name);
					}
				}
			}
			return span;
		}
		
		static Exception Error(XmlReader reader, string message)
		{
			return Error(reader as IXmlLineInfo, message);
		}
		
		static Exception Error(IXmlLineInfo lineInfo, string message)
		{
			if (lineInfo != null)
				return new HighlightingDefinitionInvalidException(HighlightingLoader.FormatExceptionMessage(message, lineInfo.LineNumber, lineInfo.LinePosition));
			else
				return new HighlightingDefinitionInvalidException(message);
		}
		
		/// <summary>
		/// Sets the element's position to the XmlReader's position.
		/// </summary>
		static void SetPosition(XshdElement element, XmlReader reader)
		{
			IXmlLineInfo lineInfo = reader as IXmlLineInfo;
			if (lineInfo != null) {
				element.LineNumber = lineInfo.LineNumber;
				element.ColumnNumber = lineInfo.LinePosition;
			}
		}
		
		static XshdReference<XshdRuleSet> ParseRuleSetReference(XmlReader reader)
		{
			string ruleSet = reader.GetAttribute("ruleSet");
			if (ruleSet != null) {
				// '/' is valid in highlighting definition names, so we need the last occurence
				int pos = ruleSet.LastIndexOf('/');
				if (pos >= 0) {
					return new XshdReference<XshdRuleSet>(ruleSet.Substring(0, pos), ruleSet.Substring(pos + 1));
				} else {
					return new XshdReference<XshdRuleSet>(null, ruleSet);
				}
			} else {
				return new XshdReference<XshdRuleSet>();
			}
		}
		
		static void CheckElementName(XmlReader reader, string name)
		{
			if (name != null) {
				if (name.Length == 0)
					throw Error(reader, "The empty string is not a valid name.");
				if (name.IndexOf('/') >= 0)
					throw Error(reader, "Element names must not contain a slash.");
			}
		}
		
		#region ParseColor
		static XshdColor ParseNamedColor(XmlReader reader)
		{
			XshdColor color = ParseColorAttributes(reader);
			// check removed: invisible named colors may be useful now that apps can read highlighting data
			//if (color.Foreground == null && color.FontWeight == null && color.FontStyle == null)
			//	throw Error(reader, "A named color must have at least one element.");
			color.Name = reader.GetAttribute("name");
			CheckElementName(reader, color.Name);
			color.ExampleText = reader.GetAttribute("exampleText");
			return color;
		}
		
		static XshdReference<XshdColor> ParseColorReference(XmlReader reader)
		{
			string color = reader.GetAttribute("color");
			if (color != null) {
				int pos = color.LastIndexOf('/');
				if (pos >= 0) {
					return new XshdReference<XshdColor>(color.Substring(0, pos), color.Substring(pos + 1));
				} else {
					return new XshdReference<XshdColor>(null, color);
				}
			} else {
				return new XshdReference<XshdColor>(ParseColorAttributes(reader));
			}
		}
		
		static XshdColor ParseColorAttributes(XmlReader reader)
		{
			XshdColor color = new XshdColor();
			SetPosition(color, reader);
			IXmlLineInfo position = reader as IXmlLineInfo;
			color.Foreground = ParseColor(position, reader.GetAttribute("foreground"));
			color.Background = ParseColor(position, reader.GetAttribute("background"));
			color.FontWeight = ParseFontWeight(reader.GetAttribute("fontWeight"));
			color.FontStyle = ParseFontStyle(reader.GetAttribute("fontStyle"));
			return color;
		}
		
		internal readonly static ColorConverter ColorConverter = new ColorConverter();
		internal readonly static FontWeightConverter FontWeightConverter = new FontWeightConverter();
		internal readonly static FontStyleConverter FontStyleConverter = new FontStyleConverter();
		
		static HighlightingBrush ParseColor(IXmlLineInfo lineInfo, string color)
		{
			if (string.IsNullOrEmpty(color))
				return null;
			if (color.StartsWith("SystemColors.", StringComparison.Ordinal))
				return GetSystemColorBrush(lineInfo, color);
			else
				return FixedColorHighlightingBrush((Color?)ColorConverter.ConvertFromInvariantString(color));
		}
		
		internal static SystemColorHighlightingBrush GetSystemColorBrush(IXmlLineInfo lineInfo, string name)
		{
			Debug.Assert(name.StartsWith("SystemColors.", StringComparison.Ordinal));
			string shortName = name.Substring(13);
			var property = typeof(SystemColors).GetProperty(shortName + "Brush");
			if (property == null)
				throw Error(lineInfo, "Cannot find '" + name + "'.");
			return new SystemColorHighlightingBrush(property);
		}
		
		static HighlightingBrush FixedColorHighlightingBrush(Color? color)
		{
			if (color == null)
				return null;
			return new SimpleHighlightingBrush(color.Value);
		}
		
		static FontWeight? ParseFontWeight(string fontWeight)
		{
			if (string.IsNullOrEmpty(fontWeight))
				return null;
			return (FontWeight?)FontWeightConverter.ConvertFromInvariantString(fontWeight);
		}
		
		static FontStyle? ParseFontStyle(string fontStyle)
		{
			if (string.IsNullOrEmpty(fontStyle))
				return null;
			return (FontStyle?)FontStyleConverter.ConvertFromInvariantString(fontStyle);
		}
		#endregion
	}
}
