import { Fragment, useEffect, useMemo, useRef, useState } from "react";
import { ListBox } from "primereact/listbox";
import { Calendar } from "primereact/calendar";
import { OverlayPanel } from "primereact/overlaypanel";
import { InputText } from "primereact/inputtext";
import { Nullable } from "primereact/ts-helpers";
import { Dropdown } from "primereact/dropdown";
import { SelectItem, SelectItemOptionsType } from "primereact/selectitem";
import { OSIcon } from "../icon";
import { OSButton } from "../button/OSButton";

interface TimeRange {
  [label: string]: Date[];
}

interface Props {
  id?: string;
  value?: [Date, Date];
  inputName?: string;

  startDate?: Date;
  endDate?: Date;
  minDate?: Date;
  maxDate?: Date;

  timePicker?: boolean;
  timePickerIncrement?: number;
  timePickerSeconds?: boolean;

  ranges?: TimeRange;
  showCustomRangeLabel?: boolean;
  alwaysShowCalendars?: boolean;
  opens?: "left" | "right" | "center";
  drops?: "down" | "up" | "auto";

  buttonClasses?: string;
  applyButtonClasses?: string;
  cancelButtonClasses?: string;

  singleDatePicker?: boolean;
  autoApply?: boolean;

  autoUpdateInput?: boolean;
  parentEl?: string;

  disabled?: boolean;

  onChange?(start: Date, end: Date, predefinedLabel?: string | null): void;
}

const DateTimePicker: React.FC<Props> = ({
  id,
  value,
  inputName,
  minDate,
  maxDate,
  timePicker,
  timePickerIncrement,
  timePickerSeconds,
  ranges,
  showCustomRangeLabel,
  alwaysShowCalendars,
  buttonClasses,
  applyButtonClasses,
  cancelButtonClasses,
  singleDatePicker,
  autoApply,
  disabled,
  onChange,
}) => {
  const calendarRef = useRef(null);
  const overlayRef = useRef<OverlayPanel>(null);

  const [predefined, setPredefined] = useState<boolean>(false);
  const [rawDates, setRawDates] = useState<Nullable<(Date | null)[]>>();
  const [rawPreset, setRawPreset] = useState<string>();

  const [startDate, setStartDate] = useState<Date>();
  const [endDate, setEndDate] = useState<Date>();

  const [tempStartDate, setTempStartDate] = useState<Date>();
  const [tempEndDate, setTempEndDate] = useState<Date>();

  const customValue = "custom";

  // if the time picker is selected, we can't auto apply
  autoApply = autoApply && !timePicker;

  // define ranges according to definitions provided
  var rangeList: SelectItemOptionsType;
  const buildRangeString = (startDate: Date, endDate: Date): string => {
    return startDate.toISOString() + " - " + endDate.toISOString();
  };
  rangeList = useMemo(() => {
    let calculatedList: SelectItemOptionsType = [];
    if (ranges != null) {
      calculatedList = Object.keys(ranges).map((key) => {
        if (ranges[key].length == 2) {
          return {
            value: buildRangeString(ranges[key][0], ranges[key][1]),
            label: key,
          };
        }
      });
      // add the custom range
      if (showCustomRangeLabel) {
        calculatedList.push({
          value: customValue,
          label: "Custom",
        });
      }
    }
    // reset the selected value
    setRawPreset(undefined)
    return calculatedList;
  }, [ranges]);

  // set the preset to a value if set
  if (rawPreset == undefined && value != undefined) {
    let dates = value;
    let standardisedString = value.toString();
    if (dates.length == 2) {
      let [valueStartDate, valueEndDate] = dates;
      setStartDate(valueStartDate);
      setTempStartDate(valueStartDate);
      setEndDate(new Date(valueEndDate));
      setTempEndDate(new Date(valueEndDate));
      standardisedString = buildRangeString(valueStartDate, valueEndDate);
    }

    let foundPreset = false;
    rangeList.forEach((element) => {
      if (element.value == standardisedString) {
        foundPreset = true;
      }
    });
    // if the value matches a preset, use that. Otherwise, set custom
    if (foundPreset) {
      setRawPreset(standardisedString);
      setPredefined(true);
    } else {
      setRawPreset(customValue);
      setPredefined(false);
    }
  }

  const displayValue =
    (startDate?.toLocaleString() ?? "") +
    " - " +
    (endDate?.toLocaleString() ?? "");
  const tempString =
    (tempStartDate?.toLocaleString() ?? "") +
    " - " +
    (tempEndDate?.toLocaleString() ?? "");

  // attributes for time picker
  var hoursList: SelectItem[] | undefined;
  var minutesList: SelectItem[] | undefined;
  var secondsList: SelectItem[] | undefined;
  if (timePicker) {
    hoursList = useMemo(() => {
      // define hours and minutes
      let calculatedList = [];
      for (let x = 0; x < 24; x++) {
        calculatedList.push({
          value: x,
          label: x.toString().padStart(2, "0"),
        });
      }
      return calculatedList;
    }, []);
    minutesList = useMemo(() => {
      let calculatedList = [];
      for (let x = 0; x < 60; x += timePickerIncrement ?? 30) {
        calculatedList.push({
          value: x,
          label: x.toString().padStart(2, "0"),
        });
      }
      return calculatedList;
    }, []);
    secondsList = useMemo(() => {
      let calculatedList = [];
      for (let x = 0; x < 60; x++) {
        calculatedList.push({
          value: x,
          label: x.toString().padStart(2, "0"),
        });
      }
      return calculatedList;
    }, []);
  }

  // split the date range array into start and end dates
  const handleDateRangeSet = (dates: Nullable<(Date | null)[]>) => {
    let newStartDate = startDate ?? new Date();
    let newEndDate = endDate ?? new Date();
    if (dates != null) {
      if (autoApply) {
        // set the actual dates
        if (dates[0] != null) {
          newStartDate = new Date(changeDate(startDate, dates[0]));
          setStartDate(newStartDate);
          // if there's only one date, use it as the end too
          if (dates.length == 1) {
            newEndDate = new Date(changeDate(endDate, dates[0]));
            setEndDate(newEndDate);
          }
        }
        if (dates[1] != null) {
          newEndDate = new Date(changeDate(endDate, dates[1]));
          setEndDate(newEndDate);
        }
        if (onChange !== undefined) {
          onChange(newStartDate, newEndDate, rawPreset);
        }
      } else {
        // set the temp dates
        if (dates[0] != null) {
          newStartDate = new Date(changeDate(tempStartDate, dates[0]));
          setTempStartDate(newStartDate);
          // if there's only one date, use it as the end too
          if (dates.length == 1) {
            newEndDate = new Date(changeDate(tempEndDate, dates[0]));
            setTempEndDate(newEndDate);
          }
        }
        if (dates[1] != null) {
          newEndDate = new Date(changeDate(tempEndDate, dates[1]));
          setTempEndDate(newEndDate);
        }
      }
    }
  };

  // handle selecting a preset
  const handlePresetSelected = (presetString: string) => {
    setRawPreset(presetString);
    if (presetString == customValue) {
      setPredefined(false);
      if (autoApply) {
        setEndDate(new Date(changeSeconds(endDate, 0)));
        if (minutesList != undefined) {
          let foundOption = false;
          minutesList.forEach((element) => {
            if (element.value == endDate?.getMinutes()) {
              foundOption = true;
            }
          });
          if (!foundOption) {
            setEndDate(
              new Date(
                changeMinutes(
                  endDate,
                  minutesList.at(minutesList.length - 1)?.value,
                ),
              ),
            );
          }
        }
      } else {
        setTempEndDate(new Date(changeSeconds(tempEndDate, 0)));
        if (minutesList != undefined) {
          let foundOption = false;
          minutesList.forEach((element) => {
            if (element.value == tempEndDate?.getMinutes()) {
              foundOption = true;
            }
          });
          if (!foundOption) {
            setTempEndDate(
              new Date(
                changeMinutes(
                  tempEndDate,
                  minutesList.at(minutesList.length - 1)?.value,
                ),
              ),
            );
          }
        }
      }
    } else if (presetString != null) {
      setPredefined(true);
      // convert the ISO times into date objects
      var dates = presetString.split(" - ", 2);
      if (dates.length == 2) {
        setStartDate(new Date(dates[0]));
        setEndDate(new Date(dates[1]));

        if (onChange !== undefined) {
          onChange(new Date(dates[0]), new Date(dates[1]), rawPreset);
        }
      }
      // hide the overlay
      overlayRef.current?.hide();
    }
  };

  // buttons to cancel or apply date changes
  const handleDateApply = () => {
    let startDate = new Date(tempStartDate ?? new Date());
    setStartDate(startDate);
    let endDate = new Date(tempEndDate ?? new Date());
    setEndDate(endDate);
    if (onChange !== undefined) {
      onChange(startDate, endDate, rawPreset);
    }
    overlayRef.current?.hide();
  };
  const handleDateCancel = () => {
    setTempStartDate(new Date(startDate ?? new Date()));
    setTempEndDate(new Date(endDate ?? new Date()));
  };

  // utility methods to transform date objects as required
  const changeDate = (alteringDate: Date | undefined, newDate: Date) => {
    if (alteringDate == undefined) {
      alteringDate = new Date();
    }
    alteringDate.setDate(newDate.getDate());
    alteringDate.setMonth(newDate.getMonth());
    alteringDate.setFullYear(newDate.getFullYear());
    return alteringDate;
  };
  const changeHours = (
    alteringDate: Date | undefined,
    newHour: string | number,
  ) => {
    if (alteringDate == undefined) {
      alteringDate = new Date();
    }
    if (typeof newHour == "string") {
      newHour = parseInt(newHour);
    }
    alteringDate.setHours(newHour);
    return alteringDate;
  };
  const changeMinutes = (
    alteringDate: Date | undefined,
    newMinute: string | number,
  ) => {
    if (alteringDate == undefined) {
      alteringDate = new Date();
    }
    if (typeof newMinute == "string") {
      newMinute = parseInt(newMinute);
    }
    alteringDate.setMinutes(newMinute);
    return alteringDate;
  };
  const changeSeconds = (
    alteringDate: Date | undefined,
    newSecond: string | number,
  ) => {
    if (alteringDate == undefined) {
      alteringDate = new Date();
    }
    if (typeof newSecond == "string") {
      newSecond = parseInt(newSecond);
    }
    alteringDate.setSeconds(newSecond);
    return alteringDate;
  };

  const getTodayRange = (): [Date, Date] => {
    const start = new Date();
    start.setHours(0, 0, 0, 0);
    const end = new Date();
    end.setHours(23, 59, 59, 999);
    return [start, end];
  };

  useEffect(() => {
    if (!startDate && !endDate) {
      const [todayStart, todayEnd] = getTodayRange();
      setStartDate(todayStart);
      setEndDate(todayEnd);

      setTempStartDate(todayStart);
      setTempEndDate(todayEnd);

      if (onChange) {
        onChange(todayStart, todayEnd, undefined);
      }
    }
  }, [rangeList]);

  // actually render the component
  return (
    <Fragment>
      <div className="p-inputgroup flex-auto">
        <span className="p-inputgroup-addon">
          <OSIcon identifier="calendar" weight="solid" />
        </span>

        <InputText
          id={id}
          value={displayValue}
          name={inputName}
          readOnly
          className="w-auto"
          onClick={(e) => {
            e.preventDefault();
            overlayRef.current?.toggle(e);
          }}
          disabled={disabled}
        />
      </div>
      <OverlayPanel ref={overlayRef}>
        <div className="grid grid-cols-12 w-full p-0 gap-0">
          <div
            className={
              !predefined ? "md:col-span-4 col-span-12" : "col-span-12"
            }
          >
            <ListBox
              value={rawPreset}
              options={rangeList}
              onChange={(e) => {
                handlePresetSelected(e.value);
              }}
            />
          </div>
          {(!predefined || alwaysShowCalendars || rangeList.length == 0) && (
            <Fragment>
              <div className="md:col-span-8 col-span-12">
                <Calendar
                  ref={calendarRef}
                  numberOfMonths={singleDatePicker ? 1 : 2}
                  inline
                  selectionMode="range"
                  maxDate={maxDate}
                  minDate={minDate}
                  value={rawDates}
                  onChange={(e) => {
                    setRawDates(e.value);
                    handleDateRangeSet(e.value);
                  }}
                  disabled={disabled}
                />
                {timePicker && (
                  <div className="grid grid-cols-12">
                    <div className="col-span-6 grid grid-cols-12">
                      <Dropdown
                        className={
                          timePickerSeconds ? "col-span-4" : "col-span-6"
                        }
                        options={hoursList}
                        value={
                          autoApply
                            ? startDate?.getHours()
                            : tempStartDate?.getHours()
                        }
                        onChange={(e) => {
                          if (autoApply) {
                            setStartDate(
                              new Date(changeHours(startDate, e.value)),
                            );
                          } else {
                            setTempStartDate(
                              new Date(changeHours(tempStartDate, e.value)),
                            );
                          }
                        }}
                        disabled={disabled}
                      />
                      <Dropdown
                        className={
                          timePickerSeconds ? "col-span-4" : "col-span-6"
                        }
                        options={minutesList}
                        value={
                          autoApply
                            ? startDate?.getMinutes()
                            : tempStartDate?.getMinutes()
                        }
                        onChange={(e) => {
                          if (autoApply) {
                            setStartDate(
                              new Date(changeMinutes(startDate, e.value)),
                            );
                          } else {
                            setTempStartDate(
                              new Date(changeMinutes(tempStartDate, e.value)),
                            );
                          }
                        }}
                        disabled={disabled}
                      />
                      {timePickerSeconds && (
                        <Dropdown
                          className="col-span-4"
                          options={secondsList}
                          value={
                            timePickerSeconds
                              ? autoApply
                                ? startDate?.getSeconds()
                                : tempStartDate?.getSeconds()
                              : 0
                          }
                          onChange={(e) => {
                            if (autoApply) {
                              setStartDate(
                                new Date(changeSeconds(startDate, e.value)),
                              );
                            } else {
                              setTempStartDate(
                                new Date(changeSeconds(tempStartDate, e.value)),
                              );
                            }
                          }}
                          disabled={disabled}
                        />
                      )}
                    </div>
                    <div className="col-span-6 grid grid-cols-12">
                      <Dropdown
                        className={
                          timePickerSeconds ? "col-span-4" : "col-span-6"
                        }
                        options={hoursList}
                        value={
                          autoApply
                            ? endDate?.getHours()
                            : tempEndDate?.getHours()
                        }
                        onChange={(e) => {
                          if (autoApply) {
                            setEndDate(new Date(changeHours(endDate, e.value)));
                          } else {
                            setTempEndDate(
                              new Date(changeHours(tempEndDate, e.value)),
                            );
                          }
                        }}
                        disabled={disabled}
                      />
                      <Dropdown
                        className={
                          timePickerSeconds ? "col-span-4" : "col-span-6"
                        }
                        options={minutesList}
                        value={
                          autoApply
                            ? endDate?.getMinutes()
                            : tempEndDate?.getMinutes()
                        }
                        onChange={(e) => {
                          if (autoApply) {
                            setEndDate(
                              new Date(changeMinutes(endDate, e.value)),
                            );
                          } else {
                            setTempEndDate(
                              new Date(changeMinutes(tempEndDate, e.value)),
                            );
                          }
                        }}
                        disabled={disabled}
                      />
                      {timePickerSeconds && (
                        <Dropdown
                          className="col-span-4"
                          options={secondsList}
                          value={
                            timePickerSeconds
                              ? autoApply
                                ? endDate?.getSeconds()
                                : tempEndDate?.getSeconds()
                              : 0
                          }
                          onChange={(e) => {
                            if (autoApply) {
                              setEndDate(
                                new Date(changeSeconds(endDate, e.value)),
                              );
                            } else {
                              setTempEndDate(
                                new Date(changeSeconds(tempEndDate, e.value)),
                              );
                            }
                          }}
                          disabled={disabled}
                        />
                      )}
                    </div>
                  </div>
                )}
              </div>
              {!autoApply && (
                <div className="col-span-12">
                  <div className="float-right gap-internal items-center flex">
                    <span className="inline-block align-middle mr-3">
                      {tempString}
                    </span>
                    <OSButton
                      label="Cancel"
                      severity="contrast"
                      onClick={handleDateCancel}
                      className={
                        (buttonClasses ?? "") + (cancelButtonClasses ?? "")
                      }
                      disabled={disabled}
                    />
                    <OSButton
                      label="Apply"
                      severity="success"
                      onClick={handleDateApply}
                      className={
                        "" + (buttonClasses ?? "") + (applyButtonClasses ?? "")
                      }
                      disabled={disabled}
                    />
                  </div>
                </div>
              )}
            </Fragment>
          )}
        </div>
      </OverlayPanel>
    </Fragment>
  );
};

export default DateTimePicker;
