import { invariant } from "../../invariant";
import { values } from "../../native-with-fallback";

export default function createRegistry() {
  const entries = {
    draggables: {},
    droppables: {}
  };
  const subscribers = [];

  function subscribe(cb) {
    subscribers.push(cb);
    return function unsubscribe() {
      const index = subscribers.indexOf(cb);

      // might have been removed by a clean
      if (index === -1) {
        return;
      }
      subscribers.splice(index, 1);
    };
  }

  function notify(event) {
    if (subscribers.length) {
      subscribers.forEach((cb) => cb(event));
    }
  }

  function findDraggableById(id) {
    return entries.draggables[id] || null;
  }

  function getDraggableById(id) {
    const entry = findDraggableById(id);
    invariant(entry, `Cannot find draggable entry with id [${id}]`);
    return entry;
  }

  const draggableAPI = {
    register: (entry) => {
      entries.draggables[entry.descriptor.id] = entry;
      notify({
        type: "ADDITION",
        value: entry
      });
    },
    update: (entry, last) => {
      const current = entries.draggables[last.descriptor.id];

      // item already removed
      if (!current) {
        return;
      }

      // id already used for another mount
      if (current.uniqueId !== entry.uniqueId) {
        return;
      }

      // We are safe to delete the old entry and add a new one
      delete entries.draggables[last.descriptor.id];
      entries.draggables[entry.descriptor.id] = entry;
    },
    unregister: (entry) => {
      const draggableId = entry.descriptor.id;
      const current = findDraggableById(draggableId);

      // can occur if cleaned before unregistration
      if (!current) {
        return;
      }

      // outdated uniqueId
      if (entry.uniqueId !== current.uniqueId) {
        return;
      }
      delete entries.draggables[draggableId];
      notify({
        type: "REMOVAL",
        value: entry
      });
    },
    getById: getDraggableById,
    findById: findDraggableById,
    exists: (id) => Boolean(findDraggableById(id)),
    getAllByType: (type) => values(entries.draggables).filter((entry) => entry.descriptor.type === type)
  };

  function findDroppableById(id) {
    return entries.droppables[id] || null;
  }

  function getDroppableById(id) {
    const entry = findDroppableById(id);
    invariant(entry, `Cannot find droppable entry with id [${id}]`);
    return entry;
  }

  const droppableAPI = {
    register: (entry) => {
      entries.droppables[entry.descriptor.id] = entry;
    },
    unregister: (entry) => {
      const current = findDroppableById(entry.descriptor.id);

      // can occur if cleaned before an unregistry
      if (!current) {
        return;
      }

      // already changed
      if (entry.uniqueId !== current.uniqueId) {
        return;
      }
      delete entries.droppables[entry.descriptor.id];
    },
    getById: getDroppableById,
    findById: findDroppableById,
    exists: (id) => Boolean(findDroppableById(id)),
    getAllByType: (type) => values(entries.droppables).filter((entry) => entry.descriptor.type === type)
  };

  function clean() {
    // kill entries
    entries.draggables = {};
    entries.droppables = {};
    // remove all subscribers
    subscribers.length = 0;
  }

  return {
    draggable: draggableAPI,
    droppable: droppableAPI,
    subscribe,
    clean
  };
}
