/////////////////////////////////
// General utility library
//

const icons = {
  image: "$ayiFileImage",
  pdf: "$ayiFilePdf",
  word: "$ayiFileWord",
  powerpoint: "$ayiFilePowerpoint",
  excel: "$ayiFileExcel",
  csv: "$ayiFileCsv",
  audio: "$ayiFileAudio",
  video: "$ayiFileVidio",
  archive: "$ayiFileArchive",
  code: "$ayiFileCode",
  text: "$ayiFileAlt",
  file: "$ayiFile"
};
const mimeTypes = {
  "image/gif": icons.image,
  "image/jpeg": icons.image,
  "image/png": icons.image,
  "image/webp": icons.image,

  "application/pdf": icons.pdf,

  "application/msword": icons.word,
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
    icons.word,

  "application/mspowerpoint": icons.powerpoint,
  "application/vnd.openxmlformats-officedocument.presentationml.presentation":
    icons.powerpoint,

  "application/msexcel": icons.excel,
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
    icons.excel,

  "text/csv": icons.csv,

  "audio/aac": icons.audio,
  "audio/wav": icons.audio,
  "audio/mpeg": icons.audio,
  "audio/mp4": icons.audio,
  "audio/ogg": icons.audio,

  "video/x-msvideo": icons.video,
  "video/mpeg": icons.video,
  "video/mp4": icons.video,
  "video/ogg": icons.video,
  "video/quicktime": icons.video,
  "video/webm": icons.video,

  "application/gzip": icons.archive,
  "application/zip": icons.archive,
  "application/x-tar": icons.archive,

  "text/css": icons.code,
  "text/html": icons.code,
  "text/javascript": icons.code,
  "application/javascript": icons.code,

  "text/plain": icons.text,
  "text/richtext": icons.text,
  "text/rtf": icons.text,
  "application/rtf": icons.text,
  "application/json": icons.text
};

const extensions = {
  gif: icons.image,
  jpeg: icons.image,
  jpg: icons.image,
  png: icons.image,
  webp: icons.image,

  pdf: icons.pdf,

  doc: icons.word,
  docx: icons.word,

  ppt: icons.powerpoint,
  pptx: icons.powerpoint,

  xls: icons.excel,
  xlsx: icons.excel,

  csv: icons.csv,

  aac: icons.audio,
  mp3: icons.audio,
  ogg: icons.audio,

  avi: icons.video,
  flv: icons.video,
  mkv: icons.video,
  mp4: icons.video,

  gz: icons.archive,
  zip: icons.archive,
  tar: icons.archive,
  "7z": icons.archive,

  css: icons.code,
  html: icons.code,
  js: icons.code,

  txt: icons.text,
  json: icons.text,
  rtf: icons.text
};

export default {
  ///////////////////////////////
  // CLEAN OBJECT
  // Clear all properties from object without resorting to assigning a new object (o={})
  // which can be problematic in some cases (IE bugs, watched data items in forms etc)

  removeAllPropertiesFromObject: function(o) {
    for (let variableKey in o) {
      if (Object.prototype.hasOwnProperty.call(o, variableKey)) {
        delete o[variableKey];
      }
    }
  },
  ///////////////////////////////
  // DEEP COPY FOR API UPDATE
  // Deep copy an object skipping all *Viz and named properties from object
  //
  deepCopySkip: function(source, skipNames) {
    if (skipNames == null) {
      skipNames = [];
    }
    let o = {};
    for (let key in source) {
      if (
        !key.endsWith("Viz") &&
        !skipNames.some(x => x == key) &&
        Object.prototype.hasOwnProperty.call(source, key)
      ) {
        o[key] = source[key];
      }
    }
    return o;
  },

  /**
   * Copy a string to clipboard
   * @param  {String} string         The string to be copied to clipboard
   * @return {Boolean}               returns a boolean correspondent to the success of the copy operation.
   * Modified from an example here: https://stackoverflow.com/a/53951634/8939
   * Basically a fallback if navigator.clipboard is not available
   */
  copyToClipboard: function(string) {
    let textarea;
    let result;

    if (navigator && navigator.clipboard) {
      navigator.clipboard.writeText(string);
    } else {
      try {
        textarea = document.createElement("textarea");
        textarea.setAttribute("readonly", true);
        textarea.setAttribute("contenteditable", true);
        textarea.style.position = "fixed"; // prevent scroll from jumping to the bottom when focus is set.
        textarea.value = string;

        document.body.appendChild(textarea);

        textarea.focus();
        textarea.select();

        const range = document.createRange();
        range.selectNodeContents(textarea);

        const sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);

        textarea.setSelectionRange(0, textarea.value.length);
        result = document.execCommand("copy");
      } catch (err) {
        result = null;
      } finally {
        document.body.removeChild(textarea);
      }

      // manual copy fallback using prompt
      if (!result) {
        const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
        const copyHotkey = isMac ? "⌘C" : "CTRL+C";
        result = prompt(`Press ${copyHotkey}`, string);
        if (!result) {
          return false;
        }
      }
    }
    return true;
  },
  ///////////////////////////////
  // ROUNDING
  // //https://medium.com/swlh/how-to-round-to-a-certain-number-of-decimal-places-in-javascript-ed74c471c1b8
  roundAccurately: function(number, decimalPlaces) {
    if (!number || number == 0 || Number.isNaN(number)) {
      return number;
    }
    const wasNegative = number < 0;
    if (wasNegative) {
      number = Math.abs(number); //make sure it's positive because rounding negative numbers is weird in JS
    }
    number = Number(
      Math.round(number + "e" + decimalPlaces) + "e-" + decimalPlaces
    );
    if (wasNegative) {
      number = 0 - number;
    }
    return number;
  },
  ///////////////////////////////
  // CLEAN TAG NAME
  // Clean up a tag with same rules as server
  //
  normalizeTag: function(tagName) {
    if (!tagName || tagName == "") {
      return null;
    }
    tagName = tagName.toLowerCase();

    //spaces to dashes
    tagName = tagName.replace(/ /gi, "-");

    //multiple dashes to single dashes
    tagName = tagName.replace(/-+/g, "-");

    //ensure doesn't start or end with a dash
    tagName = this.trimSpecific(tagName, "-");

    //No longer than 255 characters
    tagName = tagName.length > 255 ? tagName.substr(0, 255 - 1) : tagName;

    return tagName;
  },
  ///////////////////////////////
  // Quick hash for trivial purposes
  // not cryptographic
  // https://stackoverflow.com/a/7616484/8939
  //
  quickHash: function(theString) {
    let hash = 0;
    let i;
    let chr;
    if (theString.length === 0) return hash;
    for (i = 0; i < theString.length; i++) {
      chr = theString.charCodeAt(i);
      hash = (hash << 5) - hash + chr;
      hash |= 0; // Convert to 32bit integer
    }
    return hash;
  },

  ////////////////////////////////////////
  // Random password / login generator
  // https://stackoverflow.com/a/51540480/8939
  // using 32 character (128 bit) as default
  //
  getRandomPassword: function() {
    const wishlist = "0123456789abcdefghijkmnopqrstuvwxyz";

    return Array.from(crypto.getRandomValues(new Uint32Array(32)))
      .map(x => wishlist[x % wishlist.length])
      .join("");
  },
  ///////////////////////////////
  // CONVERT STRING TO BOOLEAN
  // https://stackoverflow.com/a/1414175/8939
  //
  stringToBoolean: function(string) {
    switch (string.toLowerCase().trim()) {
      case "true":
      case "yes":
      case "1":
        return true;
      case "false":
      case "no":
      case "0":
      case null:
        return false;
      default:
        return Boolean(string);
    }
  }, ///////////////////////////////
  // CONVERT STRING TO FLOAT
  // https://stackoverflow.com/a/9409894/8939
  //
  stringToFloat: function(string) {
    //null or empty then zero
    if (!string) {
      return 0;
    }

    //A number already then parse and return
    if (this.isNumeric(string)) {
      if (Number.isNaN(string)) {
        return 0;
      }
      return parseFloat(string);
    }

    //Not a string at all?
    if (!this.isString(string)) {
      return 0;
    }

    const ret = parseFloat(string.replace(/[^\d.-]/g, ""));
    if (Number.isNaN(ret)) {
      return 0;
    }

    return ret;
  },
  ///////////////////////////////
  // Is negative number
  //
  //
  isNegative: function(v) {
    //null or empty then zero
    if (!v || v == 0 || Number.isNaN(v)) {
      return false;
    }
    return parseFloat(v) < 0;
  },
  ///////////////////////////////
  // Splice a string
  //changes the content of a string by removing a range of
  // characters and/or adding new characters.
  //
  // @param {String} source string
  // @param {number} start Index at which to start changing the string.
  // @param {number} delCount An integer indicating the number of old chars to remove.
  // @param {string} newSubStr The String that is spliced in.
  // @return {string} A new string with the spliced substring.
  stringSplice: function(source, start, delCount, newSubStr) {
    if (source == null || source == "") {
      if (newSubStr) {
        return newSubStr;
      }
      return "";
    }
    return (
      source.slice(0, start) +
      newSubStr +
      source.slice(start + Math.abs(delCount))
    );
  },
  ///////////////////////////////
  // Truncate a string
  //truncates and adds ellipses
  //
  // @param {String} source string
  // @param {number} length desired
  // @return {string} A new string truncated with ellipses at end
  truncateString: function(s, len) {
    if (this.stringIsNullOrEmpty(s)) {
      return s;
    }
    if (s.length > len) {
      return s.substring(0, len) + "...";
    } else {
      return s;
    }
  },
  ///////////////////////////////
  // Format tags for display
  //
  //
  // @param {String} tags raw from server
  // @return {string} A new string with the tags formatted or an empty string if no tags
  formatTags: function(tags) {
    if (tags && tags.length > 0) {
      return tags.join(", ");
    }
    return "";
  },
  ///////////////////////////////
  // ICON FOR *ALL* OBJECT TYPES
  //(used for search results and event log / history)
  //NOTE: Any object type could appear in event log, they all need to be supported where possible
  //CoreBizObject add here
  iconForType: function(ayaType) {
    switch (ayaType) {
      case window.$gz.type.NoType:
      case null:
        return "$ayiGenderless";
      case window.$gz.type.Global:
        return "$ayiGlobe";
      case window.$gz.type.User:
        return "$ayiUser";
      case window.$gz.type.ServerState:
        return "$ayiDoorOpen";
      case window.$gz.type.License:
        return "$ayiTicket";
      case window.$gz.type.LogFile:
        return "$ayiGlasses";
      case window.$gz.type.PickListTemplate:
        return "$ayiPencilRuler";
      case window.$gz.type.Customer:
        return "$ayiAddressCard";
      case window.$gz.type.ServerJob:
        return "$ayiRobot";
      case window.$gz.type.Contract:
        return "$ayiFileContract";
      case window.$gz.type.TrialSeeder:
        return "$ayiSeedling";
      case window.$gz.type.Metrics:
        return "$ayiFileMedicalAlt";
      case window.$gz.type.Translation:
        return "$ayiLanguage";
      case window.$gz.type.UserOptions:
        return "$ayiUserCog";
      case window.$gz.type.HeadOffice:
        return "$ayiSitemap";
      case window.$gz.type.LoanUnit:
        return "$ayiPlug";
      case window.$gz.type.FileAttachment:
        return "$ayiPaperclip";
      case window.$gz.type.DataListSavedFilter:
        return "$ayiFilter";
      case window.$gz.type.FormCustom:
        return "$ayiCustomize";
      case window.$gz.type.Part:
        return "$ayiBoxes";
      case window.$gz.type.PartWarehouse:
        return "$ayiWarehouse";
      case window.$gz.type.PartAssembly:
        return "$ayiObjectGroup";
      case window.$gz.type.Project:
        return "$ayiProjectDiagram";
      case window.$gz.type.PurchaseOrder:
        return "$ayiTruckLoading";
      case window.$gz.type.Unit:
        return "$ayiFan";
      case window.$gz.type.UnitModel:
        return "$ayiDiceD20";
      case window.$gz.type.Vendor:
        return "$ayiStore";
      case window.$gz.type.Quote:
        return "$ayiPencilAlt";
      case window.$gz.type.PM:
        return "$ayiBusinessTime";
      case window.$gz.type.WorkOrder:
        return "$ayiTools";
      case window.$gz.type.WorkOrderItem:
      case window.$gz.type.PMItem:
      case window.$gz.type.QuoteItem:
        return "$ayiWrench";
      case window.$gz.type.WorkOrderItemExpense:
      case window.$gz.type.QuoteItemExpense:
      case window.$gz.type.PMItemExpense:
        return "$ayiMoneyBillWave";
      case window.$gz.type.WorkOrderItemLabor:
      case window.$gz.type.QuoteItemLabor:
      case window.$gz.type.PMItemLabor:
        return "$ayiHammer";
      case window.$gz.type.WorkOrderItemLoan:
      case window.$gz.type.QuoteItemLoan:
      case window.$gz.type.PMItemLoan:
        return "$ayiPlug";
      case window.$gz.type.WorkOrderItemPart:
      case window.$gz.type.QuoteItemPart:
      case window.$gz.type.PMItemPart:
        return "$ayiBoxes";
      case window.$gz.type.WorkOrderItemPartRequest:
        return "$ayiParachuteBox";
      case window.$gz.type.WorkOrderItemScheduledUser:
      case window.$gz.type.QuoteItemScheduledUser:
      case window.$gz.type.PMItemScheduledUser:
        return "$ayiUserClock";
      case window.$gz.type.WorkOrderItemTask:
      case window.$gz.type.QuoteItemTask:
      case window.$gz.type.PMItemTask:
      case window.$gz.type.TaskGroup:
        return "$ayiTasks";
      case window.$gz.type.WorkOrderItemTravel:
      case window.$gz.type.QuoteItemTravel:
      case window.$gz.type.PMItemTravel:
        return "$ayiTruckMonster";
      case window.$gz.type.WorkOrderItemUnit:
      case window.$gz.type.QuoteItemUnit:
      case window.$gz.type.PMItemUnit:
        return "$ayiFan";
      case window.$gz.type.WorkOrderItemOutsideService:
      case window.$gz.type.QuoteItemOutsideService:
      case window.$gz.type.PMItemOutsideService:
        return "$ayiLuggageCart";
      case window.$gz.type.Backup:
        return "$ayiFileArchive";
      case window.$gz.type.Notification:
        return "$ayiBell";
      case window.$gz.type.NotifySubscription:
        return "$ayiBullhorn";
      case window.$gz.type.Reminder:
        return "$ayiStickyNote";
      case window.$gz.type.UnitMeterReading:
        return "$ayiWeight";
      case window.$gz.type.CustomerServiceRequest:
        return "$ayiConciergeBell";
      // case window.$gz.type.ServiceBank:
      //   return "$ayiCarBattery";
      case window.$gz.type.OpsNotificationSettings:
        return "$ayiBullhorn";
      case window.$gz.type.Report:
        return "$ayiThList";
      case window.$gz.type.DashboardView:
        return "$ayiTachometer";
      case window.$gz.type.CustomerNote:
        return "$ayiClipboard";
      case window.$gz.type.Memo:
        return "$ayiInbox";
      case window.$gz.type.Review:
        return "$ayiCalendarCheck";
      case window.$gz.type.ServiceRate:
        return "$ayiCalculator";
      case window.$gz.type.TravelRate:
        return "$ayiCalculator";
      case window.$gz.type.TaxCode:
        return "$ayiPercent";
      case window.$gz.type.WorkOrderStatus:
        return "$ayiFlag";

      //scroll icon is good one for something
      default:
        return null;
    }
  },
  //https://gist.github.com/colemanw/9c9a12aae16a4bfe2678de86b661d922
  iconForFile: function(fileName, mimeType) {
    // List of official MIME Types: http://www.iana.org/assignments/media-types/media-types.xhtml

    let extension = null;
    if (fileName && fileName.includes(".")) {
      extension = fileName.split(".").pop();
      extension = extension.toLowerCase();
    }
    if (!extension && !mimeType) {
      console.log(
        "gzutil:iconForFile -> No mime or extension for " +
          fileName +
          " " +
          mimeType
      );
      return "$ayiFile";
    }

    if (!mimeType) {
      mimeType = "";
    }
    mimeType = mimeType.toLowerCase();

    const iconFromExtension = extensions[extension];
    const iconFromMIME = mimeTypes[mimeType];

    if (iconFromMIME) {
      return iconFromMIME;
    }
    if (iconFromExtension) {
      return iconFromExtension;
    }

    return "$ayiFile";
  },
  ///////////////////////////////////////////////
  // attempt to detect image extension name
  //
  isImageAttachment: function(fileName, mimeType) {
    return this.iconForFile(fileName, mimeType) == "$ayiFileImage";
  },
  ///////////////////////////////////////////////
  // Sleep async
  //
  sleepAsync: function(milliseconds) {
    // eslint-disable-next-line
    return new Promise((resolve) => setTimeout(resolve, milliseconds));
  },
  ///////////////////////////////////////////////
  // sortByKey lodash "sortBy" replacement
  // https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_sortby-and-_orderby
  //usage:
  // The native sort modifies the array in place. `_.orderBy` and `_.sortBy` do not, so we use `.concat()` to
  // copy the array, then sort.
  // fruits.concat().sort(sortBy("name"));
  // => [{name:"apple", amount: 4}, {name:"banana", amount: 2}, {name:"mango", amount: 1}, {name:"pineapple", amount: 2}]
  sortByKey: key => {
    return (a, b) => {
      const aaa = a[key].toUpperCase();
      const bbb = b[key].toUpperCase();
      return aaa > bbb ? 1 : bbb > aaa ? -1 : 0;
      //this was the original but it was sorting weird as it was taking case into account with uppercase higher than lowercase
      //so PMItem came before Part in the object lists
      //return a[key] > b[key] ? 1 : b[key] > a[key] ? -1 : 0;
    };
  },
  ///////////////////////////////////////////////
  // "has" lodash replacement
  // https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_has
  //
  has: function(obj, key) {
    var keyParts = key.split(".");
    return (
      !!obj &&
      (keyParts.length > 1
        ? this.has(obj[key.split(".")[0]], keyParts.slice(1).join("."))
        : hasOwnProperty.call(obj, key))
    );
  },
  ///////////////////////////////////////////////
  // Check if object is empty
  //
  objectIsEmpty: function(obj) {
    //https://stackoverflow.com/a/4994265/8939
    return !obj || Object.keys(obj).length === 0;
  },
  ///////////////////////////////////////////////
  // Trim specific character from start and end
  // https://stackoverflow.com/a/55292366/8939
  //
  trimSpecific: function trim(str, ch) {
    var start = 0;
    var end = str.length;
    while (start < end && str[start] === ch) ++start;
    while (end > start && str[end - 1] === ch) --end;
    return start > 0 || end < str.length ? str.substring(start, end) : str;
  },
  ///////////////////////////////////////////////
  // is numeric replacement for lodash
  // https://stackoverflow.com/a/52986361/8939
  //
  isNumeric: function(n) {
    //lodash isNumber returned false if it's a string and that's what the rest of the code expects even though it's parseable to a number
    return !this.isString(n) && !isNaN(parseFloat(n)) && isFinite(n);
  },
  ///////////////////////////////////////////////
  // is string replacement for lodash
  // https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_isString
  //
  isString: function(str) {
    return str != null && typeof str.valueOf() === "string";
  },
  ///////////////////////////////////////////////
  //
  //
  //
  stringIsNullOrEmpty: function(str) {
    if (str === null || str === undefined) {
      return true;
    }
    if (this.isString(str)) {
      if (str.trim() == "") {
        return true;
      }
    }
    return false;
  },
  ///////////////////////////////////////////////
  // is Boolean replacement for lodash
  // https://stackoverflow.com/a/43718478/8939
  //
  isBoolean: function(obj) {
    return obj === true || obj === false || typeof variable === "boolean";
  },
  ///////////////////////////////////////////////
  // parse to number or null if not a number
  // used because route params can turn into strings
  // on their own
  //
  stringToIntOrNull: function(n) {
    const ret = Number.parseInt(n, 10);
    if (Number.isNaN(ret)) {
      return null;
    }
    return ret;
  },
  ///////////////////////////////////////////////
  // Simple array equality comparison
  // (will NOT work on arrays of objects)
  // Array order is relevant here as they are not sorted
  // change of order will equal change of array
  // as this is required for datatable sortby
  //
  isEqualArraysOfPrimitives: function(a, b) {
    if (a === b) return true;
    if (a == null || b == null) return false;
    if (a.length !== b.length) return false;

    // If you don't care about the order of the elements inside
    // the array, you should sort both arrays here.
    // Please note that calling sort on an array will modify that array.
    // you might want to clone your array first.

    for (var i = 0; i < a.length; ++i) {
      if (a[i] !== b[i]) return false;
    }
    return true;
  },
  ///////////////////////////////////////////////
  // Use geolocation api to attempt to get current location
  // try high accuracy first and downgrade if unavailable
  //https://www.openstreetmap.org/?mlat=48.3911&mlon=-124.7353#map=12/48.3910/-124.7353
  //https://www.openstreetmap.org/#map=18/49.68155/-125.00435
  //https://www.openstreetmap.org/?mlat=49.71236&mlon=-124.96961#map=17/49.71236/-124.96961
  //https://www.google.com/maps/search/?api=1&query=47.5951518,-122.3316393
  getGeoLocation: async function() {
    return new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(
        function successHigh(pos) {
          resolve({
            latitude: pos.coords.latitude,
            longitude: pos.coords.longitude
          });
        },
        function error(err) {
          //if here due to timeout getting high accuracy then try again with low accuracy
          if (error.code == error.TIMEOUT) {
            navigator.geolocation.getCurrentPosition(
              function successLow(pos) {
                resolve({
                  latitude: pos.coords.latitude,
                  longitude: pos.coords.longitude
                });
              },
              function error(err) {
                reject(
                  new Error(
                    `ERROR getting location(low_accuracy: ${err.code}): ${err.message}`
                  )
                );
              },
              { maximumAge: 600000, timeout: 10000, enableHighAccuracy: false }
            );
            return;
          }

          reject(
            new Error(
              `ERROR GETTING LOCATION(high_accuracy:${err.code}): ${err.message}`
            )
          );
        },
        { maximumAge: 600000, timeout: 5000, enableHighAccuracy: true }
      );
    });
  },
  ///////////////////////////////////////////////
  // Open map url
  //
  //
  viewGeoLocation: function(obj) {
    const hasGeo =
      obj.latitude != null &&
      obj.latitude != 0 &&
      obj.longitude != null &&
      obj.longitude != 0;

    const hasAddress =
      !this.stringIsNullOrEmpty(obj.address) &&
      !this.stringIsNullOrEmpty(obj.city) &&
      !this.stringIsNullOrEmpty(obj.region) &&
      !this.stringIsNullOrEmpty(obj.country) &&
      !this.stringIsNullOrEmpty(obj.postCode);

    if (!hasGeo && !hasAddress) {
      return;
    }

    let mapUrl = window.$gz.store.state.userOptions.mapUrlTemplate;

    //No pre-set?
    if (!mapUrl || mapUrl == "") {
      mapUrl =
        "https://www.google.com/maps/search/?api=1&query={ayaddress}<|>https://www.google.com/maps/search/?api=1&query={aylatitude},{aylongitude}";
    }

    let geoMapUrl = null;
    let addressMapUrl = null;

    //Parse the map url
    let mapUrls = [mapUrl];
    if (mapUrl.includes("<|>")) {
      mapUrls = mapUrl.split("<|>");
    }

    mapUrls.forEach(z => {
      if (!geoMapUrl && z.includes("{aylatitude}")) {
        geoMapUrl = z;
      }
      if (!addressMapUrl && z.includes("{ayaddress}")) {
        addressMapUrl = z;
      }
    });

    //decide which map to use here, favor geocode
    if (hasGeo && geoMapUrl) {
      //geo view
      mapUrl = geoMapUrl;
      mapUrl = mapUrl.split("{aylatitude}").join(obj.latitude);
      mapUrl = mapUrl.split("{aylongitude}").join(obj.longitude);
    } else if (hasAddress && addressMapUrl) {
      mapUrl = addressMapUrl;
      //compile address fields together
      //order street to country seems to be standard
      //note, if google need plus symbol delimiter, if bing, need comma delimiter
      //but both might accept one big string space delimited and url encoded so test that on all first
      const delimiter = " ";
      let q = "";
      if (obj.address) {
        q += obj.address + delimiter;
      }
      if (obj.city) {
        q += obj.city + delimiter;
      }

      if (obj.region) {
        q += obj.region + delimiter;
      }

      if (obj.country) {
        q += obj.country + delimiter;
      }

      if (obj.postCode) {
        q += obj.postCode + delimiter;
      }

      if (obj.addressPostal) {
        q += obj.addressPostal + delimiter;
      }

      if (q.length > 1) {
        q = q.substring(0, q.length - 1);
      }
      //url encode the query
      q = encodeURIComponent(q);
      mapUrl = mapUrl.split("{ayaddress}").join(q);
    } else {
      throw new Error(
        "View map: error - no matching mapurl / address / geo coordinates set for display, nothing to view"
      );
    }
    window.open(mapUrl, "map");
    //This is not valid to do as some platforms don't open a new web browser window
    //but rather a map application in which case this is null and throws up the exception even though it's working
    // if (window.open(mapUrl, "map") == null) {
    //   throw new Error(
    //     "Problem displaying map in new window.  Browser must allow pop-ups to view maps; check your browser setting"
    //   );
    // }
  },
  ///////////////////////////////////////////////
  // Online mapping service url formats
  //
  //
  mapProviderUrls: function() {
    return [
      {
        name: "Apple",
        value:
          "http://maps.apple.com/?q={ayaddress}<|>http://maps.apple.com/?ll={aylatitude},{aylongitude}"
      },
      {
        name: "Bing",
        value:
          "https://bing.com/maps/default.aspx?where1={ayaddress}<|>https://bing.com/maps/default.aspx?cp={aylatitude}~{aylongitude}&lvl=17&style=r&sp=point.{aylatitude}_{aylongitude}"
      },
      {
        name: "Google",
        value:
          "https://www.google.com/maps/search/?api=1&query={ayaddress}<|>https://www.google.com/maps/search/?api=1&query={aylatitude},{aylongitude}"
      },
      {
        name: "MapQuest",
        value:
          "https://mapquest.com/?center={ayaddress}&zoom=17<|>https://mapquest.com/?center={aylatitude},{aylongitude}&zoom=17"
      },
      {
        name: "Open Street Map",
        value:
          "https://www.openstreetmap.org/search?query={ayaddress}<|>https://www.openstreetmap.org/?mlat={aylatitude}&mlon={aylongitude}#map=17/{aylatitude}/{aylongitude}"
      },
      {
        name: "geo URI",
        value: "geo:{aylatitude},{aylongitude}"
      },
      {
        name: "Waze",
        value:
          "https://waze.com/ul?q={ayaddress}<|>https://www.waze.com/ul?ll={aylatitude},{aylongitude}&navigate=yes&zoom=17"
      },
      {
        name: "Yandex",
        value:
          "https://yandex.ru/maps/?mode=search&text={ayaddress}&z=17<|>https://yandex.ru/maps/?ll={aylatitude},{aylongitude}&z=12&l=map"
      }
    ];
  },
  ///////////////////////////////////////////////
  // v-calendar view to AyaNova scheduleview enum
  //
  //
  calendarViewToAyaNovaEnum: function(view) {
    switch (view) {
      case "day":
        return 1;
      case "week":
        return 2;
      case "month":
        return 3;
      case "4day":
        return 4;
      case "category":
        return 5;

      default:
        throw new Error(
          `gzutil->calendarViewtoAyaNovaEnum - Unknown view type '${view}'`
        );
    }
  },
  ///////////////////////////////////////////////
  // GZDaysOfWeek to VCalendar weekdays
  //
  //
  DaysOfWeekToWeekdays: function(dow) {
    /*
    AyaDaysOfWeek
  Monday = 1,
        Tuesday = 2,
        Wednesday = 4,
        Thursday = 8,
        Friday = 16,
        Saturday = 32,
        Sunday = 64

  vCalendar [
  0,//sunday
  1,
  2,
  3,
  4,
  5,
  6//saturday
]
 */
    if (dow == null || dow == 0) {
      return [0, 1, 2, 3, 4, 5, 6]; //all the days
    }
    const ret = [];

    //turn EXCLUDE selected gzDaysOfWeek into INCLUDE selected days for vCalendar
    if (!(dow & 64)) {
      ret.push(0);
    }
    if (!(dow & 1)) {
      ret.push(1);
    }
    if (!(dow & 2)) {
      ret.push(2);
    }
    if (!(dow & 4)) {
      ret.push(3);
    }
    if (!(dow & 8)) {
      ret.push(4);
    }
    if (!(dow & 16)) {
      ret.push(5);
    }
    if (!(dow & 32)) {
      ret.push(6);
    }
    return ret;
  },
  ///////////////////////////////////////////////
  // Random integer from 0 to max
  //
  //
  getRandomInt: function(max) {
    return Math.floor(Math.random() * max);
  }
  /**
   *
   *
   */

  //new functions above here
};
