text: import Base64 from "crypto-js/enc-base64";
  import Hex from "crypto-js/enc-hex";
  import HmacSHA256 from "crypto-js/hmac-sha256";
  import SHA256 from "crypto-js/sha256";
  import { parse as uriParse } from "uri-js";
  
  import { iso8601Date } from "../format/date";
  import MultiMap from "../util/multiMap";
  import Environment from "./environment";
  import { HttpMethod, default as HttpHeaders } from "./httpHeaders";
  import { urlQueryParse } from "./urlQuery";
  
  /**
   * The number of milliseconds a signing key is valid for.
   * @type {number}
   * @private
   */
  const SIGNING_KEY_VALIDITY = 7 * 24 * 60 * 60 * 1000;
  
  /**
   * A builder object for the SNWS2 HTTP authorization scheme.
   *
   * This builder can be used to calculate a one-off header value, for example:
   *
   * ```
   * let authHeader = new AuthorizationV2Builder("my-token")
   *     .path("/solarquery/api/v1/pub/...")
   *     .build("my-token-secret");
   * ```
   *
   * Or the builder can be re-used for a given token:
   *
   * ```
   * // create a builder for a token
   * let builder = new AuthorizationV2Builder("my-token");
   *
   * // elsewhere, re-use the builder for repeated requests
   * builder.reset()
   *     .path("/solarquery/api/v1/pub/...")
   *     .build("my-token-secret");
   * ```
   *
   * Additionally, a signing key can be generated and re-used for up to 7 days:
   *
   * ```
   * // create a builder for a token
   * let builder = new AuthorizationV2Builder("my-token")
   *   .saveSigningKey("my-token-secret");
   *
   * // elsewhere, re-use the builder for repeated requests
   * builder.reset()
   *     .path("/solarquery/api/v1/pub/...")
   *     .buildWithSavedKey(); // note previously generated key used
   * ```
   *
   * ## Post requests
   *
   * For handling `POST` or `PUT` requests, you must make sure to configure the properties of
   * this class to match your actual HTTP request:
   *
   *  1. Use the {@link module:net~AuthorizationV2Builder#method method()} method to configure the HTTP verb (you can use the {@link module:net~HttpMethod HttpMethod} constants).
   *  2. Use the {@link module:net~AuthorizationV2Builder#contentType contentType()} method to configure the same value that will be used for the HTTP `Content-Type` header (you can use the {@link module:net~HttpContentType HttpContentType} constants).
   *  3. **If** the content type is `application/x-www-form-urlencoded` then you should use the {@link module:net~AuthorizationV2Builder#queryParams queryParams()} method to configure the request parameters.
   *  4. **If** the content type is **not** `application/x-www-form-urlencoded` then you should use the {@link module:net~AuthorizationV2Builder#computeContentDigest computeContentDigest()} method to configure a HTTP `Digest` header.
   *
   * ```
   * // create a builder for a token
   * let builder = new AuthorizationV2Builder("my-token")
   *   .saveSigningKey("my-token-secret");
   *
   * // POST request with form data
   * builder.reset()
   *     .method(HttpHeaders.POST)
   *     .path("/solarquery/api/v1/pub/...")
   *     .contentType(HttpContentType.FORM_URLENCODED_UTF8)
   *     .queryParams({foo:"bar"})
   *     .buildWithSavedKey();
   *
   * // PUT request with JSON data, with XHR style request
   * let reqJson = JSON.stringify({foo:"bar"});
   * builder.reset()
   *     .method(HttpHeaders.PUT)
   *     .path("/solarquery/api/v1/pub/...")
   *     .contentType(HttpContentType.APPLICATION_JSON_UTF8)
   *     .computeContentDigest(reqJson);
   *
   * // when making actual HTTP request, re-use the computed HTTP Digest header:
   * xhr.setRequestHeader(
   *     HttpHeaders.DIGEST,
   *     builder.httpHeaders.firstValue(HttpHeaders.DIGEST)
   * );
   * xhr.setRequestHeader(HttpHeaders.X_SN_DATE, builder.requestDateHeaderValue);
   * xhr.setRequestHeader(HttpHeaders.AUTHORIZATION, builder.buildWithSavedKey());
   * ```
   * @alias module:net~AuthorizationV2Builder
   */
  class AuthorizationV2Builder {
  	/**
  	 * Constructor.
  	 *
  	 * The {@link module:net~AuthorizationV2Builder#reset reset()} method is invoked to set up
  	 * default values for this instance.
  	 *
  	 * @param {string} token the auth token to use
  	 * @param {module:net~Environment} [environment] the environment to use; if not provided a
  	 *        default environment will be created
  	 */
  	constructor(token, environment) {
  		/**
  		 * The SolarNet auth token value.
  		 * @member {string}
  		 */
  		this.tokenId = token;
  
  		/**
  		 * The SolarNet environment.
  		 * @member {module:net~Environment}
  		 */
  		this.environment = environment || new Environment();
  
  		/**
  		 * Force a port number to be added to host values, even if port would be implied.
  		 *
  		 * This can be useful when working with a server behind a proxy, where the
  		 * proxy is configured to always forward the port even if the port is implied
  		 * (i.e. HTTPS is used on the standard port 443).
  		 *
  		 * @member {boolean}
  		 */
  		this.forceHostPort = false;
  
  		this.reset();
  	}
  
  	/**
  	 * Reset to defalut property values.
  	 *
  	 * Any previously saved signing key via {@link module:net~AuthorizationV2Builder#saveSigningKey saveSigningKey()}
  	 * or {@link module:net~AuthorizationV2Builder#key key()} is preserved. The following items are reset:
  	 *
  	 *  * {@link module:net~AuthorizationV2Builder#contentSHA256 contentSHA256()} is cleared
  	 *  * {@link module:net~AuthorizationV2Builder#method method()} is set to `GET`
  	 *  * {@link module:net~AuthorizationV2Builder#host host()} is set to `this.environment.host`
  	 *  * {@link module:net~AuthorizationV2Builder#path path()} is set to `/`
  	 *  * {@link module:net~AuthorizationV2Builder#date date()} is set to the current date
  	 *  * {@link module:net~AuthorizationV2Builder#headers headers()} is set to a new empty instance
  	 *  * {@link module:net~AuthorizationV2Builder#queryParams queryParams()} is set to a new empty instance
  	 *  * {@link module:net~AuthorizationV2Builder#signedHttpHeaders signedHttpHeaders()} is set to a new empty array
  	 *
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	reset() {
  		this.contentDigest = null;
  		var host = this.environment.host;
  		return this.headers(new HttpHeaders())
  			.queryParams(new MultiMap())
  			.signedHttpHeaders([])
  			.method(HttpMethod.GET)
  			.host(host)
  			.path("/")
  			.date(new Date());
  	}
  
  	/**
  	 * Compute and cache the signing key.
  	 *
  	 * Signing keys are derived from the token secret and valid for 7 days, so
  	 * this method can be used to compute a signing key so that {@link module:net~AuthorizationV2Builder#build build()}
  	 * can be called later. The signing date will be set to whatever date is
  	 * currently configured via {@link module:net~AuthorizationV2Builder#date date()}, which defaults to the
  	 * current time for newly created builder instances.
  	 *
  	 * If you have an externally computed signing key, such as one returned from a token refresh API call,
  	 * use the {@link module:net~AuthorizationV2Builder#key key()} method to save it rather than this method.
  	 * If you want to compute the signing key, without caching it on this builder, use the
  	 * {@link module:net~AuthorizationV2Builder#computeSigningKey computeSigningKey()} method rather than
  	 * this method.
  	 *
  	 * @param {string} tokenSecret the secret to sign the digest with
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	saveSigningKey(tokenSecret) {
  		this.key(this.computeSigningKey(tokenSecret), this.requestDate);
  		return this;
  	}
  
  	/**
  	 * Get or set the signing key.
  	 *
  	 * Use this method to save an existing signing key, for example one received via a refresh
  	 * request. The `date` parameter is used to track the expirataion date of the key, as
  	 * reported by the {@link module:net~AuthorizationV2Builder#signingKeyValid signingKeyValid}
  	 * property.
  	 *
  	 * If you have an actual token secret value, use the
  	 * {@link module:net~AuthorizationV2Builder#saveSigningKey saveSigningKey()} method to save it
  	 * rather than this method.
  	 *
  	 * @param {CryptoJS#WordArray} key the signing key to save
  	 * @param {Date} [date] an optional date the signing key was generated with; if not provided
  	 *                      the configured {@link module:net~AuthorizationV2Builder#date date()}
  	 *                      value will be used
  	 * @returns {CryptoJS#WordArray|module:net~AuthorizationV2Builder} when used as a getter, the
  	 *          current saved signing key value, otherwise this object
  	 * @see module:net~AuthorizationV2Builder#signingKeyExpirationDate
  	 */
  	key(key, date) {
  		if (key === undefined) {
  			return this.signingKey;
  		}
  		this.signingKey = key;
  		let expire = new Date(
  			(date ? date.getTime() : this.requestDate.getTime()) + SIGNING_KEY_VALIDITY
  		);
  		expire.setUTCHours(0);
  		expire.setUTCMinutes(0);
  		expire.setUTCSeconds(0);
  		expire.setUTCMilliseconds(0);
  		this.signingKeyExpiration = expire;
  		return this;
  	}
  
  	/**
  	 * Get the saved signing key expiration date.
  	 *
  	 * This will return the expiration date the signing key saved via
  	 * {@link module:net~AuthorizationV2Builder#key key()} or
  	 * {@link module:net~AuthorizationV2Builder#saveSigningKey saveSigningKey()}.
  	 *
  	 * @readonly
  	 * @type {Date}
  	 */
  	get signingKeyExpirationDate() {
  		return this.signingKeyExpiration;
  	}
  
  	/**
  	 * Test if a signing key is present and not expired.
  	 * @readonly
  	 * @type {boolean}
  	 */
  	get signingKeyValid() {
  		return this.signingKey &&
  			this.signingKeyExpiration instanceof Date &&
  			Date.now() < this.signingKeyExpiration.getTime()
  			? true
  			: false;
  	}
  
  	/**
  	 * Set the HTTP method (verb) to use.
  	 *
  	 * @param {string} val the method to use; see the {@link HttpMethod} enum for possible values
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	method(val) {
  		this.httpMethod = val;
  		return this;
  	}
  
  	/**
  	 * Set the HTTP host.
  	 *
  	 * This is a shortcut for calling `HttpHeaders#put(HttpHeaders.HOST, val)`.
  	 *
  	 * @param {string} val the HTTP host value to use
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	host(val) {
  		if (this.forceHostPort && val.indexOf(":") < 0 && this.environment.port != 80) {
  			val += ":" + this.environment.port;
  		}
  		this.httpHeaders.put(HttpHeaders.HOST, val);
  		return this;
  	}
  
  	/**
  	 * Set the HTTP request path to use.
  	 *
  	 * @param {string} val the request path to use
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	path(val) {
  		this.requestPath = val;
  		return this;
  	}
  
  	/**
  	 * Set the host, path, and query parameters via a URL string.
  	 *
  	 * @param {string} url the URL value to use
  	 * @param {boolean} [ignoreHost] if `true` then do not set the {@link module:net~AuthorizationV2Builder#host host()}
  	 *                               from the given URL; this can be useful when you do not want to override the configured
  	 *                               environment host
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	url(url, ignoreHost) {
  		const uri = uriParse(url);
  		let host = uri.host;
  		if (
  			uri.port &&
  			(((uri.scheme === "https" || uri.scheme === "wss") && uri.port !== 443) ||
  				((uri.scheme === "http" || uri.scheme === "ws") && uri.port !== 80))
  		) {
  			host += ":" + uri.port;
  		}
  		if (uri.query) {
  			this.queryParams(urlQueryParse(uri.query));
  		}
  		if (!ignoreHost) {
  			this.host(host);
  		}
  		return this.path(uri.path);
  	}
  
  	/**
  	 * Set the HTTP content type.
  	 *
  	 * This is a shortcut for calling {@link module:net~HttpHeaders#put HttpHeaders.put()} with the
  	 * key {@link module:net~HttpHeaders.CONTENT_TYPE HttpHeaders.CONTENT_TYPE}.
  	 *
  	 * @param {string} val the HTTP content type value to use
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	contentType(val) {
  		this.httpHeaders.put(HttpHeaders.CONTENT_TYPE, val);
  		return this;
  	}
  
  	/**
  	 * Set the authorization request date.
  	 *
  	 * @param {Date} val the date to use; typically the current time, e.g. `new Date()`
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	date(val) {
  		this.requestDate = val ? val : new Date();
  		return this;
  	}
  
  	/**
  	 * The authorization request date as a HTTP header string value.
  	 *
  	 * @readonly
  	 * @type {string}
  	 */
  	get requestDateHeaderValue() {
  		return this.requestDate.toUTCString();
  	}
  
  	/**
  	 * Control using the `X-SN-Date` HTTP header versus the `Date` header.
  	 *
  	 * Set to `true` to use the `X-SN-Date` header, `false` to use
  	 * the `Date` header. This will return `true` if `X-SN-Date` has been
  	 * added to the `signedHeaderNames` property or has been added to the `httpHeaders`
  	 * property.
  	 *
  	 * @type {boolean}
  	 */
  	get useSnDate() {
  		let signedHeaders = this.signedHeaderNames;
  		let existingIndex = Array.isArray(signedHeaders)
  			? signedHeaders.findIndex(caseInsensitiveEqualsFn(HttpHeaders.X_SN_DATE))
  			: -1;
  		return existingIndex >= 0 || this.httpHeaders.containsKey(HttpHeaders.X_SN_DATE);
  	}
  
  	set useSnDate(enabled) {
  		let signedHeaders = this.signedHeaderNames;
  		let existingIndex = Array.isArray(signedHeaders)
  			? signedHeaders.findIndex(caseInsensitiveEqualsFn(HttpHeaders.X_SN_DATE))
  			: -1;
  		if (enabled && existingIndex < 0) {
  			signedHeaders = signedHeaders
  				? signedHeaders.concat(HttpHeaders.X_SN_DATE)
  				: [HttpHeaders.X_SN_DATE];
  			this.signedHeaderNames = signedHeaders;
  		} else if (!enabled && existingIndex >= 0) {
  			signedHeaders.splice(existingIndex, 1);
  			this.signedHeaderNames = signedHeaders;
  		}
  
  		// also clear from httpHeaders
  		this.httpHeaders.remove(HttpHeaders.X_SN_DATE);
  	}
  
  	/**
  	 * Set the `useSnDate` property.
  	 *
  	 * @param {boolean} enabled `true` to use the `X-SN-Date` header, `false` to use `Date`
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	snDate(enabled) {
  		this.useSnDate = enabled;
  		return this;
  	}
  
  	/**
  	 * Set a HTTP header value.
  	 *
  	 * This is a shortcut for calling `HttpHeaders#put(headerName, val)`.
  	 *
  	 * @param {string} headerName the header name to set
  	 * @param {string} headerValue the header value to set
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	header(headerName, headerValue) {
  		this.httpHeaders.put(headerName, headerValue);
  		return this;
  	}
  
  	/**
  	 * Set the HTTP headers to use with the request.
  	 *
  	 * The headers object must include all headers necessary by the
  	 * authentication scheme, and any additional headers also configured via
  	 * {@link module:net~AuthorizationV2Builder#signedHttpHeaders}.
  	 *
  	 * @param {HttpHeaders} headers the HTTP headers to use
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	headers(headers) {
  		this.httpHeaders = headers;
  		return this;
  	}
  
  	/**
  	 * Set the HTTP `GET` query parameters, or `POST` form-encoded
  	 * parameters.
  	 *
  	 * @param {MultiMap|Object} params the parameters to use, as either a {@link MultiMap} or simple `Object`
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	queryParams(params) {
  		if (params instanceof MultiMap) {
  			this.parameters = params;
  		} else {
  			this.parameters.putAll(params);
  		}
  		return this;
  	}
  
  	/**
  	 * Set additional HTTP header names to sign with the authentication.
  	 *
  	 * @param {sring[]} signedHeaderNames additional HTTP header names to include in the signature
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	signedHttpHeaders(signedHeaderNames) {
  		this.signedHeaderNames = signedHeaderNames;
  		return this;
  	}
  
  	/**
  	 * Set the HTTP request body content SHA-256 digest value.
  	 *
  	 * @param {string|module:crypto-js/enc-hex~WordArray} digest the digest value to use; if a string it is assumed to be Hex encoded
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	contentSHA256(digest) {
  		var contentDigest;
  		if (typeof digest === "string") {
  			contentDigest = Hex.parse(digest);
  		} else {
  			contentDigest = digest;
  		}
  		this.contentDigest = contentDigest;
  		return this;
  	}
  
  	/**
  	 * Compute the SHA-256 digest of the request body content and configure the result on this builder.
  	 *
  	 * This method will compute the digest and then save the result via the
  	 * {@link module:net~AuthorizationV2Builder#contentSHA256 contentSHA256()}
  	 * method. In addition, it will set the `Digest` HTTP header value via
  	 * {@link module:net~AuthorizationV2Builder#header header()}.
  	 * This means you _must_ also pass the `Digest` HTTP header with the request. After calling this
  	 * method, you can retrieve the `Digest` HTTP header value via the `httpHeaders`property.
  	 *
  	 * @param {string} content the request body content to compute a SHA-256 digest value from
  	 * @returns {module:net~AuthorizationV2Builder} this object
  	 */
  	computeContentDigest(content) {
  		var digest = SHA256(content);
  		this.contentSHA256(digest);
  		this.header("Digest", "sha-256=" + Base64.stringify(digest));
  		return this;
  	}
  
  	/**
  	 * Compute the canonical query parameters.
  	 *
  	 * @returns {string} the canonical query parameters string value
  	 */
  	canonicalQueryParameters() {
  		const keys = this.parameters.keySet();
  		if (keys.length < 1) {
  			return "";
  		}
  		keys.sort();
  		const len = keys.length;
  		var first = true,
  			result = "";
  		for (let i = 0; i < len; i += 1) {
  			let key = keys[i];
  			let vals = this.parameters.value(key);
  			const valsLen = vals.length;
  			for (let j = 0; j < valsLen; j += 1) {
  				if (first) {
  					first = false;
  				} else {
  					result += "&";
  				}
  				result += _encodeURIComponent(key) + "=" + _encodeURIComponent(vals[j]);
  			}
  		}
  		return result;
  	}
  
  	/**
  	 * Compute the canonical HTTP headers string value.
  	 *
  	 * @param {string[]} sortedLowercaseHeaderNames the sorted, lower-cased HTTP header names to include
  	 * @returns {string} the canonical headers string value
  	 */
  	canonicalHeaders(sortedLowercaseHeaderNames) {
  		var result = "",
  			headerName,
  			headerValue;
  		const len = sortedLowercaseHeaderNames.length;
  		for (let i = 0; i < len; i += 1) {
  			headerName = sortedLowercaseHeaderNames[i];
  			if ("date" === headerName || "x-sn-date" === headerName) {
  				headerValue = this.requestDate.toUTCString();
  			} else {
  				headerValue = this.httpHeaders.firstValue(headerName);
  			}
  			result += headerName + ":" + (headerValue ? headerValue.trim() : "") + "\n";
  		}
  		return result;
  	}
  
  	/**
  	 * Compute the canonical signed header names value from an array of HTTP header names.
  	 *
  	 * @param {string[]} sortedLowercaseHeaderNames the sorted, lower-cased HTTP header names to include
  	 * @returns {string} the canonical signed header names string value
  	 * @private
  	 */
  	canonicalSignedHeaderNames(sortedLowercaseHeaderNames) {
  		return sortedLowercaseHeaderNames.join(";");
  	}
  
  	/**
  	 * Get the canonical request content SHA256 digest, hex encoded.
  	 *
  	 * @returns {string} the hex-encoded SHA256 digest of the request content
  	 */
  	canonicalContentSHA256() {
  		return this.contentDigest
  			? Hex.stringify(this.contentDigest)
  			: AuthorizationV2Builder.EMPTY_STRING_SHA256_HEX;
  	}
  
  	/**
  	 * Compute the canonical HTTP header names to include in the signature.
  	 *
  	 * @returns {string[]} the sorted, lower-cased HTTP header names to include
  	 */
  	canonicalHeaderNames() {
  		const httpHeaders = this.httpHeaders;
  		const signedHeaderNames = this.signedHeaderNames;
  
  		// use a MultiMap to take advantage of case-insensitive keys
  		const map = new MultiMap();
  
  		map.put(HttpHeaders.HOST, true);
  		if (this.useSnDate) {
  			map.put(HttpHeaders.X_SN_DATE, true);
  		} else {
  			map.put(HttpHeaders.DATE, true);
  		}
  		if (httpHeaders.containsKey(HttpHeaders.CONTENT_MD5)) {
  			map.put(HttpHeaders.CONTENT_MD5, true);
  		}
  		if (httpHeaders.containsKey(HttpHeaders.CONTENT_TYPE)) {
  			map.put(HttpHeaders.CONTENT_TYPE, true);
  		}
  		if (httpHeaders.containsKey(HttpHeaders.DIGEST)) {
  			map.put(HttpHeaders.DIGEST, true);
  		}
  		if (signedHeaderNames && signedHeaderNames.length > 0) {
  			signedHeaderNames.forEach(e => map.put(e, true));
  		}
  		return lowercaseSortedArray(map.keySet());
  	}
  
  	/**
  	 * Compute the canonical request data that will be included in the data to sign with the request.
  	 *
  	 * @returns {string} the canonical request data
  	 */
  	buildCanonicalRequestData() {
  		return this.computeCanonicalRequestData(this.canonicalHeaderNames());
  	}
  
  	/**
  	 * Compute the canonical request data that will be included in the data to sign with the request,
  	 * using a specific set of HTTP header names to sign.
  	 *
  	 * @param {string[]} sortedLowercaseHeaderNames the sorted, lower-cased HTTP header names to sign with the request
  	 * @returns {string} the canonical request data
  	 * @private
  	 */
  	computeCanonicalRequestData(sortedLowercaseHeaderNames) {
  		// 1: HTTP verb
  		var result = this.httpMethod + "\n";
  
  		// 2: Canonical URI
  		result += this.requestPath + "\n";
  
  		// 3: Canonical query string
  		result += this.canonicalQueryParameters() + "\n";
  
  		// 4: Canonical headers
  		result += this.canonicalHeaders(sortedLowercaseHeaderNames); // already includes newline
  
  		// 5: Signed header names
  		result += this.canonicalSignedHeaderNames(sortedLowercaseHeaderNames) + "\n";
  
  		// 6: Content SHA256, hex encoded
  		result += this.canonicalContentSHA256();
  
  		return result;
  	}
  
  	/**
  	 * Compute the signing key, from a secret key and based on the
  	 * configured {@link module:net~AuthorizationV2Builder#date date()}.
  	 *
  	 * This method does not save the signing key for future use in this builder instance
  	 * (see {@link module:net~AuthorizationV2Builder#saveSigningKey saveSigningKey()} for that).
  	 * Use this method if you want to compute a signing key that you can later pass to
  	 * {@link module:net~AuthorizationV2Builder#buildWithKey buildWithKey()} on some other builder instance.
  	 * Signing keys are valid for a maximum of 7 days, granular to whole days only.
  	 * To make a signing key expire in fewer than 7 days, configure  a
  	 * {@link module:net~AuthorizationV2Builder#date date()} value in the past before
  	 * calling this method.
  	 *
  	 * @param {string} secretKey the secret key string
  	 * @returns {CryptoJS#WordArray} the computed key
  	 */
  	computeSigningKey(secretKey) {
  		const datestring = iso8601Date(this.requestDate);
  		const key = HmacSHA256("snws2_request", HmacSHA256(datestring, "SNWS2" + secretKey));
  		return key;
  	}
  
  	/**
  	 * Compute the data to be signed by the signing key.
  	 *
  	 * @param {string} canonicalRequestData the request data, returned from {@link module:net~AuthorizationV2Builder#buildCanonicalRequestData}
  	 * @returns {string} the data to sign
  	 * @private
  	 */
  	computeSignatureData(canonicalRequestData) {
  		/*- signature data is like:
  
              SNWS2-HMAC-SHA256\n
              20170301T120000Z\n
              Hex(SHA256(canonicalRequestData))
          */
  		return (
  			"SNWS2-HMAC-SHA256\n" +
  			iso8601Date(this.requestDate, true) +
  			"\n" +
  			Hex.stringify(SHA256(canonicalRequestData))
  		);
  	}
  
  	/**
  	 * Compute a HTTP `Authorization` header value from the configured properties
  	 * on the builder, using the provided signing key.
  	 *
  	 * This method does not save the signing key for future use in this builder instance
  	 * (see {@link module:net~AuthorizationV2Builder#key key()} for that).
  	 *
  	 * @param {CryptoJS#WordArray} signingKey the key to sign the computed signature data with
  	 * @returns {string} the SNWS2 HTTP Authorization header value
  	 */
  	buildWithKey(signingKey) {
  		const sortedHeaderNames = this.canonicalHeaderNames();
  		const canonicalReq = this.computeCanonicalRequestData(sortedHeaderNames);
  		const signatureData = this.computeSignatureData(canonicalReq);
  		const signature = Hex.stringify(HmacSHA256(signatureData, signingKey));
  		let result =
  			"SNWS2 Credential=" +
  			this.tokenId +
  			",SignedHeaders=" +
  			sortedHeaderNames.join(";") +
  			",Signature=" +
  			signature;
  		return result;
  	}
  
  	/**
  	 * Compute a HTTP `Authorization` header value from the configured
  	 * properties on the builder, computing a new signing key based on the
  	 * configured {@link module:net~AuthorizationV2Builder#date}.
  	 *
  	 * @param {string} tokenSecret the secret to sign the authorization with
  	 * @return {string} the SNWS2 HTTP Authorization header value
  	 */
  	build(tokenSecret) {
  		const signingKey = this.computeSigningKey(tokenSecret);
  		return this.buildWithKey(signingKey);
  	}
  
  	/**
  	 * Compute a HTTP `Authorization` header value from the configured
  	 * properties on the builder, using a signing key configured from a previous
  	 * call to {@link module:net~AuthorizationV2Builder#saveSigningKey saveSigningKey()}
  	 * or {@link module:net~AuthorizationV2Builder#key key()}.
  	 *
  	 * @return {string} the SNWS2 HTTP Authorization header value.
  	 */
  	buildWithSavedKey() {
  		return this.buildWithKey(this.signingKey);
  	}
  }
  
  /**
   * @function stringMatchFn
   * @param {string} e the element to test
   * @returns {boolean} `true` if the element matches
   * @private
   */
  
  /**
   * Create a case-insensitive string matching function.
   *
   * @param {string} value the string to perform the case-insensitive comparison against
   * @returns {stringMatchFn} a matching function that performs a case-insensitive comparison
   * @private
   */
  function caseInsensitiveEqualsFn(value) {
  	const valueLc = value.toLowerCase();
  	return e => valueLc === e.toString().toLowerCase();
  }
  
  /**
   * Create a new array of lower-cased and sorted strings from another array.
   *
   * @param {string[]} items the items to lower-case and sort
   * @returns {string[]} a new array of the lower-cased and sorted items
   * @private
   */
  function lowercaseSortedArray(items) {
  	const sortedItems = [];
  	const len = items.length;
  	for (let i = 0; i < len; i += 1) {
  		sortedItems.push(items[i].toLowerCase());
  	}
  	sortedItems.sort();
  	return sortedItems;
  }
  
  function _hexEscapeChar(c) {
  	return (
  		"%" +
  		c
  			.charCodeAt(0)
  			.toString(16)
  			.toUpperCase()
  	);
  }
  
  function _encodeURIComponent(str) {
  	return encodeURIComponent(str).replace(/[!'()*]/g, _hexEscapeChar);
  }
  
  Object.defineProperties(AuthorizationV2Builder, {
  	/**
  	 * The hex-encoded value for an empty SHA256 digest value.
  	 *
  	 * @memberof AuthorizationV2Builder
  	 * @readonly
  	 * @type {string}
  	 */
  	EMPTY_STRING_SHA256_HEX: {
  		value: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
  	},
  
  	/**
  	 * The SolarNetwork V2 authorization scheme.
  	 *
  	 * @memberof AuthorizationV2Builder
  	 * @readonly
  	 * @type {string}
  	 */
  	SNWS2_AUTH_SCHEME: { value: "SNWS2" }
  });
  
  export default AuthorizationV2Builder;
  