import React, {
  createContext,
  useContext,
  useState,
  ReactNode,
  useEffect,
  useCallback,
} from 'react';
import dayjs from 'dayjs';
import isoWeek from 'dayjs/plugin/isoWeek';
import {
  ECalendarMode,
  IEventsGroup,
  IOptions,
  IPartialRanges,
  ISlot,
  ISlotEventsToSpaces,
  ISlotsEvents,
  ISlotsEventsAndEventsWithSpaces,
  TOnChangeInCalender,
} from '../types';
import {
  calculateMarginFromHours,
  calculateMarginFromMinutes,
  calculateMarginLeftFromHours,
  calculateMarginLeftFromMinutes,
  oneHourHeight,
  oneHourWidth,
  setNewHourHeight,
  setNewHourWidth,
} from '../utils/timeUtils';
import { usePrevious } from './usePrevious';

dayjs.extend(isoWeek);

export type DateRange = {
  [key: number]: {
    startDate: string;
    endDate: string;
  };
};

export type MiddlewareContent = {
  datesRange: DateRange;
  updateDatesRange: (
    id: number,
    startDate: string,
    endDate: string,
    onChangeData?: any
  ) => void;
  hasCollision: (event: ISlot, list: ISlot[]) => boolean;
  day: number;
  incrementDay: () => void;
  updateEventsToSpaceByDatesRange: (
    spaces: ISlotEventsToSpaces,
    onChange?: TOnChangeInCalender
  ) => {
    [x: number]: ISlot[];
  };
  draggableDatesRange: DateRange;
  setDraggableRange: (id: number, startDate: string, endDate: string) => void;
  options: IOptions;
  setOptions: (options: IOptions) => void;
  partialRanges: IPartialRanges;
  setPartialRanges: (range: IPartialRanges) => void;
  getPartialEvents: (events: ISlot[], from: number, to: number) => ISlot[];
  getTopAndHeight: (
    startDate: number,
    endDate: number,
    horizontal?: boolean
  ) => { top: number; height: number };
  getEventsGroups: (events: ISlot[], horizontal?: boolean) => IEventsGroup[];
  isResizing: boolean;
  setIsResizing: (isResizing: boolean) => void;
  initiateEventsToSpaces: (slots: ISlotsEventsAndEventsWithSpaces) => void;
  updateEventsData: () => void;
  handleDrop: (
    eventId: number | string,
    parentId: number | string,
    spaceId: number | string,
    newDate: string,
    newEndDate?: string
  ) => void;
  eventsToSpaces: ISlotEventsToSpaces;
  events: ISlotsEvents;
};

const MiddlewareContext = createContext<MiddlewareContent>({
  datesRange: {},
  updateDatesRange: (
    id: number,
    startDate: string,
    endDate: string,
    onChangeData?: any
  ) => {},
  hasCollision: (event: any, list: any[]) => false,
  day: 1,
  incrementDay: () => {},
  updateEventsToSpaceByDatesRange: (
    spaces: ISlotEventsToSpaces,
    onChange?: TOnChangeInCalender
  ) => ({}),
  draggableDatesRange: {},
  setDraggableRange: (id: number, startDate: string, endDate: string) => {},
  options: {},
  setOptions: (options: IOptions) => ({}),
  partialRanges: {
    vertical: {
      from: 0,
      to: 0,
    },
    horizontal: {
      from: 0,
      to: 0,
    },
  },
  setPartialRanges: (ranges: IPartialRanges) => ({}),
  getPartialEvents: (events: ISlot[], from: number, to: number) => [],
  getTopAndHeight: (
    startDate: number,
    endDate: number,
    horizontal?: boolean
  ) => ({ top: 0, height: 0 }),
  getEventsGroups: (events: ISlot[], horizontal?: boolean) => [],
  isResizing: false,
  setIsResizing: (isResizing: boolean) => {},
  initiateEventsToSpaces: (slots: ISlotsEventsAndEventsWithSpaces) => {},
  updateEventsData: () => {},
  handleDrop: (
    eventId: number | string,
    parentId: number | string,
    spaceId: number | string,
    newDate: string,
    newEndDate?: string
  ) => {},
  eventsToSpaces: {},
  events: {},
});

export const MiddlewareContextProvider = ({
  children,
  onChange,
}: {
  children: ReactNode;
  onChange?: TOnChangeInCalender;
}) => {
  const [eventsToSpaces, setEventsToSpaces] = useState<ISlotEventsToSpaces>({});
  const [events, setEvents] = useState<ISlotsEvents>({});

  const initiateEventsToSpaces = useCallback(
    (slots: ISlotsEventsAndEventsWithSpaces) => {
      setEventsToSpaces(slots.eventsToSpaces);
      setEvents(slots.events);
    },
    []
  );

  const handleDrop = useCallback(
    (
      eventId: number | string,
      parentId: number | string,
      spaceId: number | string,
      newDate: string,
      newEndDate?: string
    ) => {
      let newEvents = { ...eventsToSpaces };
      let fullEvents = { ...events };

      const dataEvent =
        newEvents[spaceId]?.find(
          (item: ISlot) => item.id === Number(eventId)
        ) || fullEvents[eventId];

      let event = { ...dataEvent, spaceId };

      if (newDate) {
        const date = dayjs.utc(newDate);
        let startDate = dayjs
          .utc(event.startDate)
          .set('date', date.get('date'))
          .set('month', date.get('month'))
          .set('year', date.get('year'))
          .toISOString();

        let endDate = dayjs
          .utc(event.endDate)
          .set('date', date.get('date'))
          .set('month', date.get('month'))
          .set('year', date.get('year'))
          .toISOString();

        // Drag'n'drop slot on daily view
        if (newEndDate) {
          startDate = newDate;
          endDate = newEndDate;
        }

        event = {
          ...event,
          startDate,
          endDate,
        };
      }

      // remove event from the previous space
      if (newEvents[parentId]) {
        newEvents[parentId] = newEvents[parentId].filter(
          (item: ISlot) => Number(item.id) !== Number(eventId)
        );
      }

      // add event to the new space
      if (newEvents[spaceId]) {
        newEvents[spaceId] = [...newEvents[spaceId], event];
      } else {
        newEvents[spaceId] = [event];
      }
      setEventsToSpaces(newEvents);

      // update the full events object
      const newEvent = { ...fullEvents[eventId], spaceId };
      fullEvents[eventId] = newEvent;
      setEvents(fullEvents);
    },
    [events, eventsToSpaces]
  );

  const [options, setOptions] = useState<IOptions>({});
  const previousOptions = usePrevious(options);

  useEffect(() => {
    setNewHourHeight(options?.hourSize?.vertical);
    setNewHourWidth(options?.hourSize?.horizontal);
    setDatesRange({});
  }, [options?.hourSize?.vertical, options?.hourSize?.horizontal]);

  useEffect(() => {
    if (
      options &&
      previousOptions &&
      Object.keys(previousOptions).length > 0 &&
      (options.mode !== previousOptions.mode ||
        options.date !== previousOptions.date) &&
      onChange
    ) {
      const params = {} as {
        startDate?: string;
        endDate?: string;
      };
      const dayShift = options.isSundayFirstDay ? 1 : 0;

      if (options.mode === ECalendarMode.DAILY) {
        params.startDate = dayjs.utc(options.date).format('DD/MM/YYYY');
        params.endDate = dayjs.utc(options.date).format('DD/MM/YYYY');
      } else if (options.mode === ECalendarMode.WEEKLY) {
        params.startDate = dayjs
          .utc(options.date)
          .isoWeekday(1 - dayShift)
          .format('DD/MM/YYYY');
        params.endDate = dayjs
          .utc(options.date)
          .isoWeekday(7 - dayShift)
          .format('DD/MM/YYYY');
      } else if (options.mode === ECalendarMode.MONTHLY) {
        const firstDay = dayjs.utc(options.date).date(1);
        let lastDay = dayjs.utc(options.date).date(31);
        while (lastDay.format('MM') !== firstDay.format('MM'))
          lastDay = lastDay.subtract(1, 'day');

        if (options?.monthlySpecificMonth) {
          params.startDate = firstDay.format('DD/MM/YYYY');
          params.endDate = lastDay.format('DD/MM/YYYY');
        } else {
          params.startDate = dayjs
            .utc(firstDay)
            .isoWeek(firstDay.isoWeek())
            .isoWeekday(1 - dayShift)
            .format('DD/MM/YYYY');
          params.endDate = dayjs
            .utc(lastDay)
            .isoWeek(lastDay.isoWeek())
            .isoWeekday(7 - dayShift)
            .format('DD/MM/YYYY');
        }
      }

      onChange({
        type: 'NEW_MODE',
        data: {
          mode: options.mode,
          ...params,
        },
      });
    }
  }, [options, previousOptions]);

  const [partialRanges, setPartialRanges] = useState<IPartialRanges>({
    vertical: {
      from: 0,
      to: 0,
    },
    horizontal: {
      from: 0,
      to: 0,
    },
  });

  const [datesRange, setDatesRange] = useState<DateRange>({});
  const [isResizing, setIsResizing] = useState(false);

  const updateDatesRange = useCallback(
    (id: number, startDate: string, endDate: string, onChangeData?: any) => {
      if (onChange) {
        onChange(onChangeData);
      }
      setDatesRange({ [id]: { startDate, endDate } });
    },
    [onChange]
  );

  const [draggableDatesRange, setDraggableDatesRange] = useState<DateRange>({});

  const setDraggableRange = useCallback(
    (id: number, startDate: string, endDate: string) => {
      setDraggableDatesRange({ [id]: { startDate, endDate } });
    },
    []
  );

  const hasCollision = useCallback((event: ISlot, list: ISlot[] = []) => {
    if (!event) return true;
    if (list.length === 0 || !Array.isArray(list)) return false;

    const eventStartDate = dayjs.utc(event.startDate).toDate().getTime();
    const eventEndDate = dayjs.utc(event.endDate).toDate().getTime();

    return Boolean(
      list.find((item) => {
        if (event.id === item.id) return false;

        const itemStartDate = dayjs.utc(item.startDate).toDate().getTime();
        const itemEndDate = dayjs.utc(item.endDate).toDate().getTime();
        if (eventEndDate <= itemStartDate || eventStartDate >= itemEndDate)
          return false;

        return true;
      })
    );
  }, []);

  /** Update events object after change datetime */
  const updateEventsToSpaceByDatesRange = useCallback(
    (prevSpaces: ISlotEventsToSpaces) => {
      let newSpaces = { ...prevSpaces };

      for (let eventId in datesRange) {
        for (let spaceId in newSpaces) {
          let events = newSpaces[spaceId];

          let others = events.filter((item) => item.id !== Number(eventId));
          let changeItem = events.find((item) => item.id === Number(eventId));

          if (changeItem) {
            const updatedItem = {
              ...changeItem,
              startDate: datesRange[eventId].startDate,
              endDate: datesRange[eventId].endDate,
            };

            others.push(updatedItem);
          }

          newSpaces[spaceId] = others;
        }
      }

      return newSpaces;
    },
    [datesRange]
  );

  const [day, setDay] = useState(1);

  const incrementDay = useCallback(() => {
    setDay((currentDay) => {
      onChange?.({
        type: 'SCROLL_TO_NEXT_DAY',
        data: {
          date: dayjs
            .utc(options?.date)
            .add(currentDay, 'day')
            .format('DD/MM/YYYY'),
        },
      });

      return currentDay + 1;
    });
  }, [onChange]);

  useEffect(() => {
    if (options.infiniteScrolling && options.mode === ECalendarMode.DAILY) {
      setDay(1);
      incrementDay();
    } else setDay(1);
  }, [options.mode, options.view, options.date, options.infiniteScrolling]);

  const getPartialEvents = useCallback(
    (events: ISlot[], from: number, to: number) =>
      events.filter((event) => {
        const date = dayjs.utc(options?.date);

        const difference = dayjs.utc(event.startDate).diff(date, 'hour');

        return difference >= from && difference <= to;
      }),
    [options?.date]
  );

  /** Calculating top/left position and height/width of events for daily mode */
  const getTopAndHeight = useCallback(
    (startDate: number, endDate: number, horizontal?: boolean) => {
      const hour = Number(dayjs.utc(startDate).format('HH'));
      const minutes = Number(dayjs.utc(startDate).format('mm'));

      const diff = dayjs.utc(endDate).diff(dayjs.utc(startDate), 'minutes');

      const h = horizontal
        ? calculateMarginLeftFromMinutes(diff)
        : calculateMarginFromMinutes(diff);
      const marginTop = horizontal
        ? calculateMarginLeftFromHours(hour)
        : calculateMarginFromHours(hour);
      const marginTopMin = horizontal
        ? calculateMarginLeftFromMinutes(minutes)
        : calculateMarginFromMinutes(minutes);

      const total = marginTop + marginTopMin;

      const today = dayjs.utc(options?.date);
      const date = dayjs.utc(startDate);

      const difference = date.diff(today, 'hour');
      const differenceAtDays = Math.abs(Math.floor(difference / 24));

      const size = horizontal ? oneHourWidth : oneHourHeight;

      let shift = size * 24 * differenceAtDays;
      if (difference < 0) shift *= -1;

      return { height: h || 0, top: total + shift || 0 + shift };
    },
    [options?.date]
  );

  /** Making events groups with collision */
  const getEventsGroups = useCallback(
    (events: ISlot[], horizontal?: boolean): IEventsGroup[] => {
      let groups = [] as ISlot[][];

      for (let i = 0; i < events.length; i++) {
        let group = [
          events[i],
          ...events.filter((event) => hasCollision(events[i], [event])),
        ];
        groups.push(group);
      }

      for (let i = 0; i < groups.length; i++) {
        for (let j = i + 1; j < groups.length; j++) {
          if (
            groups[i] &&
            groups[j] &&
            groups[i].find((item) =>
              groups[j].find((event) => item.id === event.id)
            )
          ) {
            groups[i] = Array.from(new Set([...groups[i], ...groups[j]]));
            delete groups[j];
          }
        }
      }

      groups = groups.filter((group) => !!group);

      const result: IEventsGroup[] = [];

      groups.forEach((events) => {
        if (events.length > 1) {
          const startDates = events.map((event) =>
            dayjs.utc(event.startDate).toDate().getTime()
          );
          const endDates = events.map((event) =>
            dayjs.utc(event.endDate).toDate().getTime()
          );

          const minStart = Math.min(...startDates);
          const maxEnd = Math.max(...endDates);

          const { top, height } = getTopAndHeight(minStart, maxEnd, horizontal);

          const groups =
            events.length > 2
              ? getSubGroups(events)
              : [[events[0]], [events[1]]].sort((a, b) => a[0].id - b[0].id);

          result.push({
            items: events.sort((a, b) => a.id - b.id),
            groups,
            top,
            height,
          });
        } else if (events.length === 1)
          result.push({ items: events, item: events[0] });
      });

      return result;
    },
    [getTopAndHeight]
  );

  /** Making groups inside group of events with collision */
  const getSubGroups = useCallback((events: ISlot[]) => {
    const groups = [] as ISlot[][];

    events.forEach((event) => {
      let added = false;
      for (let i = 0; i < groups.length; i++) {
        if (!hasCollision(event, groups[i])) {
          groups[i].push(event);
          added = true;
          break;
        }
      }
      if (!added) groups.push([event]);
    });

    for (let i = 0; i < groups.length; i++) {
      groups[i] = groups[i].sort((a, b) => a.id - b.id);
    }

    return groups.sort((a, b) => a[0].id - b[0].id);
  }, []);

  const updateEventsData = useCallback(() => {
    setEvents((prevEvents) => {
      let newEvents = { ...prevEvents };

      for (let eventId in datesRange) {
        newEvents[eventId] = {
          ...newEvents[eventId],
          startDate: datesRange[eventId].startDate,
          endDate: datesRange[eventId].endDate,
        };
      }

      return newEvents;
    });

    setEventsToSpaces((prevSpaces) =>
      updateEventsToSpaceByDatesRange(prevSpaces)
    );
  }, [updateEventsToSpaceByDatesRange]);

  useEffect(() => {
    updateEventsData();
  }, [datesRange]);

  return (
    <MiddlewareContext.Provider
      value={{
        datesRange,
        updateDatesRange,
        hasCollision,
        day,
        incrementDay,
        updateEventsToSpaceByDatesRange,
        draggableDatesRange,
        setDraggableRange,
        options,
        setOptions,
        partialRanges,
        setPartialRanges,
        getPartialEvents,
        getTopAndHeight,
        getEventsGroups,
        isResizing,
        setIsResizing,
        initiateEventsToSpaces,
        updateEventsData,
        handleDrop,
        events,
        eventsToSpaces,
      }}
    >
      {children}
    </MiddlewareContext.Provider>
  );
};

export const useMiddlewareContext = () => useContext(MiddlewareContext);
