import { Subject } from 'rxjs';
import { share } from 'rxjs/operators';
import {
  MouseEvent,
  MouseEventHandler,
  MouseEventType,
  IMouseEventProps,
  IMouseHandlers,
} from './types';
export { MouseEvent, MouseEventHandler, MouseEventType, IMouseEventProps };

export type MouseHandlerFactory = (
  type: MouseEvent['type'],
  ...handler: Array<MouseEventHandler | undefined>
) => React.MouseEventHandler | undefined;

type MouseEventInternal = MouseEvent & {
  _react: React.MouseEvent; // NB: Used internally, should not be called externally as may cause pooling errors.
};

const dummy = () => null;

/**
 * Retrieves the set of mouse-handlers from a components properties.
 */
export function fromProps(
  props: IMouseEventProps,
  args: {
    force?: MouseEventType[]; // Ensures a handler for the event-type is created, even if one is not passed as a prop.
  } = {},
) {
  const { force = [] } = args;
  const prep = (type: MouseEventType, handler?: React.MouseEventHandler) => {
    return handler ? handler : force.includes(type) ? dummy : undefined;
  };

  return handlers(props.onMouse, {
    onClick: prep('CLICK', props.onClick),
    onDoubleClick: prep('DOUBLE_CLICK', props.onDoubleClick),
    onMouseDown: prep('DOWN', props.onMouseDown),
    onMouseUp: prep('UP', props.onMouseUp),
    onMouseEnter: prep('ENTER', props.onMouseEnter),
    onMouseLeave: prep('LEAVE', props.onMouseLeave),
  });
}

/**
 * Retrieves a set of mouse-handlers to apply to a component, eg:
 *
 *    const mouse = mouse.handlers(this.props.onMouse)
 *    <div {...mouse.props} />
 *
 */
export function handlers(
  handler?: MouseEventHandler,
  args: {
    onClick?: React.MouseEventHandler;
    onDoubleClick?: React.MouseEventHandler;
    onMouseDown?: React.MouseEventHandler;
    onMouseUp?: React.MouseEventHandler;
    onMouseEnter?: React.MouseEventHandler;
    onMouseLeave?: React.MouseEventHandler;
  } = {},
): IMouseHandlers {
  const isActive =
    Boolean(handler) ||
    Object.keys(args).some(key => typeof args[key] === 'function');

  const getSingleHandler = (type: MouseEventType) => {
    switch (type) {
      case 'CLICK':
        return args.onClick;
      case 'DOUBLE_CLICK':
        return args.onDoubleClick;
      case 'DOWN':
        return args.onMouseDown;
      case 'UP':
        return args.onMouseUp;
      case 'ENTER':
        return args.onMouseEnter;
      case 'LEAVE':
        return args.onMouseLeave;
      default:
        throw new Error(`Mouse event type '${type}' not supported.`);
    }
  };

  const next$ = new Subject<MouseEvent>();
  const fireNext = (e: MouseEventInternal) => {
    next$.next(e);
    const singular = getSingleHandler(e.type);
    if (singular) {
      singular(e._react);
    }
  };

  const get: MouseHandlerFactory = type => {
    const hasSingularEvent = Boolean(getSingleHandler(type));
    const handlers = handler || hasSingularEvent ? [fireNext, handler] : [];
    return handler || hasSingularEvent ? handle(type, ...handlers) : undefined;
  };

  return {
    isActive,
    events$: next$.pipe(share()),
    events: {
      onClick: get('CLICK'),
      onDoubleClick: get('DOUBLE_CLICK'),
      onMouseDown: get('DOWN'),
      onMouseUp: get('UP'),
      onMouseEnter: get('ENTER'),
      onMouseLeave: get('LEAVE'),
    },
  };
}

/**
 * Factory for a mouse handler for a single event type.
 */
export const handle: MouseHandlerFactory = (type, ...handler) => {
  const handlers = (handler === undefined
    ? []
    : Array.isArray(handler)
    ? handler
    : [handler]
  ).filter(h => Boolean(h)) as MouseEventHandler[];

  if (handlers.length === 0) {
    return undefined;
  }

  return (e: React.MouseEvent) => {
    handlers.forEach(handler => {
      const args: MouseEventInternal = {
        type,
        button: toButton(e),
        cancel: () => {
          e.preventDefault();
          e.stopPropagation();
        },
        _react: e, // NB: Used internally only.
      };
      handler(args);
    });
  };
};

/**
 * INTERNAL
 */
const toButton = (e: React.MouseEvent): MouseEvent['button'] => {
  const button = e.button;
  switch (button) {
    case 2:
      return 'RIGHT';

    default:
      return 'LEFT';
  }
};
