import React from 'react';
import { cloneDeep, isEmpty, isEqual } from 'lodash-es';
import { BasePoint } from '../point';
import { gridDraggable, trig, types, utils } from '@pie-lib/plot';
import PropTypes from 'prop-types';
import { correct, disabled, disabledSecondary, incorrect, missing } from '../styles';
import ReactDOM from 'react-dom';
import MarkLabel from '../../../mark-label';
import { color } from '@pie-lib/render-ui';
import { equalPoints, getMiddleOfTwoPoints, sameAxes, stripEmptyLabel } from '../../../utils';
import { styled } from '@mui/material/styles';

const StyledLineGroup = styled('g')(({ disabled, correctness }) => ({
  '& line:not(.hit-area)': {
    fill: 'transparent',
    stroke: color.defaults.BLACK,
    strokeWidth: 3,
    transition: 'stroke 200ms ease-in, stroke-width 200ms ease-in',
    '&:hover': {
      strokeWidth: 6,
      stroke: color.defaults.PRIMARY_DARK,
    },
  },
  ...(disabled && {
    '& line:not(.hit-area)': {
      ...disabledSecondary('stroke'),
      strokeWidth: 2,
    },
  }),
  ...(correctness === 'correct' && {
    '& line:not(.hit-area)': {
      ...correct('stroke'),
    },
  }),
  ...(correctness === 'incorrect' && {
    '& line:not(.hit-area)': {
      ...incorrect('stroke'),
    },
  }),
  ...(correctness === 'missing' && {
    '& line:not(.hit-area)': {
      ...missing('stroke'),
      strokeWidth: 1,
      strokeDasharray: '4 3',
    },
  }),
}));

export const lineTool = (type, Component) => () => ({
  type,
  Component,
  addPoint: (point, mark) => {
    if (mark && equalPoints(mark.root, point)) {
      return mark;
    }

    if (!mark) {
      return {
        type,
        building: true,
        from: point,
      };
    }

    if (equalPoints(point, mark.from)) {
      return { ...mark };
    }

    return { ...mark, building: false, to: point };
  },
});

export const lineToolComponent = (Component) => {
  return class LineToolComponent extends React.Component {
    static propTypes = {
      ...types.ToolPropTypeFields,
      graphProps: types.GraphPropsType.isRequired,
      limitLabeling: PropTypes.bool,
      changeMarkProps: PropTypes.func,
      disabled: PropTypes.bool,
      from: types.PointType,
      to: types.PointType,
      labelModeEnabled: PropTypes.bool,
      onClick: PropTypes.func,
    };

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

    startDrag = () => {
      const { onDragStart } = this.props;
      this.setState({ mark: { ...this.props.mark } });
      if (onDragStart) onDragStart();
    };

    stopDrag = () => {
      const { onChange, mark, onDragStop } = this.props;
      const update = { ...this.state.mark };

      this.setState({ mark: undefined }, () => {
        const { type } = update;
        let shouldNotChange =
          type &&
          (type === 'parabola' || type === 'sine' || type === 'absolute' || type === 'exponential') &&
          sameAxes(update.from, update.to);
        if (!shouldNotChange && type && type === 'exponential' && update.from && update.to) {
          shouldNotChange = update.from.y === 0 || update.to.y === 0 || update.from.y * update.to.y < 0;
        }
        if (!isEqual(mark, update) && !shouldNotChange) {
          onChange(mark, update);
        }
        if (onDragStop) onDragStop();
      });
    };

    changeMark = ({ from, to, middle }) => {
      let mark = { ...this.state.mark, from, to };

      if (middle) {
        mark = { ...mark, middle };
      }

      this.setState({ mark });
    };

    changeMarkProps = ({ from, to, middle }) => {
      const { onChange, mark } = this.props;
      let update = { ...mark, ...this.state.mark };

      if (from) {
        update = { ...update, from };
      }

      if (to) {
        update = { ...update, to };
      }

      if (middle) {
        update = { ...update, middle };
      }

      if (!isEqual(mark, update)) {
        onChange(mark, update);
      }
    };

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

      const from = cloneDeep(mark.from);
      const to = cloneDeep(mark.to);
      const middle = cloneDeep(mark.middle);

      // SET DISABLED
      // if it's a background mark, we need to force disable it
      if (from && mark.isBackground) {
        from.disabled = true;
      }

      if (to && mark.isBackground) {
        to.disabled = true;
      }

      if (middle && mark.isBackground) {
        middle.disabled = true;
      }

      return (
        <Component
          disabled={mark.disabled}
          coordinatesOnHover={coordinatesOnHover}
          correctness={mark.correctness}
          from={from}
          to={to}
          middle={middle}
          graphProps={graphProps}
          onChange={this.changeMark}
          changeMarkProps={this.changeMarkProps}
          onClick={onClick}
          onDragStart={this.startDrag}
          onDragStop={this.stopDrag}
          labelNode={labelNode}
          labelModeEnabled={labelModeEnabled}
          limitLabeling={limitLabeling}
        />
      );
    }
  };
};

const dragOpts = () => ({
  bounds: (props, { domain, range }) => {
    const area = utils.lineToArea(props.from, props.to);
    return utils.bounds(area, domain, range);
  },
  anchorPoint: (props) => {
    const { from } = props;
    return from;
  },
  fromDelta: (props, delta) => {
    const { from, to } = props;
    return {
      from: utils.point(from).add(utils.point(delta)),
      to: utils.point(to).add(utils.point(delta)),
    };
  },
});

export const lineBase = (Comp, opts) => {
  const DraggableComp = gridDraggable(dragOpts())(Comp);

  const FromPoint = opts && opts.from ? opts.from : BasePoint;
  const ToPoint = opts && opts.to ? opts.to : BasePoint;

  class LineBase extends React.Component {
    static propTypes = {
      coordinatesOnHover: PropTypes.bool,
      graphProps: types.GraphPropsType,
      from: types.PointType,
      to: types.PointType,
      middle: types.PointType,
      onChange: PropTypes.func,
      onDragStart: PropTypes.func,
      onDragStop: PropTypes.func,
      onClick: PropTypes.func,
      correctness: PropTypes.string,
      disabled: PropTypes.bool,
      limitLabeling: PropTypes.bool,
      changeMarkProps: PropTypes.func,
      labelModeEnabled: PropTypes.bool,
      labelNode: PropTypes.object,
    };

    // which of from/to/middle has its label edited, 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 });
      }
    }

    onChangePoint = (point) => {
      const { middle, onChange } = this.props;
      const { from, to } = point;

      // because point.from.label and point.to.label can be different
      if (!equalPoints(from, to)) {
        if (middle) {
          point.middle = { ...middle, ...getMiddleOfTwoPoints(from, to) };
        }

        onChange(point);
      }
    };

    dragComp = ({ from: draggedFrom, to: draggedTo }) => {
      const { from, to, onChange, middle } = this.props;

      if (from.label) {
        draggedFrom.label = from.label;
      }

      if (to.label) {
        draggedTo.label = to.label;
      }

      const updated = { from: draggedFrom, to: draggedTo };

      if (middle) {
        updated.middle = { ...middle, ...getMiddleOfTwoPoints(draggedFrom, draggedTo) };
      }

      onChange(updated);
    };

    dragFrom = (draggedFrom) => {
      const { from, to } = this.props;

      if (from.label) {
        draggedFrom.label = from.label;
      }

      if (!equalPoints(draggedFrom, to)) {
        this.onChangePoint({ from: draggedFrom, to: to });
      }
    };

    dragTo = (draggedTo) => {
      const { from, to } = this.props;

      if (to.label) {
        draggedTo.label = to.label;
      }

      if (!equalPoints(from, draggedTo)) {
        this.onChangePoint({ from: from, to: draggedTo });
      }
    };

    labelChange = (point, type) => {
      const { changeMarkProps } = this.props;
      const update = { ...point };

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

      changeMarkProps({ [type]: update });
    };

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

    clickPoint = (point, type, data) => {
      const { changeMarkProps, disabled, from, to, middle, labelModeEnabled, limitLabeling, onClick } = this.props;

      if (!labelModeEnabled) {
        onClick(point || data);
        return;
      }

      if (disabled) {
        return;
      }

      // limit labeling the points of the line
      if (limitLabeling) {
        return;
      }

      if (type === 'middle' && !point && from && to) {
        point = { ...point, ...getMiddleOfTwoPoints(from, to) };
      }

      changeMarkProps({
        from: stripEmptyLabel(from),
        to: stripEmptyLabel(to),
        middle: stripEmptyLabel(middle),
        [type]: { label: '', ...point },
      });

      // 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: type });
    };

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

    render() {
      const {
        coordinatesOnHover,
        graphProps,
        onDragStart,
        onDragStop,
        from,
        to,
        middle,
        disabled,
        correctness,
        onClick,
        labelNode,
        labelModeEnabled,
      } = this.props;
      const { editingLabel } = this.state;
      const common = { graphProps, onDragStart, onDragStop, disabled, correctness, onClick };
      const angle = to ? trig.toDegrees(trig.angle(from, to)) : 0;

      let fromLabelNode = null;
      let toLabelNode = null;
      let lineLabelNode = null;

      if (labelNode) {
        if (from && Object.prototype.hasOwnProperty.call(from, 'label')) {
          fromLabelNode = ReactDOM.createPortal(
            <MarkLabel
              inputRef={(r) => (this.input.from = r)}
              autoFocus={editingLabel === 'from'}
              disabled={!labelModeEnabled}
              mark={from}
              graphProps={graphProps}
              onBlur={this.stopEditingLabel}
              onChange={(label) => this.labelChange({ ...from, label }, 'from')}
            />,
            labelNode,
          );
        }

        if (to && Object.prototype.hasOwnProperty.call(to, 'label')) {
          toLabelNode = ReactDOM.createPortal(
            <MarkLabel
              inputRef={(r) => (this.input.to = r)}
              autoFocus={editingLabel === 'to'}
              disabled={!labelModeEnabled}
              mark={to}
              graphProps={graphProps}
              onBlur={this.stopEditingLabel}
              onChange={(label) => this.labelChange({ ...to, label }, 'to')}
            />,
            labelNode,
          );
        }

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

      return (
        <StyledLineGroup disabled={disabled} correctness={correctness}>
          {to && (
            <DraggableComp
              from={from}
              to={to}
              middle={middle}
              onDrag={this.dragComp}
              {...common}
              onClick={(data) => this.clickPoint(middle, 'middle', data)}
            />
          )}
          {lineLabelNode}

          <FromPoint
            x={from.x}
            y={from.y}
            labelNode={labelNode}
            coordinatesOnHover={coordinatesOnHover}
            onDrag={this.dragFrom}
            {...common}
            onClick={(data) => this.clickPoint(from, 'from', data)}
          />
          {fromLabelNode}

          {to && (
            <ToPoint
              x={to.x}
              y={to.y}
              angle={angle} //angle + 45}
              labelNode={labelNode}
              coordinatesOnHover={coordinatesOnHover}
              onDrag={this.dragTo}
              {...common}
              onClick={(data) => this.clickPoint(to, 'to', data)}
            />
          )}
          {toLabelNode}
        </StyledLineGroup>
      );
    }
  }

  return LineBase;
};

export const styles = {
  line: () => ({
    fill: 'transparent',
    stroke: color.defaults.BLACK,
    strokeWidth: 3,
    transition: 'stroke 200ms ease-in, stroke-width 200ms ease-in',
    '&:hover': {
      strokeWidth: 6,
      stroke: color.defaults.PRIMARY_DARK,
    },
  }),
  arrow: () => ({
    fill: color.defaults.BLACK,
  }),
  disabledArrow: () => ({
    ...disabledSecondary(),
  }),
  disabledSecondary: () => ({
    ...disabledSecondary('stroke'),
    strokeWidth: 2,
  }),
  disabled: () => ({
    ...disabled('stroke'),
    strokeWidth: 2,
  }),
  correct: (theme, key) => ({
    ...correct(key),
  }),
  incorrect: (theme, key) => ({
    ...incorrect(key),
  }),
  missing: (theme, key) => ({
    ...missing(key),
    strokeWidth: 1,
    strokeDasharray: '4 3',
  }),
};
