import constrainNumeric from './helpers/constrainNumeric';

export enum HoverState {
  Normal,
  OverSelectionStart,
  OverSelectionEnd,
  OverSelection,
}

export enum EmptySelectionAction {
  SelectNothing,
  SelectPrevious,
}

export interface Selection {
  start: number;
  end: number;
}

export enum SelectionMode {
  Normal,
  Selecting,
  Dragging,
  ResizingStart,
  ResizingEnd,
}

export interface State {
  width: number;
  emptySelectionAction: EmptySelectionAction;
  hoverX: number;
  selection: Selection;
  origSelection: Selection;
  mousedownX: number;
  mode: SelectionMode;
  prevMode: SelectionMode;
  cursorClass: string;
  hoverState: HoverState;
  shouldPublish: boolean;
}

interface SelectionAction {
  type: string;
  x: number;
  // TODO: selection is only used for the setselection SelectionAction. Improve
  // the typing here.
  selection?: Selection;
}

export const stateReducer = (
  {
    selection: prevSelection,
    origSelection,
    mousedownX: prevMousedownX,
    mode: prevMode,
    width,
    emptySelectionAction,
  }: State,
  { x, type, selection: selectionToSet }: SelectionAction
): State => {
  let mode: SelectionMode;
  let newSelection: Selection;
  let mousedownX: number;
  let cursorClass: string;
  let hoverState: HoverState;
  let shouldPublish: boolean | null = null;

  switch (type) {
    case 'setselection':
      newSelection = selectionToSet || { start: 0, end: 0 };
      mousedownX = prevMousedownX;
      mode = SelectionMode.Normal;
      cursorClass = 'cursor-auto';
      hoverState = HoverState.Normal;
      shouldPublish = false;

      break;
    case 'mousedown':
      mousedownX = x;
      cursorClass = 'cursor-auto';
      hoverState = HoverState.Normal;

      if (isHoveringSelectionStart(x, prevSelection)) {
        newSelection = prevSelection;
        mode = SelectionMode.ResizingStart;
      } else if (isHoveringSelectionEnd(x, prevSelection)) {
        newSelection = prevSelection;
        mode = SelectionMode.ResizingEnd;
      } else if (isHoveringSelection(x, prevSelection)) {
        newSelection = prevSelection;
        mode = SelectionMode.Dragging;
        cursorClass = 'cursor-move';
      } else {
        newSelection = { start: x, end: x };
        mode = SelectionMode.Selecting;
        cursorClass = 'cursor-col-resize';
      }

      origSelection = newSelection;

      break;
    case 'mouseup':
      newSelection = prevSelection;
      mousedownX = prevMousedownX;
      mode = SelectionMode.Normal;
      cursorClass = 'cursor-auto';
      hoverState = HoverState.Normal;

      if (
        newSelection.start == newSelection.end &&
        emptySelectionAction == EmptySelectionAction.SelectNothing
      ) {
        newSelection = { start: x, end: x };
      }

      break;
    case 'mouseleave':
      newSelection = prevSelection;
      mousedownX = prevMousedownX;
      mode = prevMode;
      cursorClass = 'cursor-auto';
      hoverState = HoverState.Normal;

      break;
    case 'mousemove':
      mousedownX = prevMousedownX;
      hoverState = HoverState.Normal;

      switch (prevMode) {
        case SelectionMode.Normal: {
          newSelection = prevSelection;
          mode = SelectionMode.Normal;

          if (isHoveringSelectionStart(x, prevSelection)) {
            cursorClass = 'cursor-col-resize';
            hoverState = HoverState.OverSelectionStart;
          } else if (isHoveringSelectionEnd(x, prevSelection)) {
            cursorClass = 'cursor-col-resize';
            hoverState = HoverState.OverSelectionEnd;
          } else if (isHoveringSelection(x, prevSelection)) {
            cursorClass = 'cursor-move';
            hoverState = HoverState.OverSelection;
          } else {
            cursorClass = 'cursor-auto';
            hoverState = HoverState.Normal;
          }

          break;
        }
        case SelectionMode.Selecting: {
          cursorClass = 'cursor-col-resize';
          mode = SelectionMode.Selecting;
          if (x < prevMousedownX) {
            newSelection = {
              start: x,
              end: prevMousedownX,
            };
          } else {
            newSelection = {
              start: prevMousedownX,
              end: x,
            };
          }

          break;
        }
        case SelectionMode.Dragging: {
          mode = SelectionMode.Dragging;
          cursorClass = 'cursor-move';

          const diff = x - prevMousedownX;
          const selectionWidth = origSelection.end - origSelection.start;

          let start = Math.max(0, origSelection.start + diff);
          let end = start + selectionWidth;
          if (end > width) {
            end = width;
            start = end - selectionWidth;
          }
          newSelection = { start: start, end: end };

          break;
        }
        case SelectionMode.ResizingStart: {
          mode = SelectionMode.ResizingStart;
          cursorClass = 'cursor-col-resize';

          const start = constrainNumeric(x, width);
          if (start > origSelection.end) {
            newSelection = { start: origSelection.end, end: start };
          } else {
            newSelection = { ...origSelection, start: start };
          }

          break;
        }
        case SelectionMode.ResizingEnd: {
          mode = SelectionMode.ResizingEnd;
          cursorClass = 'cursor-col-resize';

          const end = constrainNumeric(x, width);
          if (end < origSelection.start) {
            newSelection = {
              start: end,
              end: origSelection.start,
            };
          } else {
            newSelection = { ...origSelection, end: x };
          }

          break;
        }
      }

      break;
    default:
      throw new Error();
  }

  // by default, only trigger the callback if the selection or mode has changed.
  if (shouldPublish == null) {
    shouldPublish = newSelection != prevSelection || mode != prevMode;
  }

  return {
    width: width,
    emptySelectionAction: emptySelectionAction,
    hoverX: x,
    selection: newSelection,
    origSelection: origSelection,
    mousedownX: mousedownX,
    mode: mode,
    prevMode: prevMode,
    cursorClass: cursorClass,
    hoverState: hoverState,
    shouldPublish: shouldPublish,
  };
};

// helpers

const hoverOffset = 10;

const isHoveringSelectionStart = (x: number, selection: Selection): boolean => {
  return x > selection.start - hoverOffset && x < selection.start + hoverOffset;
};

const isHoveringSelectionEnd = (x: number, selection: Selection): boolean => {
  return x > selection.end - hoverOffset && x < selection.end + hoverOffset;
};

const isHoveringSelection = (x: number, selection: Selection): boolean => {
  return x >= selection.start && x <= selection.end;
};
