text: function ready(fn) {
    if (document.readyState !== 'loading'){
      fn();
    } else {
      document.addEventListener('DOMContentLoaded', fn);
    }
  }
  
  function addDemo(row) {
    if (!row.Issued && !row.Due) {
      for (const key of ['Number', 'Issued', 'Due']) {
        if (!row[key]) { row[key] = key; }
      }
      for (const key of ['Subtotal', 'Deduction', 'Taxes', 'Total']) {
        if (!(key in row)) { row[key] = key; }
      }
      if (!('Note' in row)) { row.Note = '(Anything in a Note column goes here)'; }
    }
    if (!row.Invoicer) {
      row.Invoicer = {
        Name: 'Invoicer.Name',
        Street1: 'Invoicer.Street1',
        Street2: 'Invoicer.Street2',
        City: 'Invoicer.City',
        State: '.State',
        Zip: '.Zip',
        Email: 'Invoicer.Email',
        Phone: 'Invoicer.Phone',
        Website: 'Invoicer.Website'
      }
    }
    if (!row.Client) {
      row.Client = {
        Name: 'Client.Name',
        Street1: 'Client.Street1',
        Street2: 'Client.Street2',
        City: 'Client.City',
        State: '.State',
        Zip: '.Zip'
      }
    }
    if (!row.Items) {
      row.Items = [
        {
          Description: 'Items[0].Description',
          Quantity: '.Quantity',
          Total: '.Total',
          Price: '.Price',
        },
        {
          Description: 'Items[1].Description',
          Quantity: '.Quantity',
          Total: '.Total',
          Price: '.Price',
        },
      ];
    }
    return row;
  }
  
  const data = {
    count: 0,
    invoice: '',
    status: 'waiting',
    tableConnected: false,
    rowConnected: false,
    haveRows: false,
  };
  let app = undefined;
  
  Vue.filter('currency', formatNumberAsUSD)
  function formatNumberAsUSD(value) {
    if (!value) { return '—'; }
    const result = Number(value).toLocaleString('en', {
      style: 'currency', currency: 'USD'
    })
    if (result.includes('NaN')) {
      return value;
    }
    return result;
  }
  
  Vue.filter('fallback', function(value, str) {
    if (!value) {
      throw new Error("Please provide column " + str);
    }
    return value;
  });
  
  Vue.filter('asDate', function(value) {
    if (typeof(value) === 'number') {
      value = new Date(value * 1000);
    }
    const date = moment.utc(value)
    return date.isValid() ? date.format('MMMM DD, YYYY') : value;
  });
  
  function tweakUrl(url) {
    if (!url) { return url; }
    if (url.toLowerCase().startsWith('http')) {
      return url;
    }
    return 'https://' + url;
  };
  
  function handleError(err) {
    console.error(err);
    const target = app || data;
    target.invoice = '';
    target.status = String(err).replace(/^Error: /, '');
    console.log(data);
  }
  
  function prepareList(lst, order) {
    if (order) {
      let orderedLst = [];
      const remaining = new Set(lst);
      for (const key of order) {
        if (remaining.has(key)) {
          remaining.delete(key);
          orderedLst.push(key);
        }
      }
      lst = [...orderedLst].concat([...remaining].sort());
    } else {
      lst = [...lst].sort();
    }
    return lst;
  }
  
  function updateInvoice(row) {
    try {
      data.status = '';
      if (row === null) {
        throw new Error("(No data - not on row - please add or select a row)");
      }
      console.log("GOT...", JSON.stringify(row));
      if (row.References) {
        try {
          Object.assign(row, row.References);
        } catch (err) {
          throw new Error('Could not understand References column. ' + err);
        }
      }
  
      // Add some guidance about columns.
      const want = new Set(Object.keys(addDemo({})));
      const accepted = new Set(['References']);
      const importance = ['Number', 'Client', 'Items', 'Total', 'Invoicer', 'Due', 'Issued', 'Subtotal', 'Deduction', 'Taxes', 'Note'];
      if (!(row.Due || row.Issued)) {
        const seen = new Set(Object.keys(row).filter(k => k !== 'id' && k !== '_error_'));
        const help = row.Help = {};
        help.seen = prepareList(seen);
        const missing = [...want].filter(k => !seen.has(k));
        const ignoring = [...seen].filter(k => !want.has(k) && !accepted.has(k));
        const recognized = [...seen].filter(k => want.has(k) || accepted.has(k));
        if (missing.length > 0) {
          help.expected = prepareList(missing, importance);
        }
        if (ignoring.length > 0) {
          help.ignored = prepareList(ignoring);
        }
        if (recognized.length > 0) {
          help.recognized = prepareList(recognized);
        }
        if (!seen.has('References') && !(row.Issued || row.Due)) {
          row.SuggestReferencesColumn = true;
        }
      }
      addDemo(row);
      if (!row.Subtotal && !row.Total && row.Items && Array.isArray(row.Items)) {
        try {
          row.Subtotal = row.Items.reduce((a, b) => a + b.Price * b.Quantity, 0);
          row.Total = row.Subtotal;
        } catch (e) {
          console.error(e);
        }
      }
      if (row.Invoicer && row.Invoicer.Website && !row.Invoicer.Url) {
        row.Invoicer.Url = tweakUrl(row.Invoicer.Website);
      }
  
      // Fiddle around with updating Vue (I'm not an expert).
      for (const key of want) {
        Vue.delete(data.invoice, key);
      }
      for (const key of ['Help', 'SuggestReferencesColumn', 'References']) {
        Vue.delete(data.invoice, key);
      }
      data.invoice = Object.assign({}, data.invoice, row);
  
      // Make invoice information available for debugging.
      window.invoice = row;
    } catch (err) {
      handleError(err);
    }
  }
  
  ready(function() {
    // Update the invoice anytime the document data changes.
    grist.ready();
    grist.onRecord(updateInvoice);
  
    // Monitor status so we can give user advice.
    grist.on('message', msg => {
      // If we are told about a table but not which row to access, check the
      // number of rows.  Currently if the table is empty, and "select by" is
      // not set, onRecord() will never be called.
      if (msg.tableId && !app.rowConnected) {
        grist.docApi.fetchSelectedTable().then(table => {
          if (table.id && table.id.length >= 1) {
            app.haveRows = true;
          }
        }).catch(e => console.log(e));
      }
      if (msg.tableId) { app.tableConnected = true; }
      if (msg.tableId && !msg.dataChange) { app.RowConnected = true; }
    });
  
    Vue.config.errorHandler = function (err, vm, info)  {
      handleError(err);
    };
  
    app = new Vue({
      el: '#app',
      data: data
    });
  
    if (document.location.search.includes('demo')) {
      updateInvoice(exampleData);
    }
    if (document.location.search.includes('labels')) {
      updateInvoice({});
    }
  });
  