// @ts-strict-ignore
import React, { createContext, useEffect, useMemo, useContext } from 'react';

import LRU from 'lru-cache';

import { listen, send } from '../platform/client/fetch';

type SpreadsheetContextValue = ReturnType<typeof makeSpreadsheet>;
const SpreadsheetContext = createContext<SpreadsheetContextValue>(undefined);

export function useSpreadsheet() {
  return useContext(SpreadsheetContext);
}

function makeSpreadsheet() {
  const cellObservers = {};
  const LRUValueCache = new LRU({ max: 1200 });
  const cellCache = {};
  let observersDisabled = false;

  class Spreadsheet {
    observeCell(name, cb) {
      if (!cellObservers[name]) {
        cellObservers[name] = [];
      }
      cellObservers[name].push(cb);

      return () => {
        cellObservers[name] = cellObservers[name].filter(x => x !== cb);

        if (cellObservers[name].length === 0) {
          cellCache[name] = null;
        }
      };
    }

    disableObservers() {
      observersDisabled = true;
    }

    enableObservers() {
      observersDisabled = false;
    }

    prewarmCache(name, value) {
      LRUValueCache.set(name, value);
    }

    listen() {
      return listen('cells-changed', function (nodes) {
        if (!observersDisabled) {
          // TODO: batch react so only renders once
          nodes.forEach(node => {
            const observers = cellObservers[node.name];
            if (observers) {
              observers.forEach(func => func(node));
              cellCache[node.name] = Promise.resolve(node);
              LRUValueCache.set(node.name, node);
            }
          });
        }
      });
    }

    bind(sheetName = '__global', binding, fields, cb) {
      binding =
        typeof binding === 'string' ? { name: binding, value: null } : binding;

      const resolvedName = `${sheetName}!${binding.name}`;
      const cleanup = this.observeCell(resolvedName, cb);

      // Always synchronously call with the existing value if it has one.
      // This is a display optimization to avoid flicker. The LRU cache
      // will keep a number of recent nodes in memory.
      if (LRUValueCache.has(resolvedName)) {
        cb(LRUValueCache.get(resolvedName));
      }

      if (cellCache[resolvedName] != null) {
        cellCache[resolvedName].then(cb);
      } else {
        const req = this.get(sheetName, binding.name);
        cellCache[resolvedName] = req;

        req.then(result => {
          // We only want to call the callback if it's still waiting on
          // the same request. If we've received a `cells-changed` event
          // for this already then it's already been called and we don't
          // need to call it again (and potentially could be calling it
          // with an old value depending on the order of messages)
          if (cellCache[resolvedName] === req) {
            LRUValueCache.set(resolvedName, result);
            cb(result);
          }
        });
      }

      return cleanup;
    }

    get(sheetName, name) {
      return send('getCell', { sheetName, name });
    }

    getCellNames(sheetName) {
      return send('getCellNamesInSheet', { sheetName });
    }

    createQuery(sheetName, name, query) {
      return send('create-query', {
        sheetName,
        name,
        query: query.serialize(),
      });
    }
  }

  return new Spreadsheet();
}

export function SpreadsheetProvider({ children }) {
  const spreadsheet = useMemo(() => makeSpreadsheet(), []);

  useEffect(() => {
    return spreadsheet.listen();
  }, [spreadsheet]);

  return (
    <SpreadsheetContext.Provider value={spreadsheet}>
      {children}
    </SpreadsheetContext.Provider>
  );
}
