/*!
 * ${copyright}
 */
//Provides mixin sap.ui.model.odata.v4.lib._V2Requestor
sap.ui.define([
	"./_Helper",
	"./_Parser",
	"sap/ui/core/CalendarType",
	"sap/ui/core/format/DateFormat",
	"sap/ui/model/odata/ODataUtils"
], function (_Helper, _Parser, CalendarType, DateFormat, ODataUtils) {
	"use strict";

	var // Example: "/Date(1395705600000)/", matching group: ticks in milliseconds
		rDate = /^\/Date\((-?\d+)\)\/$/,
		oDateFormatter,
		// Example "/Date(1420529121547+0530)/", the offset ("+0530") is optional
		// matches: 1 = ticks in milliseconds, 2 = offset sign, 3 = offset hours, 4 = offset minutes
		rDateTimeOffset = /^\/Date\((-?\d+)(?:([-+])(\d\d)(\d\d))?\)\/$/,
		oDateTimeOffsetFormatter,
		mPattern2Formatter = {},
		rPlus = /\+/g,
		rSegmentWithPredicate = /^([^(]+)(\(.+\))$/,
		rSlash = /\//g,
		// Example: "PT11H33M55S",
		// PT followed by optional hours, optional minutes, optional seconds with optional fractions
		rTime = /^PT(?:(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(\.\d+)?S)?)$/i,
		oTimeFormatter;

	/**
	 * A mixin for a requestor using an OData V2 service.
	 *
	 * @alias sap.ui.model.odata.v4.lib._V2Requestor
	 * @constructor
	 * @mixin
	 * @private
	 */
	function _V2Requestor() {}

	/**
	 * Final (cannot be overridden) request headers for OData V2.
	 */
	_V2Requestor.prototype.mFinalHeaders = {
		"Content-Type" : "application/json;charset=UTF-8"
	};

	/**
	 * Predefined request headers in $batch parts for OData V2.
	 */
	_V2Requestor.prototype.mPredefinedPartHeaders = {
		Accept : "application/json"
	};

	/**
	 * Predefined request headers for all requests for OData V2.
	 */
	_V2Requestor.prototype.mPredefinedRequestHeaders = {
		Accept : "application/json",
		MaxDataServiceVersion : "2.0",
		DataServiceVersion : "2.0",
		"X-CSRF-Token" : "Fetch"
	};

	/**
	 * OData V2 request headers reserved for internal use.
	 */
	_V2Requestor.prototype.mReservedHeaders = {
		accept : true,
		"content-id" : true,
		"content-transfer-encoding" : true,
		"content-type" : true,
		dataserviceversion : true,
		"if-match" : true,
		"if-none-match" : true,
		maxdataserviceversion : true,
		"sap-contextid" : true,
		"x-http-method" : true
	};

	/**
	 * Converts an OData V2 value {@link https://tools.ietf.org/html/rfc3548#section-3} of type
	 * Edm.Binary to the corresponding OData V4 value
	 * {@link https://tools.ietf.org/html/rfc4648#section-5}.
	 *
	 * @param {string} sV2Value
	 *   The OData V2 value
	 * @returns {string}
	 *   The corresponding OData V4 value
	 *
	 * @private
	 */
	_V2Requestor.prototype.convertBinary = function (sV2Value) {
		return sV2Value.replace(rPlus, "-").replace(rSlash, "_");
	};

	/**
	 * Converts an OData V2 value of type Edm.DateTime with <code>sap:display-format="Date"</code>
	 * to the corresponding OData V4 Edm.Date value
	 *
	 * @param {string} sV2Value
	 *   The OData V2 value
	 * @returns {string}
	 *   The corresponding OData V4 value
	 * @throws {Error}
	 *   If the V2 value is not convertible
	 *
	 * @private
	 */
	_V2Requestor.prototype.convertDate = function (sV2Value) {
		var oDate,
			aMatches = rDate.exec(sV2Value);

		if (!aMatches) {
			throw new Error("Not a valid Edm.DateTime value '" + sV2Value + "'");
		}
		oDate = new Date(parseInt(aMatches[1]));
		if (Number(aMatches[1] % (24 * 60 * 60 * 1000)) !== 0) {
			throw new Error("Cannot convert Edm.DateTime value '" + sV2Value
				+ "' to Edm.Date because it contains a time of day");
		}
		return oDateFormatter.format(oDate);
	};

	/**
	 * Converts an OData V2 value of type Edm.DateTimeOffset or Edm.DateTime without
	 * <code>sap:display-format="Date"</code> to the corresponding OData V4 Edm.DateTimeOffset value
	 *
	 * @param {string} sV2Value
	 *   The OData V2 value
	 * @param {object} oPropertyMetadata
	 *   The property metadata
	 * @returns {string}
	 *   The corresponding OData V4 value
	 * @throws {Error}
	 *   If the V2 value is not convertible
	 *
	 * @private
	 */
	_V2Requestor.prototype.convertDateTimeOffset = function (sV2Value, oPropertyMetadata) {
		var aMatches = rDateTimeOffset.exec(sV2Value),
			sOffset,
			iOffsetHours,
			iOffsetMinutes,
			iOffsetSign,
			sPattern = "yyyy-MM-dd'T'HH:mm:ss",
			iPrecision = oPropertyMetadata.$Precision,
			iTicks;

		if (!aMatches) {
			throw new Error("Not a valid Edm.DateTimeOffset value '" + sV2Value + "'");
		}
		iTicks = parseInt(aMatches[1]);
		iOffsetHours = parseInt(aMatches[3]);
		iOffsetMinutes = parseInt(aMatches[4]);
		if (!aMatches[2] || iOffsetHours === 0 && iOffsetMinutes === 0) {
			sOffset = "Z";
		} else {
			iOffsetSign = aMatches[2] === "-" ? -1 : 1;
			iTicks += iOffsetSign * (iOffsetHours * 60 * 60 * 1000 + iOffsetMinutes * 60 * 1000);
			sOffset = aMatches[2] + aMatches[3] + ":" + aMatches[4];
		}
		if (iPrecision > 0) {
			sPattern += "." + "".padEnd(iPrecision, "S");
		}
		if (!mPattern2Formatter[sPattern]) {
			mPattern2Formatter[sPattern] = DateFormat.getDateTimeInstance({
				calendarType : CalendarType.Gregorian,
				pattern : sPattern,
				UTC : true
			});
		}
		return mPattern2Formatter[sPattern].format(new Date(iTicks)) + sOffset;
	};

	/**
	 * Converts an OData V2 value of type Edm.Double or Edm.Single (Edm.Float) to the corresponding
	 * OData V4 value.
	 *
	 * @param {string} sV2Value
	 *   The OData V2 value
	 * @returns {any}
	 *   The corresponding OData V4 value
	 *
	 * @private
	 */
	_V2Requestor.prototype.convertDoubleSingle = function (sV2Value) {
		switch (sV2Value) {
			case "NaN":
			case "INF":
			case "-INF":
				return sV2Value;
			default:
				return parseFloat(sV2Value);
		}
	};

	/**
	 * Converts the filter string literals to OData V2 syntax
	 *
	 * @param {string} sFilter The filter string
	 * @param {string} sMetaPath
	 *   The meta path corresponding to the resource path
	 * @returns {string} The filter string ready for a V2 query
	 * @throws {Error} If the filter path is invalid
	 *
	 * @private
	 */
	_V2Requestor.prototype.convertFilter = function (sFilter, sMetaPath) {
		var oFilterTree = _Parser.parseFilter(sFilter),
			that = this;

		/*
		 * Converts the given literal operand to V2 syntax using the type of the other operand.
		 * @param {object} oLiteral The token for the literal
		 * @param {object} oOtherOperand The token for the other operand
		 */
		function convertLiteral(oLiteral, oOtherOperand) {
			var vModelValue,
				oTypeInfo = getType(oOtherOperand);

			if (oTypeInfo.$Type !== "Edm.String") {
				vModelValue = _Helper.parseLiteral(oLiteral.value, oTypeInfo.$Type, oTypeInfo.path);
				oLiteral.value = that.formatPropertyAsLiteral(vModelValue, oTypeInfo);
			}
		}

		/*
		 * Throws an error that the conversion to V2 failed.
		 * @param {object} oNode the node at which it failed
		 * @param {string} sMessage The error message
		 */
		function error(oNode, sMessage) {
			throw new Error("Cannot convert filter to V2, " + sMessage + " at " + oNode.at + ": "
				+ sFilter);
		}

		/*
		 * Determines the type of a node.
		 * @param {object} oNode A node
		 * @returns {object} A pseudo property with path, $Type (and poss. $v2Type) or undefined if
		 *   the type cannot be determined
		 */
		function getType(oNode) {
			var oPropertyMetadata;

			if (oNode.type) {
				return {
					$Type : oNode.type
				};
			}
			if (oNode.id === "PATH") {
				oPropertyMetadata = that.oModelInterface
					.fetchMetadata(sMetaPath + "/" + oNode.value).getResult();
				if (!oPropertyMetadata) {
					throw new Error("Invalid filter path: " + oNode.value);
				}
				return {
					path : oNode.value,
					$Type : oPropertyMetadata.$Type,
					$v2Type : oPropertyMetadata.$v2Type
				};
			}
			// oNode must have id "FUNCTION" and type undefined here. So it must be either ceiling,
			// floor or round and the return type is determined from the first and only parameter.
			return getType(oNode.parameters[0]);
		}

		/*
		 * Visits a node in the syntax recursively.
		 * @param {object} oNode A node
		 */
		function visitNode(oNode) {
			if (oNode) {
				if (oNode.id === "VALUE" && oNode.ambiguous) {
					error(oNode, "ambiguous type for the literal");
				}
				visitNode(oNode.left);
				visitNode(oNode.right);
				if (oNode.parameters) {
					if (oNode.value === "contains") {
						oNode.value = "substringof";
						oNode.parameters.push(oNode.parameters.shift()); // swap the parameters
					}
					oNode.parameters.forEach(visitNode);
				}
				if (oNode.left && oNode.right) {
					if (oNode.left.id === "VALUE") {
						if (oNode.right.id === "VALUE") {
							error(oNode, "saw literals on both sides of '" + oNode.id + "'");
						}
						convertLiteral(oNode.left, oNode.right);
					} else if (oNode.right.id === "VALUE") {
						convertLiteral(oNode.right, oNode.left);
					}
				}
			}
		}

		visitNode(oFilterTree);
		return _Parser.buildFilterString(oFilterTree);
	};

	/**
	 * Converts an OData V4 key predicate for the given type to OData V2.
	 *
	 * @param {string} sV4KeyPredicate
	 *   The OData V4 key predicate
	 * @param {string} sPath
	 *   The binding path of the entity described by the key predicate
	 * @returns {string}
	 *   The corresponding OData V2 key predicate
	 *
	 * @private
	 */
	_V2Requestor.prototype.convertKeyPredicate = function (sV4KeyPredicate, sPath) {
		// Note: metadata can be fetched synchronously because ready() ensured that it's loaded
		var oEntityType = this.fetchTypeForPath(_Helper.getMetaPath(sPath)).getResult(),
			mKeyToValue = _Parser.parseKeyPredicate(decodeURIComponent(sV4KeyPredicate)),
			that = this;

		/*
		 * Converts the literal to V2 syntax.
		 * @param {string} sPropertyName The name of the property in the metadata
		 * @param {string} sValue The value in the key predicate in V4 syntax
		 * @returns {string} The value in V2 syntax
		 */
		function convertLiteral(sPropertyName, sValue) {
			var oPropertyMetadata = oEntityType[sPropertyName];

			if (oPropertyMetadata.$Type !== "Edm.String") {
				sValue = that.formatPropertyAsLiteral(
					_Helper.parseLiteral(sValue, oPropertyMetadata.$Type, sPath),
					oPropertyMetadata);
			}
			return encodeURIComponent(sValue);
		}

		if ("" in mKeyToValue) {
			return "(" + convertLiteral(oEntityType.$Key[0], mKeyToValue[""]) + ")";
		}
		return "(" + oEntityType.$Key.map(function (sPropertyName) {
			return encodeURIComponent(sPropertyName) + "="
				+ convertLiteral(sPropertyName, mKeyToValue[sPropertyName]);
		}).join(",") + ")";
	};

	/**
	 * Converts the resource path. Transforms literals in key predicates from V4 to V2 syntax.
	 *
	 * @param {string} sResourcePath The V4 resource path
	 * @returns {string} The resource path as required for V2
	 *
	 * @public
	 */
	// @override sap.ui.model.odata.v4.lib._Requestor#convertResourcePath
	_V2Requestor.prototype.convertResourcePath = function (sResourcePath) {
		var iIndex = sResourcePath.indexOf("?"),
			sQueryString = "",
			aSegments,
			iSubPathLength = -1,
			that = this;

		if (iIndex > 0) {
			sQueryString = sResourcePath.slice(iIndex);
			sResourcePath = sResourcePath.slice(0, iIndex);
		}
		aSegments = sResourcePath.split("/");
		return aSegments.map(function (sSegment) {
			var aMatches = rSegmentWithPredicate.exec(sSegment);

			iSubPathLength += sSegment.length + 1;
			if (aMatches) {
				sSegment = aMatches[1] + that.convertKeyPredicate(aMatches[2],
					"/" + sResourcePath.slice(0, iSubPathLength));
			}
			return sSegment;
		}).join("/") + sQueryString;
	};

	/**
	 * Converts an OData V2 value of type Edm.Time to the corresponding OData V4 Edm.TimeOfDay value
	 *
	 * @param {string} sV2Value
	 *   The OData V2 value
	 * @returns {string}
	 *   The corresponding OData V4 value
	 * @throws {Error}
	 *   If the V2 value is not convertible
	 *
	 * @private
	 */
	_V2Requestor.prototype.convertTimeOfDay = function (sV2Value) {
		var oDate,
			aMatches = rTime.exec(sV2Value),
			iTicks;

		if (!aMatches) {
			throw new Error("Not a valid Edm.Time value '" + sV2Value + "'");
		}

		iTicks = Date.UTC(1970, 0, 1, aMatches[1] || 0, aMatches[2] || 0, aMatches[3] || 0);
		oDate = new Date(iTicks);
		return oTimeFormatter.format(oDate) + (aMatches[4] || "");
	};

	/**
	 * Converts a complex value or a collection of complex values from an OData V2 response payload
	 * to an object in OData V4 JSON format.
	 *
	 * @param {object} oObject
	 *   The object to be converted
	 * @returns {object}
	 *   The converted payload
	 * @throws {Error}
	 *   If oObject does not contain inline metadata with type information
	 *
	 * @private
	 */
	_V2Requestor.prototype.convertNonPrimitive = function (oObject) {
		var sPropertyName,
			oType,
			sTypeName,
			vValue,
			that = this;

		// 'results' may be an array of entities in case of a collection request or the value when
		// requesting a property 'results' (which is not an array in V2 since collection of complex
		// or primitive values is only supported since OData V3)
		if (Array.isArray(oObject.results)) {
			oObject.results.forEach(function (oItem) {
				that.convertNonPrimitive(oItem);
			});
			return oObject.results;
		}

		// structured value
		if (!oObject.__metadata || !oObject.__metadata.type) {
			throw new Error("Cannot convert structured value without type information in "
				+ "__metadata.type: " + JSON.stringify(oObject));
		}

		sTypeName = oObject.__metadata.type;
		oType = that.getTypeForName(sTypeName); // can be entity type or complex type
		delete oObject.__metadata;
		for (sPropertyName in oObject) {
			vValue = oObject[sPropertyName];
			if (vValue === null) {
				continue;
			}
			if (typeof vValue === "object") { // non-primitive property value
				if (vValue.__deferred) {
					delete oObject[sPropertyName];
				} else {
					oObject[sPropertyName] = this.convertNonPrimitive(vValue);
				}
				continue;
			}
			// primitive property value
			oObject[sPropertyName] = this.convertPrimitive(vValue, oType[sPropertyName],
				sTypeName, sPropertyName);
		}
		return oObject;
	};

	/**
	 * Computes the OData V4 primitive value for the given OData V2 primitive value and type.
	 *
	 * @param {any} vValue
	 *   The value to be converted
	 * @param {object} oPropertyMetadata
	 *   The property metadata
	 * @param {string} sTypeName
	 *   The qualified name of the entity or complex type containing the property with the value to
	 *   be converted (for error message only)
	 * @param {string} sPropertyName
	 *   The name of the property in the entity or complex type (for error message only)
	 * @returns {any}
	 *   The converted value
	 * @throws {Error}
	 *   If the property type is unknown
	 *
	 * @private
	 */
	_V2Requestor.prototype.convertPrimitive = function (vValue, oPropertyMetadata, sTypeName,
			sPropertyName) {
		switch (oPropertyMetadata && oPropertyMetadata.$Type) {
			case "Edm.Binary":
				return this.convertBinary(vValue);
			case "Edm.Date":
				return this.convertDate(vValue);
			case "Edm.DateTimeOffset":
				return this.convertDateTimeOffset(vValue, oPropertyMetadata);
			case "Edm.Boolean":
			case "Edm.Byte":
			case "Edm.Decimal":
			case "Edm.Guid":
			case "Edm.Int16":
			case "Edm.Int32":
			case "Edm.Int64":
			case "Edm.SByte":
			case "Edm.String":
				return vValue;
			case "Edm.Double":
			case "Edm.Single":
				return this.convertDoubleSingle(vValue);
			case "Edm.TimeOfDay":
				return this.convertTimeOfDay(vValue);
			default:
				throw new Error("Type '" + (oPropertyMetadata && oPropertyMetadata.$Type)
					+ "' of property '" + sPropertyName + "' in type '" + sTypeName
					+ "' is unknown; cannot convert value: " + vValue);
		}
	};

	/**
	 * Checks whether the "DataServiceVersion" header is not set or has the value "1.0" or "2.0"
	 * otherwise an error is thrown.
	 *
	 * @param {function} fnGetHeader
	 *   A callback function to get a header attribute for a given header name with case-insensitive
	 *   search by header name
	 * @param {string} sResourcePath
	 *   The resource path of the request
	 * @param {boolean} [_bVersionOptional]
	 *   Indicates whether the OData service version is optional, which is the case for all OData V2
	 *   responses. So this parameter is ignored.
	 * @throws {Error} If the "DataServiceVersion" header is neither "1.0" nor "2.0" nor not set at
	 *   all
	 *
	 * @public
	 */
	// @override sap.ui.model.odata.v4.lib._Requestor#doCheckVersionHeader
	_V2Requestor.prototype.doCheckVersionHeader = function (fnGetHeader, sResourcePath,
			_bVersionOptional) {
		var sDataServiceVersion = fnGetHeader("DataServiceVersion"),
			vODataVersion = !sDataServiceVersion && fnGetHeader("OData-Version");

		if (vODataVersion) {
			throw new Error("Expected 'DataServiceVersion' header with value '1.0' or '2.0' but "
				+ "received 'OData-Version' header with value '" + vODataVersion
				+ "' in response for " + this.sServiceUrl + sResourcePath);
		}
		if (!sDataServiceVersion) {
			return;
		}
		sDataServiceVersion = sDataServiceVersion.split(";")[0];
		if (sDataServiceVersion === "1.0" || sDataServiceVersion === "2.0") {
			return;
		}
		throw new Error("Expected 'DataServiceVersion' header with value '1.0' or '2.0' but "
			+ "received value '" + sDataServiceVersion + "' in response for " + this.sServiceUrl
			+ sResourcePath);
	};

	/**
	 * Converts an OData V2 response payload to an OData V4 response payload.
	 *
	 * @param {object} oResponsePayload
	 *   The OData V2 response payload
	 * @param {string} [sMetaPath]
	 *   The meta path corresponding to the resource path; needed in case V2 response does not
	 *   contain <code>__metadata.type</code>, for example "2.2.7.2.4 RetrievePrimitiveProperty
	 *   Request" or "2.2.7.5 Invoke Request" for "a Function Import which returns a collection of
	 *   primitives"
	 * @returns {object}
	 *   The OData V4 response payload
	 * @throws {Error}
	 *   If the OData V2 response payload cannot be converted
	 *
	 * @public
	 */
	// @override sap.ui.model.odata.v4.lib._Requestor#doConvertResponse
	_V2Requestor.prototype.doConvertResponse = function (oResponsePayload, sMetaPath) {
		var oCandidate, bIsArray, aKeys, oPayload, oPropertyMetadata,
			that = this;

		oResponsePayload = oResponsePayload.d;
		// 'results' may be an array of entities in case of a collection request or the value when
		// requesting a property 'results' (which is not an array in V2 since collection of complex
		// or primitive values is only supported since OData V3)
		bIsArray = Array.isArray(oResponsePayload.results);

		if (!bIsArray && !oResponsePayload.__metadata) {
			// Special cases for handling of objects (for arrays, see below).
			// Note: If oResponsePayload has __metadata, just call convertNonPrimitive() below.
			aKeys = Object.keys(oResponsePayload);
			oCandidate = oResponsePayload[aKeys[0]];
			if (aKeys.length === 1) {
				if (oCandidate === null) {
					// no conversion needed
					return {value : null};
				} else if (typeof oCandidate !== "object") {
					// treat as candidate for "entityPropertyInJson"
					return {
						value : this.convertPrimitive(oCandidate,
							this.oModelInterface.fetchMetadata(sMetaPath).getResult(),
							sMetaPath, aKeys[0])
					};
				} else if (oCandidate.__metadata) {
					// drill down into candidate for "entityComplexProperty"
					oResponsePayload = oCandidate;
				}
			}
		}

		if (bIsArray && !oResponsePayload.results.length) {
			oPayload = []; // no conversion needed
		} else if (bIsArray && !oResponsePayload.results[0].__metadata) {
			oPropertyMetadata = this.oModelInterface.fetchMetadata(sMetaPath).getResult();
			oPayload = oResponsePayload.results.map(function (vValue) {
				return that.convertPrimitive(vValue, oPropertyMetadata, sMetaPath, "");
			});
		} else {
			oPayload = this.convertNonPrimitive(oResponsePayload);
		}

		if (bIsArray) {
			oPayload = {value : oPayload};
			if (oResponsePayload.__count) {
				oPayload["@odata.count"] = oResponsePayload.__count;
			}
			if (oResponsePayload.__next) {
				oPayload["@odata.nextLink"] = oResponsePayload.__next;
			}
		}
		return oPayload;
	};

	/**
	 * Converts the supported V4 OData system query options to the corresponding V2 OData system
	 * query options.
	 *
	 * @param {string} sMetaPath
	 *   The meta path corresponding to the resource path
	 * @param {object} mQueryOptions The query options
	 * @param {function (string,any)} fnResultHandler
	 *   The function to process the converted options getting the name and the value
	 * @param {boolean} [bDropSystemQueryOptions]
	 *   Whether all system query options are dropped (useful for non-GET requests)
	 * @param {boolean} [bSortExpandSelect]
	 *   Whether the paths in $expand and $select shall be sorted in the query string
	 * @throws {Error}
	 *   If a system query option other than $expand and $select is used or if any $expand value is
	 *   not an object
	 *
	 * @public
	 */
	// @override sap.ui.model.odata.v4.lib._Requestor#doConvertSystemQueryOptions
	_V2Requestor.prototype.doConvertSystemQueryOptions = function (sMetaPath, mQueryOptions,
			fnResultHandler, bDropSystemQueryOptions, bSortExpandSelect) {
		var aSelects,
			mSelects = {},
			that = this;

		/*
		 * Strips all selects to their first segment and adds them to mSelects.
		 *
		 * @param {string|string[]} vSelects The selects for the given expand path as
		 *   comma-separated list or array
		 * @param {string} [sExpandPath] The expand path
		 */
		function addSelects(vSelects, sExpandPath) {
			if (!Array.isArray(vSelects)) {
				vSelects = vSelects.split(",");
			}
			vSelects.forEach(function (sSelect) {
				var iIndex = sSelect.indexOf("/");

				if (iIndex >= 0 && !sSelect.includes(".")) {
					// only strip if there is no type cast and no bound action (avoid "correcting"
					// unsupported selects in V2)
					sSelect = sSelect.slice(0, iIndex);
				}
				mSelects[_Helper.buildPath(sExpandPath, sSelect)] = true;
			});
		}

		/*
		 * Converts the V4 $expand options to flat V2 $expand and $select structure.
		 *
		 * @param {string[]} aExpands The resulting list of $expand paths
		 * @param {object} mExpandItem The current $expand item to be processed
		 * @param {string} sPathPrefix The path prefix used to compute the absolute path
		 * @returns {string[]} The list of $expand paths
		 * @throws {Error}
		 *   If a system query option other than $expand and $select is used or if any $expand value
		 *   is not an object
		 */
		function convertExpand(aExpands, mExpandItem, sPathPrefix) {
			if (!mExpandItem || typeof mExpandItem !== "object") {
				throw new Error("$expand must be a valid object");
			}

			Object.keys(mExpandItem).forEach(function (sExpandPath) {
				var sAbsoluteExpandPath = _Helper.buildPath(sPathPrefix, sExpandPath),
					vExpandOptions = mExpandItem[sExpandPath]; // an object or true

				aExpands.push(sAbsoluteExpandPath);

				if (typeof vExpandOptions === "object") {
					Object.keys(vExpandOptions).forEach(function (sQueryOption) {
						switch (sQueryOption) {
							case "$expand":
								// process nested expands
								convertExpand(aExpands, vExpandOptions.$expand,
									sAbsoluteExpandPath);
								break;
							case "$select":
								// process nested selects
								addSelects(vExpandOptions.$select, sAbsoluteExpandPath);
								break;
							default:
								throw new Error("Unsupported query option in $expand: "
									+ sQueryOption);
						}
					});
				}
				if (!vExpandOptions.$select) {
					mSelects[sAbsoluteExpandPath + "/*"] = true;
				}
			});
			return aExpands;
		}

		Object.keys(mQueryOptions).forEach(function (sName) {
			var bIsSystemQueryOption = sName[0] === "$",
				vValue = mQueryOptions[sName];

			if (bDropSystemQueryOptions && bIsSystemQueryOption) {
				return;
			}

			switch (sName) {
				case "$count":
					sName = "$inlinecount";
					vValue = vValue ? "allpages" : "none";
					break;
				case "$expand":
					vValue = convertExpand([], vValue, ""); // Note: returns a new array
					vValue = (bSortExpandSelect ? vValue.sort() : vValue).join(",");
					break;
				case "$orderby":
				case "$search":
					break;
				case "$select":
					addSelects(vValue);
					return; // don't call fnResultHandler; this is done later
				case "$filter":
					vValue = that.convertFilter(vValue, sMetaPath);
					break;
				default:
					if (bIsSystemQueryOption) {
						throw new Error("Unsupported system query option: " + sName);
					}
			}
			fnResultHandler(sName, vValue);
		});

		// only if all (nested) query options are processed, all selects are known
		aSelects = Object.keys(mSelects);
		if (aSelects.length > 0) {
			if (!mQueryOptions.$select) {
				aSelects.push("*");
			}
			fnResultHandler("$select", (bSortExpandSelect ? aSelects.sort() : aSelects).join(","));
		}
	};

	/**
	 * Formats a given internal value into a literal suitable for usage in OData V2 URLs. See
	 * http://www.odata.org/documentation/odata-version-2-0/overview#AbstractTypeSystem.
	 *
	 * @param {any} vValue
	 *   The value
	 * @param {object} oPropertyMetadata
	 *   The property metadata
	 * @returns {string}
	 *   The literal for the URL
	 * @throws {Error}
	 *   When called for an unsupported type
	 * @see sap.ui.model.odata.ODataUtils#formatValue
	 *
	 * @public
	 */
	// @override sap.ui.model.odata.v4.lib._Requestor#formatPropertyAsLiteral
	_V2Requestor.prototype.formatPropertyAsLiteral = function (vValue, oPropertyMetadata) {
		// Parse using the given formatter and check that the result is valid
		function parseAndCheck(oDateFormat, sValue) {
			var oDate = oDateFormat.parse(sValue);

			if (!oDate) {
				throw new Error("Not a valid " + oPropertyMetadata.$Type + " value: " + sValue);
			}
			return oDate;
		}

		if (vValue === null) {
			return "null";
		}

		// Convert the value to V2 model format
		switch (oPropertyMetadata.$Type) {
			case "Edm.Boolean":
			case "Edm.Byte":
			case "Edm.Decimal":
			case "Edm.Double":
			case "Edm.Guid":
			case "Edm.Int16":
			case "Edm.Int32":
			case "Edm.Int64":
			case "Edm.SByte":
			case "Edm.Single":
			case "Edm.String":
				break;
			case "Edm.Date":
				vValue = parseAndCheck(oDateFormatter, vValue);
				break;
			case "Edm.DateTimeOffset":
				vValue = parseAndCheck(oDateTimeOffsetFormatter, vValue);
				break;
			case "Edm.TimeOfDay":
				vValue = {
					__edmType : "Edm.Time",
					ms : parseAndCheck(oTimeFormatter, vValue).getTime()
				};
				break;
			default:
				throw new Error("Type '" + oPropertyMetadata.$Type
					+ "' in the key predicate is not supported");
		}
		// Use the V2 function to format the value for a literal
		return ODataUtils.formatValue(vValue, oPropertyMetadata.$v2Type || oPropertyMetadata.$Type);
	};

	/**
	 * Returns the resource path relative to the service URL and adds query options in case of
	 * a bound operation (V2: "sap:action-for"). Operation parameters are moved to query options,
	 * undeclared parameters are removed. In case of a non-POST action, the V2 HTTP method is
	 * tunneled as a parameter "X-HTTP-Method".
	 *
	 * @param {string} sPath
	 *   The absolute binding path to the bound operation or operation import, e.g.
	 *   "/Entity('0815')/bound.Operation(...)" or "/OperationImport(...)"
	 * @param {object} oOperationMetadata
	 *   The operation's metadata
	 * @param {object} mParameters
	 *   A copy of the map of key-values pairs representing the operation's actual parameters
	 * @param {object} mQueryOptions
	 *   A copy of the map of key-value pairs representing the query string, the value in this pair
	 *   has to be a string or an array of strings
	 * @param {function|object} [vEntity]
	 *   The existing entity data (or a function which may be called to access it) in case of a
	 *   bound operation (V2: "sap:action-for")
	 * @returns {string}
	 *   The new path without leading slash and ellipsis
	 * @throws {Error}
	 *   If a collection-valued operation parameter is encountered
	 *
	 * @public
	 */
	// @override sap.ui.model.odata.v4.lib._Requestor#getPathAndAddQueryOptions
	_V2Requestor.prototype.getPathAndAddQueryOptions = function (sPath, oOperationMetadata,
		mParameters, mQueryOptions, vEntity) {
		var sName,
			oTypeMetadata,
			that = this;

		sPath = sPath.slice(1, -5);

		if (oOperationMetadata.$IsBound) {
			sPath = sPath.slice(sPath.lastIndexOf(".") + 1);
			if (typeof vEntity === "function") {
				vEntity = vEntity();
			}
			// Note: $metadata is already available because oOperationMetadata has been read!
			oTypeMetadata = this.getTypeForName(oOperationMetadata.$Parameter[0].$Type);
			oTypeMetadata.$Key.forEach(function (sName) {
				mQueryOptions[sName]
					= that.formatPropertyAsLiteral(vEntity[sName], oTypeMetadata[sName]);
			});
		}

		if (oOperationMetadata.$Parameter) {
			oOperationMetadata.$Parameter.forEach(function (oParameter) {
				sName = oParameter.$Name;
				if (sName in mParameters) {
					if (oParameter.$isCollection) {
						throw new Error("Unsupported collection-valued parameter: " + sName);
					}
					mQueryOptions[sName]
						= that.formatPropertyAsLiteral(mParameters[sName], oParameter);
					delete mParameters[sName];
				}
			});
		}
		for (sName in mParameters) {
			delete mParameters[sName];
		}
		if (oOperationMetadata.$v2HttpMethod) {
			mParameters["X-HTTP-Method"] = oOperationMetadata.$v2HttpMethod;
		}

		return sPath;
	};

	/**
	 * Returns the type with the given qualified name.
	 *
	 * @param {string} sName The qualified type name
	 * @returns {object} The type
	 *
	 * @private
	 */
	_V2Requestor.prototype.getTypeForName = function (sName) {
		var oType;

		this.mTypesByName = this.mTypesByName || {};
		oType = this.mTypesByName[sName];
		if (!oType) {
			oType = this.mTypesByName[sName]
				= this.oModelInterface.fetchMetadata("/" + sName).getResult();
		}
		return oType;
	};

	/**
	 * Tells whether an empty object in the request body is optional for (parameterless) actions.
	 * For OData V2, this is true in the sense that the request body should be empty and parameters
	 * are all part of the resource path.
	 *
	 * @returns {boolean} <code>true</code>
	 *
	 * @public
	 */
	// @override sap.ui.model.odata.v4.lib._Requestor#isActionBodyOptional
	_V2Requestor.prototype.isActionBodyOptional = function () {
		return true;
	};

	/**
	 * Tells whether change sets are optional. For OData V2, this is false, i.e. even single change
	 * requests must be wrapped within a change set.
	 *
	 * @returns {boolean} <code>false</code>
	 *
	 * @public
	 */
	// @override sap.ui.model.odata.v4.lib._Requestor#isChangeSetOptional
	_V2Requestor.prototype.isChangeSetOptional = function () {
		return false;
	};

	/**
	 * Returns a sync promise that is resolved when the requestor is ready to be used. Waits for the
	 * metadata to be available.
	 *
	 * @returns {sap.ui.base.SyncPromise} A sync promise that is resolved with no result when the
	 * metadata is available
	 *
	 * @public
	 */
	// @override sap.ui.model.odata.v4.lib._Requestor#ready
	_V2Requestor.prototype.ready = function () {
		return this.oModelInterface.fetchEntityContainer().then(function () {});
	};

	//*********************************************************************************************
	// "static" functions
	//*********************************************************************************************
	function asV2Requestor(oRequestor) {
		Object.assign(oRequestor, _V2Requestor.prototype);
		oRequestor.oModelInterface.reportStateMessages = function () {};
		oRequestor.oModelInterface.reportTransitionMessages = function () {};
	}

	/**
	 * Sets the static date and time formatter instances.
	 *
	 * @private
	 */
	asV2Requestor._setDateTimeFormatter = function () {
		oDateFormatter = DateFormat.getDateInstance({
			calendarType : CalendarType.Gregorian,
			pattern : "yyyy-MM-dd",
			UTC : true
		});
		oDateTimeOffsetFormatter = DateFormat.getDateTimeInstance({
			calendarType : CalendarType.Gregorian,
			pattern : "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
		});
		oTimeFormatter = DateFormat.getTimeInstance({
			calendarType : CalendarType.Gregorian,
			pattern : "HH:mm:ss",
			UTC : true
		});
	};

	asV2Requestor._setDateTimeFormatter();

	return asV2Requestor;
}, /* bExport= */ false);
