text: /**
   * Copyright 2018 The AMP HTML Authors. All Rights Reserved.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS-IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  import {WindowInterface} from '../../../src/window-interface';
  import {
    base64UrlDecodeFromString,
    base64UrlEncodeFromString,
  } from '../../../src/core/types/string/base64';
  import {crc32} from './crc32';
  import {user} from '../../../src/log';
  
  /** @const {string} */
  const DELIMITER = '*';
  const KEY_VALIDATOR = /^[a-zA-Z0-9\-_.]+$/;
  const CHECKSUM_OFFSET_MAX_MIN = 1;
  const VALID_VERSION = 1;
  const TAG = 'amp-analytics/linker';
  
  /**
   * Creates the linker string, in the format of
   * <version>*<checksum>*<serializedIds>
   *
   * where
   *   checksum: base36(CRC32(<fingerprint>*<minuteSinceEpoch>*<serializedIds>))
   *   serializedIds: <id1>*<idValue1>*<id2>*<idValue2>...
   *                  values are base64 encoded
   *   fingerprint: <userAgent>*<timezoneOffset>*<userLanguage>
   *
   * @param {string} version
   * @param {!Object} ids
   * @return {string}
   */
  export function createLinker(version, ids) {
    const serializedIds = serialize(ids);
    if (serializedIds === '') {
      return '';
    }
    const checksum = getCheckSum(serializedIds);
    return [version, checksum, serializedIds].join(DELIMITER);
  }
  
  /**
   * Return the key value pairs
   * @param {string} value
   * @return {?Object<string, string>}
   */
  export function parseLinker(value) {
    const linkerObj = parseLinkerParamValue(value);
    if (!linkerObj) {
      return null;
    }
    const {checksum, serializedIds} = linkerObj;
    if (!isCheckSumValid(serializedIds, checksum)) {
      user().error(TAG, 'LINKER_PARAM value checksum not valid');
      return null;
    }
    return deserialize(serializedIds);
  }
  
  /**
   * Parse the linker param value to version checksum and serializedParams
   * @param {string} value
   * @return {?Object}
   */
  function parseLinkerParamValue(value) {
    const parts = value.split(DELIMITER);
    const isEven = parts.length % 2 == 0;
  
    if (parts.length < 4 || !isEven) {
      // Format <version>*<checksum>*<key1>*<value1>
      // Note: linker makes sure there's at least one pair of non empty key value
      // Make sure there is at least three delimiters.
      user().error(TAG, `Invalid linker_param value ${value}`);
      return null;
    }
  
    const version = Number(parts.shift());
    if (version !== VALID_VERSION) {
      user().error(TAG, `Invalid version number ${version}`);
      return null;
    }
  
    const checksum = parts.shift();
    const serializedIds = parts.join(DELIMITER);
    return {
      checksum,
      serializedIds,
    };
  }
  
  /**
   * Check if the checksum is valid with time offset tolerance.
   * @param {string} serializedIds
   * @param {string} checksum
   * @return {boolean}
   */
  function isCheckSumValid(serializedIds, checksum) {
    for (let i = 0; i <= CHECKSUM_OFFSET_MAX_MIN; i++) {
      const calculateCheckSum = getCheckSum(serializedIds, i);
      if (calculateCheckSum == checksum) {
        return true;
      }
    }
    return false;
  }
  
  /**
   * Create a unique checksum hashing the fingerprint and a few other values.
   * @param {string} serializedIds
   * @param {number=} opt_offsetMin
   * @return {string}
   */
  function getCheckSum(serializedIds, opt_offsetMin) {
    const fingerprint = getFingerprint();
    const offset = opt_offsetMin || 0;
    const timestamp = getMinSinceEpoch() - offset;
    const crc = crc32([fingerprint, timestamp, serializedIds].join(DELIMITER));
    // Encoded to base36 for less bytes.
    return crc.toString(36);
  }
  
  /**
   * Generates a semi-unique value for page visitor.
   * @return {string}
   */
  function getFingerprint() {
    const date = new Date();
    const timezone = date.getTimezoneOffset();
  
    const language = WindowInterface.getUserLanguage(window);
    return [WindowInterface.getUserAgent(window), timezone, language].join(
      DELIMITER
    );
  }
  
  /**
   * Encode all values & join them together
   * @param {!Object} pairs
   * @return {string}
   */
  function serialize(pairs) {
    if (!pairs) {
      return '';
    }
    return Object.keys(pairs)
      .filter((key) => {
        const valid = KEY_VALIDATOR.test(key);
        if (!valid) {
          user().error(TAG, 'Invalid linker key: ' + key);
        }
        return valid;
      })
      .map((key) => key + DELIMITER + encode(pairs[key]))
      .join(DELIMITER);
  }
  
  /**
   * Deserialize the serializedIds and return keyValue pairs.
   * @param {string} serializedIds
   * @return {!Object<string, string>}
   */
  function deserialize(serializedIds) {
    const keyValuePairs = {};
    const params = serializedIds.split(DELIMITER);
    for (let i = 0; i < params.length; i += 2) {
      const key = params[i];
      const valid = KEY_VALIDATOR.test(key);
      if (!valid) {
        user().error(TAG, `Invalid linker key ${key}, value ignored`);
        continue;
      }
      const value = decode(params[i + 1]);
      keyValuePairs[key] = value;
    }
    return keyValuePairs;
  }
  
  /**
   * Rounded time used to check if t2 - t1 is within our time tolerance.
   * @return {number}
   */
  function getMinSinceEpoch() {
    // Timestamp in minutes, floored.
    return Math.floor(Date.now() / 60000);
  }
  
  /**
   * Function that encodesURIComponent but also tilde, since we are using it as
   * our delimiter.
   * @param {string} value
   * @return {*} TODO(#23582): Specify return type
   */
  function encode(value) {
    return base64UrlEncodeFromString(String(value));
  }
  
  /**
   * @param {string} value
   * @return {string}
   */
  function decode(value) {
    return base64UrlDecodeFromString(String(value));
  }
  