import { CdkDrag, CdkDragEnd, CdkDragMove, DragDropModule, DragRef, moveItemInArray } from '@angular/cdk/drag-drop';
import { _getShadowRoot } from '@angular/cdk/platform';
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component, ElementRef, Input, input, output, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { CommercialPropertyListing } from '../../../../../bizmatch-server/src/models/db.model';
import { environment } from '../../../environments/environment';
@Component({
  selector: 'app-drag-drop-mixed',
  standalone: true,
  imports: [CommonModule, DragDropModule],
  templateUrl: './drag-drop-mixed.component.html',
  styleUrl: './drag-drop-mixed.component.scss',
})
export class DragDropMixedComponent {
  @ViewChild('_container') _container!: ElementRef<HTMLDivElement>;
  @ViewChildren(CdkDrag) _drags!: QueryList<CdkDrag>;
  @Input() ts: number;
  listing = input<CommercialPropertyListing>();
  imageOrderChanged = output<string[]>();
  imageToDelete = output<string>();
  env = environment;
  items: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9];
  private _cachedItems: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9];

  private _itemPositions: CachedItemPosition<DragRef>[] = [];
  private _rootNode: DocumentOrShadowRoot | undefined;
  private _activeItems: DragRef[] = [];
  private _previousSwap = {
    drag: null as DragRef | null,
    deltaX: 0,
    deltaY: 0,
    overlaps: false,
  };
  private _containerStyle: CSSStyleDeclaration | null = null;
  public isAnimationActive = false;
  constructor(private cdr: ChangeDetectorRef) {}
  ngOnChanges() {
    this.items = this.listing()?.imageOrder;
    this._cachedItems = this.items.slice();
  }
  ngAfterViewInit() {
    // Führen Sie einen zusätzlichen Change Detection-Zyklus durch
    this.cdr.detectChanges();
  }
  getImageUrl(image: string): string {
    return `${this.env.imageBaseUrl}/pictures/property/${this.listing().imagePath}/${this.listing().serialId}/${image}?_ts=${this.ts}`;
  }

  dragStarted() {
    this.start();
  }

  dragMoved(event: CdkDragMove) {
    const item = event.source._dragRef;
    this.sort(item, event.pointerPosition.x, event.pointerPosition.y, event.delta);
  }

  dragEnded(event: CdkDragEnd) {
    this.imageOrderChanged.emit(this._cachedItems);
    this.reset();
  }

  start() {
    const dragRefs: DragRef[] = [];

    this._drags.forEach(drag => {
      dragRefs.push(drag._dragRef);
    });

    this._activeItems = dragRefs;
    this._cacheItemPosition();
    this.isAnimationActive = true;
  }

  sort(item: DragRef, pointerX: number, pointerY: number, pointerDelta: { x: number; y: number }) {
    const siblings = this._itemPositions.slice();
    const newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY);

    const previousSwap = this._previousSwap;

    if (newIndex === -1 || this._activeItems[newIndex] === item) {
      return;
    }

    const toSwapWith = this._activeItems[newIndex];

    if (previousSwap.drag === toSwapWith && previousSwap.overlaps && previousSwap.deltaX === pointerDelta.x && previousSwap.deltaY === pointerDelta.y) {
      return;
    }

    const previousIndex = this.getItemIndex(item);
    const siblingAtNewPosition = siblings[newIndex];
    const previousPosition = siblings[previousIndex].clientRect;
    const newPosition = siblingAtNewPosition.clientRect;

    const delta = this.getDelta(newPosition.top, previousPosition.top, pointerDelta);

    if (delta === 0) return;
    if (delta === 1 && previousIndex > newIndex) return;
    if (delta === -1 && previousIndex < newIndex) return;

    const startIndex = Math.min(previousIndex, newIndex);
    const endIndex = Math.max(previousIndex, newIndex);

    let itemPositions = this._itemPositions.slice();

    if (delta === 1) {
      for (let i = startIndex; i < endIndex; i++) {
        itemPositions = this._updateItemPosition(i, itemPositions, delta);

        const newIndex = i + 1;
        moveItemInArray(itemPositions, i, newIndex);
      }
    } else if (delta === -1) {
      for (let i = endIndex; i > startIndex; i--) {
        itemPositions = this._updateItemPosition(i, itemPositions, delta);

        const newIndex = i - 1;
        moveItemInArray(itemPositions, i, newIndex);
      }
    }

    const threshold = getMutableClientRect(this._container.nativeElement).right;

    let currentTop = itemPositions[0].clientRect.top;

    for (let i = 0; i < itemPositions.length; i++) {
      const itemPosition = itemPositions[i];
      if (Math.round(itemPosition.clientRect.right) > Math.round(threshold)) {
        const nextPosition = itemPositions[i + 1];
        if (nextPosition) {
          currentTop = nextPosition.clientRect.top;
        }
        itemPositions = this._updateItemPositionToDown(itemPositions, i);
      } else if (itemPosition.clientRect.top !== currentTop) {
        currentTop = itemPosition.clientRect.top;
        itemPositions = this._updateItemPositionToUp(itemPositions, i);
      }
    }

    const oldOrder = this._itemPositions.slice();
    this._itemPositions = itemPositions;
    moveItemInArray(this._activeItems, previousIndex, newIndex);
    moveItemInArray(this._cachedItems, previousIndex, newIndex);

    itemPositions.forEach((sibling, index) => {
      if (oldOrder[index] === sibling) {
        return;
      }

      const isDraggedItem = sibling.drag === item;
      if (isDraggedItem) return;
      const elementToOffset = sibling.drag.getRootElement();

      elementToOffset.style.transform = `translate3d(${Math.round(sibling.transform.x)}px, ${Math.round(sibling.transform.y)}px, 0)`;
    });

    previousSwap.deltaX = pointerDelta.x;
    previousSwap.deltaY = pointerDelta.y;
    previousSwap.drag = toSwapWith;
    previousSwap.overlaps = isInsideClientRect(newPosition, pointerX, pointerY);
  }

  reset() {
    // ignore animation
    this.isAnimationActive = false;
    const previousSwap = this._previousSwap;
    this.items = this._cachedItems.slice();
    this._activeItems.forEach(item => {
      item.reset();
    });
    this._itemPositions = [];
    this._activeItems = [];
    previousSwap.drag = null;
    previousSwap.deltaX = previousSwap.deltaY = 0;
    previousSwap.overlaps = false;
  }

  getItemIndex(item: DragRef): number {
    return this._activeItems.indexOf(item);
  }

  getDelta(newTop: number, previousTop: number, pointerDelta: { x: number; y: number }) {
    if (newTop === previousTop) {
      return pointerDelta.x;
    }

    return newTop > previousTop ? 1 : -1;
  }

  private _getRootNode(): DocumentOrShadowRoot {
    if (!this._rootNode) {
      this._rootNode = _getShadowRoot(this._container.nativeElement) || document;
    }
    return this._rootNode;
  }

  private _cacheItemPosition() {
    this._itemPositions = this._activeItems.map(drag => {
      const elementToMeasure = drag.getRootElement();
      return {
        drag,
        clientRect: getMutableClientRect(elementToMeasure),
        transform: {
          x: 0,
          y: 0,
        },
      };
    });

    this._containerStyle = getComputedStyle(this._container.nativeElement);
  }

  private _updateItemPosition(currentIndex: number, siblings: CachedItemPosition<DragRef>[], delta: number) {
    let siblingsUpdated = siblings.slice();
    const offsetVertical = this._getOffset(currentIndex, siblingsUpdated, delta, false);
    const offsetHorizontal = this._getOffset(currentIndex, siblingsUpdated, delta, true);

    const immediateIndex = currentIndex + delta * 1;
    const currentItem = siblingsUpdated[currentIndex];
    const immediateSibling = siblingsUpdated[immediateIndex];

    const currentItemUpdated: CachedItemPosition<DragRef> = {
      ...currentItem,
      clientRect: {
        ...currentItem.clientRect,
        x: currentItem.clientRect.x + offsetHorizontal.itemOffset,
        left: currentItem.clientRect.left + offsetHorizontal.itemOffset,
        right: currentItem.clientRect.right + offsetHorizontal.itemOffset,
        y: currentItem.clientRect.y + offsetVertical.itemOffset,
        top: currentItem.clientRect.top + offsetVertical.itemOffset,
        bottom: currentItem.clientRect.bottom + offsetVertical.itemOffset,
      },
      transform: {
        x: currentItem.transform.x + offsetHorizontal.itemOffset,
        y: currentItem.transform.y + offsetVertical.itemOffset,
      },
    };

    const immediateSiblingUpdated: CachedItemPosition<DragRef> = {
      ...immediateSibling,
      clientRect: {
        ...immediateSibling.clientRect,
        x: immediateSibling.clientRect.x + offsetHorizontal.siblingOffset,
        left: immediateSibling.clientRect.left + offsetHorizontal.siblingOffset,
        right: immediateSibling.clientRect.right + offsetHorizontal.siblingOffset,
        y: immediateSibling.clientRect.y + offsetVertical.siblingOffset,
        top: immediateSibling.clientRect.top + offsetVertical.siblingOffset,
        bottom: immediateSibling.clientRect.bottom + offsetVertical.siblingOffset,
      },
      transform: {
        x: immediateSibling.transform.x + offsetHorizontal.siblingOffset,
        y: immediateSibling.transform.y + offsetVertical.siblingOffset,
      },
    };

    if (offsetVertical.itemOffset !== offsetVertical.siblingOffset) {
      const offset = (currentItemUpdated.clientRect.right - immediateSibling.clientRect.right) * delta;
      const top = delta === 1 ? immediateSibling.clientRect.top : currentItem.clientRect.top;

      const ignoreItem = delta === 1 ? immediateSibling.drag : currentItem.drag;

      siblingsUpdated = this._updateItemPositionHorizontalOnRow(siblingsUpdated, top, offset, ignoreItem);
    }
    siblingsUpdated[currentIndex] = currentItemUpdated;
    siblingsUpdated[immediateIndex] = immediateSiblingUpdated;

    return siblingsUpdated;
  }

  private _updateItemPositionToUp(siblings: CachedItemPosition<DragRef>[], currentIndex: number) {
    let siblingsUpdated = siblings.slice();
    const immediateSibling = siblingsUpdated[currentIndex - 1];
    const currentItem = siblingsUpdated[currentIndex];

    const nextEmptySlotLeft = immediateSibling.clientRect.right + this.getContainerGapPixel();

    const threshold = getMutableClientRect(this._container.nativeElement).right;
    if (nextEmptySlotLeft + currentItem.clientRect.right - currentItem.clientRect.left <= threshold) {
      const offsetLeft = nextEmptySlotLeft - currentItem.clientRect.left;
      const offsetTop = immediateSibling.clientRect.top - currentItem.clientRect.top;

      const nextSibling = siblingsUpdated[currentIndex + 1];
      if (nextSibling) {
        const offset = currentItem.clientRect.left - nextSibling.clientRect.left;
        siblingsUpdated = this._updateItemPositionHorizontalOnRow(siblingsUpdated, currentItem.clientRect.top, offset, currentItem.drag);
      }

      siblingsUpdated[currentIndex] = {
        ...currentItem,
        clientRect: {
          ...currentItem.clientRect,
          x: nextEmptySlotLeft,
          left: nextEmptySlotLeft,
          right: currentItem.clientRect.right - currentItem.clientRect.left + nextEmptySlotLeft,
          y: immediateSibling.clientRect.y,
          top: immediateSibling.clientRect.top,
          bottom: currentItem.clientRect.bottom - currentItem.clientRect.top + immediateSibling.clientRect.top,
        },
        transform: {
          x: currentItem.transform.x + offsetLeft,
          y: currentItem.transform.y + offsetTop,
        },
      };
    }

    return siblingsUpdated;
  }

  private _updateItemPositionToDown(siblings: CachedItemPosition<DragRef>[], currentIndex: number) {
    let siblingsUpdated = siblings.slice();
    const currentItem = siblingsUpdated[currentIndex];
    const immediateSibling = siblingsUpdated[currentIndex + 1];
    let offsetLeft = 0;
    let offsetTop = 0;

    if (immediateSibling) {
      offsetLeft = immediateSibling.clientRect.left - currentItem.clientRect.left;
      offsetTop = immediateSibling.clientRect.top - currentItem.clientRect.top;
    } else {
      const firstSibling = siblings.find(item => item.clientRect.top === currentItem.clientRect.top);

      if (firstSibling) {
        offsetLeft = firstSibling.clientRect.left - currentItem.clientRect.left;
      }

      offsetTop = currentItem.clientRect.bottom - currentItem.clientRect.top + this.getContainerGapPixel();
    }

    const currentItemUpdated: CachedItemPosition<DragRef> = {
      ...currentItem,
      clientRect: {
        ...currentItem.clientRect,
        x: currentItem.clientRect.x + offsetLeft,
        left: currentItem.clientRect.left + offsetLeft,
        right: currentItem.clientRect.right + offsetLeft,
        y: currentItem.clientRect.y + offsetTop,
        top: currentItem.clientRect.top + offsetTop,
        bottom: currentItem.clientRect.bottom + offsetTop,
      },
      transform: {
        x: currentItem.transform.x + offsetLeft,
        y: currentItem.transform.y + offsetTop,
      },
    };

    if (immediateSibling) {
      const offset = currentItemUpdated.clientRect.right - immediateSibling.clientRect.left + this.getContainerGapPixel();

      siblingsUpdated = this._updateItemPositionHorizontalOnRow(siblingsUpdated, immediateSibling.clientRect.top, offset);
    }

    siblingsUpdated[currentIndex] = currentItemUpdated;
    return siblingsUpdated;
  }

  private _updateItemPositionHorizontalOnRow(siblings: CachedItemPosition<DragRef>[], top: number, offset: number, ignoreItem?: DragRef) {
    const siblingsUpdated = siblings.slice();

    siblingsUpdated
      .filter(item => (!ignoreItem || item.drag !== ignoreItem) && item.clientRect.top === top)
      .forEach(currentItem => {
        const index = siblingsUpdated.findIndex(item => item.drag === currentItem.drag);

        siblingsUpdated[index] = {
          ...siblingsUpdated[index],
          clientRect: {
            ...siblingsUpdated[index].clientRect,
            x: siblingsUpdated[index].clientRect.x + offset,
            left: siblingsUpdated[index].clientRect.left + offset,
            right: siblingsUpdated[index].clientRect.right + offset,
          },
          transform: {
            ...siblingsUpdated[index].transform,
            x: siblingsUpdated[index].transform.x + offset,
          },
        };
      });

    return siblingsUpdated;
  }

  private _getItemIndexFromPointerPosition(item: DragRef, pointerX: number, pointerY: number) {
    const elementAtPoints = this._getRootNode().elementsFromPoint(Math.floor(pointerX), Math.floor(pointerY));

    const elementAtPoint = elementAtPoints.find(element => {
      // ignore element is transiting
      const animations = element.getAnimations();
      const isTransitionRunning = animations.length > 0;

      return !isTransitionRunning && this._itemPositions.some(item => item.drag.getRootElement() === element) && element !== item.getRootElement();
    });

    const index = elementAtPoint
      ? this._itemPositions.findIndex(({ drag }) => {
          // Skip the item itself.
          if (drag === item) {
            return false;
          }

          const root = drag.getRootElement();
          return elementAtPoint === root || root.contains(elementAtPoint);
        })
      : -1;
    return index;
  }

  private _getOffset(currentIndex: number, siblings: CachedItemPosition<DragRef>[], delta: number, isHorizontal: boolean) {
    const currentPosition = siblings[currentIndex].clientRect;
    const immediateSibling = siblings[currentIndex + delta].clientRect;

    let itemOffset = 0;
    let siblingOffset = 0;

    if (immediateSibling) {
      const start = isHorizontal ? 'left' : 'top';
      const end = isHorizontal ? 'right' : 'bottom';

      if (delta === 1) {
        itemOffset = immediateSibling[end] - currentPosition[end];
        siblingOffset = currentPosition[start] - immediateSibling[start];

        if (isHorizontal && immediateSibling[end] < currentPosition[end]) {
          itemOffset = immediateSibling[start] - currentPosition[start];
        }
      } else {
        itemOffset = immediateSibling[start] - currentPosition[start];
        siblingOffset = currentPosition[end] - immediateSibling[end];

        if (isHorizontal && immediateSibling[end] > currentPosition[end]) {
          siblingOffset = currentPosition[start] - immediateSibling[start];
        }
      }
    }

    return {
      itemOffset,
      siblingOffset,
    };
  }

  private getContainerGapPixel() {
    if (this._containerStyle && (this._containerStyle.display === 'flex' || this._containerStyle.display === 'grid')) {
      return this._containerStyle.gap ? +this._containerStyle.gap.split('px')[0] : 0;
    }

    return 0;
  }
}

const getMutableClientRect = (element: Element): DOMRect => {
  const rect = element.getBoundingClientRect();

  return {
    top: rect.top,
    right: rect.right,
    bottom: rect.bottom,
    left: rect.left,
    width: rect.width,
    height: rect.height,
    x: rect.x,
    y: rect.y,
  } as DOMRect;
};

const isInsideClientRect = (clientRect: DOMRect, x: number, y: number) => {
  const { top, bottom, left, right } = clientRect;
  return y >= top && y <= bottom && x >= left && x <= right;
};

interface CachedItemPosition<T> {
  drag: T;
  clientRect: DOMRect;
  transform: {
    x: number;
    y: number;
  };
}
