// GridTracker Copyright © 2023 GridTracker.org
// All rights reserved.
// See LICENSE for more information.

const fs = require("fs");

// var CR is in screen.js
CR.developerMode = process.versions["nw-flavor"] == "sdk";
CR.callRoster = {};
CR.blockedCalls = {};
CR.blockedCQ = {};
CR.blockedDxcc = {};
CR.blockedCQz = {};
CR.blockedITUz = {};
CR.scriptReport = {};
CR.worked = {};
CR.confirmed = {};
CR.modes = {};
CR.modes_phone = {};
CR.currentUSCallsigns = null;
CR.currentUSState = "";
CR.currentDXCCs = -1;
CR.callsignManifest = null;
CR.rosterSettings = {};
CR.day = 0;
CR.dayAsString = "0";
CR.menu = null;
CR.callMenu = null;
CR.ageMenu = null;
CR.callingMenu = null;
CR.compactMenu = null;
CR.menuItemForCurrentColumn = null;
CR.currentColumnName = null;
CR.targetHash = "";
CR.clearIgnores = null;
CR.clearIgnoresCall = null;
CR.dxccMenu = null;
CR.targetDxcc = -1;
CR.clearDxccIgnore = null;
CR.clearDxccIgnoreMainMenu = null;
CR.CQMenu = null;
CR.targetCQ = "";
CR.clearCQIgnore = null;
CR.clearCQIgnoreMainMenu = null;
CR.clearCQzIgnore = null;
CR.clearCQzIgnoreMainMenu = null;
CR.clearITUzIgnore = null;
CR.clearITUzIgnoreMainMenu = null;
CR.timerInterval = null;
CR.typingInRoster = false;
CR.awards = {};
CR.awardTypes = {};
CR.awardTracker = {};
CR.callsignDatabaseDXCC = {};
CR.callsignDatabaseUS = {};
CR.callsignDatabaseUSplus = {};
CR.modeColors = {};
CR.modeColors.FT4 = "1111FF";
CR.modeColors.FT8 = "11FF11";
CR.modeColors.JT4 = "EE1111";
CR.modeColors.JT9 = "7CFC00";
CR.modeColors.JT65 = "E550E5";
CR.modeColors.QRA64 = "FF00FF";
CR.modeColors.MSK144 = "4949FF";
CR.rosterTimeout = null;
CR.rosterFocus = false;

CR.defaultSettings = {
  callsign: "all",
  hunting: "dxcc",
  huntNeed: "confirmed",
  requireGrid: false,
  wantMaxDT: false,
  wantMinDB: false,
  wantMinFreq: false,
  wantMaxFreq: false,
  wantRRCQ: false,
  maxDT: 0.5,
  minDb: -25,
  minFreq: 0,
  maxFreq: 3500,
  noMyDxcc: false,
  onlyMyDxcc: false,
  noMsg: false,
  noMsgValue: "CQ RU",
  onlyMsg: false,
  onlyMsgValue: "CQ FD",
  cqOnly: true,
  usesLoTW: false,
  maxLoTW: 27,
  useseQSL: false,
  usesOQRS: false,
  onlySpot: false,
  allOnlyNew: false,
  useRegex: false,
  noUnknownDXCC: true,
  callsignRegex: "",
  huntRegexValue: "",
  realtime: true,
  wanted: {
    huntCallsign: false,
    huntGrid: true,
    huntDXCC: true,
    huntCQz: false,
    huntITUz: false,
    huntMarathon: false,
    huntState: false,
    huntCounty: false,
    huntCont: false,
    huntPX: false,
    huntPOTA: false,
    huntQRZ: true,
    huntOAMS: false,
    huntRegex: false
  },
  columns: {
    Callsign: true,
    Band: false,
    Mode: false,
    Calling: true,
    Rig: false,
    Grid: true,
    Msg: false,
    DXCC: true,
    Flag: true,
    State: true,
    County: false,
    POTA: false,
    Cont: false,
    dB: true,
    Freq: false,
    DT: false,
    Dist: false,
    Azim: true,
    CQz: false,
    ITUz: false,
    PX: false,
    LoTW: false,
    eQSL: false,
    OQRS: false,
    Spot: false,
    Life: false,
    OAMS: true,
    Age: true,
    UTC: true
  },
  displayFilters: {
    brightness: 100,
    contrast: 100,
    saturate: 100,
    invert: 0,
    grayscale: 0,
    sepia: 0,
    huerotate: 0
  },
  reference: 0,
  controls: true,
  controlsExtended: true,
  compact: false,
  settingProfiles: false,
  sortColumn: "Age",
  sortReverse: true,
  clearRosterOnBandChange: true,
  rosterAlwaysOnTop: false,
  rosterDelayOnFocus: false,
  rosterDelayTime: 1500,
  rosterTime: 120,
  compactEntity: "DXCC"
};

CR.def_displayFilters = {
  brightness: 100,
  contrast: 100,
  saturate: 100,
  invert: 0,
  sepia: 0,
  huerotate: 0
};

const LOGBOOK_LIVE_BAND_LIVE_MODE = "0";
const LOGBOOK_LIVE_BAND_MIX_MODE = "1";
const LOGBOOK_LIVE_BAND_DIGI_MODE = "2";
const LOGBOOK_MIX_BAND_LIVE_MODE = "3";
const LOGBOOK_MIX_BAND_MIX_MODE = "4";
const LOGBOOK_MIX_BAND_DIGI_MODE = "5";
const LOGBOOK_AWARD_TRACKER = "6";

const LAYERED_MODE_FOR = {}
LAYERED_MODE_FOR[LOGBOOK_MIX_BAND_LIVE_MODE] = LOGBOOK_LIVE_BAND_LIVE_MODE;
LAYERED_MODE_FOR[LOGBOOK_MIX_BAND_MIX_MODE] = LOGBOOK_LIVE_BAND_MIX_MODE;
LAYERED_MODE_FOR[LOGBOOK_MIX_BAND_DIGI_MODE] = LOGBOOK_LIVE_BAND_DIGI_MODE;

document.addEventListener("dragover", function (event)
{
  event.preventDefault();
});

document.addEventListener("drop", function (event)
{
  event.preventDefault();
});

window.addEventListener("message", receiveMessage, false);

if (typeof localStorage.blockedCQ == "undefined")
{
  localStorage.blockedCQ = "{}";
}

if (typeof localStorage.blockedCQz == "undefined")
{
  localStorage.blockedCQz = "{}";
}

if (typeof localStorage.blockedITUz == "undefined")
{
  localStorage.blockedITUz = "{}";
}

if (typeof localStorage.awardTracker == "undefined")
{
  localStorage.awardTracker = "{}";
  CR.rosterSettings = {};
  writeRosterSettings();
}

CR.awardTracker = JSON.parse(localStorage.awardTracker);

if (typeof localStorage.blockedCalls != "undefined")
{
  CR.blockedCalls = JSON.parse(localStorage.blockedCalls);
  CR.blockedCQ = JSON.parse(localStorage.blockedCQ);
  CR.blockedDxcc = JSON.parse(localStorage.blockedDxcc);
  CR.blockedCQz = JSON.parse(localStorage.blockedCQz);
  CR.blockedITUz = JSON.parse(localStorage.blockedITUz);
}

function storeBlocks()
{
  localStorage.blockedCalls = JSON.stringify(CR.blockedCalls);
  localStorage.blockedCQ = JSON.stringify(CR.blockedCQ);
  localStorage.blockedDxcc = JSON.stringify(CR.blockedDxcc);
  localStorage.blockedCQz = JSON.stringify(CR.blockedCQz);
  localStorage.blockedITUz = JSON.stringify(CR.blockedITUz);
}

function storeAwardTracker()
{
  localStorage.awardTracker = JSON.stringify(CR.awardTracker);
}

function loadSettings()
{
  let readSettings = {};
  if (typeof localStorage.rosterSettings != "undefined")
  {
    readSettings = JSON.parse(localStorage.rosterSettings);
  }
  CR.rosterSettings = deepmerge(CR.defaultSettings, readSettings);

  fixLegacySettings();

  writeRosterSettings();
}

function fixLegacySettings()
{
  // Not sure why, but Paul Traina added this settings cleanup in August 2020.
  if ("GT" in CR.rosterSettings.columns) delete CR.rosterSettings.columns.GT;

  // In January 2022, we refactored roster column sorting
  if (CR.rosterSettings.lastSortIndex)
  {
    CR.rosterSettings.sortColumn = LEGACY_COLUMN_SORT_ID[CR.rosterSettings.lastSortIndex] || "Age";
    delete CR.rosterSettings.lastSortIndex;
  }

  // In January 2022, we refactored roster column sorting
  if (CR.rosterSettings.lastSortReverse)
  {
    CR.rosterSettings.sortReverse = CR.rosterSettings.lastSortReverse;
    delete CR.rosterSettings.lastSortReverse;
  }

  // In January 2022, we added a `columnOrder` setting, which we need to ensure always includes all columns
  CR.rosterSettings.columnOrder = validateRosterColumnOrder(CR.rosterSettings.columnOrder);
}

function writeRosterSettings()
{
  localStorage.rosterSettings = JSON.stringify(CR.rosterSettings);
}

function isKnownCallsignDXCC(dxcc)
{
  if (dxcc in CR.callsignDatabaseDXCC) return true;
  return false;
}

function isKnownCallsignUS(dxcc)
{
  if (dxcc in CR.callsignDatabaseUS) return true;
  return false;
}

function isKnownCallsignUSplus(dxcc)
{
  if (dxcc in CR.callsignDatabaseUSplus) return true;
  return false;
}

function timeNowSec()
{
  return parseInt(Date.now() / 1000);
}

function lockNewWindows()
{
  if (typeof nw != "undefined")
  {
    let gui = require("nw.gui");
    let win = gui.Window.get();
    win.on("new-win-policy", function (frame, url, policy)
    {
      gui.Shell.openExternal(url);
      policy.ignore();
    });
  }
}

function hashMaker(start, callObj, reference)
{
  if (reference == LOGBOOK_LIVE_BAND_LIVE_MODE) return `${start}${callObj.band}${callObj.mode}`;

  if (reference == LOGBOOK_AWARD_TRACKER) return `${start}${callObj.band}${callObj.mode}`;

  if (reference == LOGBOOK_LIVE_BAND_MIX_MODE) return `${start}${callObj.band}`;

  if (reference == LOGBOOK_LIVE_BAND_DIGI_MODE) return `${start}${callObj.band}dg`;

  if (reference == LOGBOOK_MIX_BAND_LIVE_MODE) return `${start}${callObj.mode}`;

  if (reference == LOGBOOK_MIX_BAND_MIX_MODE) return `${start}`;

  if (reference == LOGBOOK_MIX_BAND_DIGI_MODE) return `${start}dg`;

  return "";
}

function rosterInFocus()
{
  if (CR.rosterSettings.rosterDelayOnFocus)
  {
    CR.rosterFocus = true;
  }
}

function rosterNoFocus()
{
  CR.rosterFocus = false;
  if (CR.rosterTimeout != null)
  {
    nodeTimers.clearTimeout(CR.rosterTimeout);
    CR.rosterTimeout = null;
    viewRoster();
  }
}

function processRoster(roster)
{
  CR.callRoster = roster;
  if (CR.rosterTimeout != null)
  {
    nodeTimers.clearTimeout(CR.rosterTimeout);
    CR.rosterTimeout = null;
  }

  if (CR.rosterFocus)
  {
    CR.rosterTimeout = nodeTimers.setTimeout(viewRoster, CR.rosterSettings.rosterDelayTime);
    rosterDelayDiv.style.display = "inline-block";
  }
  else
  {
    viewRoster();
  }
}

function viewRoster()
{
  CR.rosterTimeout = null;
  rosterDelayDiv.style.display = "none";
  let rosterSettings = prepareRosterSettings();
  processRosterFiltering(CR.callRoster, rosterSettings);
  processRosterHunting(CR.callRoster, rosterSettings, CR.awardTracker);
  renderRoster(CR.callRoster, rosterSettings);
  sendAlerts(CR.callRoster, rosterSettings);
}

function realtimeRoster()
{
  let now = timeNowSec();
  CR.day = parseInt(now / 86400);
  CR.dayAsString = String(CR.day);

  if (CR.rosterSettings.realtime == false) return;

  let timeCols = document.getElementsByClassName("timeCol");
  for (const x in timeCols)
  {
    if ((typeof timeCols[x].id != "undefined") && (typeof CR.callRoster[timeCols[x].id.substr(2)] != "undefined"))
    {
      let when = now - CR.callRoster[timeCols[x].id.substr(2)].callObj.age;
      timeCols[x].innerHTML = toDHMS(when);
    }
  }
  let lifeCols = document.getElementsByClassName("lifeCol");
  for (const x in lifeCols)
  {
    if ((typeof lifeCols[x].id != "undefined") && (typeof CR.callRoster[lifeCols[x].id.substr(2)] != "undefined"))
    {
      let when = now - CR.callRoster[lifeCols[x].id.substr(2)].callObj.life;
      lifeCols[x].innerHTML = toDHMS(when);
    }
  }

  if (CR.rosterSettings.columns.Spot)
  {
    let spotCols = document.getElementsByClassName("spotCol");
    for (const x in spotCols)
    {
      if ((typeof spotCols[x].id != "undefined") && (typeof CR.callRoster[spotCols[x].id.substr(2)] != "undefined"))
      {
        spotCols[x].innerHTML = getSpotString(CR.callRoster[spotCols[x].id.substr(2)].callObj);
      }
    }
  }
}

function getSpotString(callObj)
{
  let result = "&nbsp;";
  if (callObj.spot && callObj.spot.when > 0)
  {
    when = timeNowSec() - callObj.spot.when;
    if (when <= window.opener.GT.receptionSettings.viewHistoryTimeSec)
    { result = toDHM(parseInt(when)); }
  }
  if (result != "&nbsp;") result += " / " + callObj.spot.snr;
  return result;
}

function openChatToCid(cid)
{
  window.opener.showMessaging(true, cid);
}

function initiateQso(thisHash)
{
  window.opener.initiateQso(thisHash);
}

function callLookup(thisHash, grid)
{
  window.opener.startLookup(
    CR.callRoster[thisHash].DEcall,
    CR.callRoster[thisHash].grid
  );
}

function callingLookup(thisHash, grid)
{
  let thisCall = CR.callRoster[thisHash].DXcall;
  window.opener.startLookup(thisCall, grid);
}

function callGenMessage(thisHash, grid)
{
  let thisCall = CR.callRoster[thisHash].DEcall;
  let instance = CR.callRoster[thisHash].callObj.instance;

  window.opener.startGenMessages(thisCall, grid, instance);
}

function callingGenMessage(thisHash, grid)
{
  let thisCall = CR.callRoster[thisHash].DXcall;
  let instance = CR.callRoster[thisHash].callObj.instance;

  window.opener.startGenMessages(thisCall, grid, instance);
}

function centerOn(grid)
{
  window.opener.centerOn(grid);
}

function instanceChange(what)
{
  window.opener.GT.instances[what.id].crEnable = what.checked;
  viewRoster();
}

function updateInstances()
{
  if (window.opener.GT.instancesIndex.length > 1)
  {
    let instances = window.opener.GT.instances;

    let worker = "";

    let keys = Object.keys(instances).sort();
    for (const key in keys)
    {
      let inst = keys[key];
      let sp = inst.split(" - ");
      let shortInst = sp[sp.length - 1].substring(0, 18);
      let color = "blue";

      if (instances[inst].open == false)
      {
        color = "purple";
      }
      worker +=
        `<div class='button' style='background-color:${color};'>` +
        `<input type='checkbox' id='${inst}' onchange='instanceChange(this);' ` +
        (instances[inst].crEnable ? "checked" : "") +
        `>&nbsp;${shortInst}</div>`
    }
    instancesDiv.innerHTML = worker;
    instancesWrapper.style.display = "";
  }
  else
  {
    instancesDiv.innerHTML = "";
    instancesWrapper.style.display = "none";
  }
}

function processStatus(newMessage)
{
  if (newMessage.Transmitting == 0)
  {
    // Not Transmitting
    if (newMessage.Decoding == 1)
    {
      // Decoding
      txrxdec.style.backgroundColor = "Blue";
      txrxdec.style.borderColor = "Cyan";
      txrxdec.innerHTML = "DECODE";
    }
    else
    {
      txrxdec.style.backgroundColor = "Green";
      txrxdec.style.borderColor = "GreenYellow";
      txrxdec.innerHTML = "RECEIVE";
    }
  }
  else
  {
    txrxdec.style.backgroundColor = "Red";
    txrxdec.style.borderColor = "Orange";
    txrxdec.innerHTML = "TRANSMIT";
  }
}

function toTitleCase(str)
{
  return str.replace(/\w\S*/g, function (txt)
  {
    return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
  });
}

function newOption(value, text)
{
  if (typeof text == "undefined") text = value;
  let option = document.createElement("option");
  option.value = value;
  option.text = text;
  return option;
}

function createSelectOptions(
  selectElementString,
  selectNameDefault,
  forObject,
  altName = null,
  defaultValue = null,
  checkSponsor = null
)
{
  let selector = document.getElementById(selectElementString);
  selector.innerHTML = "";

  let option = document.createElement("option");
  option.value = defaultValue;
  option.text = selectNameDefault;
  option.selected = true;
  option.disabled = true;
  option.style.display = "none";
  selector.appendChild(option);

  let obj = null;
  if (forObject)
  {
    obj = Object.keys(forObject).sort();
  }
  for (const k in obj)
  {
    let opt = obj[k];
    let option = document.createElement("option");
    option.value = opt;
    option.text = altName ? forObject[opt][altName] : opt;
    if (checkSponsor && opt + "-" + checkSponsor in CR.awardTracker)
    { option.disabled = true; }

    selector.appendChild(option);
  }
}

function awardSponsorChanged()
{
  awardName.style.display = "";
  createSelectOptions(
    "awardName",
    "Select Award",
    CR.awards[awardSponsor.value].awards,
    "name",
    null,
    awardSponsor.value
  );
}

function awardNameChanged()
{
  let awardToAdd = newAwardTrackerObject(
    awardSponsor.value,
    awardName.value,
    true
  );

  let hash = awardToAdd.name + "-" + awardToAdd.sponsor;
  if (!(hash in CR.awardTracker))
  {
    CR.awardTracker[hash] = awardToAdd;
    storeAwardTracker();
    processAward(hash);
    updateAwardList(hash);
    viewRoster();
  }
  createSelectOptions(
    "awardName",
    "Select Award",
    CR.awards[awardToAdd.sponsor].awards,
    "name",
    null,
    awardToAdd.sponsor
  );
}

function updateAwardList(target = null)
{
  let worker =
    "<table id='awardTable' class='awardTableCSS' >";
  worker += "<tr>";
  worker += "<th align='left'>Name</th><th>Award</th><th>Track</th><th></th>";
  worker += "</tr>";

  worker += "</table>";

  AwardWantedList.innerHTML = worker;

  let keys = Object.keys(CR.awardTracker).sort();

  for (const key in keys)
  {
    let award = CR.awardTracker[keys[key]];
    let rule = CR.awards[award.sponsor].awards[award.name].rule;
    let row = awardTable.insertRow();
    row.id = keys[key];
    let baseAward = false;
    let baseCount = 0;

    let endorseCount = 0;
    let endorseTotal = 0;
    let allEndorse = false;

    let tooltip = CR.awards[award.sponsor].awards[award.name].tooltip + " (" + CR.awards[award.sponsor].sponsor + ")\n";
    tooltip += toTitleCase(award.test.qsl_req) + " QSO\n";
    for (const mode in award.comp.counts)
    {
      tooltip += mode + "\n";
      for (const count in award.comp.counts[mode])
      {
        endorseTotal++;
        if (award.comp.counts[mode][count].per == 100)
        {
          baseAward = true;
          endorseCount++;
        }
        if (award.comp.counts[mode][count].num > baseCount)
        { baseCount = award.comp.counts[mode][count].num; }

        tooltip += "\t" + award.comp.counts[mode][count].num + "/" + count + " (" + award.comp.counts[mode][count].per + "%)\n";
        let wrk = "";
        if (Object.keys(award.comp.endorse).length > 0)
        {
          for (const band in award.comp.endorse[mode])
          {
            endorseTotal++;
            if (award.comp.endorse[mode][band][count] == true)
            {
              endorseCount++;
              wrk += band + " ";
            }
          }
        }
        if (wrk.length > 0)
        {
          tooltip += "\t\t" + wrk + "\n";
        }
      }
    }
    if (baseCount > 0 && endorseCount == endorseTotal) allEndorse = true;

    let cell = createCellHtml(row, award.name + " - " + award.sponsor);
    cell.style.textAlign = "left";
    cell.style.color = "lightblue";

    createCellHtml(
      row, (allEndorse
        ? "<img src='./img/award-trophy.png' height='14px'>"
        : baseAward
          ? "<img src='./img/award-medal.png' height='13px'>"
          : baseCount > 0
            ? "<img src='./img/award-tally.png' height='15px'>"
            : "<img src='./img/award-empty.png' height='12px'>"),
      tooltip
    );
    createCell(row, "enable", award.enable, award.enable, "Toggle Tracking", true);
    createCellHtml(row, "<img title='Remove Tracker' onclick='deleteAwardTracker(this)' style='margin:0;padding:0;margin-top:2px;cursor:pointer;' src='./img/award-delete.png' height='12px'>");
  }
}

function deleteAwardTracker(sender)
{
  let id = sender.parentNode.parentNode.id;
  delete CR.awardTracker[id];
  storeAwardTracker();
  resetAwardAdd();
  updateAwardList();
  viewRoster();
}

function awardCheckboxChanged(sender)
{
  let awardId = sender.target.parentNode.parentNode.id;
  CR.awardTracker[sender.target.parentNode.parentNode.id][sender.target.name] =
    sender.target.checked;
  storeAwardTracker();
  viewRoster();
}

function awardValueChanged(sender)
{
  let awardId = sender.target.parentNode.parentNode.id;
  CR.awardTracker[sender.target.parentNode.parentNode.id][sender.target.name] =
    sender.target.value;
  storeAwardTracker();
  viewRoster();
}

function createCell(
  row,
  target,
  value,
  data = null,
  title = null,
  checkbox = false
)
{
  let cell = row.insertCell();
  if (data == null) cell.innerHTML = value;
  if (title) cell.title = title;
  if (checkbox)
  {
    let x = document.createElement("INPUT");
    x.setAttribute("type", "checkbox");
    x.checked = value;
    x.name = target;
    x.addEventListener("change", awardCheckboxChanged);
    cell.appendChild(x);
  }
  else if (data)
  {
    cell.appendChild(createAwardSelector(cell, target, value, data));
  }
  return cell;
}

function createCellHtml(row, html, title = null)
{
  let cell = row.insertCell();
  cell.innerHTML = html;
  if (title) cell.title = title;

  return cell;
}

function createAwardSelector(cell, target, value, forObject)
{
  let selector = document.createElement("select");
  selector.name = target;
  selector.value = value;
  selector.disabled = forObject.length == 1;
  selector.style.margin = "0px";
  selector.style.padding = "1px";
  if (selector.disabled) selector.style.cursor = "auto";
  selector.addEventListener("change", awardValueChanged);
  for (const opt in forObject)
  {
    let option = document.createElement("option");
    option.value = forObject[opt];
    if (option.value == "Phone" || option.value == "CW") option.disabled = true;
    option.text = forObject[opt];
    selector.appendChild(option);
  }
  return selector;
}

function resetAwardAdd()
{
  awardName.style.display = "none";
  createSelectOptions("awardName", "Select Award", null);
  createSelectOptions("awardSponsor", "Select Sponsor", CR.awards, "sponsor");
}

function openAwardPopup()
{
  awardHunterDiv.style.display = "";
  resetAwardAdd();
}

function closeAwardPopup()
{
  awardHunterDiv.style.display = "none";
  resetAwardAdd();
}

function toggleMoreControls()
{
  CR.rosterSettings.controlsExtended = !CR.rosterSettings.controlsExtended;
  localStorage.rosterSettings = JSON.stringify(CR.rosterSettings);

  setVisual();
}

function setVisual()
{
  huntNeed.style.display = "none";
  stateSelect.style.display = "none";
  DXCCsSelect.style.display = "none";

  if (CR.rosterSettings.controls)
  {
    if (CR.rosterSettings.controlsExtended)
    {
      RosterControls.className = "extended";
      instancesWrapper.style.display = "";
    }
    else
    {
      RosterControls.className = "normal";
      instancesWrapper.style.display = "none";
    }
  }
  else
  {
    RosterControls.className = "hidden";
    instancesWrapper.style.display = "none";
  }

  // Award Hunter
  if (referenceNeed.value == LOGBOOK_AWARD_TRACKER)
  {
    HuntModeControls.style.display = "none";
    CallsignsControls.style.display = "none";
    AwardTrackerControls.style.display = "";
    updateAwardList();
  }
  else
  {
    for (const key in CR.rosterSettings.wanted)
    {
      if (document.getElementById(key))
      {
        document.getElementById(key).checked = CR.rosterSettings.wanted[key];
      }
    }

    AwardTrackerControls.style.display = "none";
    HuntModeControls.style.display = "";
    huntMode.style.display = "";
    CallsignsControls.style.display = "";
    closeAwardPopup();
    if (callsignNeed.value == "all" || callsignNeed.value == "hits")
    {
      huntingMatrixDiv.style.display = "";
      huntNeed.style.display = "";
      huntMode.style.display = "none";
    }
    else
    {
      huntingMatrixDiv.style.display = "none";
      huntMode.style.display = "";

      if (huntMode.value != "callsign" && huntMode.value != "usstate" && huntMode.value != "dxccs")
      {
        huntNeed.style.display = "";
      }
      if (huntMode.value == "usstate")
      {
        stateSelect.style.display = "";
      }
      if (huntMode.value == "usstates")
      {
        huntNeed.style.display = "";
      }
      if (huntMode.value == "dxccs")
      {
        DXCCsSelect.style.display = "";
      }
    }
  }

  if (huntRegex.checked == true)
  {
    huntRegexValue.style.display = "";
  }
  else
  {
    huntRegexValue.style.display = "none";
  }

  if (wantMaxDT.checked == true)
  {
    maxDT.style.display = "";
    maxDTView.style.display = "";
  }
  else
  {
    maxDT.style.display = "none";
    maxDTView.style.display = "none";
  }

  if (wantMinDB.checked == true)
  {
    minDb.style.display = "";
    minDbView.style.display = "";
  }
  else
  {
    minDb.style.display = "none";
    minDbView.style.display = "none";
  }

  if (wantMinFreq.checked == true)
  {
    minFreq.style.display = "";
    minFreqView.style.display = "";
  }
  else
  {
    minFreq.style.display = "none";
    minFreqView.style.display = "none";
  }

  if (wantMaxFreq.checked == true)
  {
    maxFreq.style.display = "";
    maxFreqView.style.display = "";
  }
  else
  {
    maxFreq.style.display = "none";
    maxFreqView.style.display = "none";
  }

  if (useRegex.checked == true)
  {
    callsignRegex.style.display = "";
  }
  else
  {
    callsignRegex.style.display = "none";
  }

  if (window.opener.GT.callsignLookups.lotwUseEnable == true)
  {
    usesLoTWDiv.style.display = "";
    if (CR.rosterSettings.usesLoTW == true)
    {
      maxLoTW.style.display = "";
      maxLoTWView.style.display = "";
    }
    else
    {
      maxLoTW.style.display = "none";
      maxLoTWView.style.display = "none";
    }
  }
  else
  {
    usesLoTWDiv.style.display = "none";
    maxLoTW.style.display = "none";
    maxLoTWView.style.display = "none";
  }

  if (window.opener.GT.callsignLookups.eqslUseEnable == true)
  {
    useseQSLDiv.style.display = "";
  }
  else
  {
    useseQSLDiv.style.display = "none";
  }

  if (window.opener.GT.callsignLookups.oqrsUseEnable == true)
  {
    usesOQRSDiv.style.display = "";
  }
  else
  {
    usesOQRSDiv.style.display = "none";
  }

  if (CR.rosterSettings.columns.Spot == true)
  {
    onlySpotDiv.style.display = "";
  }
  else
  {
    onlySpotDiv.style.display = "none";
  }

  if (CR.rosterSettings.callsign == "all" || CR.rosterSettings.callsign == "hits")
  {
    allOnlyNewDiv.style.display = "";
  }
  else
  {
    allOnlyNewDiv.style.display = "none";
  }

  rosterBody.style.display = "block";

  resize();
}

function wantedChanged(element)
{
  CR.rosterSettings.wanted[element.id] = element.checked;

  if (element.checked == true)
  {
    let t = element.id.replace("hunt", "");

    if (t in CR.rosterSettings.columns)
    {
      CR.rosterSettings.columns[t] = true;

      for (const i in CR.menu.items)
      {
        if (
          typeof CR.menu.items[i].checked != "undefined" &&
          CR.menu.items[i].label == t
        )
        { CR.menu.items[i].checked = true; }
      }
    }
  }

  setVisual();

  writeRosterSettings();

  CR.scriptReport = Object();
  for (const callHash in window.opener.GT.callRoster)
  {
    window.opener.GT.callRoster[callHash].callObj.alerted = false;
  }
  viewRoster();
}

function valuesChanged()
{
  CR.rosterSettings.callsign = callsignNeed.value;
  CR.rosterSettings.hunting = huntMode.value;
  CR.rosterSettings.huntNeed = huntNeed.value;
  CR.rosterSettings.requireGrid = wantGrid.checked;

  CR.rosterSettings.wantMaxDT = wantMaxDT.checked;
  CR.rosterSettings.wantMinDB = wantMinDB.checked;
  CR.rosterSettings.wantMinFreq = wantMinFreq.checked;
  CR.rosterSettings.wantMaxFreq = wantMaxFreq.checked;
  CR.rosterSettings.wantRRCQ = wantRRCQ.checked;

  maxDTView.innerHTML = CR.rosterSettings.maxDT = maxDT.value;
  minDbView.innerHTML = CR.rosterSettings.minDb = minDb.value;
  minFreqView.innerHTML = CR.rosterSettings.minFreq = minFreq.value;
  maxFreqView.innerHTML = CR.rosterSettings.maxFreq = maxFreq.value;
  CR.rosterSettings.maxLoTW = maxLoTW.value;
  maxLoTWView.innerHTML = CR.rosterSettings.maxLoTW < 27 ? toYM(Number(CR.rosterSettings.maxLoTW)) : "<b>&infin;</b>";
  CR.rosterSettings.maxLoTW = maxLoTW.value;
  CR.rosterSettings.cqOnly = cqOnly.checked;
  CR.rosterSettings.noMyDxcc = noMyDxcc.checked;
  CR.rosterSettings.onlyMyDxcc = onlyMyDxcc.checked;
  if (noMsg.checked && onlyMsg.checked && noMsgValue.value == onlyMsgValue.value)
  {
    if (CR.rosterSettings.noMsg)
    {
      noMsg.checked = false;
    }
    else
    {
      onlyMsg.checked = false;
    }
  }
  CR.rosterSettings.noMsg = noMsg.checked;
  CR.rosterSettings.onlyMsg = onlyMsg.checked;
  CR.rosterSettings.noMsgValue = noMsgValue.value;
  CR.rosterSettings.onlyMsgValue = onlyMsgValue.value;
  CR.rosterSettings.usesLoTW = usesLoTW.checked;
  CR.rosterSettings.useseQSL = useseQSL.checked;
  CR.rosterSettings.usesOQRS = usesOQRS.checked;
  CR.rosterSettings.onlySpot = onlySpot.checked;
  CR.rosterSettings.reference = referenceNeed.value;
  CR.rosterSettings.allOnlyNew = allOnlyNew.checked;
  CR.rosterSettings.useRegex = useRegex.checked;
  CR.rosterSettings.callsignRegex = callsignRegex.value;
  CR.rosterSettings.huntRegexValue = huntRegexValue.value;
  CR.rosterSettings.noUnknownDXCC = noUnknownDXCC.checked;

  writeRosterSettings();

  CR.scriptReport = Object();
  for (const callHash in window.opener.GT.callRoster)
  {
    window.opener.GT.callRoster[callHash].callObj.alerted = false;
  }

  setVisual();
}

function loadFilterSettings()
{
  let filters = "";
  for (const filter in CR.rosterSettings.displayFilters)
  {
    let slider = document.getElementById("filter" + filter + "Slider");

    if (slider)
    {
      slider.value = CR.rosterSettings.displayFilters[filter];
      let td = document.getElementById("filter" + filter + "Td");
      if (filter != "huerotate")
      {
        td.innerHTML = slider.value + "%";
        filters += filter + "(" + slider.value + "%) ";
      }
      else
      {
        td.innerHTML = slider.value + " deg";
        filters += "hue-rotate(" + slider.value + "deg) ";
      }
    }
  }
  document.documentElement.style.filter = filters;
}

function filtersChanged()
{
  for (const filter in CR.rosterSettings.displayFilters)
  {
    let slider = document.getElementById("filter" + filter + "Slider");

    if (slider)
    {
      CR.rosterSettings.displayFilters[filter] = slider.value;
    }
    else
    {
      // no longer a filter, get rid of it
      delete CR.rosterSettings.displayFilters[filter];
    }
  }
  loadFilterSettings();
}

function resetFilters()
{
  for (const filter in CR.rosterSettings.displayFilters)
  {
    CR.rosterSettings.displayFilters[filter] = CR.def_displayFilters[filter];
  }
  loadFilterSettings();
}

function getBuffer(file_url, callback, flag, mode, port, cookie)
{
  let url = require("url");
  let http = require(mode);
  let fileBuffer = null;
  let options = null;
  if (cookie != null)
  {
    options = {
      host: url.parse(file_url).host, // eslint-disable-line node/no-deprecated-api
      port: port,
      path: url.parse(file_url).path, // eslint-disable-line node/no-deprecated-api
      headers: {
        Cookie: cookie
      }
    };
  }
  else
  {
    options = {
      host: url.parse(file_url).host, // eslint-disable-line node/no-deprecated-api
      port: port,
      path: url.parse(file_url).path // eslint-disable-line node/no-deprecated-api
    };
  }
  http.get(options, function (res)
  {
    // let fsize = res.headers["content-length"];
    let cookies = null;
    if (typeof res.headers["set-cookie"] != "undefined")
    { cookies = res.headers["set-cookie"]; }
    res
      .on("data", function (data)
      {
        if (fileBuffer == null) fileBuffer = data;
        else fileBuffer += data;
      })
      .on("end", function ()
      {
        if (typeof callback == "function")
        {
          // Call it, since we have confirmed it is callable
          callback(fileBuffer, flag, cookies);
        }
      })
      .on("error", function () {});
  });
}

function callsignResult(buffer, flag)
{
  let rawData = JSON.parse(buffer);
  CR.currentUSState = flag;

  CR.currentUSCallsigns = Object();
  for (const key in rawData.c) CR.currentUSCallsigns[rawData.c[key]] = true;

  viewRoster();
}

function stateChangedValue(what)
{
  if (CR.currentUSState != stateSelect.value && stateSelect.value != "")
  {
    CR.currentUSState = stateSelect.value;

    if (window.opener.GT.mapSettings.offlineMode == false)
    {
      let callState = CR.currentUSState.replace("CN-", "");
      getBuffer(
        "https://storage.googleapis.com/gt_app/callsigns/" + callState + ".callsigns.json",
        callsignResult,
        CR.currentUSState,
        "http",
        80
      );
    }
    else
    {
      viewRoster();
      CR.currentUSState = "";
      CR.currentUSCallsigns = null;
      stateSelect.value = "";

      return;
    }
  }

  if (stateSelect.value == "")
  {
    CR.currentUSState = "";
    CR.currentUSCallsigns = null;

    viewRoster();
  }
}

function DXCCsChangedValue(what)
{
  CR.currentDXCCs = DXCCsSelect.value;
  viewRoster();
}

function initDXCCSelector()
{
  let items = Object.keys(window.opener.GT.dxccToAltName).sort(function (a, b)
  {
    return window.opener.GT.dxccToAltName[a].localeCompare(
      window.opener.GT.dxccToAltName[b]
    );
  });
  let newSelect = document.getElementById("DXCCsSelect");

  for (const i in items)
  {
    let key = items[i];

    if (
      window.opener.GT.dxccInfo[key].geo !=
      "deleted"
    )
    {
      let option = document.createElement("option");
      option.value = key;
      option.text =
        window.opener.GT.dxccToAltName[key] +
        " (" +
        window.opener.GT.dxccInfo[key].pp +
        ")";

      newSelect.appendChild(option);
    }
  }
  newSelect.oninput = DXCCsChangedValue;
}

function manifestResult(buffer, flag)
{
  CR.callsignManifest = JSON.parse(buffer);
  let newSelect = document.getElementById("stateSelect");

  for (const key in CR.callsignManifest.cnt)
  {
    let option = document.createElement("option");
    if (window.opener.GT.enums[key])
    {
      option.value = key;
      option.text = window.opener.GT.enums[key];
    }
    else
    {
      option.value = "CN-" + key;
      option.text = window.opener.GT.enums["CN-" + key];
    }
    newSelect.appendChild(option);
  }
  newSelect.oninput = stateChangedValue;
}

function receiveMessage(event) {}

CR.tracker = {};

function updateWorked()
{
  CR.worked = window.opener.GT.tracker.worked;
  CR.confirmed = window.opener.GT.tracker.confirmed;
  CR.modes = window.opener.GT.modes;
  CR.modes_phone = window.opener.GT.modes_phone;
  CR.tracker = window.opener.GT.tracker;

  processAllAwardTrackers();
}

function deleteCallsignIgnore(key)
{
  delete CR.blockedCalls[key];
  storeBlocks();
  openIgnoreEdit();
  viewRoster();
}

function deleteDxccIgnore(key)
{
  delete CR.blockedDxcc[key];
  storeBlocks();
  openIgnoreEdit();
  viewRoster();
}

function deleteCQIgnore(key)
{
  delete CR.blockedCQ[key];
  storeBlocks();
  openIgnoreEdit();
  viewRoster();
}

function deleteCQzIgnore(key)
{
  delete CR.blockedCQz[key];
  storeBlocks();
  openIgnoreEdit();
  viewRoster();
}

function deleteITUzIgnore(key)
{
  delete CR.blockedITUz[key];
  storeBlocks();
  openIgnoreEdit();
  viewRoster();
}

function clearAllCallsignIgnores()
{
  CR.blockedCalls = Object();
  storeBlocks();
  openIgnoreEdit();
  viewRoster();
}

function clearAllDxccIgnores()
{
  CR.blockedDxcc = Object();
  storeBlocks();
  openIgnoreEdit();
  viewRoster();
}

function clearAllCQIgnores()
{
  CR.blockedCQ = Object();
  storeBlocks();
  openIgnoreEdit();
  viewRoster();
}

function clearAllCQzIgnores()
{
  CR.blockedCQz = Object();
  storeBlocks();
  openIgnoreEdit();
  viewRoster();
}

function clearAllITUzIgnores()
{
  CR.blockedITUz = Object();
  storeBlocks();
  openIgnoreEdit();
  viewRoster();
}

function openSettings()
{
  MainCallRoster.style.display = "block";
  settingsDiv.style.display = "block";
  editView.style.display = "none";
}

function closeSettings()
{
  MainCallRoster.style.display = "block";
  settingsDiv.style.display = "none";
  editView.style.display = "none";
}

function closeEditIgnores()
{
  MainCallRoster.style.display = "block";
  editView.style.display = "none";
}

function openIgnoreEdit()
{
  MainCallRoster.style.display = "none";
  editView.style.display = "inline-block";
  let worker = "";
  let clearString = "<th>none</th>";

  if (Object.keys(CR.blockedCalls).length > 0)
  {
    clearString =
      "<th style='cursor:pointer;' onclick='clearAllCallsignIgnores()'>Clear All</th>";
  }
  worker +=
    "<div  style='margin:10px;padding:0px;vertical-align:top;display:inline-block;margin-right:2px;overflow:auto;overflow-x:hidden;height:" +
    (window.innerHeight - 135) +
    "px;'><table class='darkTable' align=center><tr><th align=left>Callsigns</th>" +
    clearString +
    "</tr>";
  Object.keys(CR.blockedCalls)
    .sort()
    .forEach(function (key, i)
    {
      worker +=
        "<tr><td align=left style='color:#FFFF00;' >" +
        key +
        "</td><td style='cursor:pointer;' onclick='deleteCallsignIgnore(\"" +
        key +
        "\")'><img src='/img/trash_24x48.png' style='height:17px;margin:-1px;margin-bottom:-3px;padding:0px'></td></tr>";
    });
  worker += "</table></div>";

  clearString = "<th>none</th>";
  if (Object.keys(CR.blockedCQ).length > 0)
  {
    clearString =
      "<th style='cursor:pointer;' onclick='clearAllCQIgnores()'>Clear All</th>";
  }
  worker +=
    "<div  style='margin:10px;padding:0px;vertical-align:top;display:inline-block;margin-right:2px;overflow:auto;overflow-x:hidden;height:" +
    (window.innerHeight - 135) +
    "px;'><table class='darkTable' align=center><tr><th align=left>CQ</th>" +
    clearString +
    "</tr>";
  Object.keys(CR.blockedCQ)
    .sort()
    .forEach(function (key, i)
    {
      worker +=
        "<tr><td align=left style='color:cyan;' >" +
        key +
        "</td><td style='cursor:pointer;' onclick='deleteCQIgnore(\"" +
        key +
        "\")'><img src='/img/trash_24x48.png' style='height:17px;margin:-1px;margin-bottom:-3px;padding:0px'></td></tr>";
    });
  worker += "</table></div>";

  clearString = "<th>none</th>";
  if (Object.keys(CR.blockedDxcc).length > 0)
  {
    clearString =
      "<th style='cursor:pointer;' onclick='clearAllDxccIgnores()'>Clear All</th>";
  }
  worker +=
    "<div  style='margin:10px;vertical-align:top;display:inline-block;overflow:auto;overflow-x:hidden;height:" +
    (window.innerHeight - 135) +
    "px;'><table class='darkTable' align=center><tr><th align=left>DXCCs</th>" +
    clearString +
    "</tr>";
  Object.keys(CR.blockedDxcc)
    .sort()
    .forEach(function (key, i)
    {
      worker +=
        "<tr><td align=left style='color:#FFA500' >" +
        window.opener.GT.dxccToAltName[key] +
        " (" +
        window.opener.GT.dxccInfo[key].pp +
        ")</td><td style='cursor:pointer;' onclick='deleteDxccIgnore(\"" +
        key +
        "\")'><img src='/img/trash_24x48.png' style='height:17px;margin:-1px;margin-bottom:-3px;padding:0px'></td></tr>";
    });
  worker += "</table></div>";

  if (Object.keys(CR.blockedCQz).length > 0)
  {
    clearString =
      "<th style='cursor:pointer;' onclick='clearAllCQzIgnores()'>Clear All</th>";
  }
  worker +=
    "<div  style='margin:10px;padding:0px;vertical-align:top;display:inline-block;margin-right:2px;overflow:auto;overflow-x:hidden;height:" +
    (window.innerHeight - 135) +
    "px;'><table class='darkTable' align=center><tr><th align=left>CQ Zones</th>" +
    clearString +
    "</tr>";
  Object.keys(CR.blockedCQz)
    .sort()
    .forEach(function (key, i)
    {
      worker +=
        "<tr><td align=left style='color:cyan;' >" +
        key +
        "</td><td style='cursor:pointer;' onclick='deleteCQzIgnore(\"" +
        key +
        "\")'><img src='/img/trash_24x48.png' style='height:17px;margin:-1px;margin-bottom:-3px;padding:0px'></td></tr>";
    });
  worker += "</table></div>";

  if (Object.keys(CR.blockedITUz).length > 0)
  {
    clearString =
      "<th style='cursor:pointer;' onclick='clearAllITUzIgnores()'>Clear All</th>";
  }
  worker +=
    "<div  style='margin:10px;padding:0px;vertical-align:top;display:inline-block;margin-right:2px;overflow:auto;overflow-x:hidden;height:" +
    (window.innerHeight - 135) +
    "px;'><table class='darkTable' align=center><tr><th align=left>ITU Zones</th>" +
    clearString +
    "</tr>";
  Object.keys(CR.blockedITUz)
    .sort()
    .forEach(function (key, i)
    {
      worker +=
        "<tr><td align=left style='color:cyan;' >" +
        key +
        "</td><td style='cursor:pointer;' onclick='deleteITUzIgnore(\"" +
        key +
        "\")'><img src='/img/trash_24x48.png' style='height:17px;margin:-1px;margin-bottom:-3px;padding:0px'></td></tr>";
    });
  worker += "</table></div>";

  editTables.innerHTML = worker;
  editView.style.height = (window.innerHeight - 45) + "px";
}

function onMyKeyDown(event)
{
  if (!CR.typingInRoster)
  {
    if (event.code == "KeyS" && event.ctrlKey == true)
    {
      openSettings();
    }
    else if (event.code == "KeyR" && event.ctrlKey == true)
    {
      resetFilters();
    }
    else
    {
      window.opener.onMyKeyDown(event);
    }
  }
}

function blurOnEnter(ele)
{
  if (event.key == "Enter")
  {
    ele.blur();
  }
}

function resize()
{
  if (editView.style.display == "inline-block") openIgnoreEdit();

  window.opener.goProcessRoster();
}

function init()
{
  window.opener.GT.rosterInitialized = true;

  CR.callsignDatabaseDXCC = window.opener.GT.callsignDatabaseDXCC;
  CR.callsignDatabaseUS = window.opener.GT.callsignDatabaseUS;
  CR.callsignDatabaseUSplus = window.opener.GT.callsignDatabaseUSplus;
  loadAwardJson();

  updateWorked();
  // addAllAwards();

  window.addEventListener("message", receiveMessage, false);
  lockNewWindows();

  if (window.opener.GT.mapSettings.offlineMode == false)
  {
    getBuffer(
      "https://storage.googleapis.com/gt_app/callsigns/manifest.json",
      manifestResult,
      null,
      "http",
      80
    );
  }
  loadSettings();
  loadFilterSettings();
  updateInstances();

  // callback to addControls();
  loadRosterI18n();

  setRosterTop();
}

// From i18n.js
function addControls()
{
  WANTED_LABELS = {
    cont: $.i18n("rosterColumns.Wanted.cont"),
    cqz: $.i18n("rosterColumns.Wanted.cqz"),
    ituz: $.i18n("rosterColumns.Wanted.ituz"),
    dxcc: $.i18n("rosterColumns.Wanted.dxcc"),
    dxccMarathon: $.i18n("rosterColumns.Wanted.dxccMarathon"),
    cqzMarathon: $.i18n("rosterColumns.Wanted.cqzMarathon"),
    state: $.i18n("rosterColumns.Wanted.state"),
    grid: $.i18n("rosterColumns.Wanted.grid"),
    cnty: $.i18n("rosterColumns.Wanted.cnty"),
    wpx: $.i18n("rosterColumns.Wanted.wpx"),
    call: $.i18n("rosterColumns.Wanted.call"),
    regex: $.i18n("rosterColumns.Wanted.regex"),
    oams: $.i18n("rosterColumns.Wanted.oams"),
    pota: $.i18n("rosterColumns.Wanted.pota")
  }

  window.opener.setRosterSpot(CR.rosterSettings.columns.Spot);

  for (const key in CR.rosterSettings.wanted)
  {
    if (document.getElementById(key))
    { document.getElementById(key).checked = CR.rosterSettings.wanted[key]; }
  }

  CR.menu = new nw.Menu();
  CR.compactMenu = new nw.Menu();
  let showControlsText = $.i18n("roster.menu.ShowControls");
  let hideControlsText = $.i18n("roster.menu.HideControls");
  let item = new nw.MenuItem({
    type: "normal",
    label: CR.rosterSettings.controls ? hideControlsText : showControlsText,
    click: function ()
    {
      if (this.label == "Hide Controls")
      {
        this.label = showControlsText;
        CR.rosterSettings.controls = false;
      }
      else
      {
        this.label = hideControlsText;
        CR.rosterSettings.controls = true;
      }
      CR.compactMenu.items[0].label = CR.rosterSettings.controls ? hideControlsText : showControlsText;
      localStorage.rosterSettings = JSON.stringify(CR.rosterSettings);
      setVisual();
    }
  });
  CR.menu.append(item);

  item = new nw.MenuItem({
    type: "normal",
    label: CR.rosterSettings.controls ? hideControlsText : showControlsText,
    click: function ()
    {
      if (this.label == hideControlsText)
      {
        this.label = showControlsText;
        CR.rosterSettings.controls = false;
      }
      else
      {
        this.label = hideControlsText;
        CR.rosterSettings.controls = true;
      }
      CR.menu.items[0].label = CR.rosterSettings.controls ? hideControlsText : showControlsText;
      localStorage.rosterSettings = JSON.stringify(CR.rosterSettings);
      setVisual();
    }
  });
  CR.compactMenu.append(item);

  item = new nw.MenuItem({
    type: "normal",
    label: $.i18n("roster.menu.Settings"),
    click: function ()
    {
      openSettings();
    }
  });
  CR.menu.append(item);

  item = new nw.MenuItem({
    type: "normal",
    label: $.i18n("roster.menu.Settings"),
    click: function ()
    {
      openSettings();
    }
  });
  CR.compactMenu.append(item);

  item = new nw.MenuItem({
    type: "normal",
    label: $.i18n("roster.menu.CompactMode"),
    click: function ()
    {
      CR.rosterSettings.compact = true;
      localStorage.rosterSettings = JSON.stringify(CR.rosterSettings);
      resize();
    }
  });
  CR.menu.append(item);

  item = new nw.MenuItem({
    type: "normal",
    label: $.i18n("roster.menu.RosterMode"),
    click: function ()
    {
      CR.rosterSettings.compact = false;
      localStorage.rosterSettings = JSON.stringify(CR.rosterSettings);
      resize();
    }
  });
  CR.compactMenu.append(item);

  CR.callMenu = new nw.Menu();

  item = new nw.MenuItem({
    type: "normal",
    label: $.i18n("roster.menu.Lookup"),
    click: function ()
    {
      callLookup(CR.targetHash, "");
    }
  });

  CR.callMenu.append(item);

  item = new nw.MenuItem({
    type: "normal",
    label: $.i18n("roster.menu.GenMesg"),
    click: function ()
    {
      callGenMessage(CR.targetHash, "");
    }
  });
  CR.callMenu.append(item);

  item = new nw.MenuItem({ type: "separator" });
  CR.callMenu.append(item);

  if (window.opener.GT.pstrotatorSettings.enable)
  {
    item = new nw.MenuItem({
      type: "normal",
      label: $.i18n("roster.menu.AimRotator"),
      click: function ()
      {
        let target = CR.callRoster[CR.targetHash]
        window.opener.aimRotator(target, "");
      }
    });
    CR.callMenu.append(item);

    item = new nw.MenuItem({ type: "separator" });
    CR.callMenu.append(item);
  }

  item = new nw.MenuItem({
    type: "normal",
    label: $.i18n("roster.menu.IgnoreCall"),
    click: function ()
    {
      let thisCall = CR.callRoster[CR.targetHash].DEcall;
      CR.blockedCalls[thisCall] = true;
      storeBlocks();
      viewRoster();
    }
  });

  CR.callMenu.append(item);

  CR.callingMenu = new nw.Menu();
  item = new nw.MenuItem({
    type: "normal",
    label: $.i18n("roster.menu.Lookup"),
    click: function ()
    {
      callingLookup(CR.targetHash, "");
    }
  });

  CR.callingMenu.append(item);

  item = new nw.MenuItem({
    type: "normal",
    label: $.i18n("roster.menu.GenMesg"),
    click: function ()
    {
      callingGenMessage(CR.targetHash, "");
    }
  });

  CR.callingMenu.append(item);

  item = new nw.MenuItem({ type: "separator" });
  CR.menu.append(item);

  if (window.opener.GT.pstrotatorSettings.enable)
  {
    item = new nw.MenuItem({
      type: "normal",
      label: $.i18n("roster.menu.AimRotator"),
      click: function ()
      {
        let target = CR.callRoster[CR.targetHash]
        window.opener.aimRotator(target, "");
      }
    });
    CR.callingMenu.append(item);

    item = new nw.MenuItem({ type: "separator" });
    CR.callingMenu.append(item);
  }

  item = new nw.MenuItem({
    type: "checkbox",
    label: $.i18n("roster.menu.Realtime"),
    checked: CR.rosterSettings.realtime,
    click: function ()
    {
      CR.rosterSettings.realtime = this.checked;
      writeRosterSettings();
      viewRoster();
    }
  });
  CR.menu.append(item);

  item = new nw.MenuItem({ type: "separator" });
  CR.menu.append(item);

  CR.menuItemForCurrentColumn = new nw.MenuItem({
    type: "normal",
    label: $.i18n("roster.menu.MoveLeft"),
    click: function ()
    {
      moveColumnLeft(CR.currentColumnName);
    }
  })
  CR.menu.append(CR.menuItemForCurrentColumn)

  item = new nw.MenuItem({ type: "separator" });
  CR.menu.append(item);

  for (const columnIndex in CR.rosterSettings.columnOrder)
  {
    let key = CR.rosterSettings.columnOrder[columnIndex];
    if (key != "Callsign")
    {
      let itemx = new nw.MenuItem({
        type: "checkbox",
        label: key,
        checked: CR.rosterSettings.columns[key],
        click: function ()
        {
          CR.rosterSettings.columns[this.label] = this.checked;
          if (this.label == "Spot")
          { window.opener.setRosterSpot(CR.rosterSettings.columns.Spot); }
          writeRosterSettings();
          viewRoster();
          resize();
        }
      });

      CR.menu.append(itemx);
    }
  }

  item = new nw.MenuItem({ type: "separator" });
  CR.menu.append(item);

  CR.clearIgnores = new nw.MenuItem({
    type: "normal",
    label: "Clear Call Ignore",
    enabled: false,
    click: function ()
    {
      CR.blockedCalls = Object();
      storeBlocks();
      viewRoster();
    }
  });
  CR.menu.append(CR.clearIgnores);

  CR.clearIgnoresCall = new nw.MenuItem({
    type: "normal",
    label: "Clear Ignore",
    enabled: false,
    click: function ()
    {
      CR.blockedCalls = Object();
      storeBlocks();
      viewRoster();
    }
  });
  CR.callMenu.append(CR.clearIgnoresCall);

  CR.CQMenu = new nw.Menu();

  item = new nw.MenuItem({
    type: "normal",
    label: "Ignore CQ from DXCC",
    click: function ()
    {
      CR.blockedCQ[
        CR.callRoster[CR.targetCQ].DXcall +
          " from " +
          window.opener.GT.dxccToAltName[CR.callRoster[CR.targetCQ].callObj.dxcc]
      ] = true;
      storeBlocks();
      viewRoster();
    }
  });

  CR.CQMenu.append(item);

  item = new nw.MenuItem({
    type: "normal",
    label: "Ignore CQ from All",
    click: function ()
    {
      CR.blockedCQ[CR.callRoster[CR.targetCQ].DXcall + " from All"] = true;
      storeBlocks();
      viewRoster();
    }
  });

  CR.CQMenu.append(item);

  CR.clearCQIgnoreMainMenu = new nw.MenuItem({
    type: "normal",
    label: "Clear CQ Ignore",
    enabled: false,
    click: function ()
    {
      CR.blockedCQ = Object();
      storeBlocks();
      viewRoster();
    }
  });
  CR.menu.append(CR.clearCQIgnoreMainMenu);

  CR.clearCQIgnore = new nw.MenuItem({
    type: "normal",
    label: "Clear Ignore",
    enabled: false,
    click: function ()
    {
      CR.blockedCQ = Object();
      storeBlocks();
      viewRoster();
    }
  });
  CR.CQMenu.append(CR.clearCQIgnore);

  item = new nw.MenuItem({
    type: "normal",
    label: "Edit Ignores",
    enabled: true,
    click: function ()
    {
      openIgnoreEdit();
    }
  });
  CR.CQMenu.append(item);

  CR.CQzMenu = new nw.Menu();

  item = new nw.MenuItem({
    type: "normal",
    label: "Ignore CQ Zone",
    click: function ()
    {
      CR.blockedCQz[CR.callRoster[CR.targetCQz].callObj.cqz] = true;
      storeBlocks();
      viewRoster();
    }
  });

  CR.CQzMenu.append(item);

  CR.clearCQzIgnoreMainMenu = new nw.MenuItem({
    type: "normal",
    label: "Clear CQ Zone Ignore",
    enabled: false,
    click: function ()
    {
      CR.blockedCQz = Object();
      storeBlocks();
      viewRoster();
    }
  });

  CR.clearCQzIgnore = new nw.MenuItem({
    type: "normal",
    label: "Clear Ignore",
    enabled: false,
    click: function ()
    {
      CR.blockedCQz = Object();
      storeBlocks();
      viewRoster();
    }
  });
  CR.CQzMenu.append(CR.clearCQzIgnore);

  CR.CQzMenu.append(CR.clearCQzIgnoreMainMenu);

  item = new nw.MenuItem({
    type: "normal",
    label: "Edit Ignores",
    enabled: true,
    click: function ()
    {
      openIgnoreEdit();
    }
  });

  CR.CQzMenu.append(item);

  CR.ITUzMenu = new nw.Menu();

  item = new nw.MenuItem({
    type: "normal",
    label: "Ignore ITU Zone",
    click: function ()
    {
      CR.blockedITUz[CR.callRoster[CR.targetITUz].callObj.ituz] = true;
      storeBlocks();
      viewRoster();
    }
  });

  CR.ITUzMenu.append(item);

  CR.clearITUzIgnoreMainMenu = new nw.MenuItem({
    type: "normal",
    label: "Clear ITU Zone Ignore",
    enabled: false,
    click: function ()
    {
      CR.blockedITUz = Object();
      storeBlocks();
      viewRoster();
    }
  });

  CR.ITUzMenu.append(CR.clearITUzIgnoreMainMenu);

  CR.clearITUzIgnore = new nw.MenuItem({
    type: "normal",
    label: "Clear Ignore",
    enabled: false,
    click: function ()
    {
      CR.blockedITUz = Object();
      storeBlocks();
      viewRoster();
    }
  });

  CR.ITUzMenu.append(CR.clearITUzIgnore);

  item = new nw.MenuItem({
    type: "normal",
    label: "Edit Ignores",
    enabled: true,
    click: function ()
    {
      openIgnoreEdit();
    }
  });

  CR.ITUzMenu.append(item);

  CR.dxccMenu = new nw.Menu();

  item = new nw.MenuItem({
    type: "normal",
    label: "Ignore DXCC",
    click: function ()
    {
      CR.blockedDxcc[CR.targetDxcc] = true;
      storeBlocks();
      viewRoster();
    }
  });

  CR.dxccMenu.append(item);

  CR.clearDxccIgnoreMainMenu = new nw.MenuItem({
    type: "normal",
    label: "Clear DXCC Ignore",
    enabled: false,
    click: function ()
    {
      CR.blockedDxcc = Object();
      storeBlocks();
      viewRoster();
    }
  });
  CR.menu.append(CR.clearDxccIgnoreMainMenu);

  CR.clearDxccIgnore = new nw.MenuItem({
    type: "normal",
    label: "Clear Ignore",
    enabled: false,
    click: function ()
    {
      CR.blockedDxcc = Object();
      storeBlocks();
      viewRoster();
    }
  });
  CR.dxccMenu.append(CR.clearDxccIgnore);

  item = new nw.MenuItem({
    type: "normal",
    label: "Edit Ignores",
    enabled: true,
    click: function ()
    {
      openIgnoreEdit();
    }
  });
  CR.menu.append(item);

  item = new nw.MenuItem({
    type: "normal",
    label: "Edit Ignores",
    enabled: true,
    click: function ()
    {
      openIgnoreEdit();
    }
  });
  CR.callMenu.append(item);

  item = new nw.MenuItem({
    type: "normal",
    label: "Edit Ignores",
    enabled: true,
    click: function ()
    {
      openIgnoreEdit();
    }
  });
  CR.dxccMenu.append(item);

  callsignNeed.value = CR.rosterSettings.callsign;
  huntMode.value = CR.rosterSettings.hunting;
  huntNeed.value = CR.rosterSettings.huntNeed;
  wantGrid.checked = CR.rosterSettings.requireGrid;

  wantMaxDT.checked = CR.rosterSettings.wantMaxDT;
  wantMinDB.checked = CR.rosterSettings.wantMinDB;
  wantMinFreq.checked = CR.rosterSettings.wantMinFreq;
  wantMaxFreq.checked = CR.rosterSettings.wantMaxFreq;
  wantRRCQ.checked = CR.rosterSettings.wantRRCQ;

  maxDTView.innerHTML = maxDT.value = CR.rosterSettings.maxDT;
  minDbView.innerHTML = minDb.value = CR.rosterSettings.minDb;
  minFreqView.innerHTML = minFreq.value = CR.rosterSettings.minFreq;
  maxFreqView.innerHTML = maxFreq.value = CR.rosterSettings.maxFreq;

  maxLoTW.value = CR.rosterSettings.maxLoTW;
  maxLoTWView.innerHTML = maxLoTW.value < 27 ? toYM(Number(maxLoTW.value)) : "<b>&infin;</b>";

  cqOnly.checked = CR.rosterSettings.cqOnly;
  noMyDxcc.checked = CR.rosterSettings.noMyDxcc;
  onlyMyDxcc.checked = CR.rosterSettings.onlyMyDxcc;

  noMsg.checked = CR.rosterSettings.noMsg;
  onlyMsg.checked = CR.rosterSettings.onlyMsg;
  noMsgValue.value = CR.rosterSettings.noMsgValue;
  onlyMsgValue.value = CR.rosterSettings.onlyMsgValue;

  usesLoTW.checked = CR.rosterSettings.usesLoTW;
  useseQSL.checked = CR.rosterSettings.useseQSL;
  onlySpot.checked = CR.rosterSettings.onlySpot;
  usesOQRS.checked = CR.rosterSettings.usesOQRS;

  referenceNeed.value = CR.rosterSettings.reference;
  allOnlyNew.checked = CR.rosterSettings.allOnlyNew;
  useRegex.checked = CR.rosterSettings.useRegex;
  callsignRegex.value = CR.rosterSettings.callsignRegex;
  huntRegexValue.value = CR.rosterSettings.huntRegexValue;

  noUnknownDXCC.checked = CR.rosterSettings.noUnknownDXCC;

  clearRosterOnBandChange.checked = CR.rosterSettings.clearRosterOnBandChange;
  rosterAlwaysOnTop.checked = CR.rosterSettings.rosterAlwaysOnTop;
  rosterDelayOnFocus.checked = CR.rosterSettings.rosterDelayOnFocus;
  displayDelayOnFocus();
  rosterDelayTime.value = CR.rosterSettings.rosterDelayTime;
  rosterDelayTimeTd.innerHTML = rosterDelayTime.value + "ms";
  setRosterTimeView();
  
  for (const column in ROSTER_COLUMNS)
  {
    if (column != "Callsign")
    {
      let option = newOption(column, column);
      if (column == CR.rosterSettings.compactEntity)
      {
        option.selected = true;
      }
      compactEntitySelect.appendChild(option);
    }
  }
  
  setVisual();
  document.addEventListener("keydown", onMyKeyDown, false);

  initDXCCSelector();
  CR.timerInterval = nodeTimers.setInterval(realtimeRoster, 1000);

  updateInstances();
}

function compactEntityChanged()
{
  CR.rosterSettings.compactEntity = compactEntitySelect.value;
  viewRoster();
}

function clearRosterOnBandChangeValueChanged(what)
{
  CR.rosterSettings.clearRosterOnBandChange = clearRosterOnBandChange.checked;
  writeRosterSettings();
}

function rosterDelayOnFocusValueChanged(what)
{
  CR.rosterSettings.rosterDelayOnFocus = rosterDelayOnFocus.checked;
  displayDelayOnFocus();
  writeRosterSettings();
}

function displayDelayOnFocus()
{
  if (CR.rosterSettings.rosterDelayOnFocus)
  {
    rosterDelayTimeTd.style.display = "block";
    rosterDelayTime.style.display = "block";
  }
  else
  {
    rosterDelayTimeTd.style.display = "none";
    rosterDelayTime.style.display = "none";
  }
}

function changeRosterDelayTime()
{
  CR.rosterSettings.rosterDelayTime = rosterDelayTime.value;
  rosterDelayTimeTd.innerHTML = rosterDelayTime.value + "ms";
  writeRosterSettings();
}

function changeRosterTime()
{
  CR.rosterSettings.rosterTime = rosterTime.value;
  setRosterTimeView();
  writeRosterSettings();
  viewRoster();
}

function changeRosterTop(butt)
{
  CR.rosterSettings.rosterAlwaysOnTop = butt.checked;
  setRosterTop();
}

function setRosterTop()
{
  nw.Window.get().setAlwaysOnTop(CR.rosterSettings.rosterAlwaysOnTop);
}

function setRosterTimeView()
{
  rosterTime.value = CR.rosterSettings.rosterTime;
  rosterTimeTd.innerHTML = toDHMS(Number(rosterTime.value));
}

function handleContextMenu(ev)
{
  if (editView.style.display == "inline-block") return false;

  let mouseX = Math.round(ev.x);
  let mouseY = Math.round(ev.y);

  let len = Object.keys(CR.blockedCalls).length;
  if (len > 0)
  {
    CR.clearIgnores.enabled = true;
    CR.clearIgnores.label =
      "Clear Call Ignore" + (len > 1 ? "s (" + len + ")" : "");
    CR.clearIgnoresCall.enabled = true;
    CR.clearIgnoresCall.label =
      "Clear Ignore" + (len > 1 ? "s (" + len + ")" : "");
  }
  else
  {
    CR.clearIgnores.label = "Clear Call Ignore";
    CR.clearIgnores.enabled = false;
    CR.clearIgnoresCall.label = "Clear Ignore";
    CR.clearIgnoresCall.enabled = false;
  }

  len = Object.keys(CR.blockedDxcc).length;
  if (len > 0)
  {
    CR.clearDxccIgnoreMainMenu.enabled = true;
    CR.clearDxccIgnoreMainMenu.label =
      "Clear DXCC Ignore" + (len > 1 ? "s (" + len + ")" : "");
    CR.clearDxccIgnore.enabled = true;
    CR.clearDxccIgnore.label =
      "Clear Ignore" + (len > 1 ? "s (" + len + ")" : "");
  }
  else
  {
    CR.clearDxccIgnoreMainMenu.label = "Clear DXCC Ignore";
    CR.clearDxccIgnoreMainMenu.enabled = false;
    CR.clearDxccIgnore.label = "Clear Ignore";
    CR.clearDxccIgnore.enabled = false;
  }

  len = Object.keys(CR.blockedCQ).length;
  if (len > 0)
  {
    CR.clearCQIgnoreMainMenu.enabled = true;
    CR.clearCQIgnoreMainMenu.label =
      "Clear CQ Ignore" + (len > 1 ? "s (" + len + ")" : "");
    CR.clearCQIgnore.enabled = true;
    CR.clearCQIgnore.label = "Clear Ignore" + (len > 1 ? "s (" + len + ")" : "");
  }
  else
  {
    CR.clearCQIgnoreMainMenu.label = "Clear CQ Ignore";
    CR.clearCQIgnoreMainMenu.enabled = false;
    CR.clearCQIgnore.label = "Clear Ignore";
    CR.clearCQIgnore.enabled = false;
  }

  len = Object.keys(CR.blockedCQz).length;
  if (len > 0)
  {
    CR.clearCQzIgnoreMainMenu.enabled = true;
    CR.clearCQzIgnoreMainMenu.label =
      "Clear CQ Zone Ignore" + (len > 1 ? "s (" + len + ")" : "");
    CR.clearCQzIgnore.enabled = true;
    CR.clearCQzIgnore.label = "Clear Ignore" + (len > 1 ? "s (" + len + ")" : "");
  }
  else
  {
    CR.clearCQzIgnoreMainMenu.label = "Clear CQ Zone Ignore";
    CR.clearCQzIgnoreMainMenu.enabled = false;
    CR.clearCQzIgnore.label = "Clear Ignore";
    CR.clearCQzIgnore.enabled = false;
  }

  len = Object.keys(CR.blockedITUz).length;
  if (len > 0)
  {
    CR.clearITUzIgnoreMainMenu.enabled = true;
    CR.clearITUzIgnoreMainMenu.label =
      "Clear ITU Zone Ignore" + (len > 1 ? "s (" + len + ")" : "");
    CR.clearITUzIgnore.enabled = true;
    CR.clearITUzIgnore.label = "Clear Ignore" + (len > 1 ? "s (" + len + ")" : "");
  }
  else
  {
    CR.clearITUzIgnoreMainMenu.label = "Clear ITU Zone Ignore";
    CR.clearITUzIgnoreMainMenu.enabled = false;
    CR.clearITUzIgnore.label = "Clear Ignore";
    CR.clearITUzIgnore.enabled = false;
  }

  if (typeof ev.target != "undefined")
  {
    if (CR.developerMode)
    {
      if ((ev.target.id == "ShowMoreControlsLink") ||
        (ev.target.id == "ShowFewerControlsLink") ||
        (ev.target.id == "txrxdec"))
      {
        // Allow event to bubble up so that NWJS will show the developer menu
        return true;
      }
    }

    let name = "";
    if (ev.target.tagName == "TD" || (CR.rosterSettings.compact && ev.target.tagName == "DIV"))
    {
      name = ev.target.getAttribute("name");
    }

    if (name == "Callsign")
    {
      CR.targetHash = ev.target.parentNode.id;
      CR.callMenu.popup(mouseX, mouseY);
    }
    else if (name == "Calling")
    {
      CR.targetHash = ev.target.parentNode.id;
      CR.callingMenu.popup(mouseX, mouseY);
    }
    else if (name == "CQ")
    {
      if (CR.callRoster[ev.target.parentNode.id].DXcall != "CQ")
      {
        CR.targetCQ = ev.target.parentNode.id;
        CR.CQMenu.popup(mouseX, mouseY);
      }
    }
    else if (name == "CQz")
    {
      CR.targetCQz = ev.target.parentNode.id;
      CR.CQzMenu.popup(mouseX, mouseY);
    }
    else if (name == "ITUz")
    {
      CR.targetITUz = ev.target.parentNode.id;
      CR.ITUzMenu.popup(mouseX, mouseY);
    }
    else if (name && name.startsWith("DXCC"))
    {
      let dxcca = name.split("(");
      let dxcc = parseInt(dxcca[1]);
      CR.targetDxcc = dxcc;
      CR.dxccMenu.popup(mouseX, mouseY);
    }
    else
    {
      if (CR.rosterSettings.compact)
      {
        CR.compactMenu.popup(mouseX, mouseY);
      }
      else
      {
        if (ev.target.tagName == "TH" && ev.target.getAttribute("name"))
        {
          CR.menuItemForCurrentColumn.enabled = true;
          CR.currentColumnName = ev.target.getAttribute("name");
        }
        else
        {
          CR.menuItemForCurrentColumn.enabled = false;
          CR.currentColumnName = null;
        }

        CR.menu.popup(mouseX, mouseY);
      }
    }
  }
  else
  {
    if (CR.rosterSettings.compact == false)
    {
      CR.menu.popup(mouseX, mouseY);
    }
    else
    {
      CR.compactMenu.popup(mouseX, mouseY);
    }
  }

  ev.preventDefault();

  return false;
}

function getTypeFromMode(mode)
{
  if (mode in CR.modes)
  {
    if (CR.modes[mode] == true) return "Digital";
    else if (CR.modes_phone[mode] == true) return "Phone";
  }
  return "";
}

function testAward(awardName, obj, baseHash)
{
  if (
    CR.awardTracker[awardName].test.dxcc &&
    CR.awardTracker[awardName].rule.dxcc.indexOf(obj.dxcc) == -1
  )
  { return false; }

  if (
    CR.awardTracker[awardName].test.mode &&
    CR.awardTracker[awardName].rule.mode.indexOf(obj.mode) == -1
  )
  { return false; }

  if (
    CR.awardTracker[awardName].test.band &&
    CR.awardTracker[awardName].rule.band.indexOf(obj.band) == -1
  )
  { return false; }

  if (
    CR.awardTracker[awardName].test.DEcall &&
    CR.awardTracker[awardName].rule.call.indexOf(obj.DEcall) == -1
  )
  { return false; }

  if (
    CR.awardTracker[awardName].test.cont &&
    CR.awardTracker[awardName].rule.cont.indexOf(obj.cont) == -1
  )
  { return false; }

  if (
    CR.awardTracker[awardName].test.prop &&
    CR.awardTracker[awardName].rule.propMode != obj.propMode
  )
  { return false; }

  if (
    CR.awardTracker[awardName].test.sat &&
    CR.awardTracker[awardName].rule.satName.indexOf(obj.satName) == -1
  )
  { return false; }

  return CR.awardTypes[CR.awardTracker[awardName].rule.type].test(
    CR.awardTracker[awardName],
    obj,
    baseHash
  );
}

function processAward(awardName)
{
  let award =
    CR.awards[CR.awardTracker[awardName].sponsor].awards[
      CR.awardTracker[awardName].name
    ];
  CR.awardTracker[awardName].rule = award.rule;
  let test = (CR.awardTracker[awardName].test = {});
  let mode = award.rule.mode.slice();

  let Index = mode.indexOf("Mixed");
  if (Index > -1) mode.splice(Index, 1);

  Index = mode.indexOf("Digital");
  if (Index > -1) mode.splice(Index, 1);

  Index = mode.indexOf("Phone");
  if (Index > -1) mode.splice(Index, 1);

  test.mode = mode.length > 0;
  test.confirmed = "qsl_req" in CR.awards[CR.awardTracker[awardName].sponsor].awards[CR.awardTracker[awardName].name].rule ? CR.awards[CR.awardTracker[awardName].sponsor].awards[CR.awardTracker[awardName].name].rule.qsl_req == "confirmed" : CR.awards[CR.awardTracker[awardName].sponsor].qsl_req == "confirmed";
  test.look = "confirmed";
  test.qsl_req = "qsl_req" in CR.awards[CR.awardTracker[awardName].sponsor].awards[CR.awardTracker[awardName].name].rule ? CR.awards[CR.awardTracker[awardName].sponsor].awards[CR.awardTracker[awardName].name].rule.qsl_req : CR.awards[CR.awardTracker[awardName].sponsor].qsl_req;
  test.DEcall = "call" in award.rule;
  test.band = "band" in award.rule && award.rule.band.indexOf("Mixed") == -1;
  test.dxcc = "dxcc" in award.rule;
  test.cont = "cont" in award.rule;
  test.prop = "propMode" in award.rule;
  test.sat = "satName" in award.rule;

  CR.awardTracker[awardName].stat = {};

  for (const i in window.opener.GT.QSOhash)
  {
    let obj = window.opener.GT.QSOhash[i];

    if (test.confirmed && !obj.confirmed) continue;

    if (obj.dxcc < 1) continue;

    if (test.dxcc && award.rule.dxcc.indexOf(obj.dxcc) == -1) continue;

    if (test.mode && award.rule.mode.indexOf(obj.mode) == -1) continue;

    if (test.band && award.rule.band.indexOf(obj.band) == -1) continue;

    if (test.DEcall && award.rule.call.indexOf(obj.DEcall) == -1) continue;

    if (test.cont && award.rule.cont.indexOf(obj.cont) == -1) continue;

    if (test.prop && award.rule.propMode != obj.propMode) continue;

    if (test.sat && award.rule.satName.indexOf(obj.satName) == -1) continue;

    CR.awardTypes[award.rule.type].score(CR.awardTracker[awardName], obj);
  }

  CR.awardTracker[awardName].comp = CR.awardTypes[award.rule.type].compile(
    CR.awardTracker[awardName],
    CR.awardTracker[awardName].stat
  );
  CR.awardTracker[awardName].stat = {};
}

function newAwardCountObject()
{
  let statCountObject = {};

  statCountObject.bands = {};
  statCountObject.bands.Mixed = {};
  statCountObject.bands.Digital = {};
  statCountObject.bands.Phone = {};
  statCountObject.modes = {};
  statCountObject.modes.Mixed = {};
  statCountObject.modes.Digital = {};
  statCountObject.modes.Phone = {};
  statCountObject.unique = null;
  return statCountObject;
}

function workAwardObject(obj, band, mode, isDigital, isPhone, unique = null)
{
  obj.bands.Mixed[band] = ~~obj.bands.Mixed[band] + 1;
  if (!(mode in obj.bands)) obj.bands[mode] = {};
  obj.bands[mode][band] = ~~obj.bands[mode][band] + 1;
  obj.modes.Mixed[mode] = ~~obj.modes.Mixed[mode] + 1;

  if (isDigital)
  {
    obj.bands.Digital[band] = ~~obj.bands.Digital[band] + 1;
    obj.modes.Digital[mode] = ~~obj.modes.Digital[mode] + 1;
  }
  if (isPhone)
  {
    obj.bands.Phone[band] = ~~obj.bands.Phone[band] + 1;
    obj.modes.Phone[mode] = ~~obj.modes.Phone[mode] + 1;
  }
  if (unique)
  {
    if (obj.unique == null) obj.unique = {};
    if (!(unique in obj.unique)) obj.unique[unique] = newAwardCountObject();
    workAwardObject(obj.unique[unique], band, mode, isDigital, isPhone);
  }
  return true;
}

function buildAwardTypeHandlers()
{
  CR.awardTypes = {
    IOTA: { name: "Islands On The Air" },
    call: { name: "Callsign" },
    callarea: { name: "Call Area" },
    calls2dxcc: { name: "Stations per DXCC" },
    cnty: { name: "County" },
    cont: { name: "Continents" },
    cont5: { name: "5 Continents" },
    cont52band: { name: "5 Continents per Band" },
    cqz: { name: "CQ Zone" },
    dxcc: { name: "DXCC" },
    grids: { name: "Grids" },
    numsfx: { name: "Call Area + Suffix" },
    px: { name: "Prefix" },
    pxa: { name: "Prefixes" },
    pxplus: { name: "Special Calls" },
    sfx: { name: "Suffix" },
    states: { name: "States" },
    cont2band: { name: "Continents per Band" },
    calls2band: { name: "Stations per Band" },
    dxcc2band: { name: "DXCC per Band" },
    states2band: { name: "States per Band" }
  };

  CR.awardTypes.IOTA.score = scoreAIOTA;
  CR.awardTypes.call.score = scoreAcall;
  CR.awardTypes.callarea.score = scoreAcallarea;
  CR.awardTypes.calls2dxcc.score = scoreAcalls2dxcc;
  CR.awardTypes.cnty.score = scoreAcnty;
  CR.awardTypes.cont.score = scoreAcont;
  CR.awardTypes.cont5.score = scoreAcont5;
  CR.awardTypes.cont52band.score = scoreAcont52band;
  CR.awardTypes.cqz.score = scoreAcqz;
  CR.awardTypes.dxcc.score = scoreAdxcc;
  CR.awardTypes.grids.score = scoreAgrids;
  CR.awardTypes.numsfx.score = scoreAnumsfx;
  CR.awardTypes.px.score = scoreApx;
  CR.awardTypes.pxa.score = scoreApxa;
  CR.awardTypes.pxplus.score = scoreApxplus;
  CR.awardTypes.sfx.score = scoreAsfx;
  CR.awardTypes.states.score = scoreAstates;
  CR.awardTypes.cont2band.score = scoreAcont2band;
  CR.awardTypes.calls2band.score = scoreAcalls2band;
  CR.awardTypes.dxcc2band.score = scoreAdxcc2band;
  CR.awardTypes.states2band.score = scoreAstates2band;

  CR.awardTypes.IOTA.test = testAIOTA;
  CR.awardTypes.call.test = testAcall;
  CR.awardTypes.callarea.test = testAcallarea;
  CR.awardTypes.calls2dxcc.test = testAcalls2dxcc;
  CR.awardTypes.cnty.test = testAcnty;
  CR.awardTypes.cont.test = testAcont;
  CR.awardTypes.cont5.test = testAcont5;
  CR.awardTypes.cont52band.test = testAcont52band;
  CR.awardTypes.cqz.test = testAcqz;
  CR.awardTypes.dxcc.test = testAdxcc;
  CR.awardTypes.grids.test = testAgrids;
  CR.awardTypes.numsfx.test = testAnumsfx;
  CR.awardTypes.px.test = testApx;
  CR.awardTypes.pxa.test = testApxa;
  CR.awardTypes.pxplus.test = testApxplus;
  CR.awardTypes.sfx.test = testAsfx;
  CR.awardTypes.states.test = testAstates;
  CR.awardTypes.cont2band.test = testAcont2band;
  CR.awardTypes.calls2band.test = testAcalls2band;
  CR.awardTypes.dxcc2band.test = testAdxcc2band;
  CR.awardTypes.states2band.test = testAstates;

  CR.awardTypes.IOTA.compile = singleCompile;
  CR.awardTypes.call.compile = singleCompile;
  CR.awardTypes.callarea.compile = singleCompile;
  CR.awardTypes.calls2dxcc.compile = doubleCompile;
  CR.awardTypes.cnty.compile = singleCompile;
  CR.awardTypes.cont.compile = singleCompile;
  CR.awardTypes.cont5.compile = singleCompile;
  CR.awardTypes.cont52band.compile = doubleCompile;
  CR.awardTypes.cqz.compile = singleCompile;
  CR.awardTypes.dxcc.compile = singleCompile;
  CR.awardTypes.grids.compile = singleCompile;
  CR.awardTypes.numsfx.compile = singleCompile;
  CR.awardTypes.px.compile = singleCompile;
  CR.awardTypes.pxa.compile = singleCompile;
  CR.awardTypes.pxplus.compile = singleCompile;
  CR.awardTypes.sfx.compile = singleCompile;
  CR.awardTypes.states.compile = singleCompile;
  CR.awardTypes.cont2band.compile = doubleCompile;
  CR.awardTypes.calls2band.compile = doubleCompile;
  CR.awardTypes.dxcc2band.compile = doubleCompile;
  CR.awardTypes.states2band.compile = doubleCompile;
}

function scoreAstates(award, obj)
{
  if (obj.state)
  {
    if (!(obj.state in award.stat))
    { award.stat[obj.state] = newAwardCountObject(); }
    return workAwardObject(
      award.stat[obj.state],
      obj.band,
      obj.mode,
      obj.digital,
      obj.phone
    );
  }
  return false;
}

function testAstates(award, obj, baseHash)
{
  // calls with empty state will not match anything in the hash map. so filter those out
  if (!obj.state || obj.state + baseHash in CR.tracker[award.test.look].state)
  {
    return false;
  }
  return true;
}

function scoreAstates2band(award, obj)
{
  if (obj.state)
  {
    if (!(obj.band in award.stat)) award.stat[obj.band] = newAwardCountObject();
    return workAwardObject(
      award.stat[obj.band],
      obj.band,
      obj.mode,
      obj.digital,
      obj.phone,
      obj.state
    );
  }
  return false;
}

function scoreAdxcc(award, obj)
{
  if (!(obj.dxcc in award.stat)) award.stat[obj.dxcc] = newAwardCountObject();
  return workAwardObject(
    award.stat[obj.dxcc],
    obj.band,
    obj.mode,
    obj.digital,
    obj.phone
  );
}

function testAdxcc(award, obj, baseHash)
{
  if (String(obj.dxcc) + "|" + baseHash in CR.tracker[award.test.look].dxcc)
  {
    return false;
  }
  return true;
}

function scoreAcont(award, obj)
{
  if (obj.cont)
  {
    let cont = obj.cont;
    if (cont == "AN") cont = "OC";
    if (!(cont in award.stat)) award.stat[cont] = newAwardCountObject();
    return workAwardObject(
      award.stat[cont],
      obj.band,
      obj.mode,
      obj.digital,
      obj.phone
    );
  }
  return false;
}

function testAcont(award, obj, baseHash)
{
  if (obj.cont)
  {
    let cont = obj.cont;
    if (cont == "AN") cont = "OC";

    if (cont + baseHash in CR.tracker[award.test.look].cont)
    {
      return false;
    }
  }
  return true;
}

function scoreAcont5(award, obj, baseHash)
{
  if (obj.cont)
  {
    let cont = obj.cont;
    if (cont == "NA" || cont == "SA") cont = "AM";
    if (cont == "AN") cont = "OC";

    if (!(cont in award.stat)) award.stat[cont] = newAwardCountObject();
    return workAwardObject(
      award.stat[cont],
      obj.band,
      obj.mode,
      obj.digital,
      obj.phone
    );
  }
  return false;
}

function testAcont5(award, obj, baseHash)
{
  if (obj.cont)
  {
    let cont = obj.cont;
    if (cont == "NA" || cont == "SA") cont = "AM";
    if (cont == "AN") cont = "OC";

    if (cont + baseHash in CR.tracker[award.test.look].cont)
    {
      return false;
    }
  }
  return true;
}

function scoreAcont2band(award, obj)
{
  if (!(obj.band in award.stat)) award.stat[obj.band] = newAwardCountObject();

  return workAwardObject(
    award.stat[obj.band],
    obj.band,
    obj.mode,
    obj.digital,
    obj.phone,
    obj.cont
  );
}

function testAcont2band(award, obj, baseHash)
{
  if (obj.cont)
  {
    let cont = obj.cont;
    if (cont == "AN") cont = "OC";

    if (cont + baseHash in CR.tracker[award.test.look].cont)
    {
      return false;
    }
  }
  return true;
}

function scoreAcont52band(award, obj)
{
  if (obj.cont)
  {
    let cont = obj.cont;
    if (cont == "NA" || cont == "SA") cont = "AM";
    if (cont == "AN") cont = "OC";

    if (!(obj.band in award.stat)) award.stat[obj.band] = newAwardCountObject();
    return workAwardObject(
      award.stat[obj.band],
      obj.band,
      obj.mode,
      obj.digital,
      obj.phone,
      cont
    );
  }
  return false;
}

function testAcont52band(award, obj, baseHash)
{
  if (obj.cont)
  {
    let cont = obj.cont;
    if (cont == "NA" || cont == "SA") cont = "AM";
    if (cont == "AN") cont = "OC";

    if (cont + baseHash in CR.tracker[award.test.look].cont)
    {
      return false;
    }
  }
  return true;
}

function scoreAgrids(award, obj)
{
  if (obj.grid && obj.grid.length > 0)
  {
    let grid = obj.grid.substr(0, 4);

    if (!(grid in award.stat)) award.stat[grid] = newAwardCountObject();
    return workAwardObject(
      award.stat[grid],
      obj.band,
      obj.mode,
      obj.digital,
      obj.phone
    );
  }
  return false;
}

function testAgrids(award, obj, baseHash)
{
  if (obj.grid && obj.grid + baseHash in CR.tracker[award.test.look].grid)
  {
    return false;
  }
  if (!obj.grid || obj.grid.length == 0)
  {
    return false;
  }
  return true;
}

function scoreAcnty(award, obj)
{
  if (obj.cnty)
  {
    if (!(obj.cnty in award.stat)) award.stat[obj.cnty] = newAwardCountObject();
    return workAwardObject(
      award.stat[obj.cnty],
      obj.band,
      obj.mode,
      obj.digital,
      obj.phone
    );
  }
  return false;
}

function testAcnty(award, obj, baseHash)
{
  if (obj.cnty && obj.cnty + baseHash in CR.tracker[award.test.look].cnty)
  {
    return false;
  }
  return true;
}

function scoreAcall(award, obj)
{
  let call = obj.DEcall;

  if (call.indexOf("/") > -1)
  {
    if (call.endsWith("/MM")) return false;
    call = call.replace("/P", "").replace("/R", "").replace("/QRP");
  }

  if (!(call in award.stat)) award.stat[call] = newAwardCountObject();
  return workAwardObject(
    award.stat[call],
    obj.band,
    obj.mode,
    obj.digital,
    obj.phone
  );
}

function testAcall(award, obj, baseHash)
{
  if (obj.DEcall.indexOf("/") > -1 && obj.DEcall.endsWith("/MM")) return false;

  if (obj.DEcall + baseHash in CR.tracker[award.test.look].call)
  {
    return false;
  }
  return true;
}

function scoreAIOTA(award, obj)
{
  if (obj.IOTA)
  {
    let test = CR.awards[award.sponsor].awards[award.name];

    if ("IOTA" in test.rule && test.rule.IOTA.indexOf(obj.IOTA) == -1)
    { return false; }

    if (!(obj.IOTA in award.stat)) award.stat[obj.IOTA] = newAwardCountObject();
    return workAwardObject(
      award.stat[obj.IOTA],
      obj.band,
      obj.mode,
      obj.digital,
      obj.phone
    );
  }
  return false;
}

// NO IOTA YET
function testAIOTA(award, obj, baseHash)
{
  /* if ( obj.IOTA )
  {
    let test = CR.awards[award.sponsor].awards[award.name];

    if ( "IOTA" in test.rule && test.rule.IOTA.indexOf(obj.IOTA) == -1 )
      return false;

  } */

  return false;
}

function scoreAcallarea(award, obj)
{
  if (obj.zone != null)
  {
    let test = CR.awards[award.sponsor].awards[award.name];

    if ("zone" in test.rule && test.rule.zone.indexOf(obj.zone) == -1)
    { return false; }

    if (!(obj.zone in award.stat)) award.stat[obj.zone] = newAwardCountObject();
    return workAwardObject(
      award.stat[obj.zone],
      obj.band,
      obj.mode,
      obj.digital,
      obj.phone
    );
  }
  return false;
}

function testAcallarea(award, obj, baseHash)
{
  if (obj.zone != null)
  {
    let test = CR.awards[award.sponsor].awards[award.name];

    if ("zone" in test.rule && test.rule.zone.indexOf(obj.zone) == -1)
    { return false; }
  }
  return true;
}

function scoreApx(award, obj)
{
  if (obj.px)
  {
    let test = CR.awards[award.sponsor].awards[award.name];
    let px = obj.px;
    if ("px" in test.rule)
    {
      px = px.substr(0, test.rule.px[0].length);
      if (test.rule.px.indexOf(px) == -1) return false;
    }

    if (!(px in award.stat)) award.stat[px] = newAwardCountObject();
    return workAwardObject(
      award.stat[px],
      obj.band,
      obj.mode,
      obj.digital,
      obj.phone
    );
  }
  return false;
}

function testApx(award, obj, baseHash)
{
  if (obj.px)
  {
    let test = CR.awards[award.sponsor].awards[award.name];
    let px = obj.px;
    if ("px" in test.rule)
    {
      px = px.substr(0, test.rule.px[0].length);
      if (test.rule.px.indexOf(px) == -1) return false;
    }

    if (String(obj.px) + baseHash in CR.tracker[award.test.look].px)
    {
      return false;
    }
  }
  return true;
}

function scoreApxa(award, obj)
{
  if (obj.px)
  {
    let test = CR.awards[award.sponsor].awards[award.name];
    for (const i in test.rule.pxa)
    {
      if (test.rule.pxa[i].indexOf(obj.px) > -1)
      {
        if (!(i in award.stat)) award.stat[i] = newAwardCountObject();
        return workAwardObject(
          award.stat[i],
          obj.band,
          obj.mode,
          obj.digital,
          obj.phone
        );
      }
    }
  }
  return false;
}

function testApxa(award, obj, baseHash)
{
  if (obj.px)
  {
    let test = CR.awards[award.sponsor].awards[award.name];
    for (const i in test.rule.pxa)
    {
      if (test.rule.pxa[i].indexOf(obj.px) > -1)
      {
        if (String(obj.px) + baseHash in CR.tracker[award.test.look].px)
        {
          return false;
        }
        else
        {
          return true;
        }
      }
    }
  }
  return false;
}

function scoreAsfx(award, obj)
{
  let test = CR.awards[award.sponsor].awards[award.name];
  let suf = obj.DEcall.replace(obj.px, "");
  for (const i in test.rule.sfx)
  {
    for (const s in test.rule.sfx[i])
    {
      if (suf.indexOf(test.rule.sfx[i][s]) == 0)
      {
        if (!(i in award.stat)) award.stat[i] = newAwardCountObject();
        return workAwardObject(
          award.stat[i],
          obj.band,
          obj.mode,
          obj.digital,
          obj.phone
        );
      }
    }
  }

  return false;
}

function testAsfx(award, obj, baseHash)
{
  let test = CR.awards[award.sponsor].awards[award.name];
  let suf = obj.DEcall.replace(obj.px, "");
  for (const i in test.rule.sfx)
  {
    for (const s in test.rule.sfx[i])
    {
      if (suf.indexOf(test.rule.sfx[i][s]) == 0)
      {
        return false;
      }
    }
  }

  return true;
}

function scoreAcalls2dxcc(award, obj)
{
  if (!(obj.dxcc in award.stat)) award.stat[obj.dxcc] = newAwardCountObject();

  return workAwardObject(
    award.stat[obj.dxcc],
    obj.band,
    obj.mode,
    obj.digital,
    obj.phone,
    obj.DEcall
  );
}

function testAcalls2dxcc(award, obj, baseHash)
{
  if (obj.DEcall + baseHash in CR.tracker[award.test.look].call)
  {
    return false;
  }
  return true;
}

function scoreAcalls2band(award, obj)
{
  if (!(obj.band in award.stat)) award.stat[obj.band] = newAwardCountObject();

  return workAwardObject(
    award.stat[obj.band],
    obj.band,
    obj.mode,
    obj.digital,
    obj.phone,
    obj.DEcall
  );
}

function testAcalls2band(award, obj, baseHash)
{
  if (obj.DEcall + baseHash in CR.tracker[award.test.look].call)
  {
    return false;
  }
  return true;
}

function scoreAdxcc2band(award, obj)
{
  if (!(obj.band in award.stat)) award.stat[obj.band] = newAwardCountObject();

  return workAwardObject(
    award.stat[obj.band],
    obj.band,
    obj.mode,
    obj.digital,
    obj.phone,
    obj.dxcc
  );
}

function testAdxcc2band(award, obj, baseHash)
{
  if (String(obj.dxcc) + "|" + baseHash in CR.tracker[award.test.look].dxcc)
  {
    return false;
  }
  return true;
}

function scoreAcqz(award, obj)
{
  if (obj.cqz)
  {
    if (!(obj.cqz in award.stat)) award.stat[obj.cqz] = newAwardCountObject();

    return workAwardObject(
      award.stat[obj.cqz],
      obj.band,
      obj.mode,
      obj.digital,
      obj.phone
    );
  }
  return false;
}

function testAcqz(award, obj, baseHash)
{
  // calls with empty cqz will not match anything in the hash map. so filter those out
  if (!obj.cqz || obj.cqz + "|" + baseHash in CR.tracker[award.test.look].cqz)
  {
    return false;
  }
  return true;
}

function scoreAnumsfx(award, obj)
{
  if (obj.px)
  {
    let test = CR.awards[award.sponsor].awards[award.name];
    let px = obj.px.substr(0, obj.px.length - 1);
    let suf = obj.DEcall.replace(px, "");
    suf = suf.substr(0, test.rule.numsfx[0][0].length);
    for (const i in test.rule.numsfx)
    {
      for (const s in test.rule.numsfx[i])
      {
        if (suf.indexOf(test.rule.numsfx[i][s]) == 0)
        {
          if (!(i in award.stat)) award.stat[i] = newAwardCountObject();
          return workAwardObject(
            award.stat[i],
            obj.band,
            obj.mode,
            obj.digital,
            obj.phone
          );
        }
      }
    }
  }
  return false;
}

function testAnumsfx(award, obj)
{
  if (obj.px)
  {
    let test = CR.awards[award.sponsor].awards[award.name];
    let px = obj.px.substr(0, obj.px.length - 1);
    let suf = obj.DEcall.replace(px, "");
    suf = suf.substr(0, test.rule.numsfx[0][0].length);
    for (const i in test.rule.numsfx)
    {
      for (const s in test.rule.numsfx[i])
      {
        if (suf.indexOf(test.rule.numsfx[i][s]) == 0)
        {
          return false;
        }
      }
    }
  }

  return true;
}

function scoreApxplus(award, obj)
{
  let test = CR.awards[award.sponsor].awards[award.name];

  if (test.rule.pxplus)
  {
    for (const i in test.rule.pxplus)
    {
      if (obj.DEcall.indexOf(test.rule.pxplus[i]) == 0)
      {
        if (!(i in award.stat)) award.stat[i] = newAwardCountObject();
        return workAwardObject(
          award.stat[i],
          obj.band,
          obj.mode,
          obj.digital,
          obj.phone
        );
      }
    }
  }
  return false;
}

function testApxplus(award, obj)
{
  let test = CR.awards[award.sponsor].awards[award.name];

  if (test.rule.pxplus)
  {
    for (const i in test.rule.pxplus)
    {
      if (obj.DEcall.indexOf(test.rule.pxplus[i]) == 0)
      {
        return false;
      }
    }
  }
  return true;
}

function loadAwardJson()
{
  CR.awards = {};
  let fs = require("fs");
  if (fs.existsSync("./data/awards.json"))
  {
    fileBuf = fs.readFileSync("./data/awards.json");
    try
    {
      CR.awards = JSON.parse(fileBuf);
      // fs.writeFileSync("./data/awards.json", JSON.stringify(CR.awards,null,2));

      for (const sp in CR.awards)
      {
        for (const aw in CR.awards[sp].awards)
        {
          if (!("unique" in CR.awards[sp].awards[aw].rule))
          { CR.awards[sp].awards[aw].rule.unique = 1; }

          if (CR.awards[sp].awards[aw].rule.band[0] == "Mixed")
          {
            CR.awards[sp].awards[aw].rule.band.shift();
          }

          if (CR.awards[sp].awards[aw].rule.band.length == 0)
          {
            CR.awards[sp].awards[aw].rule.band = [];
            for (let key in CR.awards[sp].mixed)
            {
              CR.awards[sp].awards[aw].rule.band.push(CR.awards[sp].mixed[key]);
            }
          }
          if (
            CR.awards[sp].awards[aw].rule.endorse.length == 1 &&
            CR.awards[sp].awards[aw].rule.endorse[0] == "Mixed"
          )
          {
            CR.awards[sp].awards[aw].rule.endorse = [];
            for (let key in CR.awards[sp].mixed)
            {
              CR.awards[sp].awards[aw].rule.endorse.push(
                CR.awards[sp].mixed[key]
              );
            }
          }
        }
      }

      buildAwardTypeHandlers();
    }
    catch (e)
    {
      alert("Core awards.json : " + e);
      CR.awards = {};
    }
  }
  else alert("Missing core awards.json");
}

function processAllAwardTrackers()
{
  for (let tracker in CR.awardTracker)
  {
    if (!(CR.awardTracker[tracker].sponsor in CR.awards))
    {
      delete CR.awardTracker[tracker];
      continue;
    }
    if (
      !(
        CR.awardTracker[tracker].name in
        CR.awards[CR.awardTracker[tracker].sponsor].awards
      )
    )
    {
      delete CR.awardTracker[tracker];
      continue;
    }
    processAward(tracker);
  }
  updateAwardList();
}

function newAwardTrackerObject(sponsor, award, enable)
{
  let newAward = {};
  newAward.sponsor = sponsor;
  newAward.name = award;
  newAward.enable = enable;
  newAward.mode = CR.awards[sponsor].awards[award].rule.mode[0];
  newAward.band = CR.awards[sponsor].awards[award].rule.band[0];
  newAward.count = CR.awards[sponsor].awards[award].rule.count[0];
  newAward.stat = {};
  newAward.comp = {};
  newAward.test = {};
  return newAward;
}

function addAllAwards()
{
  for (let sponsor in CR.awards)
  {
    for (let award in CR.awards[sponsor].awards)
    {
      let awardToAdd = newAwardTrackerObject(sponsor, award, true);

      let hash = awardToAdd.name + "-" + awardToAdd.sponsor;
      if (!(hash in CR.awardTracker))
      {
        CR.awardTracker[hash] = awardToAdd;
        processAward(hash);
        storeAwardTracker();
      }
    }
  }
  updateAwardList();
  viewRoster();
}

function delAllAwards()
{
  CR.awardTracker = {};
  storeAwardTracker();
  updateAwardList();
  viewRoster();
}

function newCompileCountObject()
{
  let compileCountObject = {};
  compileCountObject.bands = {};
  compileCountObject.modes = {};
  compileCountObject.endorse = {};
  compileCountObject.counts = {};
  return compileCountObject;
}

function singleCompile(award, obj)
{
  let test = CR.awards[award.sponsor].awards[award.name];
  let rule = test.rule;
  let comp = newCompileCountObject();
  for (let mode in rule.mode)
  {
    comp.modes[rule.mode[mode]] = 0;
    comp.bands[rule.mode[mode]] = {};

    for (let band in rule.band)
    {
      comp.bands[rule.mode[mode]][rule.band[band]] = 0;
    }
    for (let key in obj)
    {
      if (
        rule.mode[mode] in obj[key].bands &&
        Object.keys(obj[key].bands[rule.mode[mode]]).length
      )
      {
        comp.modes[rule.mode[mode]] += 1;

        for (let band in rule.band)
        {
          if (rule.band[band] in obj[key].bands[rule.mode[mode]])
          { comp.bands[rule.mode[mode]][rule.band[band]] += 1; }
        }
      }
    }
  }

  for (let mode in comp.modes)
  {
    comp.endorse[mode] = {};
    comp.counts[mode] = {};
    for (let cnts in rule.count)
    {
      comp.counts[mode][rule.count[cnts]] = {
        num: comp.modes[mode],
        per: parseInt(
          Math.min(100, (comp.modes[mode] / rule.count[cnts]) * 100.0)
        )
      };
    }

    for (let endorse in rule.endorse)
    {
      comp.endorse[mode][rule.endorse[endorse]] = {};
      for (let cnts in rule.count)
      {
        comp.endorse[mode][rule.endorse[endorse]][rule.count[cnts]] =
          comp.bands[mode][rule.endorse[endorse]] >= rule.count[cnts];
      }
    }
  }

  return comp;
}

function doubleCompile(award, firstLevel)
{
  let test = CR.awards[award.sponsor].awards[award.name];
  let rule = test.rule;

  for (let k in firstLevel)
  {
    firstLevel[k].bands = {};
    // firstLevel[k].modes = {};
    let obj = singleCompile(award, firstLevel[k].unique);

    for (let mode in obj.bands)
    {
      for (let cnt in test.rule.count)
      {
        if (obj.counts[mode][test.rule.count[cnt]].num >= test.rule.unique)
        {
          for (let band in obj.bands[mode])
          {
            if (!(mode in firstLevel[k].bands)) firstLevel[k].bands[mode] = {};

            if (obj.bands[mode][band] > 0)
            {
              firstLevel[k].bands[mode][band] =
                ~~firstLevel[k].bands[mode][band] + 1;
            }
          }
        }
      }
    }
    /* for ( let mode in obj.modes )
    {
      if ( !(mode in firstLevel[k].modes) )
        firstLevel[k].modes[mode] = 0;
      if ( obj.modes[mode] > 0 )
        firstLevel[k].modes[mode] +=  1;
    } */

    delete firstLevel[k].unique;
    firstLevel[k].unique = null;
  }

  return singleCompile(award, firstLevel);
}

function listShortInstances()
{
  let shortInstances = [];
  if (typeof window.opener.GT.instancesIndex != "undefined" && typeof window.opener.GT.instances != "undefined")
  {
    if (window.opener.GT.instancesIndex.length > 1)
    {
      let instances = window.opener.GT.instances;
      let keys = Object.keys(instances).sort();
      for (let key in keys)
      {
        let inst = keys[key];
        let sp = inst.split(" - ");
        let shortInst = sp[sp.length - 1].substring(0, 18);
        shortInstances.push(shortInst);
      }
    }
  }
  return shortInstances;
}
