import type { Position } from 'css-box-model';
import type {
  DraggableId,
  DroppableId,
  DraggableDescriptor,
  Published,
  DraggableDimension,
  DroppablePublish,
  DroppableIdMap,
  DraggableIdMap,
} from '../../types';
import type {
  DroppableEntry,
  Registry,
  DraggableEntry,
  DraggableEntryMap,
} from '../registry/registry-types';
import * as timings from '../../debug/timings';
import { origin } from '../position';

export interface WhileDraggingPublisher {
  add: (entry: DraggableEntry) => void;
  remove: (entry: DraggableEntry) => void;
  stop: () => void;
}

interface Staging {
  additions: DraggableEntryMap;
  removals: DraggableIdMap;
  modified: DroppableIdMap;
}

interface Callbacks {
  publish: (args: Published) => unknown;
  collectionStarting: () => unknown;
}

interface Args {
  registry: Registry;
  callbacks: Callbacks;
}

const clean = (): Staging => ({
  additions: {},
  removals: {},
  modified: {},
});

const timingKey = 'Publish collection from DOM';

export default function createPublisher({
  registry,
  callbacks,
}: Args): WhileDraggingPublisher {
  let staging: Staging = clean();
  let frameId: AnimationFrameID | null = null;

  const collect = () => {
    if (frameId) {
      return;
    }

    callbacks.collectionStarting();
    frameId = requestAnimationFrame(() => {
      frameId = null;
      timings.start(timingKey);

      const { additions, removals, modified } = staging;

      const added: DraggableDimension[] = Object.keys(additions)
        .map(
          // Using the origin as the window scroll. This will be adjusted when processing the published values
          (id: DraggableId): DraggableDimension =>
            registry.draggable.getById(id).getDimension(origin),
        )
        // Dimensions are not guarenteed to be ordered in the same order as keys
        // So we need to sort them so they are in the correct order
        .sort(
          (a: DraggableDimension, b: DraggableDimension): number =>
            a.descriptor.index - b.descriptor.index,
        );

      const updated: DroppablePublish[] = Object.keys(modified).map(
        (id: DroppableId) => {
          const entry: DroppableEntry = registry.droppable.getById(id);

          const scroll: Position = entry.callbacks.getScrollWhileDragging();
          return {
            droppableId: id,
            scroll,
          };
        },
      );

      const result: Published = {
        additions: added,
        removals: Object.keys(removals),
        modified: updated,
      };

      staging = clean();

      timings.finish(timingKey);
      callbacks.publish(result);
    });
  };

  const add = (entry: DraggableEntry) => {
    const id: DraggableId = entry.descriptor.id;
    staging.additions[id] = entry;
    staging.modified[entry.descriptor.droppableId] = true;

    if (staging.removals[id]) {
      delete staging.removals[id];
    }
    collect();
  };

  const remove = (entry: DraggableEntry) => {
    const descriptor: DraggableDescriptor = entry.descriptor;
    staging.removals[descriptor.id] = true;
    staging.modified[descriptor.droppableId] = true;

    if (staging.additions[descriptor.id]) {
      delete staging.additions[descriptor.id];
    }
    collect();
  };

  const stop = () => {
    if (!frameId) {
      return;
    }

    cancelAnimationFrame(frameId);
    frameId = null;
    staging = clean();
  };

  return {
    add,
    remove,
    stop,
  };
}
