import React from 'react';
import PropTypes from 'prop-types';
import { ToolPropTypeFields } from '../shared/types';
import { BasePoint } from '../shared/point';
import { chunk, initial, isEmpty } from 'lodash-es';
import debug from 'debug';
import Line from './line';
import DraggablePolygon, { Polygon } from './polygon';
import { types } from '@pie-lib/plot';
import invariant from 'invariant';
import ReactDOM from 'react-dom';
import MarkLabel from '../../mark-label';
import { equalPoints, getMiddleOfTwoPoints, getRightestPoints, stripEmptyLabel } from '../../utils';

const log = debug('pie-lib:graphing:polygon');

export const buildLines = (points, closed) => {
  const expanded = points.reduce((acc, p, index) => {
    acc.push({ ...p, index });

    const isLast = index === points.length - 1;
    const next = isLast ? 0 : index + 1;

    acc.push({ ...points[next], index: next });

    return acc;
  }, []);

  const all = chunk(expanded, 2).map(([from, to]) => {
    return { from, to };
  });

  return closed ? all : initial(all);
};

export const swap = (arr, ...rest) => {
  const pairs = chunk(rest, 2);

  return pairs.reduce(
    (acc, pr) => {
      if (pr.length === 2) {
        let [e, replacement] = pr;

        invariant(Number.isFinite(e.index), 'Index must be defined');

        const index = e.index;
        // const i = acc.findIndex(pt => pt.x === e.x && pt.y === e.y);

        if (index >= 0) {
          acc.splice(index, 1, replacement);

          return acc;
        } else {
          return acc;
        }
      } else {
        return acc;
      }
    },
    [...arr],
  );
};

export class RawBaseComponent extends React.Component {
  static propTypes = {
    classes: PropTypes.object,
    className: PropTypes.string,
    disabled: PropTypes.bool,
    correctness: PropTypes.string,
    points: PropTypes.arrayOf(types.PointType),
    closed: PropTypes.bool,
    coordinatesOnHover: PropTypes.bool,
    onChange: PropTypes.func.isRequired,
    onClosePolygon: PropTypes.func.isRequired,
    onDragStart: PropTypes.func,
    onDragStop: PropTypes.func,
    onClick: PropTypes.func,
    graphProps: types.GraphPropsType.isRequired,
    isToolActive: PropTypes.bool,
    middle: PropTypes.object,
    labelNode: PropTypes.object,
    labelModeEnabled: PropTypes.bool,
    limitLabeling: PropTypes.bool,
    onChangeLabelProps: PropTypes.func,
    onSetMiddleLabel: PropTypes.func,
    onChangeProps: PropTypes.func,
  };

  static defaultProps = {
    points: [],
  };

  // index of the point whose label is edited (points.length for the polygon label), null if none
  state = { editingLabel: null };

  componentDidUpdate(prevProps) {
    // leaving label mode closes any label that is still being edited
    if (prevProps.labelModeEnabled && !this.props.labelModeEnabled && this.state.editingLabel !== null) {
      this.setState({ editingLabel: null });
    }
  }

  dragPoint = (index, from, to) => {
    log('[dragPoint] from, to:', from, to);
    const { onChange, points } = this.props;
    const update = [...points];
    const overlapPoint = !!(points || []).find((p) => equalPoints(p, to));

    if (equalPoints(from, to) || overlapPoint) {
      return;
    }

    if (points[index].label) {
      to.label = points[index].label;
    }

    update.splice(index, 1, to);
    onChange(update);
  };

  dragLine = (existing, next) => {
    log('[dragLine]: ', existing, next);
    const { onChange } = this.props;

    if (existing.from.label) {
      next.from.label = existing.from.label;
    }

    if (existing.to.label) {
      next.to.label = existing.to.label;
    }

    const points = swap(this.props.points, existing.from, next.from, existing.to, next.to);
    onChange(points);
  };

  dragPoly = (existing, next) => {
    log('[dragPoly] ', existing, next);
    const { onChange } = this.props;

    next.forEach((point, index) => {
      if (existing[index].label) {
        next[index].label = existing[index].label;
      }
    });

    onChange(next);
  };

  close = () => {
    const { points, onClosePolygon } = this.props;
    log('[close] ...');

    if (points.length >= 3) {
      onClosePolygon();
    } else {
      log('[close] - nope');
    }
  };

  labelChange = (point, index) => {
    const { points, onChangeProps } = this.props;
    const updatedPoint = { ...point };

    if (!point.label || isEmpty(point.label)) {
      delete updatedPoint.label;
    }

    const update = [...points];

    update.splice(index, 1, updatedPoint);
    onChangeProps(update);
  };

  stopEditingLabel = () => {
    if (this.state.editingLabel !== null) {
      this.setState({ editingLabel: null });
    }
  };

  clickPoint = (point, index, data) => {
    const {
      closed,
      disabled,
      middle: currentMiddle,
      onClick,
      isToolActive,
      labelModeEnabled,
      limitLabeling,
      onSetMiddleLabel,
      points,
    } = this.props;

    if (labelModeEnabled) {
      // limit labeling the points of the polygon
      if (disabled || limitLabeling) {
        return;
      }

      if (points && index === points.length) {
        const { a, b } = getRightestPoints(points);
        const middle = { label: '', ...point, ...getMiddleOfTwoPoints(a, b) };

        onSetMiddleLabel(middle, points.map(stripEmptyLabel));
      } else {
        const strippedPoints = points.map(stripEmptyLabel);
        strippedPoints.splice(index, 1, { label: '', ...point });

        const strippedMiddle = currentMiddle ? stripEmptyLabel(currentMiddle) : currentMiddle;
        onSetMiddleLabel(strippedMiddle, strippedPoints);
      }

      // MarkLabel focuses itself once it is rendered - it also holds on to the focus if the model
      // that comes back from the host (api save) remounts the input.
      this.setState({ editingLabel: index });

      return;
    }

    if (isToolActive && !closed && index === 0) {
      this.close();
      return;
    }

    onClick(point || data);
  };

  // IMPORTANT, do not remove
  input = {};

  render() {
    const {
      closed,
      coordinatesOnHover,
      correctness,
      disabled,
      graphProps,
      onChangeLabelProps,
      onDragStart,
      onDragStop,
      points,
      middle,
      labelNode,
      labelModeEnabled,
    } = this.props;
    const { editingLabel } = this.state;
    const lines = buildLines(points, closed);
    const common = { onDragStart, onDragStop, graphProps, disabled, correctness };
    const polygonLabelIndex = (points && points.length) || 0;
    let polygonLabelNode = null;

    if (labelNode && middle && Object.prototype.hasOwnProperty.call(middle, 'label')) {
      polygonLabelNode = ReactDOM.createPortal(
        <MarkLabel
          inputRef={(r) => (this.input[polygonLabelIndex] = r)}
          autoFocus={editingLabel === polygonLabelIndex}
          disabled={disabled || !labelModeEnabled}
          mark={middle}
          graphProps={graphProps}
          onBlur={this.stopEditingLabel}
          onChange={(label) => onChangeLabelProps({ ...middle, label })}
        />,
        labelNode,
      );
    }

    return (
      <g>
        {closed ? (
          <React.Fragment>
            <DraggablePolygon
              points={points}
              onDrag={this.dragPoly.bind(this, points)}
              closed={closed}
              {...common}
              onClick={this.clickPoint.bind(this, middle, polygonLabelIndex)}
            />
            {polygonLabelNode}
          </React.Fragment>
        ) : (
          <Polygon points={points} graphProps={graphProps} closed={closed} />
        )}
        {(lines || []).map((l, index) => (
          <Line
            key={`line-${index}`}
            from={l.from}
            to={l.to}
            onDrag={this.dragLine.bind(this, l)}
            {...common}
            onClick={this.clickPoint.bind(this, middle, polygonLabelIndex)}
          />
        ))}

        {(points || []).map((p, index) => {
          return [
            <BasePoint
              key={`point-${index}`}
              coordinatesOnHover={coordinatesOnHover}
              labelNode={labelNode}
              x={p.x}
              y={p.y}
              onDrag={this.dragPoint.bind(this, index, p)}
              {...common}
              onClick={this.clickPoint.bind(this, p, index)}
            />,
            labelNode && Object.prototype.hasOwnProperty.call(p, 'label')
              ? ReactDOM.createPortal(
                  <MarkLabel
                    inputRef={(r) => (this.input[index] = r)}
                    autoFocus={editingLabel === index}
                    disabled={disabled || !labelModeEnabled}
                    mark={p}
                    graphProps={graphProps}
                    onBlur={this.stopEditingLabel}
                    onChange={(label) => this.labelChange({ ...p, label }, index)}
                  />,
                  labelNode,
                )
              : null,
          ];
        })}
      </g>
    );
  }
}

export const BaseComponent = RawBaseComponent;

export default class Component extends React.Component {
  static propTypes = {
    ...ToolPropTypeFields,
    graphProps: types.GraphPropsType.isRequired,
  };

  static defaultProps = {};

  constructor(props) {
    super(props);
    this.state = {};
  }

  change = (points) => {
    const {
      mark: { middle },
    } = this.props;
    const mark = { ...this.state.mark, points };

    if (middle) {
      const { a, b } = getRightestPoints(points);

      mark.middle = { ...middle, ...getMiddleOfTwoPoints(a, b) };
    }

    this.setState({ mark });
  };

  changeProps = (points) => {
    const mark = { ...this.props.mark, points };

    this.props.onChange(this.props.mark, mark);
  };

  changeLabelProps = (point) => {
    const { mark, onChange } = this.props;
    const middle = { ...mark.middle, ...point };

    if (!middle.label || isEmpty(middle.label)) {
      delete middle.label;
    }

    onChange(mark, { ...mark, middle });
  };

  setMiddleLabel = (middle, strippedPoints) => {
    const { mark, onChange } = this.props;
    const next = { ...mark, middle };
    if (strippedPoints) {
      next.points = strippedPoints;
    }
    onChange(mark, next);
  };

  closePolygon = () => {
    log('[closePolygon] ...');
    const { onComplete, mark } = this.props;
    const update = { ...mark, closed: true };

    onComplete(mark, update);
  };

  dragStart = () => this.setState({ mark: this.props.mark });

  dragStop = () => {
    const { onChange } = this.props;
    const m = { ...this.state.mark };

    this.setState({ mark: undefined }, () => {
      onChange(this.props.mark, m);
    });
  };

  render() {
    const { coordinatesOnHover, mark, graphProps, onClick, isToolActive, labelNode, labelModeEnabled, limitLabeling } =
      this.props;
    const { mark: stateMark } = this.state;

    return (
      <BaseComponent
        {...(stateMark || mark)}
        coordinatesOnHover={coordinatesOnHover}
        onChange={this.change}
        onChangeLabelProps={this.changeLabelProps}
        onSetMiddleLabel={this.setMiddleLabel}
        onChangeProps={this.changeProps}
        onClosePolygon={this.closePolygon}
        onDragStart={this.dragStart}
        onDragStop={this.dragStop}
        onClick={onClick}
        graphProps={graphProps}
        isToolActive={isToolActive}
        labelNode={labelNode}
        labelModeEnabled={labelModeEnabled}
        limitLabeling={limitLabeling}
      />
    );
  }
}
