#region Copyright 2010-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0
/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Text;
using CSharpTest.Net.IO;
namespace CSharpTest.Net.Html
{
    [AttributeUsage(AttributeTargets.Field)]
    class DOCTYPEAttribute : Attribute
    {
        public DOCTYPEAttribute(string resource) : this(resource, String.Empty, String.Empty) { }
        public DOCTYPEAttribute(string resource, string Public, string System)
        { RESOURCE = resource; PUBLIC = Public; SYSTEM = System; }
        public readonly String RESOURCE;
        public String PUBLIC;
        public String SYSTEM;
        public XhtmlDTDSpecification DTD;
    }
    /// 
    /// Defines the required DTD specification
    /// 
    public enum XhtmlDTDSpecification 
    {
        ///  Use DTD only if defined 
        None,
        ///  
        /// Use the XHTML 1.0 Transitional DTD 
        /// <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
        /// 
        [DOCTYPE("Xhtml1_0.xhtml1-strict.dtd", PUBLIC = "-/W3C/DTD XHTML 1.0 Strict/EN", SYSTEM = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd")]
        XhtmlStrict_10,
        ///  
        /// Use the XHTML 1.0 Transitional DTD 
        /// <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
        /// 
        [DOCTYPE("Xhtml1_0.xhtml1-transitional.dtd", PUBLIC = "-/W3C/DTD XHTML 1.0 Transitional/EN", SYSTEM = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd")]
        XhtmlTransitional_10,
        ///  
        /// Use the XHTML 1.0 Transitional DTD 
        /// <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
        /// 
        [DOCTYPE("Xhtml1_0.xhtml1-frameset.dtd", PUBLIC = "-/W3C/DTD XHTML 1.0 Frameset/EN", SYSTEM = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd")]
        XhtmlFrameset_10,
        ///  
        /// Allow any of the supported DTDs, but must be declared and compliant 
        /// 
        Any
    }
    /// 
    /// Provides validation of Xhtml documents based on w3c DTDs
    /// 
    public class XhtmlValidation
    {
        readonly XmlNameTable _nameTable;
        readonly XmlNamespaceManager _namespaces;
        readonly XhtmlDTDSpecification _requiresDtd;
        ///  Creates a validator that requires documents to use any of the three DTD specifications 
        public XhtmlValidation()
            : this(XhtmlDTDSpecification.Any) { }
        ///  Creates a validator that requires documents to use the specified DTD 
        public XhtmlValidation(XhtmlDTDSpecification dtdRequired)
        {
            _requiresDtd = dtdRequired;
            _nameTable = new NameTable();
            _namespaces = new XmlNamespaceManager(_nameTable);
            _namespaces.AddNamespace("htm", "http://www.w3.org/1999/xhtml");
        }
        ///  Validate the input textreader 
        public void Validate(string originalFilename, TextReader reader)
        {
            ValidateDocument(_requiresDtd, originalFilename, reader.ReadToEnd());
        }
        ///  Validate the input textreader 
        public void Validate(TextReader reader)
        {
            using (TempFile temp = new TempFile())
            {
                string content = reader.ReadToEnd();
                temp.WriteAllText(content);
                ValidateDocument(_requiresDtd, temp.TempPath, content);
            }
        }
        ///  Validate the input filename 
        public void Validate(string filename)
        {
            ValidateDocument(_requiresDtd, filename, File.ReadAllText(filename));
        }
        XmlReaderSettings MakeReaderSettings(System.Xml.XmlResolver resolver, ValidationEventHandler errorHandler)
        {
            XmlReaderSettings settings = null;
            settings = new XmlReaderSettings();
            settings.CheckCharacters = true;
            settings.ConformanceLevel = ConformanceLevel.Document;
            settings.IgnoreComments = true;
            settings.NameTable = _nameTable;
            settings.ValidationFlags = XmlSchemaValidationFlags.ReportValidationWarnings;
#pragma warning disable 612, 618
            settings.ValidationType = _requiresDtd == XhtmlDTDSpecification.None ? ValidationType.Auto : ValidationType.DTD;
#pragma warning restore 612, 618
            settings.ValidationEventHandler += errorHandler;
#if NET20 || NET35
            settings.ProhibitDtd = false;
#else
            settings.DtdProcessing = DtdProcessing.Parse;
#endif
            settings.XmlResolver = resolver;
            return settings;
        }
        void ValidateDocument(XhtmlDTDSpecification expect, string filename, string contents)
        {
            XmlResolver resolver = new XmlResolver();
            resolver.Credentials = null;
            ValidationErrorsList errors = new ValidationErrorsList(filename, contents);
            using (XmlReader reader = XmlReader.Create(new StringReader(contents), MakeReaderSettings(resolver, errors.OnValidationError)))
            {
                try
                {
                    reader.MoveToContent();
                    if (expect != XhtmlDTDSpecification.None)
                    {
                        if (expect != XhtmlDTDSpecification.Any && expect != resolver.DTDSpecification)
                            throw new XmlException("Missing required DTD specification.", null, 1, 1);
                        if (reader.LocalName != "html")
                            throw new XmlException(String.Format("Unexpected root element: {0}", reader.LocalName));
                        if (reader.NamespaceURI != _namespaces.LookupNamespace("htm"))
                            throw new XmlException(String.Format("Unexpected root namespace: {0}", reader.NamespaceURI));
                    }
                    while (reader.Read())
                    { }
                }
                catch (XmlException xmlEx)
                {
                    errors.OnXmlException(xmlEx);
                }
            }
            errors.Assert();
        }
        // 
        // 
        // 
        class XmlResolver : System.Xml.XmlResolver
        {
            static DOCTYPEAttribute[] _docTypes;
            static XmlResolver()
            {
                List docTypes = new List();
                foreach (System.Reflection.FieldInfo f in typeof(XhtmlDTDSpecification).GetFields())
                foreach (DOCTYPEAttribute attr in f.GetCustomAttributes(typeof(DOCTYPEAttribute), false))
                {
                    attr.DTD = (XhtmlDTDSpecification)Enum.Parse(typeof(XhtmlDTDSpecification), f.Name);
                    docTypes.Add(attr);
                }
                _docTypes = docTypes.ToArray();
            }
            public XhtmlDTDSpecification DTDSpecification = XhtmlDTDSpecification.None;
            public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
            {
                Uri cwd = new Uri(Environment.CurrentDirectory);
                string loc = absoluteUri.ToString();
                if (loc.StartsWith(cwd.AbsoluteUri))
                    loc = loc.Substring(cwd.AbsoluteUri.Length).TrimStart('/');
                foreach (DOCTYPEAttribute attr in _docTypes)
                {
                    if (attr.PUBLIC == loc || attr.SYSTEM == loc)
                    {
                        this.DTDSpecification = attr.DTD;
                        return Check.NotNull(GetType().Assembly.GetManifestResourceStream(GetType(), attr.RESOURCE));
                    }
                }
                if (this.DTDSpecification == XhtmlDTDSpecification.XhtmlStrict_10 ||
                    this.DTDSpecification == XhtmlDTDSpecification.XhtmlFrameset_10 ||
                    this.DTDSpecification == XhtmlDTDSpecification.XhtmlTransitional_10)
                {
                    if (loc.EndsWith("/xhtml-lat1.ent"))
                        return Check.NotNull(GetType().Assembly.GetManifestResourceStream(GetType(), "Xhtml1_0.xhtml-lat1.ent"));
                    else if (loc.EndsWith("/xhtml-special.ent"))
                        return Check.NotNull(GetType().Assembly.GetManifestResourceStream(GetType(), "Xhtml1_0.xhtml-special.ent"));
                    else if (loc.EndsWith("/xhtml-symbol.ent"))
                        return Check.NotNull(GetType().Assembly.GetManifestResourceStream(GetType(), "Xhtml1_0.xhtml-symbol.ent"));
                }
                return null;
            }
            public override System.Net.ICredentials Credentials
            { set { } }
        }
        class ValidationErrorsList : List
        {
            readonly string _filename;
            readonly string[] _lines;
            XmlException _first;
            public ValidationErrorsList(string filename, string contents)
            {
                _first = null;
                _filename = filename;
                _lines = contents.Split('\n');
            }
            public void Assert()
            {
                if (_first != null)
                {
                    string msg = String.Join(Environment.NewLine, this.ToArray());
                    throw new XmlException(msg, _first, _first.LineNumber, _first.LinePosition);
                }
            }
            public void OnXmlException(XmlException e)
            {
                _first = _first ?? e;
                HandleError(e.LineNumber, e.LinePosition, XmlSeverityType.Error, e.Message);
            }
            public void OnValidationError(object sender, ValidationEventArgs e)
            {
                if (e.Exception != null)
                {
                    _first = _first ?? new XmlException(e.Exception.Message, e.Exception, e.Exception.LineNumber, e.Exception.LinePosition);
                    HandleError(e.Exception.LineNumber, e.Exception.LinePosition, e.Severity, e.Message);
                }
            }
            private void HandleError(int line, int pos, XmlSeverityType severity, string message)
            {
                StringBuilder errorText = new StringBuilder();
                errorText.AppendFormat("{0}({1},{2}): {3}: {4}", _filename, line, pos, severity.ToString().ToLower(), message);
                System.Diagnostics.Trace.WriteLine(errorText.ToString());
                if (line > 0 && line <= _lines.Length)
                {
                    for (int ix = Math.Max(0, line - 3); ix < Math.Min(_lines.Length, line + 2); ix++)
                        System.Diagnostics.Trace.WriteLine((ix == line ? "! " : "  ") + _lines[ix]);
                    string lineText = _lines[line - 1].TrimEnd();
                    if (pos > 0 && pos < lineText.Length)
                    {
                        if (pos > 1 && lineText[pos - 2] == '<')
                            pos--;
                        lineText = lineText.Substring(pos - 1);
                    }
                    if (lineText.Length > 0 && lineText[0] == '<')
                    {
                        int ixEnd = lineText.IndexOf('>');
                        if (ixEnd > 1 && ixEnd < lineText.Length)
                            lineText = lineText.Substring(0, ixEnd + 1);
                    }
                    else
                        lineText = lineText.Substring(0, Math.Min(lineText.Length, 120));
                    if (!String.IsNullOrEmpty(lineText))
                    {
                        errorText.AppendLine();
                        errorText.Append('\t');
                        errorText.AppendLine(lineText);
                    }
                }
                base.Add(errorText.ToString());
            }
        }
    }
}