Source: utils.js

import 'isomorphic-fetch';


/**
 * @module opensearch/utils
 */

export function parseURLQuery(url) {
  const search = (url.indexOf('?') === -1) ? url : url.substring(url.indexOf('?'));
  const vars = search.split('&');
  const parsed = {};
  for (let i = 0; i < vars.length; i++) {
    const pair = vars[i].split('=');
    parsed[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
  }
  return parsed;
}

export function parseXml(xmlStr) {
  if (typeof DOMParser !== 'undefined') {
    return (new DOMParser()).parseFromString(xmlStr, 'text/xml');
  } else if (typeof ActiveXObject !== 'undefined') {
    const xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); // eslint-disable-line no-undef
    xmlDoc.async = 'false';
    xmlDoc.loadXML(xmlStr);
    return xmlDoc;
  }
  throw new Error('Could not parse XML document.');
}

/*
 * Some common namespace definitions
 */
export const namespaces = {
  os: 'http://a9.com/-/spec/opensearch/1.1/',
  parameters: 'http://a9.com/-/spec/opensearch/extensions/parameters/1.0/',
  atom: 'http://www.w3.org/2005/Atom',
  georss: 'http://www.georss.org/georss',
  dc: 'http://purl.org/dc/elements/1.1/',
  media: 'http://search.yahoo.com/mrss/',

  // EOP and OM related namespaces
  opt: 'http://www.opengis.net/opt/2.1',
  om: 'http://www.opengis.net/om/2.0',
  eop: 'http://www.opengis.net/eop/2.0',
};

/*
 * Get an array of all child elements
 */
function getChildren(element) {
  if (element.children) {
    return Array.from(element.children);
  }
  return Array.from(element.childNodes).filter(node => node.nodeType === 1); // Node.ELEMENT_NODE
}

/*
 * Get an array of all *direct* descendants (in contrast to getElementsByTagName)
 * of an element with a certain namespace URI and tag name.
 */
export function getElements(element, namespace, tagName) {
  if (!element) {
    return [];
  }
  const namespaceURI = namespaces[namespace] || namespace;
  const children = getChildren(element);
  if (tagName && namespaceURI) {
    return children.filter(
      child => child.localName === tagName && child.namespaceURI === namespaceURI
    );
  } else if (tagName) {
    return children.filter(child => child.localName === tagName);
  }
  return children;
}

/*
 * Get the first direct descendant element with the given namespace URI and tag name.
 */
export function getFirstElement(element, namespace, tagName) {
  // use shortcut; when available
  if (!namespace && !tagName && element.firstElementChild) {
    return element.firstElementChild;
  }
  const elements = getElements(element, namespace, tagName);
  if (elements.length) {
    return elements[0];
  }
  return null;
}

/*
 * Get the text of the first direct descendant element with the given namespace
 * URI and tag name.
 */
export function getText(element, namespace, tagName) {
  const first = getFirstElement(element, namespace, tagName);
  return first ? first.textContent : null;
}

/*
 * Get the value of the namespaced attribute or return a default.
 */
export function getAttributeNS(node, namespace, name, defaultValue) {
  const namespaceURI = namespaces[namespace] || namespace;
  if (node.hasAttributeNS(namespaceURI, name)) {
    return node.getAttributeNS(namespaceURI, name);
  }
  return defaultValue;
}

function splitNamespace(name) {
  return (name.indexOf(':') !== -1) ? name.split(':') : [null, name];
}

/**
 * Resolves an xPath like query with the given element as basis. All parts of
 * the path must be specified, none may be omitted. Allows to select attributes
 * using the `@attrName` postfix or the text of an element using the `text()`
 * as the last path part.
 * @param {object} element The root element to start the query on. Must be a DOM
 *                         compliant object.
 * @param {string} path The search path: parts are separated by the `/` character
 *                      and may contain a supported namespace prefix (separated
 *                      by the color character).
 *                      Examples: `os:Url@type`, `atom:entry/atom:title/text()`,
 *                      `channel/item/georss:box/text()`
 * @param {boolean} [single=false] Whether multiple elements are expected. When
 *                                 false, an array is returned, otherwise single
 *                                 values.
 * @returns {object|string|object[]|string[]} Depending on the query and the
 *                                            single parameter, either a DOM Node
 *                                            or a string, or arrays thereof.
 */
export function simplePath(element, path, single = false) {
  // split path and discard empty parts
  const parts = path.split('/').filter(part => part.length);
  let current = single ? element : [element];

  for (let i = 0; i < parts.length; ++i) {
    const part = parts[i];

    // single values are treated differently
    if (single) {
      if (part === 'text()') {
        return current.textContent;
      } else if (part.indexOf('@') !== -1) {
        const [nodePart, attrPart] = part.split('@');
        const [namespace, tagName] = splitNamespace(nodePart);
        const [attrNamespace, attrName] = splitNamespace(attrPart);
        current = getFirstElement(current, namespace, tagName);
        return getAttributeNS(current, attrNamespace, attrName);
      }
      const [namespace, tagName] = splitNamespace(part);
      current = getFirstElement(current, namespace, tagName);
      if (!current) {
        return null;
      }
    } else if (part === 'text()') {
      return current.map(currentElement => currentElement.textContent);
    } else if (part.indexOf('@') !== -1) {
      const [nodePart, attrPart] = part.split('@');
      const [namespace, tagName] = splitNamespace(nodePart);
      const [attrNamespace, attrName] = splitNamespace(attrPart);
      return current.map(currentElement => getElements(currentElement, namespace, tagName))
        .reduce((acc, value) => acc.concat(value), [])
        .map(finalElement => getAttributeNS(finalElement, attrNamespace, attrName));
    } else {
      const [namespace, tagName] = splitNamespace(part);
      current = current.map(currentElement => getElements(currentElement, namespace, tagName))
        .reduce((acc, value) => acc.concat(value), []);
    }
  }

  return current;
}


// adapted from https://developer.mozilla.org/en-US/Add-ons/Code_snippets/LookupPrefix
// Private function for lookupPrefix only
// eslint-disable-next-line no-underscore-dangle
function _lookupNamespacePrefix(namespaceURI, originalElement) {
  const xmlnsPattern = /^xmlns:(.*)$/;
  if (originalElement.namespaceURI && originalElement.namespaceURI === namespaceURI
    && originalElement.lookupNamespaceURI(originalElement.prefix) === namespaceURI) {
    return originalElement.prefix;
  }
  if (originalElement.attributes && originalElement.attributes.length) {
    for (let i = 0; i < originalElement.attributes.length; i++) {
      const att = originalElement.attributes[i];
      xmlnsPattern.lastIndex = 0;
      let localName = att.localName || att.name.substr(att.name.indexOf(':') + 1); // latter test for IE which doesn't support localName
      if (localName.indexOf(':') !== -1) { // For Firefox when in HTML mode
        localName = localName.substr(att.name.indexOf(':') + 1);
      }
      if (xmlnsPattern.test(att.name) && att.value === namespaceURI) {
        return localName;
      }
    }
  }
  if (originalElement.parentNode) {
    // EntityReferences may have to be skipped to get to it
    return _lookupNamespacePrefix(namespaceURI, originalElement.parentNode);
  }
  return null;
}

/**
 * Looks up the the namespace prefix on the given DOM Node and the given namespace
 * @param {DOMNode} node The node to look up the namespace prefix
 * @param {String} namespaceURI The namespace URI to look up the namespace definition
 * @returns {String} The namespace prefix
 */
export function lookupPrefix(node, namespaceURI) {
  // Depends on private function _lookupNamespacePrefix() below and on https://developer.mozilla.org/En/Code_snippets/LookupNamespaceURI
  // http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-lookupNamespacePrefix
  // http://www.w3.org/TR/DOM-Level-3-Core/namespaces-algorithms.html#lookupNamespacePrefixAlgo
  // (The above had a few apparent 'bugs' in the pseudo-code which were corrected here)
  if (node.lookupPrefix) { // Shouldn't use this in text/html for Mozilla as will return null
    return node.lookupPrefix(namespaceURI);
  }
  if (namespaceURI === null || namespaceURI === '') {
    return null;
  }
  switch (node.nodeType) {
    case 1: // Node.ELEMENT_NODE
      return _lookupNamespacePrefix(namespaceURI, node);
    case 9: // Node.DOCUMENT_NODE
      return _lookupNamespacePrefix(namespaceURI, node.documentElement);
    case 6: // Node.ENTITY_NODE
    case 12: // Node.NOTATION_NODE
    case 11: // Node.DOCUMENT_FRAGMENT_NODE
    case 10: // Node.DOCUMENT_TYPE_NODE
      return null; // type is unknown
    case 2: // Node.ATTRIBUTE_NODE
      if (node.ownerElement) {
        return _lookupNamespacePrefix(namespaceURI, node.ownerElement);
      }
      return null;
    default:
      if (node.parentNode) {
        // EntityReferences may have to be skipped to get to it
        return _lookupNamespacePrefix(namespaceURI, node.parentNode);
      }
      return null;
  }
}

export function fetchAndCheck(...args) {
  return fetch(...args).then((response) => {
    if (response.status >= 400) {
      throw new Error('Bad response from server');
    }
    return response;
  });
}

export function isNullOrUndefined(value) {
  return typeof value === 'undefined' || value === null;
}

/*
 * Forked from https://github.com/mapbox/wellknown/blob/87965f6f46ee38355e7e1f82107aa832ea29bc6c/wellknown.js
 * Removed whitespaces after geometry type to be more robust with some (FedEO)
 * services.
 */

/* eslint-disable no-param-reassign, prefer-template */

export function toWKT(gj) {
  if (gj.type === 'Feature') {
    gj = gj.geometry;
  }

  function wrapParens(s) { return '(' + s + ')'; }

  function pairWKT(c) {
    return c.join(' ');
  }

  function ringWKT(r) {
    return r.map(pairWKT).join(', ');
  }

  function ringsWKT(r) {
    return r.map(ringWKT).map(wrapParens).join(', ');
  }

  function multiRingsWKT(r) {
    return r.map(ringsWKT).map(wrapParens).join(', ');
  }

  switch (gj.type) {
    case 'Point':
      return 'POINT(' + pairWKT(gj.coordinates) + ')';
    case 'LineString':
      return 'LINESTRING(' + ringWKT(gj.coordinates) + ')';
    case 'Polygon':
      return 'POLYGON(' + ringsWKT(gj.coordinates) + ')';
    case 'MultiPoint':
      return 'MULTIPOINT(' + ringWKT(gj.coordinates) + ')';
    case 'MultiPolygon':
      return 'MULTIPOLYGON(' + multiRingsWKT(gj.coordinates) + ')';
    case 'MultiLineString':
      return 'MULTILINESTRING(' + ringsWKT(gj.coordinates) + ')';
    case 'GeometryCollection':
      return 'GEOMETRYCOLLECTION(' + gj.geometries.map(toWKT).join(', ') + ')';
    default:
      throw new Error('stringify requires a valid GeoJSON Feature or geometry object as input');
  }
}

/* eslint-enable no-param-reassign, prefer-template */

/**
 * Returns a [Request]{@link https://developer.mozilla.org/en-US/docs/Web/API/Request}
 * object for the fetch API.
 * @param {string} url The request URL
 * @param {object} [baseRequest] the baseRequest
 * @returns {Request} The constructed request.
 */
export function createRequest(url, baseRequest) {
  return new Request(url, baseRequest);
}

/**
 * Creates (and sends) an [XMLHttpRequest]{@link https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest}.
 * @param {string} url The request URL
 * @param {object} [baseRequest] the baseRequest
 * @returns {XMLHttpRequest} The constructed request.
 */
export function createXHR(url, baseRequest = {}) {
  const xhr = new XMLHttpRequest();
  xhr.open(baseRequest.method || 'GET', url);
  if (baseRequest.headers) {
    Object.keys(baseRequest.headers).forEach((key) => {
      xhr.setRequestHeader(key, baseRequest.headers[key]);
    });
  }
  xhr.send(baseRequest.body ? baseRequest.body : null);
  return xhr;
}

/**
 * Sort of polyfill for [Array.prototype.find]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find}
 * @param {Array} arr the array to find the entry on.
 * @param {function} predicate the callback to find the value.
 * @param {*} thisArg the `this` for the predicate function.
 * @returns {*} the found item or undefined
 */
export function find(arr, predicate, thisArg) {
  if (Array.prototype.find) {
    return arr.find(predicate, thisArg);
  }
  for (let i = 0; i < arr.length; ++i) {
    const v = arr[i];
    if (predicate(v, i, arr)) {
      return v;
    }
  }
  return undefined;
}

/**
 * Sort of polyfill for [Object.assign]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign}
 * @param {object} target the target to set the properties on.
 * @param {...object} sources the source objects to copy properties from.
 * @returns {object} the target
 */
export function assign(target, ...sources) {
  if (Object.assign) {
    return Object.assign(target, ...sources);
  }
  for (let i = 0; i < sources.length; ++i) {
    const source = sources[i];
    if (source) {
      for (const key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
          target[key] = source[key]; // eslint-disable-line no-param-reassign
        }
      }
    }
  }
  return target;
}