import { Arr } from '@ephox/katamari';
import { Insert, Remove, SugarBody } from '@ephox/sugar';

import type { AlloyComponent } from '../api/component/ComponentApi';
import type { AlloySpec } from '../api/component/SpecTypes';
import * as AlloyTriggers from '../api/events/AlloyTriggers';
import * as SystemEvents from '../api/events/SystemEvents';
import * as GuiTypes from '../api/ui/GuiTypes';

const isConnected = (comp: AlloyComponent) =>
  comp.getSystem().isConnected();

const fireDetaching = (component: AlloyComponent): void => {
  AlloyTriggers.emit(component, SystemEvents.detachedFromDom());
  const children = component.components();
  Arr.each(children, fireDetaching);
};

const fireAttaching = (component: AlloyComponent): void => {
  const children = component.components();
  Arr.each(children, fireAttaching);
  AlloyTriggers.emit(component, SystemEvents.attachedToDom());
};

// Unlike attach, a virtualAttach makes no actual DOM changes.
// This is because it should only be used in a situation
// where we are patching an existing element.
const virtualAttach = (parent: AlloyComponent, child: AlloyComponent): void => {
  // So we still add it to the world
  parent.getSystem().addToWorld(child);
  // And we fire attaching ONLY if it's already in the DOM
  if (SugarBody.inBody(parent.element)) {
    fireAttaching(child);
  }
};

// Unlike detach, a virtualDetach makes no actual DOM changes.
// This is because it's used in patching circumstances.
const virtualDetach = (comp: AlloyComponent): void => {
  fireDetaching(comp);
  comp.getSystem().removeFromWorld(comp);
};

const attach = (parent: AlloyComponent, child: AlloyComponent): void => {
  Insert.append(parent.element, child.element);
};

const detachChildren = (component: AlloyComponent): void => {
  // This will not detach the component, but will detach its children and sync at the end.
  Arr.each(component.components(), (childComp) => Remove.remove(childComp.element));
  // Clear the component also.
  Remove.empty(component.element);
  component.syncComponents();
};

const replaceChildren = (component: AlloyComponent, newSpecs: AlloySpec[], buildNewChildren: (newSpecs: AlloySpec[]) => AlloyComponent[]): void => {
  // Detach all existing children
  const subs = component.components();
  detachChildren(component);

  const newChildren = buildNewChildren(newSpecs);

  // Determine which components have been deleted and remove them from the world
  const deleted = Arr.difference(subs, newChildren);
  Arr.each(deleted, (comp) => {
    fireDetaching(comp);
    component.getSystem().removeFromWorld(comp);
  });

  // Add all new components
  Arr.each(newChildren, (childComp) => {
    // If the component isn't connected, ie is new, then we also need to add it to the world
    if (!isConnected(childComp)) {
      component.getSystem().addToWorld(childComp);
      attach(component, childComp);
      if (SugarBody.inBody(component.element)) {
        fireAttaching(childComp);
      }
    } else {
      attach(component, childComp);
    }
  });
  component.syncComponents();
};

const virtualReplaceChildren = (component: AlloyComponent, newSpecs: AlloySpec[], buildNewChildren: (newSpecs: AlloySpec[]) => AlloyComponent[]): void => {
  // When replacing we don't want to fire detachedFromDom and attachedToDom again for a premade that has just had its position in the children moved around,
  // so we only detach initially if we aren't a premade. Premades will be detached later, but only if they are no longer in the child list.
  const subs = component.components();
  const existingComps = Arr.bind(newSpecs, (spec) => GuiTypes.getPremade(spec).toArray());
  Arr.each(subs, (childComp) => {
    if (!Arr.contains(existingComps, childComp)) {
      virtualDetach(childComp);
    }
  });

  const newChildren = buildNewChildren(newSpecs);

  // Determine which components have been deleted and remove them from the world
  // It's probable the component has already been detached beforehand so only
  // detach what's still attached to the world (i.e removed premades)
  const deleted = Arr.difference(subs, newChildren);
  Arr.each(deleted, (deletedComp) => {
    if (isConnected(deletedComp)) {
      virtualDetach(deletedComp);
    }
  });

  // Add all new components
  Arr.each(newChildren, (childComp) => {
    // If the component isn't connected, ie is new, then we also need to add it to the world
    if (!isConnected(childComp)) {
      virtualAttach(component, childComp);
    } else {
      // Already attached so do nothing
    }
  });
  component.syncComponents();
};

export {
  fireAttaching,
  fireDetaching,

  detachChildren,
  replaceChildren,

  virtualAttach,
  virtualDetach,
  virtualReplaceChildren
};
