import { autoinject, customElement, bindable, bindingMode, TaskQueue, BindingEngine, ICollectionObserverSplice, computedFrom, Disposable } from 'aurelia-framework';
import dragula from 'dragula';
import moment from 'moment';
import { UIInternal } from '../../utils/ui-internal';
import { debounce } from 'debounce';
import { DateUtils } from '../../utils/date-utils';

export class Draggable {
  public static DraggableClassName = "draggable";
  public static PlanningColumnClassName = "planning-column";
  public static DraggableHandleClassName = "draggable-handle";
}

export type UISchedulerViewType = "Week" | "TimelineWeek" | "TimelineMonth" | "TimelineDay";
export interface ElemTargetSource {
  element: Element,
  target: Element,
  source: Element
}
export class UISchedulerColumn {
  public element: HTMLElement;
  public key: Date;
  public date: Date;
  public day: string;
  public dayNumber: string;
  public contextMenuDisabled: boolean;
  public customData: any;
  public cells: UISchedulerCell[];
  public rowGroupColumn?: boolean;
  public class?: string;
  public dataBefore?: string;

  public constructor(init?: Partial<UISchedulerColumn>) {
    Object.assign(this, init);
  }
}
type CellEventType = UISchedulerEvent | UISchedulerEvent[];
export class UISchedulerCell {
  public data?: any;
  public class?: any;
  public style?: any;
  public startTime?: Date;
  public endTime?: Date;
  public rowGroupCell?: boolean;
  public rowGroupData?: any;
  public event?: CellEventType;
  public element: HTMLElement;

  public isShowMoreBtnOpen: boolean = false;
  public nbOfEvents: number = 0;
  public nbOfShownedEvents: number = 0;

  public constructor(init?: Partial<UISchedulerCell>) {
    Object.assign(this, init);
  }
}
export interface UISchedulerDayOff {
  date: Date;
  label?: string;
}
export interface UISchedulerRessourceItem {
  field: string;
  name: string;
  dataSource: UISchedulerRessourceData[];
}
export interface UISchedulerRessourceData {
  text: string;
  id: number;
  color?: string;
}
export class UISchedulerEvent<T = any> {
  public startTime?: Date;
  public endTime?: Date;
  public class?: string;
  public colorLeft?: string;
  public view: UISchedulerViewType;
  public element: HTMLElement;
  public fixed: boolean;
  public style: string;
  public selectedDate: Date;
  public group: string;
  public type: string;
  public isSelectable: boolean;
  public isSelected: boolean;
  public removeAfterDrop: boolean;
  public cell: UISchedulerCell;

  constructor(public data: T, startTime: Date = null, endTime: Date = null) {
    this.startTime = startTime;
    this.endTime = endTime;
    this.fixed = false;
    this.isSelectable = false;
    this.isSelected = false;
    this.removeAfterDrop = true;
  }
  @computedFrom("startTime", "endTime", "view")
  public get height() {
    if (this.startTime == null || this.endTime == null) return "height: initial;";
    if (this.view == "TimelineMonth") return "";
    if (this.view == "TimelineWeek") return "";
    if (this.view == "TimelineDay") return "height: initial;";
    let style = "";
    let startTimeM = moment(this.startTime);
    let minutes = startTimeM.minutes();
    let top = 0;
    if (minutes != 0 && minutes != 30) {
      if (minutes > 30) minutes -= 30;
      top = minutes * 2;

    }
    let duration = moment.duration(moment(this.endTime).diff(moment(this.startTime)));
    let height = duration.asMinutes() * 2;
    if (top > 0) {
      style += `top: ${top}px; `;
    }
    style += `height: ${height}px;`;
    return style;
  }
  @computedFrom("startTime", "endTime", "view", "element")
  public get width() {
    if (this.view == "Week") return "width: initial;";
    if (this.view == "TimelineMonth") {
      if (this.durationDays <= 1) return "width: initial;";
      else {
        return `width: calc(${this.durationDays * 100}% + ${this.durationDays - 2}px);`;
      }
    } else if (this.view == "TimelineDay") {
      if (this.element == null) return;
      const $schedulerContent = $(this.element).parents(".scheduler-content");
      const contentWidth = $schedulerContent.width() - 20;
      const offset = $schedulerContent.get(0).offsetLeft;
      const xOffset = this.element.offsetLeft - offset;
      const columnWidth = this.element?.parentElement.clientWidth ?? 70;
      const duration = moment(this.endTime).diff(moment(this.startTime), 'minute') / 60;
      let width = duration * columnWidth;

      const totalWidth = width + xOffset;
      if (totalWidth > contentWidth) {
        width = contentWidth - xOffset;
      }

      return `width: ${width}px;`;
    }
  }
  @computedFrom("startTime", "view", "element")
  public get leftOffset() {
    if (this.view == "TimelineDay") {
      if (this.element == null) return;
      const minutes = moment(this.startTime).minute();
      const columnWidth = this.element?.parentElement.clientWidth ?? 70;
      const duration = minutes / 60;
      const offset = duration * columnWidth;
      return `left: ${offset}px;`;
    }
  }
  private getFirstDayOfCurrentMonth() {
    let tempDate = new Date(this.selectedDate.getUTCFullYear(), this.selectedDate.getUTCMonth(), 1, 15);
    tempDate.setUTCHours(0);
    return tempDate;
  }
  private getLastDayOfCurrentMonth() {
    let tempDate = new Date(this.selectedDate.getUTCFullYear(), this.selectedDate.getUTCMonth(), 1, 12);
    tempDate.setUTCHours(23);
    let mTempDate = moment(tempDate).add(1, 'months');
    mTempDate = mTempDate.subtract(1, "days");
    return mTempDate.toDate();;
  }
  @computedFrom("startTime", "endTime")
  public get durationDays() {
    let endDate = new Date(this.endTime).setUTCHours(24);
    let startDate = new Date(this.startTime).setUTCHours(0);
    if (this.startTime.getTime() < this.getFirstDayOfCurrentMonth().getTime()) {
      startDate = new Date(this.getFirstDayOfCurrentMonth()).getTime();
    }
    if (this.endTime.getTime() > this.getLastDayOfCurrentMonth().getTime()) {
      endDate = new Date(this.getLastDayOfCurrentMonth()).getTime();
    }
    let duration = moment.duration(moment(endDate).diff(moment(startDate)));
    let days = duration.asDays();
    return Math.ceil(days);
  }
  @computedFrom("height", "width", "leftOffset", "style")
  public get computedStyle() {
    let height = this.height;
    let width = this.width;
    let style = "";
    if (height != null && height.length > 0) style += height;
    if (width != null && width.length > 0) style += width;
    if (this.leftOffset != null && this.leftOffset.length > 0) style += this.leftOffset;
    if (this.style != null && this.style.length > 0) style += this.style;
    return style;
  }
}

@autoinject
@customElement('ui-scheduler')
export class UIScheduler {
  @bindable({ defaultBindingMode: bindingMode.twoWay }) columnList: Array<UISchedulerColumn>;
  @bindable({ defaultBindingMode: bindingMode.twoWay }) events: Array<UISchedulerEvent>;
  @bindable({ defaultBindingMode: bindingMode.toView }) ressources: Array<UISchedulerRessourceItem>;
  @bindable({ defaultBindingMode: bindingMode.toView }) group: Array<string> = [];
  @bindable({ defaultBindingMode: bindingMode.twoWay }) selectedDate: Date;
  @bindable({ defaultBindingMode: bindingMode.twoWay }) showModalIcon: boolean = true;
  @bindable({ defaultBindingMode: bindingMode.oneTime }) onUpdate: ({ }) => Promise<boolean>;
  @bindable({ defaultBindingMode: bindingMode.oneTime }) onUpdateMultiple: ({ }) => Promise<boolean>;
  @bindable({ defaultBindingMode: bindingMode.oneTime }) onEditFn: (event: { event: UISchedulerEvent }) => void;
  @bindable({ defaultBindingMode: bindingMode.oneTime }) onDblClickFn: (event: { event: UISchedulerEvent }) => void;
  @bindable({ defaultBindingMode: bindingMode.oneTime }) accepts: ({ }) => boolean;
  @bindable({ defaultBindingMode: bindingMode.oneTime }) canMove: (event: { event: UISchedulerEvent }) => boolean;
  @bindable({ defaultBindingMode: bindingMode.oneTime }) startHour: number = 7;
  @bindable({ defaultBindingMode: bindingMode.oneTime }) endHour: number = 19;
  @bindable({ defaultBindingMode: bindingMode.twoWay }) view: UISchedulerViewType = "Week";
  @bindable({ defaultBindingMode: bindingMode.toView }) sidebarLabel: string;
  @bindable({ defaultBindingMode: bindingMode.toView }) hasGroup: boolean = false;
  @bindable({ defaultBindingMode: bindingMode.twoWay }) sidebarCollapsed: boolean = false;
  @bindable({ defaultBindingMode: bindingMode.oneTime }) sidebarWidth: string = "24rem";
  @bindable({ defaultBindingMode: bindingMode.twoWay }) sortGroupAscendant: boolean = true;
  @bindable({ defaultBindingMode: bindingMode.toView }) dayOffs: Array<UISchedulerDayOff | Date>;
  @bindable({ defaultBindingMode: bindingMode.oneTime }) enableDragAndDrop: boolean = true;
  @bindable({ defaultBindingMode: bindingMode.fromView }) beginFromMonday: boolean = true;
  @bindable({ defaultBindingMode: bindingMode.twoWay }) displayWeekend: boolean = true;
  @bindable({ defaultBindingMode: bindingMode.fromView }) shiftKeyPressed: boolean = false;
  @bindable({ defaultBindingMode: bindingMode.oneTime }) getMore: ({ topIndex, isAtBottom, isAtTop }) => (Promise<any> | any) = () => { };
  @bindable({ defaultBindingMode: bindingMode.toView }) nbWeeksToShow: number = 1;
  @bindable({ defaultBindingMode: bindingMode.toView }) maxSelectedEvents: number = null;
  @bindable({ defaultBindingMode: bindingMode.toView }) displaySidebar: boolean = true;

  private drake: any;
  private usageMatrix: boolean[][];
  private disposables: Array<Disposable>;
  private resizeObserver: ResizeObserver;

  internalGetMore(topIndex: number, isAtBottom, isAtTop) {
    if (this.getMore != null) {
      return this.getMore({
        "topIndex": topIndex,
        "isAtBottom": isAtBottom,
        "isAtTop": isAtTop
      });
    }
  }

  constructor(private element: Element, private taskQueue: TaskQueue, private bindingEngine: BindingEngine, private dateUtils: DateUtils) {
    this.columnList = [];
    this.usageMatrix = [];
    this.disposables = [];
    this.dayOffs = [];
  }

  @computedFrom('selectedDate')
  get displayDate() {
    if (this.view == "Week" || this.view == "TimelineWeek") {
      let month = this.selectedDate.toLocaleString('fr', { month: 'short' });
      let fixMonth = month[0].toUpperCase() + month.substring(1);
      let year = this.selectedDate.getFullYear();
      let start = this.dateUtils.format(this.dateUtils.getStartOfWeek(this.selectedDate), 'D');
      let end = this.dateUtils.format(this.dateUtils.getEndOfWeek(this.selectedDate), 'D');
      return `${fixMonth} ${start}-${end}, ${year}`;
    }
    else if (this.view == "TimelineMonth") {
      let month = this.selectedDate.toLocaleString('fr', { month: 'short' });
      let fixMonth = month[0].toUpperCase() + month.substring(1);
      let year = this.selectedDate.getFullYear();
      return `${fixMonth} ${year}`;
    }
    else {
      return this.selectedDate.toLocaleDateString('fr');
    }
  }

  @computedFrom('view')
  get dateSelector() {
    if (this.view == "Week" || this.view == "TimelineWeek") {
      return "Week";
    }
    return "Month";
  }

  @computedFrom('view', 'group')
  get allowMultipleEventsPerCell() {
    return (this.view == "TimelineWeek" && this.group.length > 0) || this.view == "TimelineDay" || this.view == "Week";
  }

  bind() {
    this.generateColumns();
    this.initialize();
    this.constructUI();

    this.registerObservers();
  }

  detached() {
    this.resizeObserver.unobserve(this.element);
    this.drake?.destroy();
    this.disposables.forEach(disposable => {
      disposable.dispose();
    });
  }

  registerObservers() {
    this.disposables.push(
      this.bindingEngine.propertyObserver(this, "selectedDate").subscribe((newVal: Date, oldVal: Date) => {
        if (!moment(newVal).isSame(oldVal, "day")) {
          this.selectedDate.setUTCHours(12);
          this.generateColumns();
        }
      }),
      this.bindingEngine.propertyObserver(this, "view").subscribe(() => {
        this.generateColumns();
        this.triggerEventsChanged();
      }),
      this.bindingEngine.collectionObserver(this.events).subscribe((changes) => {
        if (changes.length > 0) {
          this.eventsCollectionChanged(changes);
        }
      }),
      this.bindingEngine.collectionObserver(this.dayOffs).subscribe((changes) => {
        this.setDayOffColumns();
      }),
      this.bindingEngine.propertyObserver(this, "displayWeekend").subscribe(() => {
        this.generateColumns();
        this.triggerEventsChanged();
      }),
      this.bindingEngine.propertyObserver(this, "nbWeeksToShow").subscribe(() => {
        this.generateColumns();
        this.triggerEventsChanged();
      }));
    if (this.ressources != null) {
      this.disposables.push(this.bindingEngine.collectionObserver(this.ressources).subscribe((changes) => {
        if (changes.length > 0) {
          this.ressourcesChanged(changes);
        }
      }));
    }
  }

  setDayOffColumns() {
    for (const dayOff of this.dayOffs) {
      let dayOffDate: Date;
      if (dayOff instanceof Date) {
        dayOffDate = dayOff;
      }
      else {
        dayOffDate = dayOff.date;
      }
      let column = this.columnList.find(x => !x.rowGroupColumn && moment(x.key).isSame(moment(dayOffDate), "day"));
      if (column != null) {
        column.class ??= "";
        column.class += " day-off";
        if (!(dayOff instanceof Date) && dayOff.label != null) {
          column.dataBefore = dayOff.label;
        }
      }
    }
  }

  generateColumns() {
    let columnList: UISchedulerColumn[] = [];
    if (this.view == "Week") {
      columnList.push(...this.generateWeekColumns());
    }
    else if (this.view.startsWith("Timeline")) {
      if (this.group == null || this.group.length == 0) {
        columnList.push(...this.generateTimelineColumns());
      }
      else {
        columnList.push(...this.generateGroupedTimelineColumns());
      }
    }
    this.columnList.splice(0, this.columnList.length, ...columnList);
    this.setDayOffColumns();
  }

  private generateWeekColumns() {
    let columnList: UISchedulerColumn[] = [];
    let nbDays = this.displayWeekend ? 7 : 5;
    for (var i = 0; i < nbDays; i++) {
      let currDate = moment(this.selectedDate).weekday(i);

      columnList.push(new UISchedulerColumn({
        key: currDate.toDate(),
        day: currDate.format("dddd").charAt(0).toUpperCase() + currDate.format("dddd").substr(1),
        dayNumber: currDate.format("DD/MM"),
        contextMenuDisabled: true,
        cells: this.generateColumnCells(currDate.toDate()),
        class: (currDate.format('yyyy MM DD') == moment().format('yyyy MM DD')) ? 'current-date' : ''
      }));
    }
    return columnList;
  }

  private computeStartDate(): Date {
    if (this.view == "TimelineMonth") {
      return this.dateUtils.getStartOfMonth(moment(this.selectedDate).utc());
    }
    else if (this.view == "TimelineWeek") {
      return this.dateUtils.getStartOfWeek(moment(this.selectedDate).utc());
    }
    else if (this.view == "TimelineDay") {
      return this.dateUtils.setHours(this.selectedDate, this.startHour);
    }
  }

  private computedEndDate(startDate: Date): Date {
    if (this.view == "TimelineMonth") {
      return this.dateUtils.getEndOfMonth(moment(startDate).utc());
    }
    else if (this.view == "TimelineWeek") {
      let endWeek: Date | moment.Moment = this.dateUtils.getEndOfWeek(moment(startDate).utc());
      if (!this.displayWeekend) {
        endWeek = this.dateUtils.subtractDays(endWeek, 2)
      }
      return this.dateUtils.addWeeks(endWeek, this.nbWeeksToShow - 1);
    }
    else if (this.view == "TimelineDay") {
      return this.dateUtils.setHours(startDate, this.endHour);
    }
  }

  private generateTimelineColumns() {
    let columnList: UISchedulerColumn[] = [];
    let startDate = this.computeStartDate();
    let mStartDate = moment(startDate);
    let colIndex = 0;
    let endDate = this.computedEndDate(startDate);
    let mEndDate = moment(endDate);
    while (mStartDate <= mEndDate) {
      let cells = this.generateColumnCells(mStartDate.toDate());

      if (this.view == "TimelineDay") {
        columnList.push(new UISchedulerColumn({
          key: mStartDate.toDate(),
          day: mStartDate.format("H") + " h",
          dayNumber: null,
          contextMenuDisabled: true,
          cells: cells,
          class: ''
        }));

        mStartDate.add(1, 'hour');
      }
      else {
        if (this.displayWeekend == true || !this.dateUtils.isWeekend(mStartDate)) {
          let isLastDayForWeek = mStartDate != mEndDate ? this.displayWeekend == true ? mStartDate.isoWeekday() == 7 : mStartDate.isoWeekday() == 5 : false;
          let isWeekend = this.displayWeekend == true ? this.dateUtils.isWeekend(mStartDate) : false;

          columnList.push(new UISchedulerColumn({
            key: mStartDate.toDate(),
            day: null,
            dayNumber: mStartDate.format("DD/MM"),
            contextMenuDisabled: true,
            cells: cells,
            class: ((mStartDate.format('yyyy MM DD') == moment().format('yyyy MM DD')) ? 'current-date' : '') + (isLastDayForWeek ? ' last-day-for-week' : '') + (isWeekend ? ' ui-scheduler-week-end' : '')
          }));
        }
        mStartDate.add(24, 'hour');
      }
      this.usageMatrix.push([]);
      cells.forEach((c, i) => {
        this.usageMatrix[colIndex][i] = false;
      });
      colIndex++;
    }
    return columnList;
  }
  generateGroupedTimelineColumns() {
    let columnList: UISchedulerColumn[] = [];
    this.group.forEach(g => {
      let ressource = this.ressources.find(r => r.name == g);
      if (ressource != null) {
        columnList.push(new UISchedulerColumn({
          rowGroupColumn: true,
          cells: this.generateColumnCells(),
          class: 'group'
        }));
      }
    })
    columnList.push(...this.generateTimelineColumns());
    return columnList;
  }

  generateColumnCells(currDate: Date = null): UISchedulerCell[] {
    let cells: UISchedulerCell[] = [];
    if (this.view == "Week") {
      currDate.setHours(this.startHour);
      let currDay = currDate.getDate();

      while (currDate.getHours() <= this.endHour && currDay == currDate.getDate()) {
        let cell: UISchedulerCell = new UISchedulerCell({
          startTime: currDate,
          endTime: moment(currDate).add(30, 'minute').toDate()
        });

        cells.push(cell);
        currDate = new Date(cell.endTime);
      }
    }
    else if (this.view.startsWith("Timeline") && this.group.length == 0) {
      for (let i = 0; i < 20; i++) {
        let cell: UISchedulerCell = new UISchedulerCell({
          startTime: currDate,
          endTime: moment(currDate).add(24, 'hours').toDate()
        });

        cells.push(cell);
      }
    } else if (this.view.startsWith("Timeline")) {
      if (currDate == null) {
        this.group.forEach(gr => {
          let ressource = this.ressources.find(r => r.name == gr);
          ressource.dataSource.forEach(data => {
            let cell: UISchedulerCell = new UISchedulerCell({
              rowGroupCell: true,
              rowGroupData: data,
              style: `border-left-color: ${data.color}; border-left-width: 5px; border-left-style: solid;
              display: flex;
              align-items: center;
              justify-content: center;`

            });
            cells.push(cell);
          });
        });
      } else {
        this.group.forEach(gr => {
          let ressource = this.ressources.find(r => r.name == gr);
          ressource.dataSource.forEach(data => {
            let cell: UISchedulerCell = new UISchedulerCell({
              startTime: currDate,
              rowGroupData: data,
              endTime: moment(currDate).add(24, 'hours').toDate()
            });

            cells.push(cell);
          });
        });
      }
    }
    return cells;
  }

  attached() {
    $(".scheduler-content").scroll(function () {
      let left = $(".scheduler-container")[0].getBoundingClientRect().left;
      $(".scheduler-header").offset({ left: -1 * this.scrollLeft + left });
    });

    $(document).on("keydown", (event) => {
      if (event.shiftKey === true) {
        this.shiftKeyPressed = true;
      }
    });
    $(document).on("keyup", (event) => {
      if (event.shiftKey === false) {
        this.shiftKeyPressed = false;
      }
    });

    this.resizeObserver = new ResizeObserver(
      debounce(() => {
        this.constructUI();
      }, 500)
    );
    this.resizeObserver.observe(this.element);
  }

  public computeOverflowness(cell: UISchedulerCell) {
    this.taskQueue.queueTask(() => {
      if (cell != null && this.allowMultipleEventsPerCell && this.view != "Week") {
        let nbOfShownedEvents = 0;
        let nbOfEvents = 0;
        if (cell.event != null && isArray(cell.event) && cell.element != null) {
          nbOfEvents = cell.event.length;
          let totalHeight = 0;
          for (let event of cell.event) {
            if (event.element != null) {
              totalHeight += event.element.clientHeight;
              if (totalHeight <= cell.element.clientHeight - 20) {
                nbOfShownedEvents++;
              }
            }
          }
          if (totalHeight <= cell.element.clientHeight) {
            nbOfShownedEvents = nbOfEvents;
          }
        }
        cell.nbOfShownedEvents = nbOfShownedEvents;
        cell.nbOfEvents = nbOfEvents;
      }
      return;
    });
  }

  unplannedEventsCount() {
    if (this.events == null) return 0;
    return this.events.filter(e => e.startTime == null).length;
  }

  eventsCollectionChanged(changes: Array<ICollectionObserverSplice<UISchedulerEvent>>) {
    this.constructUI();
  }

  private initialize() {
    this.taskQueue.queueTask({
      call: () => {
        if (this.drake != null) {
          this.drake.destroy();
          this.drake = null;
        }
        if (this.enableDragAndDrop === true) {
          let drake = new dragula(new Array<Element>(),
            {
              accepts: (el: Element, target: Element, source: Element): boolean => {
                if (target.classList.contains(Draggable.DraggableClassName) && !this.hasParent(source, target)) {
                  let targetModel = this.getModelFromElement(target) as UISchedulerCell;
                  if (this.accepts != null) {
                    let event = this.getModelFromElement(el) as UISchedulerEvent;
                    let sourceModel = this.getModelFromElement(source) as UISchedulerCell;
                    return this.accepts({ event: event, target: targetModel, source: sourceModel });
                  }
                  else if (targetModel != null) {
                    return targetModel.event == null || this.allowMultipleEventsPerCell;
                  }
                  return true;
                }
                return false;
              },
              moves: (el: Element, source, handle, sibling) => {
                if (el.classList.contains('scheduler-event')) {
                  if (this.canMove != null) {
                    let eventEl = this.getModelFromElement(el) as UISchedulerEvent;
                    return this.canMove({ "event": eventEl });
                  }
                  return true;
                }
                return false;
              },
              isContainer: (el: Element) => {
                return el.classList.contains(Draggable.DraggableClassName);
              },
              revertOnSpill: false,
              copy: (el, source: Element) => {
                if (source.classList?.contains("scheduler-unplanned-events"))
                  return false;
                if (this.view == "Week")
                  return true;
                if (this.shiftKeyPressed === true) return true;
                return false;
              }
            })
            .on('cloned', (clone: HTMLElement, original, type) => {
              if (this.shiftKeyPressed === true) {
                clone.classList.add('gu-copy');
              }
            })
            .on('drop', async (el: Element & { auTargetId: number; }, target: Element, source: Element) => {
              try {
                let event = this.getModelFromElement(el) as UISchedulerEvent;
                let sourceModel = this.getModelFromElement(source) as UISchedulerCell;
                let targetModel = this.getModelFromElement(target) as UISchedulerCell;

                if (event == null) {
                  event = sourceModel.event as UISchedulerEvent;
                  if (Array.isArray(event)) {
                    const findEvent = event.find(e => e.element.auTargetId == el.auTargetId);
                    if (findEvent != null)
                      event = findEvent;
                  }
                }
                if (this.onUpdate != null && event != null && !event.fixed) {
                  let selectedEvents = this.events.filter(x => !x.fixed && x.isSelected);
                  if (!selectedEvents.includes(event)) {
                    selectedEvents.push(event);
                  }
                  let executeIndividually: boolean = selectedEvents.length <= 1 || this.onUpdateMultiple == null;
                  if (!executeIndividually) {
                    executeIndividually = !(await this.onUpdateMultiple({ events: selectedEvents, target: targetModel, source: sourceModel }));
                  }

                  for (const selectedEvent of selectedEvents) {
                    if (executeIndividually) {
                      if (!(await this.onUpdate({ event: selectedEvent, target: targetModel, source: sourceModel }))) {
                        this.events.splice(this.events.indexOf(selectedEvent), 1, Object.create(selectedEvent));
                      }
                    }
                    if (selectedEvent.removeAfterDrop) {
                      el.parentElement.removeChild(el);
                    }
                    if (selectedEvent.cell != null) {
                      selectedEvent.cell.isShowMoreBtnOpen = false;
                    }
                    selectedEvent.isSelected = false;
                  }

                  this.triggerEventsChanged();
                }
                else {
                  this.drake.cancel(true);
                }
              }
              catch {
                this.drake.cancel(true);
              }
            });
          this.drake = drake;
        }
      }
    });
  }

  private constructUI() {
    this.taskQueue.queueTask(() => {
      this.resetMatrix();
      this.resetCells();
      this.assignEventsToCorrespondingCells();
      for (let column of this.columnList) {
        for (let cell of column.cells) {
          this.computeOverflowness(cell);
        }
      }
      UIInternal.broadcast(UIInternal.EVT_SCHEDULER_REFRESHED);
    });
  }

  ressourcesChanged(changes: Array<ICollectionObserverSplice<UISchedulerEvent>>) {
    this.generateColumns();
  }

  private assignEventsToCorrespondingCells() {
    if (this.events != null && this.events.length > 0) {
      let plannedEvents = this.events.filter(e => e.startTime != null);
      for (const event of plannedEvents) {
        event.view = this.view;
        event.selectedDate = this.selectedDate;
        let startTime = event.startTime;
        let cell = this.findCell(event);
        if (cell != null) {
          if (cell.event != null) {
            if (this.allowMultipleEventsPerCell) {
              (cell.event as UISchedulerEvent[]).push(event);
            }
            else {
              throw "Cannot assign multiple events to a cell";
            }
          }
          else {
            if (this.allowMultipleEventsPerCell) {
              if (cell.event == null) cell.event = [] as UISchedulerEvent[];
              (cell.event as UISchedulerEvent[]).push(event);
            }
            else {
              cell.event = event;
            }
          }
          event.cell = cell;
        }
        else {
          console.info("Cannot find a cell to put event with starttime " + startTime);
        }
      }
    }
  }

  private resetCells() {
    this.columnList.forEach(column => {
      column.cells.filter(c => c.event != null).forEach(cell => {
        cell.event = null;
      });
    });
  }

  private resetMatrix() {
    let i = 0;
    while (i < this.usageMatrix.length) {
      let j = 0;
      while (j < this.usageMatrix[i].length) {
        this.usageMatrix[i][j] = false;
        j++;
      }
      i++;
    }
  }

  triggerEventsChanged() {
    let copy = this.events.splice(0);
    this.taskQueue.queueMicroTask(() => this.events.push(...copy));
  }

  findCell(event: UISchedulerEvent) {
    if (this.columnList == null || this.columnList.length == 0) return null;
    if (this.view == "Week") {
      let startTime = event.startTime;
      let copyStartTime = new Date(startTime);
      copyStartTime.setUTCHours(0, 0, 0, 0);

      let column = this.columnList.find(c => moment(copyStartTime).isSame(moment(c.key), 'day'));
      if (column == null) return null;
      copyStartTime = new Date(startTime);
      if (startTime.getHours() < this.startHour) {
        copyStartTime.setUTCHours(this.startHour, 0, 0);
      }
      let cell = column.cells.find(c => copyStartTime >= c.startTime && copyStartTime < c.endTime);
      return cell;
    } else if (this.view == "TimelineMonth" && this.group.length == 0) {
      let startTime = new Date(event.startTime);
      let endTime = new Date(event.endTime);

      if (startTime.getMonth() < this.selectedDate.getMonth() || startTime.getFullYear() < this.selectedDate.getFullYear()) {
        startTime.setUTCFullYear(this.selectedDate.getFullYear());
        startTime.setUTCMonth(this.selectedDate.getMonth());
        startTime.setUTCDate(1);
      }

      startTime.setUTCHours(0, 0, 0, 0);
      endTime.setUTCHours(0, 0, 0, 0);

      let mStartTime = moment(startTime);
      let columnIndex = this.columnList.findIndex(c => this.dateUtils.isSameDay(c.key, mStartTime));
      let column = this.columnList[columnIndex];
      if (column != null) {
        let found = false;
        event.selectedDate = this.selectedDate;
        let duration = event.durationDays;
        let colIndex = 0;
        let rowIndex = 0;
        while (found == false) {
          while (colIndex < duration) {
            if (this.usageMatrix[columnIndex + colIndex][rowIndex] == true) {
              break;
            }
            colIndex++;
          }
          if (colIndex == duration) found = true;
          rowIndex++;
        }
        if (found) {
          rowIndex--;
          colIndex = 0;
          while (colIndex < duration) {
            this.usageMatrix[columnIndex + colIndex][rowIndex] = true;
            colIndex++;
          }
          return column.cells[rowIndex];
        }
      }
      return null;
    } else if (this.view == "TimelineWeek" && this.group.length > 0) {
      let startTime = new Date(event.startTime);
      let g = this.group[0];
      let ressource = this.ressources.find(x => x.name == g);
      let fieldName = ressource.field;

      let fieldVal = event.data[fieldName];

      startTime.setUTCHours(0, 0, 0, 0);

      let mStartTime = moment(startTime);
      let column = this.columnList.find(c => this.dateUtils.isSameDay(mStartTime, c.key));
      if (column != null) {
        return column.cells.find(c => this.dateUtils.isSameDay(mStartTime, c.startTime) && c.rowGroupData.id == fieldVal);
      }
      return null;
    } else if (this.view == "TimelineDay" && this.group.length > 0) {
      let startTime = new Date(event.startTime);
      let g = this.group[0];
      let ressource = this.ressources.find(x => x.name == g);
      let fieldName = ressource.field;

      let fieldVal = event.data[fieldName];

      const mStartTime = moment(startTime);
      let columnIndex = this.columnList.findIndex(c => moment(c.key).diff(mStartTime, 'hour', false) == 0);
      if (columnIndex > -1) {
        let column = this.columnList[columnIndex];
        let cell = column.cells.find(c => c.rowGroupData.id == fieldVal);
        return cell;
      }
      return null;
    }
  }

  public editEvent($event, event: UISchedulerEvent) {
    if ($event.detail == 1) {
      if (event.isSelectable) {
        if (!event.isSelected) {
          event.isSelected = this.maxSelectedEvents == null || this.events.filter(x => x.isSelected).length < this.maxSelectedEvents;
        }
        else {
          event.isSelected = false;
        }
      }
      if (this.onEditFn != null) {
        this.onEditFn({ "event": event });
      }
    }
    else if ($event.detail == 2) {
      if (this.onDblClickFn != null) {
        this.onDblClickFn({ "event": event });
      }
    }
  }

  hasParent(element: Element, parent: Element) {
    if (element.parentElement != null && element.parentElement == parent) {
      return true;
    }

    if (element.parentElement != null) {
      return this.hasParent(element.parentElement, parent);
    }
    else {
      return false;
    }
  }

  getModelFromElement(element: Element) {
    var modelProperty = Object.keys(element).find(k => k == 'model');
    if (modelProperty != null) {
      return element['model'];
    }
    else if (element.parentElement != null) {
      return this.getModelFromElement(element.parentElement);
    }
    else {
      return null;
    }
  }

  public today() {
    this.selectedDate = new Date();
  }

  public prevDate() {
    if (this.view == "Week" || this.view == "TimelineWeek") {
      this.selectedDate = moment(this.selectedDate).add(-1, "week").toDate();
    } else if (this.view == "TimelineMonth") {
      this.selectedDate = moment(this.selectedDate).add(-1, "month").toDate();
    } else if (this.view == "TimelineDay") {
      this.selectedDate = moment(this.selectedDate).add(-1, 'day').toDate();;
    }
  }

  public nextDate() {
    if (this.view == "Week" || this.view == "TimelineWeek") {
      this.selectedDate = moment(this.selectedDate).add(1, "week").toDate();
    } else if (this.view == "TimelineMonth") {
      this.selectedDate = moment(this.selectedDate).add(1, "month").toDate();
    } else if (this.view == "TimelineDay") {
      this.selectedDate = moment(this.selectedDate).add(1, 'day').toDate();;
    }
  }
}