/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import * as m from 'mithril';
import {globals} from './globals';

export type DropDirection = 'left'|'right';

export interface ReorderableCellGroupAttrs {
  cells: m.Children[];
  onReorder: (from: number, to: number, side: DropDirection) => void;
}

const placeholderElement = document.createElement('span');

// A component that renders a group of cells on the same row that can be
// reordered between each other by using drag'n'drop.
//
// On completed reorder, a callback is fired.
export class ReorderableCellGroup implements
    m.ClassComponent<ReorderableCellGroupAttrs> {
  // Index of a cell being dragged.
  draggingFrom: number = -1;

  // Index of a cell cursor is hovering over.
  draggingTo: number = -1;

  // Whether the cursor hovering on the left or right side of the element: used
  // to add the dragged element either before or after the drop target.
  dropDirection: DropDirection = 'left';

  // Auxillary array used to count entrances into `dragenter` event: these are
  // incremented not only when hovering over a cell, but also for any child of
  // the tree.
  enterCounters: number[] = [];

  getClassForIndex(index: number): string {
    if (this.draggingFrom === index) {
      return 'dragged';
    }
    if (this.draggingTo === index) {
      return this.dropDirection === 'left' ? 'highlight-left' :
                                             'highlight-right';
    }
    return '';
  }

  view(vnode: m.Vnode<ReorderableCellGroupAttrs>): m.Children {
    return vnode.attrs.cells.map(
        (cell, index) => m(
            'td.reorderable-cell',
            {
              draggable: 'draggable',
              class: this.getClassForIndex(index),
              ondragstart: (e: DragEvent) => {
                this.draggingFrom = index;
                if (e.dataTransfer !== null) {
                  e.dataTransfer.setDragImage(placeholderElement, 0, 0);
                }

                globals.rafScheduler.scheduleFullRedraw();
              },
              ondragover: (e: DragEvent) => {
                let target = e.target as HTMLElement;
                if (this.draggingFrom === index || this.draggingFrom === -1) {
                  // Don't do anything when hovering on the same cell that's
                  // been dragged, or when dragging something other than the
                  // cell from the same group
                  return;
                }

                while (target.tagName.toLowerCase() !== 'td' &&
                       target.parentElement !== null) {
                  target = target.parentElement;
                }

                // When hovering over cell on the right half, the cell will be
                // moved to the right of it, vice versa for the left side. This
                // is done such that it's possible to put dragged cell to every
                // possible position.
                const offset = e.clientX - target.getBoundingClientRect().x;
                const newDropDirection =
                    (offset > target.clientWidth / 2) ? 'right' : 'left';
                const redraw = (newDropDirection !== this.dropDirection) ||
                    (index !== this.draggingTo);
                this.dropDirection = newDropDirection;
                this.draggingTo = index;


                if (redraw) {
                  globals.rafScheduler.scheduleFullRedraw();
                }
              },
              ondragenter: (e: DragEvent) => {
                this.enterCounters[index]++;

                if (this.enterCounters[index] === 1 &&
                    e.dataTransfer !== null) {
                  e.dataTransfer.dropEffect = 'move';
                }
              },
              ondragleave: (e: DragEvent) => {
                this.enterCounters[index]--;
                if (this.draggingFrom === -1 || this.enterCounters[index] > 0) {
                  return;
                }

                if (e.dataTransfer !== null) {
                  e.dataTransfer.dropEffect = 'none';
                }

                this.draggingTo = -1;
                globals.rafScheduler.scheduleFullRedraw();
              },
              ondragend: () => {
                if (this.draggingTo !== this.draggingFrom &&
                    this.draggingTo !== -1) {
                  vnode.attrs.onReorder(
                      this.draggingFrom, this.draggingTo, this.dropDirection);
                }

                this.draggingFrom = -1;
                this.draggingTo = -1;
                globals.rafScheduler.scheduleFullRedraw();
              },
            },
            cell));
  }

  oncreate(vnode: m.VnodeDOM<ReorderableCellGroupAttrs, this>) {
    this.enterCounters = Array(vnode.attrs.cells.length).fill(0);
  }

  onupdate(vnode: m.VnodeDOM<ReorderableCellGroupAttrs, this>) {
    if (this.enterCounters.length !== vnode.attrs.cells.length) {
      this.enterCounters = Array(vnode.attrs.cells.length).fill(0);
    }
  }
}
