Source: odata/utils.js

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
 /** @module odata/utils */

var utils    = require('./../odatajs.js').utils;

// Imports
var assigned = utils.assigned;
var contains = utils.contains;
var find = utils.find;
var isArray = utils.isArray;
var isDate = utils.isDate;
var isObject = utils.isObject;
var parseInt10 = utils.parseInt10;


/** Gets the type name of a data item value that belongs to a feed, an entry, a complex type property, or a collection property
 * @param {string} value - Value of the data item from which the type name is going to be retrieved.
 * @param {object} [metadata] - Object containing metadata about the data tiem.
 * @returns {string} Data item type name; null if the type name cannot be found within the value or the metadata
 * This function will first try to get the type name from the data item's value itself if it is an object with a __metadata property; otherwise
 * it will try to recover it from the metadata.  If both attempts fail, it will return null.
 */
var dataItemTypeName = function (value, metadata) {
    var valueTypeName = ((value && value.__metadata) || {}).type;
    return valueTypeName || (metadata ? metadata.type : null);
};

var EDM = "Edm.";
var EDM_BOOLEAN = EDM + "Boolean";
var EDM_BYTE = EDM + "Byte";
var EDM_SBYTE = EDM + "SByte";
var EDM_INT16 = EDM + "Int16";
var EDM_INT32 = EDM + "Int32";
var EDM_INT64 = EDM + "Int64";
var EDM_SINGLE = EDM + "Single";
var EDM_DOUBLE = EDM + "Double";
var EDM_DECIMAL = EDM + "Decimal";
var EDM_STRING = EDM + "String";

var EDM_BINARY = EDM + "Binary";
var EDM_DATE = EDM + "Date";
var EDM_DATETIMEOFFSET = EDM + "DateTimeOffset";
var EDM_DURATION = EDM + "Duration";
var EDM_GUID = EDM + "Guid";
var EDM_TIMEOFDAY = EDM + "Time";

var GEOGRAPHY = "Geography";
var EDM_GEOGRAPHY = EDM + GEOGRAPHY;
var EDM_GEOGRAPHY_POINT = EDM_GEOGRAPHY + "Point";
var EDM_GEOGRAPHY_LINESTRING = EDM_GEOGRAPHY + "LineString";
var EDM_GEOGRAPHY_POLYGON = EDM_GEOGRAPHY + "Polygon";
var EDM_GEOGRAPHY_COLLECTION = EDM_GEOGRAPHY + "Collection";
var EDM_GEOGRAPHY_MULTIPOLYGON = EDM_GEOGRAPHY + "MultiPolygon";
var EDM_GEOGRAPHY_MULTILINESTRING = EDM_GEOGRAPHY + "MultiLineString";
var EDM_GEOGRAPHY_MULTIPOINT = EDM_GEOGRAPHY + "MultiPoint";

var GEOGRAPHY_POINT = GEOGRAPHY + "Point";
var GEOGRAPHY_LINESTRING = GEOGRAPHY + "LineString";
var GEOGRAPHY_POLYGON = GEOGRAPHY + "Polygon";
var GEOGRAPHY_COLLECTION = GEOGRAPHY + "Collection";
var GEOGRAPHY_MULTIPOLYGON = GEOGRAPHY + "MultiPolygon";
var GEOGRAPHY_MULTILINESTRING = GEOGRAPHY + "MultiLineString";
var GEOGRAPHY_MULTIPOINT = GEOGRAPHY + "MultiPoint";

var GEOMETRY = "Geometry";
var EDM_GEOMETRY = EDM + GEOMETRY;
var EDM_GEOMETRY_POINT = EDM_GEOMETRY + "Point";
var EDM_GEOMETRY_LINESTRING = EDM_GEOMETRY + "LineString";
var EDM_GEOMETRY_POLYGON = EDM_GEOMETRY + "Polygon";
var EDM_GEOMETRY_COLLECTION = EDM_GEOMETRY + "Collection";
var EDM_GEOMETRY_MULTIPOLYGON = EDM_GEOMETRY + "MultiPolygon";
var EDM_GEOMETRY_MULTILINESTRING = EDM_GEOMETRY + "MultiLineString";
var EDM_GEOMETRY_MULTIPOINT = EDM_GEOMETRY + "MultiPoint";

var GEOMETRY_POINT = GEOMETRY + "Point";
var GEOMETRY_LINESTRING = GEOMETRY + "LineString";
var GEOMETRY_POLYGON = GEOMETRY + "Polygon";
var GEOMETRY_COLLECTION = GEOMETRY + "Collection";
var GEOMETRY_MULTIPOLYGON = GEOMETRY + "MultiPolygon";
var GEOMETRY_MULTILINESTRING = GEOMETRY + "MultiLineString";
var GEOMETRY_MULTIPOINT = GEOMETRY + "MultiPoint";

var GEOJSON_POINT = "Point";
var GEOJSON_LINESTRING = "LineString";
var GEOJSON_POLYGON = "Polygon";
var GEOJSON_MULTIPOINT = "MultiPoint";
var GEOJSON_MULTILINESTRING = "MultiLineString";
var GEOJSON_MULTIPOLYGON = "MultiPolygon";
var GEOJSON_GEOMETRYCOLLECTION = "GeometryCollection";

var primitiveEdmTypes = [
    EDM_STRING,
    EDM_INT32,
    EDM_INT64,
    EDM_BOOLEAN,
    EDM_DOUBLE,
    EDM_SINGLE,
    EDM_DATE,
    EDM_DATETIMEOFFSET,
    EDM_DURATION,
    EDM_TIMEOFDAY,
    EDM_DECIMAL,
    EDM_GUID,
    EDM_BYTE,
    EDM_INT16,
    EDM_SBYTE,
    EDM_BINARY
];

var geometryEdmTypes = [
    EDM_GEOMETRY,
    EDM_GEOMETRY_POINT,
    EDM_GEOMETRY_LINESTRING,
    EDM_GEOMETRY_POLYGON,
    EDM_GEOMETRY_COLLECTION,
    EDM_GEOMETRY_MULTIPOLYGON,
    EDM_GEOMETRY_MULTILINESTRING,
    EDM_GEOMETRY_MULTIPOINT
];

var geometryTypes = [
    GEOMETRY,
    GEOMETRY_POINT,
    GEOMETRY_LINESTRING,
    GEOMETRY_POLYGON,
    GEOMETRY_COLLECTION,
    GEOMETRY_MULTIPOLYGON,
    GEOMETRY_MULTILINESTRING,
    GEOMETRY_MULTIPOINT
];

var geographyEdmTypes = [
    EDM_GEOGRAPHY,
    EDM_GEOGRAPHY_POINT,
    EDM_GEOGRAPHY_LINESTRING,
    EDM_GEOGRAPHY_POLYGON,
    EDM_GEOGRAPHY_COLLECTION,
    EDM_GEOGRAPHY_MULTIPOLYGON,
    EDM_GEOGRAPHY_MULTILINESTRING,
    EDM_GEOGRAPHY_MULTIPOINT
];

var geographyTypes = [
    GEOGRAPHY,
    GEOGRAPHY_POINT,
    GEOGRAPHY_LINESTRING,
    GEOGRAPHY_POLYGON,
    GEOGRAPHY_COLLECTION,
    GEOGRAPHY_MULTIPOLYGON,
    GEOGRAPHY_MULTILINESTRING,
    GEOGRAPHY_MULTIPOINT
];

/** Invokes a function once per schema in metadata.
 * @param metadata - Metadata store; one of edmx, schema, or an array of any of them.
 * @param {Function} callback - Callback function to invoke once per schema.
 * @returns The first truthy value to be returned from the callback; null or the last falsy value otherwise.
 */
function forEachSchema(metadata, callback) {
    

    if (!metadata) {
        return null;
    }

    if (isArray(metadata)) {
        var i, len, result;
        for (i = 0, len = metadata.length; i < len; i++) {
            result = forEachSchema(metadata[i], callback);
            if (result) {
                return result;
            }
        }

        return null;
    } else {
        if (metadata.dataServices) {
            return forEachSchema(metadata.dataServices.schema, callback);
        }

        return callback(metadata);
    }
}

/** Formats a millisecond and a nanosecond value into a single string.
 * @param {Numaber} ms - Number of milliseconds to format.</param>
 * @param {Numaber} ns - Number of nanoseconds to format.</param>
 * @returns {String} Formatted text.
 * If the value is already as string it's returned as-is.</remarks>
 */
function formatMilliseconds(ms, ns) {

    // Avoid generating milliseconds if not necessary.
    if (ms === 0) {
        ms = "";
    } else {
        ms = "." + formatNumberWidth(ms.toString(), 3);
    }
    if (ns > 0) {
        if (ms === "") {
            ms = ".000";
        }
        ms += formatNumberWidth(ns.toString(), 4);
    }
    return ms;
}

function formatDateTimeOffsetJSON(value) {
    return "\/Date(" + value.getTime() + ")\/";
}

/** Formats a DateTime or DateTimeOffset value a string.
 * @param {Date} value - Value to format
 * @returns {String} Formatted text.
 * If the value is already as string it's returned as-is
´*/
function formatDateTimeOffset(value) {

    if (typeof value === "string") {
        return value;
    }

    var hasOffset = isDateTimeOffset(value);
    var offset = getCanonicalTimezone(value.__offset);
    if (hasOffset && offset !== "Z") {
        // We're about to change the value, so make a copy.
        value = new Date(value.valueOf());

        var timezone = parseTimezone(offset);
        var hours = value.getUTCHours() + (timezone.d * timezone.h);
        var minutes = value.getUTCMinutes() + (timezone.d * timezone.m);

        value.setUTCHours(hours, minutes);
    } else if (!hasOffset) {
        // Don't suffix a 'Z' for Edm.DateTime values.
        offset = "";
    }

    var year = value.getUTCFullYear();
    var month = value.getUTCMonth() + 1;
    var sign = "";
    if (year <= 0) {
        year = -(year - 1);
        sign = "-";
    }

    var ms = formatMilliseconds(value.getUTCMilliseconds(), value.__ns);

    return sign +
        formatNumberWidth(year, 4) + "-" +
        formatNumberWidth(month, 2) + "-" +
        formatNumberWidth(value.getUTCDate(), 2) + "T" +
        formatNumberWidth(value.getUTCHours(), 2) + ":" +
        formatNumberWidth(value.getUTCMinutes(), 2) + ":" +
        formatNumberWidth(value.getUTCSeconds(), 2) +
        ms + offset;
}

/** Converts a duration to a string in xsd:duration format.
 * @param {Object} value - Object with ms and __edmType properties.
 * @returns {String} String representation of the time object in xsd:duration format.
 */
function formatDuration(value) {

    var ms = value.ms;

    var sign = "";
    if (ms < 0) {
        sign = "-";
        ms = -ms;
    }

    var days = Math.floor(ms / 86400000);
    ms -= 86400000 * days;
    var hours = Math.floor(ms / 3600000);
    ms -= 3600000 * hours;
    var minutes = Math.floor(ms / 60000);
    ms -= 60000 * minutes;
    var seconds = Math.floor(ms / 1000);
    ms -= seconds * 1000;

    return sign + "P" +
           formatNumberWidth(days, 2) + "DT" +
           formatNumberWidth(hours, 2) + "H" +
           formatNumberWidth(minutes, 2) + "M" +
           formatNumberWidth(seconds, 2) +
           formatMilliseconds(ms, value.ns) + "S";
}

/** Formats the specified value to the given width.
 * @param {Number} value - Number to format (non-negative).
 * @param {Number} width - Minimum width for number.
 * @param {Boolean} append - Flag indicating if the value is padded at the beginning (false) or at the end (true).
 * @returns {String} Text representation.
 */
function formatNumberWidth(value, width, append) {
    var result = value.toString(10);
    while (result.length < width) {
        if (append) {
            result += "0";
        } else {
            result = "0" + result;
        }
    }

    return result;
}

/** Gets the canonical timezone representation.
 * @param {String} timezone - Timezone representation.
 * @returns {String} An 'Z' string if the timezone is absent or 0; the timezone otherwise.
 */
function getCanonicalTimezone(timezone) {

    return (!timezone || timezone === "Z" || timezone === "+00:00" || timezone === "-00:00") ? "Z" : timezone;
}

/** Gets the type of a collection type name.
 * @param {String} typeName - Type name of the collection.
 * @returns {String} Type of the collection; null if the type name is not a collection type.
 */
function getCollectionType(typeName) {

    if (typeof typeName === "string") {
        var end = typeName.indexOf(")", 10);
        if (typeName.indexOf("Collection(") === 0 && end > 0) {
            return typeName.substring(11, end);
        }
    }
    return null;
}

/** Sends a request containing OData payload to a server.
* @param request - Object that represents the request to be sent..
* @param success - Callback for a successful read operation.
* @param error - Callback for handling errors.
* @param handler - Handler for data serialization.
* @param httpClient - HTTP client layer.
* @param context - Context used for processing the request
*/
function invokeRequest(request, success, error, handler, httpClient, context) {

    return httpClient.request(request, function (response) {
        try {
            if (response.headers) {
                normalizeHeaders(response.headers);
            }

            if (response.data === undefined && response.statusCode !== 204) {
                handler.read(response, context);
            }
        } catch (err) {
            if (err.request === undefined) {
                err.request = request;
            }
            if (err.response === undefined) {
                err.response = response;
            }
            error(err);
            return;
        }
        // errors in success handler for sync requests result in error handler calls. So here we fix this. 
        try {
            success(response.data, response);
        } catch (err) {
            err.bIsSuccessHandlerError = true;
            throw err;
        }
    }, error);
}

/** Tests whether a value is a batch object in the library's internal representation.
 * @param value - Value to test.
 * @returns {Boolean} True is the value is a batch object; false otherwise.
 */
function isBatch(value) {

    return isComplex(value) && isArray(value.__batchRequests);
}

// Regular expression used for testing and parsing for a collection type.
var collectionTypeRE = /Collection\((.*)\)/;

/** Tests whether a value is a collection value in the library's internal representation.
 * @param value - Value to test.
 * @param {Sting} typeName - Type name of the value. This is used to disambiguate from a collection property value.
 * @returns {Boolean} True is the value is a feed value; false otherwise.
 */
function isCollection(value, typeName) {

    var colData = value && value.results || value;
    return !!colData &&
        (isCollectionType(typeName)) ||
        (!typeName && isArray(colData) && !isComplex(colData[0]));
}

/** Checks whether the specified type name is a collection type.
 * @param {String} typeName - Name of type to check.
 * @returns {Boolean} True if the type is the name of a collection type; false otherwise.
 */
function isCollectionType(typeName) {
    return collectionTypeRE.test(typeName);
}

/** Tests whether a value is a complex type value in the library's internal representation.
 * @param value - Value to test.
 * @returns {Boolean} True is the value is a complex type value; false otherwise.
 */
function isComplex(value) {

    return !!value &&
        isObject(value) &&
        !isArray(value) &&
        !isDate(value);
}

/** Checks whether a Date object is DateTimeOffset value
 * @param {Date} value - Value to check
 * @returns {Boolean} true if the value is a DateTimeOffset, false otherwise.
 */
function isDateTimeOffset(value) {
    return (value.__edmType === "Edm.DateTimeOffset" || (!value.__edmType && value.__offset));
}

/** Tests whether a value is a deferred navigation property in the library's internal representation.
 * @param value - Value to test.
 * @returns {Boolean} True is the value is a deferred navigation property; false otherwise.
 */
function isDeferred(value) {

    if (!value && !isComplex(value)) {
        return false;
    }
    var metadata = value.__metadata || {};
    var deferred = value.__deferred || {};
    return !metadata.type && !!deferred.uri;
}

/** Tests whether a value is an entry object in the library's internal representation.
 * @param value - Value to test.
 * @returns {Boolean} True is the value is an entry object; false otherwise.
 */
function isEntry(value) {

    return isComplex(value) && value.__metadata && "uri" in value.__metadata;
}

/** Tests whether a value is a feed value in the library's internal representation.
 * @param value - Value to test.
 * @param {Sting} typeName - Type name of the value. This is used to disambiguate from a collection property value.
 * @returns {Boolean} True is the value is a feed value; false otherwise.
 */
function isFeed(value, typeName) {

    var feedData = value && value.results || value;
    return isArray(feedData) && (
        (!isCollectionType(typeName)) &&
        (isComplex(feedData[0]))
    );
}

/** Checks whether the specified type name is a geography EDM type.
 * @param {String} typeName - Name of type to check.
 * @returns {Boolean} True if the type is a geography EDM type; false otherwise.
 */
function isGeographyEdmType(typeName) {

    //check with edm
    var ret = contains(geographyEdmTypes, typeName) || 
        (typeName.indexOf('.') === -1 && contains(geographyTypes, typeName));
    return ret; 
        
}

/** Checks whether the specified type name is a geometry EDM type.
 * @param {String} typeName - Name of type to check.
 * @returns {Boolean} True if the type is a geometry EDM type; false otherwise.
 */
function isGeometryEdmType(typeName) {

    var ret = contains(geometryEdmTypes, typeName) ||
        (typeName.indexOf('.') === -1 && contains(geometryTypes, typeName));
    return ret; 
}

/** Tests whether a value is a named stream value in the library's internal representation.
 * @param value - Value to test.
 * @returns {Boolean} True is the value is a named stream; false otherwise.
 */
function isNamedStream(value) {

    if (!value && !isComplex(value)) {
        return false;
    }
    var metadata = value.__metadata;
    var mediaResource = value.__mediaresource;
    return !metadata && !!mediaResource && !!mediaResource.media_src;
}

/** Tests whether a value is a primitive type value in the library's internal representation.
 * @param value - Value to test.
 * @returns {Boolean} True is the value is a primitive type value.
 * Date objects are considered primitive types by the library.
 */
function isPrimitive(value) {

    return isDate(value) ||
        typeof value === "string" ||
        typeof value === "number" ||
        typeof value === "boolean";
}

/** Checks whether the specified type name is a primitive EDM type.
 * @param {String} typeName - Name of type to check.
 * @returns {Boolean} True if the type is a primitive EDM type; false otherwise.
 */
function isPrimitiveEdmType(typeName) {

    return contains(primitiveEdmTypes, typeName);
}

/** Gets the kind of a navigation property value.
 * @param value - Value of the navigation property.
 * @param {Object} [propertyModel] - Object that describes the navigation property in an OData conceptual schema.
 * @returns {String} String value describing the kind of the navigation property; null if the kind cannot be determined.
 */
function navigationPropertyKind(value, propertyModel) {

    if (isDeferred(value)) {
        return "deferred";
    }
    if (isEntry(value)) {
        return "entry";
    }
    if (isFeed(value)) {
        return "feed";
    }
    if (propertyModel && propertyModel.relationship) {
        if (value === null || value === undefined || !isFeed(value)) {
            return "entry";
        }
        return "feed";
    }
    return null;
}

/** Looks up a property by name.
 * @param {Array} properties - Array of property objects as per EDM metadata (may be null)
 * @param {String} name - Name to look for.
 * @returns {Object} The property object; null if not found.
 */
function lookupProperty(properties, name) {

    return find(properties, function (property) {
        return property.name === name;
    });
}

/** Looks up a type object by name.
 * @param {String} name - Name, possibly null or empty.
 * @param metadata - Metadata store; one of edmx, schema, or an array of any of them.
 * @param {String} kind - Kind of object to look for as per EDM metadata.
 * @returns An type description if the name is found; null otherwise
 */
function lookupInMetadata(name, metadata, kind) {

    return (name) ? forEachSchema(metadata, function (schema) {
        return lookupInSchema(name, schema, kind);
    }) : null;
}

/** Looks up a entity set by name.
 * @param {Array} properties - Array of entity set objects as per EDM metadata( may be null)
 * @param {String} name - Name to look for.
 * @returns {Object} The entity set object; null if not found.
 */
function lookupEntitySet(entitySets, name) {

    return find(entitySets, function (entitySet) {
        return entitySet.name === name;
    });
}

/** Looks up a entity set by name.
 * @param {Array} properties - Array of entity set objects as per EDM metadata (may be null)
 * @param {String} name - Name to look for.
 * @returns {Object} The entity set object; null if not found.
 */
function lookupSingleton(singletons, name) {

    return find(singletons, function (singleton) {
        return singleton.name === name;
    });
}

/** Looks up a complex type object by name.
 * @param {String} name - Name, possibly null or empty.
 * @param metadata - Metadata store; one of edmx, schema, or an array of any of them.
 * @returns A complex type description if the name is found; null otherwise.</returns>
 */
function lookupComplexType(name, metadata) {

    return lookupInMetadata(name, metadata, "complexType");
}

/** Looks up an entity type object by name.
 * @param {String} name - Name, possibly null or empty.
 * @param metadata - Metadata store; one of edmx, schema, or an array of any of them.
 * @returns An entity type description if the name is found; null otherwise.</returns>
 */
function lookupEntityType(name, metadata) {

    return lookupInMetadata(name, metadata, "entityType");
}


/** Looks up an
 * @param {String} name - Name, possibly null or empty.
 * @param metadata - Metadata store; one of edmx, schema, or an array of any of them.
 * @returns An entity container description if the name is found; null otherwise.</returns>
 */
function lookupDefaultEntityContainer(metadata) {

    return forEachSchema(metadata, function (schema) {
        if (isObject(schema.entityContainer)) { 
            return schema.entityContainer;
        }
    });
}

/** Looks up an entity container object by name.
 * @param {String} name - Name, possibly null or empty.
 * @param metadata - Metadata store; one of edmx, schema, or an array of any of them.
 * @returns An entity container description if the name is found; null otherwise.</returns>
 */
function lookupEntityContainer(name, metadata) {

    return lookupInMetadata(name, metadata, "entityContainer");
}

/** Looks up a function import by name.
 * @param {Array} properties - Array of function import objects as per EDM metadata (May be null)
 * @param {String} name - Name to look for.
 * @returns {Object} The entity set object; null if not found.
 */
function lookupFunctionImport(functionImports, name) {
    return find(functionImports, function (functionImport) {
        return functionImport.name === name;
    });
}

/** Looks up the target entity type for a navigation property.
 * @param {Object} navigationProperty - 
 * @param {Object} metadata - 
 * @returns {String} The entity type name for the specified property, null if not found.
 */
function lookupNavigationPropertyType(navigationProperty, metadata) {

    var result = null;
    if (navigationProperty) {
        var rel = navigationProperty.relationship;
        var association = forEachSchema(metadata, function (schema) {
            // The name should be the namespace qualified name in 'ns'.'type' format.
            var nameOnly = removeNamespace(schema.namespace, rel);
            var associations = schema.association;
            if (nameOnly && associations) {
                var i, len;
                for (i = 0, len = associations.length; i < len; i++) {
                    if (associations[i].name === nameOnly) {
                        return associations[i];
                    }
                }
            }
            return null;
        });

        if (association) {
            var end = association.end[0];
            if (end.role !== navigationProperty.toRole) {
                end = association.end[1];
                // For metadata to be valid, end.role === navigationProperty.toRole now.
            }
            result = end.type;
        }
    }
    return result;
}

/** Looks up the target entityset name for a navigation property.
 * @param {Object} navigationProperty - 
 * @param {Object} metadata - 
 * @returns {String} The entityset name for the specified property, null if not found.
 */
function lookupNavigationPropertyEntitySet(navigationProperty, sourceEntitySetName, metadata) {

    if (navigationProperty) {
        var rel = navigationProperty.relationship;
        var associationSet = forEachSchema(metadata, function (schema) {
            var containers = schema.entityContainer;
            for (var i = 0; i < containers.length; i++) {
                var associationSets = containers[i].associationSet;
                if (associationSets) {
                    for (var j = 0; j < associationSets.length; j++) {
                        if (associationSets[j].association == rel) {
                            return associationSets[j];
                        }
                    }
                }
            }
            return null;
        });
        if (associationSet && associationSet.end[0] && associationSet.end[1]) {
            return (associationSet.end[0].entitySet == sourceEntitySetName) ? associationSet.end[1].entitySet : associationSet.end[0].entitySet;
        }
    }
    return null;
}

/** Gets the entitySet info, container name and functionImports for an entitySet
 * @param {Object} navigationProperty - 
 * @param {Object} metadata - 
 * @returns {Object} The info about the entitySet.
 */
function getEntitySetInfo(entitySetName, metadata) {

    var info = forEachSchema(metadata, function (schema) {
        var container = schema.entityContainer;
        var entitySets = container.entitySet;
        if (entitySets) {
            for (var j = 0; j < entitySets.length; j++) {
                if (entitySets[j].name == entitySetName) {
                    return { entitySet: entitySets[j], containerName: container.name, functionImport: container.functionImport };
                }
            }
        }
        return null;
    });

    return info;
}

/** Given an expected namespace prefix, removes it from a full name.
 * @param {String} ns - Expected namespace.
 * @param {String} fullName - Full name in 'ns'.'name' form.
 * @returns {String} The local name, null if it isn't found in the expected namespace.
 */
function removeNamespace(ns, fullName) {

    if (fullName.indexOf(ns) === 0 && fullName.charAt(ns.length) === ".") {
        return fullName.substr(ns.length + 1);
    }

    return null;
}

/** Looks up a schema object by name.
 * @param {String} name - Name (assigned).
 * @param schema - Schema object as per EDM metadata.
 * @param {String} kind - Kind of object to look for as per EDM metadata.
 * @returns An entity type description if the name is found; null otherwise.</returns>
 */
function lookupInSchema(name, schema, kind) {

    if (name && schema) {
        // The name should be the namespace qualified name in 'ns'.'type' format.
        var nameOnly = removeNamespace(schema.namespace, name);
        if (nameOnly) {
            return find(schema[kind], function (item) {
                return item.name === nameOnly;
            });
        }
    }
    return null;
}

/** Compares to version strings and returns the higher one.
 * @param {String} left - Version string in the form "major.minor.rev"
 * @param {String} right - Version string in the form "major.minor.rev"
 * @returns {String} The higher version string.
 */
function maxVersion(left, right) {

    if (left === right) {
        return left;
    }

    var leftParts = left.split(".");
    var rightParts = right.split(".");

    var len = (leftParts.length >= rightParts.length) ?
        leftParts.length :
        rightParts.length;

    for (var i = 0; i < len; i++) {
        var leftVersion = leftParts[i] && parseInt10(leftParts[i]);
        var rightVersion = rightParts[i] && parseInt10(rightParts[i]);
        if (leftVersion > rightVersion) {
            return left;
        }
        if (leftVersion < rightVersion) {
            return right;
        }
    }
}

var normalHeaders = {
    // Headers shared by request and response
    "content-type": "Content-Type",
    "content-encoding": "Content-Encoding",
    "content-length": "Content-Length",
    "odata-version": "OData-Version",
    
    // Headers used by request
    "accept": "Accept",
    "accept-charset": "Accept-Charset",
    "if-match": "If-Match",
    "if-none-match": "If-None-Match",
    "odata-isolation": "OData-Isolation",
    "odata-maxversion": "OData-MaxVersion",
    "prefer": "Prefer",
    "content-id": "Content-ID",
    "content-transfer-encoding": "Content-Transfer-Encoding",
    
    // Headers used by response
    "etag": "ETag",
    "location": "Location",
    "odata-entityid": "OData-EntityId",
    "preference-applied": "Preference-Applied",
    "retry-after": "Retry-After"
};

/** Normalizes headers so they can be found with consistent casing.
 * @param {Object} headers - Dictionary of name/value pairs.
 */
function normalizeHeaders(headers) {

    for (var name in headers) {
        var lowerName = name.toLowerCase();
        var normalName = normalHeaders[lowerName];
        if (normalName && name !== normalName) {
            var val = headers[name];
            delete headers[name];
            headers[normalName] = val;
        }
    }
}

/** Parses a string into a boolean value.
 * @param propertyValue - Value to parse.
 * @returns {Boolean} true if the property value is 'true'; false otherwise.
 */
function parseBool(propertyValue) {

    if (typeof propertyValue === "boolean") {
        return propertyValue;
    }

    return typeof propertyValue === "string" && propertyValue.toLowerCase() === "true";
}


// The captured indices for this expression are:
// 0     - complete input
// 1,2,3 - year with optional minus sign, month, day
// 4,5,6 - hours, minutes, seconds
// 7     - optional milliseconds
// 8     - everything else (presumably offset information)
var parseDateTimeRE = /^(-?\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d+))?(.*)$/;

/** Parses a string into a DateTime value.
 * @param {String} value - Value to parse.
 * @param {Boolean} withOffset - Whether offset is expected.
 * @returns {Date} The parsed value.
 */
function parseDateTimeMaybeOffset(value, withOffset, nullOnError) {

    // We cannot parse this in cases of failure to match or if offset information is specified.
    var parts = parseDateTimeRE.exec(value);
    var offset = (parts) ? getCanonicalTimezone(parts[8]) : null;

    if (!parts || (!withOffset && offset !== "Z")) {
        if (nullOnError) {
            return null;
        }
        throw { message: "Invalid date/time value" };
    }

    // Pre-parse years, account for year '0' being invalid in dateTime.
    var year = parseInt10(parts[1]);
    if (year <= 0) {
        year++;
    }

    // Pre-parse optional milliseconds, fill in default. Fail if value is too precise.
    var ms = parts[7];
    var ns = 0;
    if (!ms) {
        ms = 0;
    } else {
        if (ms.length > 7) {
            if (nullOnError) {
                return null;
            }
            throw { message: "Cannot parse date/time value to given precision." };
        }

        ns = formatNumberWidth(ms.substring(3), 4, true);
        ms = formatNumberWidth(ms.substring(0, 3), 3, true);

        ms = parseInt10(ms);
        ns = parseInt10(ns);
    }

    // Pre-parse other time components and offset them if necessary.
    var hours = parseInt10(parts[4]);
    var minutes = parseInt10(parts[5]);
    var seconds = parseInt10(parts[6]) || 0;
    if (offset !== "Z") {
        // The offset is reversed to get back the UTC date, which is
        // what the API will eventually have.
        var timezone = parseTimezone(offset);
        var direction = -(timezone.d);
        hours += timezone.h * direction;
        minutes += timezone.m * direction;
    }

    // Set the date and time separately with setFullYear, so years 0-99 aren't biased like in Date.UTC.
    var result = new Date();
    result.setUTCFullYear(
        year,                       // Year.
        parseInt10(parts[2]) - 1,   // Month (zero-based for Date.UTC and setFullYear).
        parseInt10(parts[3])        // Date.
        );
    result.setUTCHours(hours, minutes, seconds, ms);

    if (isNaN(result.valueOf())) {
        if (nullOnError) {
            return null;
        }
        throw { message: "Invalid date/time value" };
    }

    if (withOffset) {
        result.__edmType = "Edm.DateTimeOffset";
        result.__offset = offset;
    }

    if (ns) {
        result.__ns = ns;
    }

    return result;
}

/** Parses a string into a Date object.
 * @param {String} propertyValue - Value to parse.
 * @returns {Date} The parsed with year, month, day set, time values are set to 0
 */
function parseDate(propertyValue, nullOnError) {
    var parts = propertyValue.split('-');

    if (parts.length != 3 && nullOnError) {
        return null;
    }
    return new Date(
        parseInt10(parts[0]),       // Year.
        parseInt10(parts[1]) - 1,   // Month (zero-based for Date.UTC and setFullYear).
        parseInt10(parts[2],
        0,0,0,0)        // Date.
        );

}

var parseTimeOfDayRE = /^(\d+):(\d+)(:(\d+)(.(\d+))?)?$/;

function parseTimeOfDay(propertyValue, nullOnError) {
    var parts = parseTimeOfDayRE.exec(propertyValue);


    return {
        'h' :parseInt10(parts[1]),
        'm' :parseInt10(parts[2]),
        's' :parseInt10(parts[4]),
        'ms' :parseInt10(parts[6]),
     };
}

/** Parses a string into a DateTimeOffset value.
 * @param {String} propertyValue - Value to parse.
 * @returns {Date} The parsed value.


 * The resulting object is annotated with an __edmType property and
 * an __offset property reflecting the original intended offset of
 * the value. The time is adjusted for UTC time, as the current
 * timezone-aware Date APIs will only work with the local timezone.
 */
function parseDateTimeOffset(propertyValue, nullOnError) {
    

    return parseDateTimeMaybeOffset(propertyValue, true, nullOnError);
}

// The captured indices for this expression are:
// 0       - complete input
// 1       - direction
// 2,3,4   - years, months, days
// 5,6,7,8 - hours, minutes, seconds, miliseconds

var parseTimeRE = /^([+-])?P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(?:\.(\d+))?S)?)?/;

function isEdmDurationValue(value) {
    parseTimeRE.test(value);
}

/** Parses a string in xsd:duration format.
 * @param {String} duration - Duration value.

 * This method will throw an exception if the input string has a year or a month component.

 * @returns {Object} Object representing the time
 */
function parseDuration(duration) {

    var parts = parseTimeRE.exec(duration);

    if (parts === null) {
        throw { message: "Invalid duration value." };
    }

    var years = parts[2] || "0";
    var months = parts[3] || "0";
    var days = parseInt10(parts[4] || 0);
    var hours = parseInt10(parts[5] || 0);
    var minutes = parseInt10(parts[6] || 0);
    var seconds = parseFloat(parts[7] || 0);

    if (years !== "0" || months !== "0") {
        throw { message: "Unsupported duration value." };
    }

    var ms = parts[8];
    var ns = 0;
    if (!ms) {
        ms = 0;
    } else {
        if (ms.length > 7) {
            throw { message: "Cannot parse duration value to given precision." };
        }

        ns = formatNumberWidth(ms.substring(3), 4, true);
        ms = formatNumberWidth(ms.substring(0, 3), 3, true);

        ms = parseInt10(ms);
        ns = parseInt10(ns);
    }

    ms += seconds * 1000 + minutes * 60000 + hours * 3600000 + days * 86400000;

    if (parts[1] === "-") {
        ms = -ms;
    }

    var result = { ms: ms, __edmType: "Edm.Time" };

    if (ns) {
        result.ns = ns;
    }
    return result;
}

/** Parses a timezone description in (+|-)nn:nn format.
 * @param {String} timezone - Timezone offset.
 * @returns {Object} An object with a (d)irection property of 1 for + and -1 for -, offset (h)ours and offset (m)inutes.
 */
function parseTimezone(timezone) {

    var direction = timezone.substring(0, 1);
    direction = (direction === "+") ? 1 : -1;

    var offsetHours = parseInt10(timezone.substring(1));
    var offsetMinutes = parseInt10(timezone.substring(timezone.indexOf(":") + 1));
    return { d: direction, h: offsetHours, m: offsetMinutes };
}

/** Prepares a request object so that it can be sent through the network.
* @param request - Object that represents the request to be sent.
* @param handler - Handler for data serialization
* @param context - Context used for preparing the request
*/
function prepareRequest(request, handler, context) {

    // Default to GET if no method has been specified.
    if (!request.method) {
        request.method = "GET";
    }

    if (!request.headers) {
        request.headers = {};
    } else {
        normalizeHeaders(request.headers);
    }

    if (request.headers.Accept === undefined) {
        request.headers.Accept = handler.accept;
    }

    if (assigned(request.data) && request.body === undefined) {
        handler.write(request, context);
    }

    if (!assigned(request.headers["OData-MaxVersion"])) {
        request.headers["OData-MaxVersion"] = handler.maxDataServiceVersion || "4.0";
    }

    if (request.async === undefined) {
        request.async = true;
    }

}

/** Traverses a tree of objects invoking callback for every value.
 * @param {Object} item - Object or array to traverse.
 * @param {Function} callback - Callback function with key and value, similar to JSON.parse reviver.
 * @returns {Object} The object with traversed properties.
 Unlike the JSON reviver, this won't delete null members.</remarks>
*/
function traverseInternal(item, owner, callback) {

    if (item && typeof item === "object") {
        for (var name in item) {
            var value = item[name];
            var result = traverseInternal(value, name, callback);
            result = callback(name, result, owner);
            if (result !== value) {
                if (value === undefined) {
                    delete item[name];
                } else {
                    item[name] = result;
                }
            }
        }
    }

    return item;
}

/** Traverses a tree of objects invoking callback for every value.
 * @param {Object} item - Object or array to traverse.
 * @param {Function} callback - Callback function with key and value, similar to JSON.parse reviver.
 * @returns {Object} The traversed object.
 * Unlike the JSON reviver, this won't delete null members.</remarks>
*/
function traverse(item, callback) {

    return callback("", traverseInternal(item, "", callback));
}

exports.dataItemTypeName = dataItemTypeName;
exports.EDM_BINARY = EDM_BINARY;
exports.EDM_BOOLEAN = EDM_BOOLEAN;
exports.EDM_BYTE = EDM_BYTE;
exports.EDM_DATE = EDM_DATE;
exports.EDM_DATETIMEOFFSET = EDM_DATETIMEOFFSET;
exports.EDM_DURATION = EDM_DURATION;
exports.EDM_DECIMAL = EDM_DECIMAL;
exports.EDM_DOUBLE = EDM_DOUBLE;
exports.EDM_GEOGRAPHY = EDM_GEOGRAPHY;
exports.EDM_GEOGRAPHY_POINT = EDM_GEOGRAPHY_POINT;
exports.EDM_GEOGRAPHY_LINESTRING = EDM_GEOGRAPHY_LINESTRING;
exports.EDM_GEOGRAPHY_POLYGON = EDM_GEOGRAPHY_POLYGON;
exports.EDM_GEOGRAPHY_COLLECTION = EDM_GEOGRAPHY_COLLECTION;
exports.EDM_GEOGRAPHY_MULTIPOLYGON = EDM_GEOGRAPHY_MULTIPOLYGON;
exports.EDM_GEOGRAPHY_MULTILINESTRING = EDM_GEOGRAPHY_MULTILINESTRING;
exports.EDM_GEOGRAPHY_MULTIPOINT = EDM_GEOGRAPHY_MULTIPOINT;
exports.EDM_GEOMETRY = EDM_GEOMETRY;
exports.EDM_GEOMETRY_POINT = EDM_GEOMETRY_POINT;
exports.EDM_GEOMETRY_LINESTRING = EDM_GEOMETRY_LINESTRING;
exports.EDM_GEOMETRY_POLYGON = EDM_GEOMETRY_POLYGON;
exports.EDM_GEOMETRY_COLLECTION = EDM_GEOMETRY_COLLECTION;
exports.EDM_GEOMETRY_MULTIPOLYGON = EDM_GEOMETRY_MULTIPOLYGON;
exports.EDM_GEOMETRY_MULTILINESTRING = EDM_GEOMETRY_MULTILINESTRING;
exports.EDM_GEOMETRY_MULTIPOINT = EDM_GEOMETRY_MULTIPOINT;
exports.EDM_GUID = EDM_GUID;
exports.EDM_INT16 = EDM_INT16;
exports.EDM_INT32 = EDM_INT32;
exports.EDM_INT64 = EDM_INT64;
exports.EDM_SBYTE = EDM_SBYTE;
exports.EDM_SINGLE = EDM_SINGLE;
exports.EDM_STRING = EDM_STRING;
exports.EDM_TIMEOFDAY = EDM_TIMEOFDAY;
exports.GEOJSON_POINT = GEOJSON_POINT;
exports.GEOJSON_LINESTRING = GEOJSON_LINESTRING;
exports.GEOJSON_POLYGON = GEOJSON_POLYGON;
exports.GEOJSON_MULTIPOINT = GEOJSON_MULTIPOINT;
exports.GEOJSON_MULTILINESTRING = GEOJSON_MULTILINESTRING;
exports.GEOJSON_MULTIPOLYGON = GEOJSON_MULTIPOLYGON;
exports.GEOJSON_GEOMETRYCOLLECTION = GEOJSON_GEOMETRYCOLLECTION;
exports.forEachSchema = forEachSchema;
exports.formatDateTimeOffset = formatDateTimeOffset;
exports.formatDateTimeOffsetJSON = formatDateTimeOffsetJSON;
exports.formatDuration = formatDuration;
exports.formatNumberWidth = formatNumberWidth;
exports.getCanonicalTimezone = getCanonicalTimezone;
exports.getCollectionType = getCollectionType;
exports.invokeRequest = invokeRequest;
exports.isBatch = isBatch;
exports.isCollection = isCollection;
exports.isCollectionType = isCollectionType;
exports.isComplex = isComplex;
exports.isDateTimeOffset = isDateTimeOffset;
exports.isDeferred = isDeferred;
exports.isEntry = isEntry;
exports.isFeed = isFeed;
exports.isGeographyEdmType = isGeographyEdmType;
exports.isGeometryEdmType = isGeometryEdmType;
exports.isNamedStream = isNamedStream;
exports.isPrimitive = isPrimitive;
exports.isPrimitiveEdmType = isPrimitiveEdmType;
exports.lookupComplexType = lookupComplexType;
exports.lookupDefaultEntityContainer = lookupDefaultEntityContainer;
exports.lookupEntityContainer = lookupEntityContainer;
exports.lookupEntitySet = lookupEntitySet;
exports.lookupSingleton = lookupSingleton;
exports.lookupEntityType = lookupEntityType;
exports.lookupFunctionImport = lookupFunctionImport;
exports.lookupNavigationPropertyType = lookupNavigationPropertyType;
exports.lookupNavigationPropertyEntitySet = lookupNavigationPropertyEntitySet;
exports.lookupInSchema = lookupInSchema;
exports.lookupProperty = lookupProperty;
exports.lookupInMetadata = lookupInMetadata;
exports.getEntitySetInfo = getEntitySetInfo;
exports.maxVersion = maxVersion;
exports.navigationPropertyKind = navigationPropertyKind;
exports.normalizeHeaders = normalizeHeaders;
exports.parseBool = parseBool;


exports.parseDate = parseDate;
exports.parseDateTimeOffset = parseDateTimeOffset;
exports.parseDuration = parseDuration;
exports.parseTimeOfDay = parseTimeOfDay;

exports.parseInt10 = parseInt10;
exports.prepareRequest = prepareRequest;
exports.removeNamespace = removeNamespace;
exports.traverse = traverse;