text: 'use strict';
  
  const Assert = require('./assert');
  const DeepEqual = require('./deepEqual');
  const EscapeRegex = require('./escapeRegex');
  const Utils = require('./utils');
  
  
  const internals = {};
  
  
  module.exports = function (ref, values, options = {}) {        // options: { deep, once, only, part, symbols }
  
      /*
          string -> string(s)
          array -> item(s)
          object -> key(s)
          object -> object (key:value)
      */
  
      if (typeof values !== 'object') {
          values = [values];
      }
  
      Assert(!Array.isArray(values) || values.length, 'Values array cannot be empty');
  
      // String
  
      if (typeof ref === 'string') {
          return internals.string(ref, values, options);
      }
  
      // Array
  
      if (Array.isArray(ref)) {
          return internals.array(ref, values, options);
      }
  
      // Object
  
      Assert(typeof ref === 'object', 'Reference must be string or an object');
      return internals.object(ref, values, options);
  };
  
  
  internals.array = function (ref, values, options) {
  
      if (!Array.isArray(values)) {
          values = [values];
      }
  
      if (!ref.length) {
          return false;
      }
  
      if (options.only &&
          options.once &&
          ref.length !== values.length) {
  
          return false;
      }
  
      let compare;
  
      // Map values
  
      const map = new Map();
      for (const value of values) {
          if (!options.deep ||
              !value ||
              typeof value !== 'object') {
  
              const existing = map.get(value);
              if (existing) {
                  ++existing.allowed;
              }
              else {
                  map.set(value, { allowed: 1, hits: 0 });
              }
          }
          else {
              compare = compare || internals.compare(options);
  
              let found = false;
              for (const [key, existing] of map.entries()) {
                  if (compare(key, value)) {
                      ++existing.allowed;
                      found = true;
                      break;
                  }
              }
  
              if (!found) {
                  map.set(value, { allowed: 1, hits: 0 });
              }
          }
      }
  
      // Lookup values
  
      let hits = 0;
      for (const item of ref) {
          let match;
          if (!options.deep ||
              !item ||
              typeof item !== 'object') {
  
              match = map.get(item);
          }
          else {
              for (const [key, existing] of map.entries()) {
                  if (compare(key, item)) {
                      match = existing;
                      break;
                  }
              }
          }
  
          if (match) {
              ++match.hits;
              ++hits;
  
              if (options.once &&
                  match.hits > match.allowed) {
  
                  return false;
              }
          }
      }
  
      // Validate results
  
      if (options.only &&
          hits !== ref.length) {
  
          return false;
      }
  
      for (const match of map.values()) {
          if (match.hits === match.allowed) {
              continue;
          }
  
          if (match.hits < match.allowed &&
              !options.part) {
  
              return false;
          }
      }
  
      return !!hits;
  };
  
  
  internals.object = function (ref, values, options) {
  
      Assert(options.once === undefined, 'Cannot use option once with object');
  
      const keys = Utils.keys(ref, options);
      if (!keys.length) {
          return false;
      }
  
      // Keys list
  
      if (Array.isArray(values)) {
          Assert(options.deep === undefined, 'Cannot use option once with object keys list');
          return internals.array(keys, values, options);
      }
  
      // Key value pairs
  
      const symbols = Object.getOwnPropertySymbols(values).filter((sym) => values.propertyIsEnumerable(sym));
      const targets = [...Object.keys(values), ...symbols];
  
      const compare = internals.compare(options);
      const set = new Set(targets);
  
      for (const key of keys) {
          if (!set.has(key)) {
              if (options.only) {
                  return false;
              }
  
              continue;
          }
  
          if (!compare(values[key], ref[key])) {
              return false;
          }
  
          set.delete(key);
      }
  
      if (set.size) {
          return !!options.part;
      }
  
      return true;
  };
  
  
  internals.string = function (ref, values, options) {
  
      // Empty string
  
      if (ref === '') {
          return values.length === 1 && values[0] === '' ||               // '' contains ''
              !options.once && !values.some((v) => v !== '');             // '' contains multiple '' if !once
      }
  
      // Map values
  
      const map = new Map();
      const patterns = [];
  
      for (const value of values) {
          Assert(typeof value === 'string', 'Cannot compare string reference to non-string value');
  
          if (value) {
              const existing = map.get(value);
              if (existing) {
                  ++existing.allowed;
              }
              else {
                  map.set(value, { allowed: 1, hits: 0 });
                  patterns.push(EscapeRegex(value));
              }
          }
          else if (options.once ||
              options.only) {
  
              return false;
          }
      }
  
      if (!patterns.length) {                     // Non-empty string contains unlimited empty string
          return true;
      }
  
      // Match patterns
  
      const regex = new RegExp(`(${patterns.join('|')})`, 'g');
      const leftovers = ref.replace(regex, ($0, $1) => {
  
          ++map.get($1).hits;
          return '';                              // Remove from string
      });
  
      // Validate results
  
      if (options.only &&
          leftovers) {
  
          return false;
      }
  
      for (const match of map.values()) {
          if (match.hits === match.allowed) {
              continue;
          }
  
          if (match.hits < match.allowed &&
              !options.part) {
  
              return false;
          }
  
          // match.hits > match.allowed
  
          if (options.once) {
              return false;
          }
      }
  
      return true;
  };
  
  
  internals.compare = function (options) {
  
      if (!options.deep) {
          return internals.shallow;
      }
  
      const hasOnly = options.only !== undefined;
      const hasPart = options.part !== undefined;
  
      const flags = {
          prototype: hasOnly ? options.only : hasPart ? !options.part : false,
          part: hasOnly ? !options.only : hasPart ? options.part : false
      };
  
      return (a, b) => DeepEqual(a, b, flags);
  };
  
  
  internals.shallow = function (a, b) {
  
      return a === b;
  };
  