// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.

namespace System.Data.Entity.Core.SchemaObjectModel
{
    using System.Collections.Generic;
    using System.Data.Entity.Core.Common;
    using System.Data.Entity.Core.Common.Utils;
    using System.Data.Entity.Core.Mapping;
    using System.Data.Entity.Core.Metadata.Edm;
    using System.Data.Entity.Core.Metadata.Edm.Provider;
    using System.Data.Entity.Resources;
    using System.Diagnostics;
    using System.Linq;
    using System.Xml;

    internal delegate void AttributeValueNotification(string token, Action<string, ErrorCode, EdmSchemaErrorSeverity> addError);

    internal delegate DbProviderManifest ProviderManifestNeeded(Action<string, ErrorCode, EdmSchemaErrorSeverity> addError);

    /// <summary>
    ///     Class responsible for parsing,validating a collection of schema
    /// </summary>
    [DebuggerDisplay("DataModel={DataModel}")]
    internal class SchemaManager
    {
        #region Instance Fields

        // This keeps track of all the possible namespaces encountered till now. This helps in displaying the error to the
        // user - if the particular type is not found, we can report whether the namespace was invalid or the type with the
        // given name was not found in the given namespace. This also helps in validating the namespace in the using elements
        private readonly HashSet<string> _namespaceLookUpTable = new HashSet<string>(StringComparer.Ordinal);

        // List of all the schema types across all the schemas. This is to ensure that there is no duplicate type encountered
        // across schemas
        private readonly SchemaElementLookUpTable<SchemaType> _schemaTypes = new SchemaElementLookUpTable<SchemaType>();

        // We want to stop parsing/resolving/validation after the first 100 errors
        private const int MaxErrorCount = 100;

        // delay loaded
        private DbProviderManifest _providerManifest;
        private PrimitiveSchema _primitiveSchema;
        private double effectiveSchemaVersion = XmlConstants.UndefinedVersion;

        private readonly SchemaDataModelOption _dataModel;
        private readonly ProviderManifestNeeded _providerManifestNeeded;
        private readonly AttributeValueNotification _providerNotification;
        private readonly AttributeValueNotification _providerManifestTokenNotification;

        #endregion

        #region Constructor

        private SchemaManager(
            SchemaDataModelOption dataModel, AttributeValueNotification providerNotification,
            AttributeValueNotification providerManifestTokenNotification, ProviderManifestNeeded providerManifestNeeded)
        {
            _dataModel = dataModel;
            _providerNotification = providerNotification;
            _providerManifestTokenNotification = providerManifestTokenNotification;
            _providerManifestNeeded = providerManifestNeeded;
        }

        #endregion

        #region Public Methods

        public static IList<EdmSchemaError> LoadProviderManifest(
            XmlReader xmlReader, string location,
            bool checkForSystemNamespace, out Schema schema)
        {
            IList<Schema> schemaCollection = new List<Schema>(1);

            DbProviderManifest providerManifest = checkForSystemNamespace ? EdmProviderManifest.Instance : null;
            var errors = ParseAndValidate(
                new[] { xmlReader },
                new[] { location }, SchemaDataModelOption.ProviderManifestModel,
                providerManifest, out schemaCollection);

            // In case of errors, there are no schema in the schema collection
            if (schemaCollection.Count != 0)
            {
                schema = schemaCollection[0];
            }
            else
            {
                Debug.Assert(errors.Count != 0, "There must be some error encountered");
                schema = null;
            }

            return errors;
        }

        public static void NoOpAttributeValueNotification(string attributeValue, Action<string, ErrorCode, EdmSchemaErrorSeverity> addError)
        {
        }

        public static IList<EdmSchemaError> ParseAndValidate(
            IEnumerable<XmlReader> xmlReaders,
            IEnumerable<string> sourceFilePaths, SchemaDataModelOption dataModel,
            DbProviderManifest providerManifest,
            out IList<Schema> schemaCollection)
        {
            return ParseAndValidate(
                xmlReaders,
                sourceFilePaths,
                dataModel,
                NoOpAttributeValueNotification,
                NoOpAttributeValueNotification,
                error => providerManifest ?? MetadataItem.EdmProviderManifest,
                out schemaCollection);
        }

        public static IList<EdmSchemaError> ParseAndValidate(
            IEnumerable<XmlReader> xmlReaders,
            IEnumerable<string> sourceFilePaths, SchemaDataModelOption dataModel,
            AttributeValueNotification providerNotification,
            AttributeValueNotification providerManifestTokenNotification,
            ProviderManifestNeeded providerManifestNeeded,
            out IList<Schema> schemaCollection)
        {
            var schemaManager = new SchemaManager(
                dataModel, providerNotification, providerManifestTokenNotification, providerManifestNeeded);
            var errorCollection = new List<EdmSchemaError>();
            schemaCollection = new List<Schema>();
            var errorEncountered = false;

            List<string> filePathList;
            if (sourceFilePaths != null)
            {
                filePathList = new List<string>(sourceFilePaths);
            }
            else
            {
                filePathList = new List<string>();
            }

            var index = 0;
            foreach (var xmlReader in xmlReaders)
            {
                string location = null;
                if (filePathList.Count <= index)
                {
                    TryGetBaseUri(xmlReader, out location);
                }
                else
                {
                    location = filePathList[index];
                }

                var schema = new Schema(schemaManager);

                var errorsForCurrentSchema = schema.Parse(xmlReader, location);

                CheckIsSameVersion(schema, schemaCollection, errorCollection);

                // If the number of errors exceeded the max error count, then return
                if (UpdateErrorCollectionAndCheckForMaxErrors(errorCollection, errorsForCurrentSchema, ref errorEncountered))
                {
                    return errorCollection;
                }

                // Add the schema to the collection if there are no errors. There are errors in which schema do not have any namespace.
                // Also if there is an error encountered in one of the schema, we do not need to add the remaining schemas since
                // we will never go to the resolve phase.
                if (!errorEncountered)
                {
                    schemaCollection.Add(schema);
                    schemaManager.AddSchema(schema);
                    Debug.Assert(
                        schemaCollection.All(
                            s => s.SchemaVersion == schema.SchemaVersion || s.SchemaVersion != XmlConstants.UndefinedVersion));
                }
                index++;
            }

            // If there are no errors encountered in the parsing stage, we can proceed to the 
            // parsing and validating phase
            if (!errorEncountered)
            {
                foreach (var schema in schemaCollection)
                {
                    if (UpdateErrorCollectionAndCheckForMaxErrors(errorCollection, schema.Resolve(), ref errorEncountered))
                    {
                        return errorCollection;
                    }
                }

                // If there are no errors encountered in the parsing stage, we can proceed to the 
                // parsing and validating phase
                if (!errorEncountered)
                {
                    foreach (var schema in schemaCollection)
                    {
                        if (UpdateErrorCollectionAndCheckForMaxErrors(errorCollection, schema.ValidateSchema(), ref errorEncountered))
                        {
                            return errorCollection;
                        }
                    }
                }
            }

            return errorCollection;
        }

        // this method will move skip down to the first element, or to the end if it doesn't find one
        internal static bool TryGetSchemaVersion(XmlReader reader, out double version, out DataSpace dataSpace)
        {
            // to make life simpler, we skip down to the first/root element, unless we're
            // already there
            if (!reader.EOF
                && reader.NodeType != XmlNodeType.Element)
            {
                while (reader.Read()
                       && reader.NodeType != XmlNodeType.Element)
                {
                }
            }

            if (!reader.EOF
                &&
                (reader.LocalName == XmlConstants.Schema || reader.LocalName == StorageMslConstructs.MappingElement))
            {
                return TryGetSchemaVersion(reader.NamespaceURI, out version, out dataSpace);
            }

            version = default(double);
            dataSpace = default(DataSpace);
            return false;
        }

        internal static bool TryGetSchemaVersion(string xmlNamespaceName, out double version, out DataSpace dataSpace)
        {
            switch (xmlNamespaceName)
            {
                case XmlConstants.ModelNamespace_1:
                    version = XmlConstants.EdmVersionForV1;
                    dataSpace = DataSpace.CSpace;
                    return true;
                case XmlConstants.ModelNamespace_1_1:
                    version = XmlConstants.EdmVersionForV1_1;
                    dataSpace = DataSpace.CSpace;
                    return true;
                case XmlConstants.ModelNamespace_2:
                    version = XmlConstants.EdmVersionForV2;
                    dataSpace = DataSpace.CSpace;
                    return true;
                case XmlConstants.ModelNamespace_3:
                    version = XmlConstants.EdmVersionForV3;
                    dataSpace = DataSpace.CSpace;
                    return true;
                case XmlConstants.TargetNamespace_1:
                    version = XmlConstants.StoreVersionForV1;
                    dataSpace = DataSpace.SSpace;
                    return true;
                case XmlConstants.TargetNamespace_2:
                    version = XmlConstants.StoreVersionForV2;
                    dataSpace = DataSpace.SSpace;
                    return true;
                case XmlConstants.TargetNamespace_3:
                    version = XmlConstants.StoreVersionForV3;
                    dataSpace = DataSpace.SSpace;
                    return true;
                case StorageMslConstructs.NamespaceUriV1:
                    version = StorageMslConstructs.MappingVersionV1;
                    dataSpace = DataSpace.CSSpace;
                    return true;
                case StorageMslConstructs.NamespaceUriV2:
                    version = StorageMslConstructs.MappingVersionV2;
                    dataSpace = DataSpace.CSSpace;
                    return true;
                case StorageMslConstructs.NamespaceUriV3:
                    version = StorageMslConstructs.MappingVersionV3;
                    dataSpace = DataSpace.CSSpace;
                    return true;
                default:
                    version = default(Double);
                    dataSpace = default(DataSpace);
                    return false;
            }
        }

        private static bool CheckIsSameVersion(
            Schema schemaToBeAdded, IEnumerable<Schema> schemaCollection, List<EdmSchemaError> errorCollection)
        {
            if (schemaToBeAdded.SchemaVersion != XmlConstants.UndefinedVersion
                && schemaCollection.Count() > 0)
            {
                if (
                    schemaCollection.Any(
                        s => s.SchemaVersion != XmlConstants.UndefinedVersion && s.SchemaVersion != schemaToBeAdded.SchemaVersion))
                {
                    errorCollection.Add(
                        new EdmSchemaError(
                            Strings.CannotLoadDifferentVersionOfSchemaInTheSameItemCollection,
                            (int)ErrorCode.CannotLoadDifferentVersionOfSchemaInTheSameItemCollection,
                            EdmSchemaErrorSeverity.Error));
                }
            }
            return true;
        }

        public double SchemaVersion
        {
            get { return effectiveSchemaVersion; }
        }

        /// <summary>
        ///     Add the namespace of the given schema to the namespace lookup table
        /// </summary>
        public void AddSchema(Schema schema)
        {
            Debug.Assert(schema.DataModel == _dataModel, "DataModel must match");

            if (_namespaceLookUpTable.Count == 0
                && schema.DataModel != SchemaDataModelOption.ProviderManifestModel)
            {
                // Add the primitive type namespace to the namespace look up table
                if (PrimitiveSchema.Namespace != null)
                {
                    _namespaceLookUpTable.Add(PrimitiveSchema.Namespace);
                }
            }

            // Add the namespace to the namespaceLookUpTable. 
            // Its okay to have multiple schemas with the same namespace
            _namespaceLookUpTable.Add(schema.Namespace);
        }

        /// <summary>
        ///     Resolve the type - if the type is not found, return appropriate error
        /// </summary>
        /// <returns> </returns>
        public bool TryResolveType(string namespaceName, string typeName, out SchemaType schemaType)
        {
            // For resolving entity container names, namespace can be null
            var fullyQualifiedName = String.IsNullOrEmpty(namespaceName) ? typeName : namespaceName + "." + typeName;

            schemaType = SchemaTypes.LookUpEquivalentKey(fullyQualifiedName);
            if (schemaType != null)
            {
                return true;
            }

            return false;
        }

        /// <summary>
        ///     Returns true if this is a valid namespace name or else returns false
        /// </summary>
        public bool IsValidNamespaceName(string namespaceName)
        {
            return _namespaceLookUpTable.Contains(namespaceName);
        }

        #endregion // Public Methods

        #region Private Methods

        /// <summary>
        ///     Checks if the xml reader has base uri. If it doesn't have, it adds error, other
        ///     returns the location from the base uri
        /// </summary>
        /// <returns> </returns>
        internal static bool TryGetBaseUri(XmlReader xmlReader, out string location)
        {
            var baseUri = xmlReader.BaseURI;
            Uri uri = null;

            if (!string.IsNullOrEmpty(baseUri)
                &&
                Uri.TryCreate(baseUri, UriKind.Absolute, out uri)
                &&
                uri.Scheme == "file")
            {
                location = Helper.GetFileNameFromUri(uri);
                return true;
            }
            else
            {
                location = null;
                return false;
            }
        }

        /// <summary>
        ///     Add the given list of newErrors to the error collection. If there is a error in the new errors,
        ///     it sets the errorEncountered to true. Returns true if the number of errors encountered is more
        ///     than max errors
        /// </summary>
        /// <returns> </returns>
        private static bool UpdateErrorCollectionAndCheckForMaxErrors(
            List<EdmSchemaError> errorCollection,
            IList<EdmSchemaError> newErrors, ref bool errorEncountered)
        {
            // If we have encountered error already in one of the schemas, then we don't need to check for errors in the remaining schemas.
            // Just keep aggregating the errors and throw them at the end.
            if (!errorEncountered)
            {
                if (!MetadataHelper.CheckIfAllErrorsAreWarnings(newErrors))
                {
                    errorEncountered = true;
                }
            }

            // Add the new errors to the error collection
            errorCollection.AddRange(newErrors);

            if (errorEncountered &&
                errorCollection.Where(e => e.Severity == EdmSchemaErrorSeverity.Error).Count() > MaxErrorCount)
            {
                return true;
            }
            return false;
        }

        #endregion

        #region Internal Properties

        internal SchemaElementLookUpTable<SchemaType> SchemaTypes
        {
            get { return _schemaTypes; }
        }

        internal DbProviderManifest GetProviderManifest(Action<string, ErrorCode, EdmSchemaErrorSeverity> addError)
        {
            if (_providerManifest == null)
            {
                _providerManifest = _providerManifestNeeded(addError);
            }
            return _providerManifest;
        }

        internal SchemaDataModelOption DataModel
        {
            get { return _dataModel; }
        }

        internal void EnsurePrimitiveSchemaIsLoaded(double forSchemaVersion)
        {
            if (_primitiveSchema == null)
            {
                effectiveSchemaVersion = forSchemaVersion;
                _primitiveSchema = new PrimitiveSchema(this);
            }
        }

        internal PrimitiveSchema PrimitiveSchema
        {
            get { return _primitiveSchema; }
        }

        internal AttributeValueNotification ProviderNotification
        {
            get { return _providerNotification; }
        }

        internal AttributeValueNotification ProviderManifestTokenNotification
        {
            get { return _providerManifestTokenNotification; }
        }

        #endregion
    }
}
