import type { Position } from 'css-box-model';
import { invariant } from '../../invariant';
import type {
  DimensionMarshal,
  Callbacks,
  StartPublishingResult,
} from './dimension-marshal-types';
import createPublisher from './while-dragging-publisher';
import type { WhileDraggingPublisher } from './while-dragging-publisher';
import getInitialPublish from './get-initial-publish';
import type {
  Registry,
  DroppableEntry,
  DraggableEntry,
  Subscriber,
  Unsubscribe,
  RegistryEvent,
} from '../registry/registry-types';
import type {
  DroppableId,
  DroppableDescriptor,
  LiftRequest,
  Critical,
  DraggableDescriptor,
} from '../../types';
import { warning } from '../../dev-warning';

interface Collection {
  critical: Critical;
  unsubscribe: Unsubscribe;
}

function shouldPublishUpdate(
  registry: Registry,
  dragging: DraggableDescriptor,
  entry: DraggableEntry,
): boolean {
  // do not publish updates for the critical draggable
  if (entry.descriptor.id === dragging.id) {
    return false;
  }
  // do not publish updates for draggables that are not of a type that we care about
  if (entry.descriptor.type !== dragging.type) {
    return false;
  }

  const home: DroppableEntry = registry.droppable.getById(
    entry.descriptor.droppableId,
  );

  if (home.descriptor.mode !== 'virtual') {
    warning(`
      You are attempting to add or remove a Draggable [id: ${entry.descriptor.id}]
      while a drag is occurring. This is only supported for virtual lists.

      See https://github.com/hello-pangea/dnd/blob/main/docs/patterns/virtual-lists.md
    `);
    return false;
  }

  return true;
}

export default (registry: Registry, callbacks: Callbacks) => {
  let collection: Collection | null = null;

  const publisher: WhileDraggingPublisher = createPublisher({
    callbacks: {
      publish: callbacks.publishWhileDragging,
      collectionStarting: callbacks.collectionStarting,
    },
    registry,
  });

  const updateDroppableIsEnabled = (id: DroppableId, isEnabled: boolean) => {
    invariant(
      registry.droppable.exists(id),
      `Cannot update is enabled flag of Droppable ${id} as it is not registered`,
    );

    // no need to update the application state if a collection is not occurring
    if (!collection) {
      return;
    }

    // At this point a non primary droppable dimension might not yet be published
    // but may have its enabled state changed. For now we still publish this change
    // and let the reducer exit early if it cannot find the dimension in the state.
    callbacks.updateDroppableIsEnabled({ id, isEnabled });
  };

  const updateDroppableIsCombineEnabled = (
    id: DroppableId,
    isCombineEnabled: boolean,
  ) => {
    // no need to update
    if (!collection) {
      return;
    }

    invariant(
      registry.droppable.exists(id),
      `Cannot update isCombineEnabled flag of Droppable ${id} as it is not registered`,
    );

    callbacks.updateDroppableIsCombineEnabled({ id, isCombineEnabled });
  };

  const updateDroppableScroll = (id: DroppableId, newScroll: Position) => {
    // no need to update the application state if a collection is not occurring
    if (!collection) {
      return;
    }

    invariant(
      registry.droppable.exists(id),
      `Cannot update the scroll on Droppable ${id} as it is not registered`,
    );

    callbacks.updateDroppableScroll({ id, newScroll });
  };

  const scrollDroppable = (id: DroppableId, change: Position) => {
    if (!collection) {
      return;
    }
    registry.droppable.getById(id).callbacks.scroll(change);
  };

  const stopPublishing = () => {
    // This function can be called defensively
    if (!collection) {
      return;
    }
    // Stop any pending dom collections or publish
    publisher.stop();

    // Tell all droppables to stop watching scroll
    // all good if they where not already listening
    const home: DroppableDescriptor = collection.critical.droppable;
    registry.droppable
      .getAllByType(home.type)
      .forEach((entry: DroppableEntry) => entry.callbacks.dragStopped());

    // Unsubscribe from registry updates
    collection.unsubscribe();
    // Finally - clear our collection
    collection = null;
  };

  const subscriber: Subscriber = (event: RegistryEvent) => {
    invariant(
      collection,
      'Should only be subscribed when a collection is occurring',
    );
    // The dragging item can be add and removed when using a clone
    // We do not publish updates for the critical item
    const dragging: DraggableDescriptor = collection.critical.draggable;

    if (event.type === 'ADDITION') {
      if (shouldPublishUpdate(registry, dragging, event.value)) {
        publisher.add(event.value);
      }
    }
    if (event.type === 'REMOVAL') {
      if (shouldPublishUpdate(registry, dragging, event.value)) {
        publisher.remove(event.value);
      }
    }
  };

  const startPublishing = (request: LiftRequest): StartPublishingResult => {
    invariant(
      !collection,
      'Cannot start capturing critical dimensions as there is already a collection',
    );
    const entry: DraggableEntry = registry.draggable.getById(
      request.draggableId,
    );
    const home: DroppableEntry = registry.droppable.getById(
      entry.descriptor.droppableId,
    );

    const critical: Critical = {
      draggable: entry.descriptor,
      droppable: home.descriptor,
    };

    const unsubscribe = registry.subscribe(subscriber);

    collection = {
      critical,
      unsubscribe,
    };

    return getInitialPublish({
      critical,
      registry,
      scrollOptions: request.scrollOptions,
    });
  };

  const marshal: DimensionMarshal = {
    // Droppable changes
    updateDroppableIsEnabled,
    updateDroppableIsCombineEnabled,
    scrollDroppable,
    updateDroppableScroll,

    // Entry
    startPublishing,
    stopPublishing,
  };

  return marshal;
};
