import { first } from 'rxjs/operators';

import { type Placement } from 'app/plugins/panel/canvas/panelcfg.gen';
import { LayerActionID } from 'app/plugins/panel/canvas/types';

import { type ElementState } from './element';
import { FrameState } from './frame';
import { type RootElement } from './root';
import { type Scene } from './scene';

// TODO: Consider whether or not reorderElements + updateElements should be moved to TreeNavigationEditor
// Reorder elements in the DOM when rearranging them in the tree navigation editor
export const reorderElements = (src: ElementState, dest: ElementState, dragToGap: boolean, destPosition: number) => {
  switch (dragToGap) {
    case true:
      switch (destPosition) {
        case -1:
          // top of the tree
          if (src.parent instanceof FrameState) {
            // move outside the frame
            if (dest.parent) {
              updateElements(src, dest.parent, dest.parent.elements.length);
              src.updateData(dest.parent.scene.context);
            }
          } else {
            dest.parent?.reorderTree(src, dest, true);
          }
          break;
        default:
          if (dest.parent) {
            updateElements(src, dest.parent, dest.parent.elements.indexOf(dest));
            src.updateData(dest.parent.scene.context);
          }
          break;
      }
      break;
    case false:
      if (dest instanceof FrameState) {
        if (src.parent === dest) {
          // same frame parent
          src.parent?.reorderTree(src, dest, true);
        } else {
          updateElements(src, dest);
          src.updateData(dest.scene.context);
        }
      } else if (src.parent === dest.parent) {
        src.parent?.reorderTree(src, dest);
      } else {
        if (dest.parent) {
          updateElements(src, dest.parent);
          src.updateData(dest.parent.scene.context);
        }
      }
      break;
  }
};

// Reorders canvas elements
const updateElements = (src: ElementState, dest: FrameState | RootElement, idx: number | null = null) => {
  src.parent?.doAction(LayerActionID.Delete, src);
  src.parent = dest;

  const elementContainer = src.div?.getBoundingClientRect();
  src.setPlacementFromConstraint(elementContainer, dest.div?.getBoundingClientRect());

  const destIndex = idx ?? dest.elements.length - 1;
  dest.elements.splice(destIndex, 0, src);
  dest.scene.save();

  dest.reinitializeMoveable();
};

// Finds the element state if it exists for a given DOM element
export const findElementByTarget = (target: Element, sceneElements: ElementState[]): ElementState | undefined => {
  // We will probably want to add memoization to this as we are calling on drag / resize

  const stack = [...sceneElements];
  while (stack.length > 0) {
    const currentElement = stack.shift();

    if (currentElement && currentElement.div && currentElement.div === target) {
      return currentElement;
    }

    const nestedElements = currentElement instanceof FrameState ? currentElement.elements : [];
    for (const nestedElement of nestedElements) {
      stack.unshift(nestedElement);
    }
  }

  return undefined;
};

// Nest selected elements into a frame object
export const frameSelection = (scene: Scene) => {
  scene.selection.pipe(first()).subscribe((currentSelectedElements) => {
    const currentLayer = currentSelectedElements[0].parent!;

    const newLayer = new FrameState(
      {
        type: 'frame',
        name: scene.getNextElementName(true),
        elements: [],
      },
      scene,
      currentSelectedElements[0].parent
    );

    const framePlacement = generateFrameContainer(currentSelectedElements);

    newLayer.options.placement = framePlacement;

    currentSelectedElements.forEach((element: ElementState) => {
      const elementContainer = element.div?.getBoundingClientRect();

      // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
      element.setPlacementFromConstraint(elementContainer, framePlacement as DOMRect);
      currentLayer.doAction(LayerActionID.Delete, element);
      newLayer.doAction(LayerActionID.Duplicate, element, false, false);
    });

    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
    newLayer.setPlacementFromConstraint(framePlacement as DOMRect, currentLayer.div?.getBoundingClientRect());

    currentLayer.elements.push(newLayer);

    scene.byName.set(newLayer.getName(), newLayer);

    scene.save();
  });
};

// Helper function to generate the a frame object based on the selected elements' dimensions
const generateFrameContainer = (elements: ElementState[]): Placement => {
  let minTop = Infinity;
  let minLeft = Infinity;
  let maxRight = 0;
  let maxBottom = 0;

  elements.forEach((element: ElementState) => {
    const elementContainer = element.div?.getBoundingClientRect();

    if (!elementContainer) {
      return;
    }

    if (minTop > elementContainer.top) {
      minTop = elementContainer.top;
    }

    if (minLeft > elementContainer.left) {
      minLeft = elementContainer.left;
    }

    if (maxRight < elementContainer.right) {
      maxRight = elementContainer.right;
    }

    if (maxBottom < elementContainer.bottom) {
      maxBottom = elementContainer.bottom;
    }
  });

  return {
    top: minTop,
    left: minLeft,
    width: maxRight - minLeft,
    height: maxBottom - minTop,
  };
};
