import update from 'react-addons-update';
import * as actions from '../../../actions/index.js';
import createDebug from 'debug';
import { removeObject, addObjectActive2D, setActive2D } from '../../../reducer/objectReducers.js';
import { SNAPPING_DISTANCE, LINE_WIDTH } from '../../../constants/d2Constants.js';
import { SHAPE_TYPE_PROPERTIES } from '../../../constants/shapeTypeProperties.js';
import { getSnappingPoints } from '../../../utils/objectSelectors.js';
import { fitPath, segmentBezierPath } from '../../../utils/curveUtils.js';
import * as tools from '../../../constants/d2Tools.js';
const debug = createDebug('d3d:reducer:pen');

const LINE_DIAMETER = LINE_WIDTH / 2.0;
const FREE_HAND = 'FREE_HAND';
const POLYGON = 'POLYGON';
const BRUSH = 'BRUSH';

export default function penReducer(state, action) {
  if (action.log !== false && action.type) debug(action.type);
  const screenPosition = action.position;

  const activeShape = state.d2.activeShape;
  const activeTool = state.d2.tool;

  let screenMatrixZoom;
  let screenMatrixZoomInverse;
  if (action.screenMatrixZoom) {
    screenMatrixZoom = action.screenMatrixZoom;
    screenMatrixZoomInverse = action.screenMatrixZoom.inverseMatrix();
  }

  switch (action.type) {
    case actions.D2_DRAG_START: {
      const preDrags = action.preDrags.map((preDrag) => preDrag.applyMatrix(screenMatrixZoomInverse));

      switch (activeTool) {
        case tools.POLYGON: {
          return addObjectActive2D(state, {
            type: POLYGON,
            // draw straight line between drag start and current position
            points: [preDrags[0], preDrags[preDrags.length - 1]]
          });
        }

        case tools.BRUSH: {
          return addObjectActive2D(state, {
            type: BRUSH,
            strokeWidth: state.d2.brush.size,
            points: preDrags
          });
        }

        case tools.FREE_HAND: {
          return addObjectActive2D(state, {
            type: FREE_HAND,
            points: preDrags
          });
        }

        default:
          return state;
      }
    }
    case actions.D2_DRAG: {
      if (activeShape) {
        const shapeData = state.objectsById[activeShape];
        const points = shapeData.points;

        const matrix = shapeData.transform.multiplyMatrix(screenMatrixZoom).inverseMatrix();
        const position = screenPosition.applyMatrix(matrix);

        switch (activeTool) {
          case tools.POLYGON: {
            return update(state, {
              objectsById: {
                [activeShape]: {
                  points: { [points.length - 1]: { $set: position } }
                }
              }
            }); // update last point to curren mouse position
          }
          case tools.BRUSH:
          case tools.FREE_HAND: {
            state = update(state, {
              objectsById: {
                [activeShape]: {
                  points: { $push: [position] }
                }
              }
            }); // add current mouse position to points array
          }
          default:
            return state;
        }
      }
      return state;
    }
    case actions.D2_DRAG_END: {
      if (activeShape) {
        state = setActive2D(state, null);

        const currentShapeData = state.objectsById[activeShape];
        const shapeProps = SHAPE_TYPE_PROPERTIES[currentShapeData.type];
        if (!shapeProps.snapping) return state;

        let currentPoints = currentShapeData.points;

        // smooth
        if (activeTool === tools.FREE_HAND) {
          // convert to screen space
          currentPoints = currentPoints.map(point => point.applyMatrix(screenMatrixZoom));
          // smooth path
          currentPoints = segmentBezierPath(fitPath(currentPoints));
          // convert back to object space
          currentPoints = currentPoints.map(point => point.applyMatrix(screenMatrixZoomInverse));
        }

        const currentStartPoint = currentPoints[0].applyMatrix(screenMatrixZoom);
        const currentEndPoint = currentPoints[currentPoints.length - 1].applyMatrix(screenMatrixZoom);

        // create list of possible snapping connections
        // sorts on distance to connection and filters out connections that exceed the snapping threshold
        const snappingDistance = SNAPPING_DISTANCE + LINE_DIAMETER * screenMatrixZoom.sx;
        const snappingPoints = getSnappingPoints(state, screenMatrixZoom)
          .reduce((points, { shapeData, endPoint, startPoint }) => {
            if (shapeData.UID === activeShape) {
              if (currentPoints.length >= 3) {
                points.push({ shapeData, hit: 'self', distance: endPoint.distanceTo(startPoint) });
              }
            } else {
              points.push(
                { shapeData, hit: 'start-start', distance: currentStartPoint.distanceTo(startPoint) },
                { shapeData, hit: 'start-end', distance: currentStartPoint.distanceTo(endPoint) },
                { shapeData, hit: 'end-start', distance: currentEndPoint.distanceTo(startPoint) },
                { shapeData, hit: 'end-end', distance: currentEndPoint.distanceTo(endPoint) }
              );
            }
            return points;
          }, [])
          .filter(({ distance }) => distance < snappingDistance)
          .sort((a, b) => a.distance - b.distance);

        // the active shape's start and end points can only be connected to one other shape,
        // this variable can be used to check if the start or end point is already been snapped to
        // when the point has a connection it stores the shape UID of the connected shape
        let currentEndPointHit = false;
        let currentStartPointHit = false;

        for (const { hit, shapeData } of snappingPoints) {
          let newPoints;
          if (hit !== 'self') {
            const newMatrix = shapeData.transform.multiplyMatrix(currentShapeData.transform.inverseMatrix());
            newPoints = shapeData.points.map(point => point.applyMatrix(newMatrix));
          }

          if (hit === 'start-start' || hit === 'end-end') newPoints.reverse();

          switch (hit) {
            case 'self':
              // can only connect to self if end point AND start point aren't connected yet
              if (!currentEndPointHit && !currentStartPointHit) {
                currentEndPointHit = shapeData.UID;
                currentStartPointHit = shapeData.UID;

                currentPoints = update(currentPoints, {
                  [currentPoints.length - 1]: { $set: currentPoints[0].clone() }
                });
              }
              break;

            case 'start-start':
            case 'start-end':
              // can only connect start point if start point isn't connected yet
              if (!currentStartPointHit) {
                currentStartPointHit = shapeData.UID;

                if (currentEndPointHit === shapeData.UID) {
                  // if the end point is connected to the same shape it has already been added to
                  // the end of the shape and does not have to be added to the start of the shape
                  if (activeTool === tools.FREE_HAND) {
                    currentPoints = update(currentPoints, {
                      $push: [currentPoints[0].clone()]
                    });
                  } else if (activeTool === tools.POLYGON) {
                    currentPoints = update(currentPoints, {
                      [currentPoints.length - 1]: { $set: currentPoints[0].clone() }
                    });
                  }
                } else {
                  // add the shape to the start of the active shape
                  if (activeTool === tools.FREE_HAND) {
                    currentPoints = update(currentPoints, {
                      $splice: [[0, 0, ...newPoints]]
                    });
                  } else if (activeTool === tools.POLYGON) {
                    currentPoints = update(currentPoints, {
                      $splice: [[0, 1, ...newPoints]]
                    });
                  }
                }

                if (state.objectsById[shapeData.UID]) state = removeObject(state, shapeData.UID);
              }
              break;

            case 'end-start':
            case 'end-end':
              if (!currentEndPointHit) {
                currentEndPointHit = shapeData.UID;

                if (currentStartPointHit === shapeData.UID) {
                  // if the start point is connected to the same shape it has already been added to
                  // the start of the shape and does not have to be added to the end of the shape
                  if (activeTool === tools.FREE_HAND) {
                    currentPoints = update(currentPoints, {
                      $push: [currentPoints[0].clone()]
                    });
                  } else if (activeTool === tools.POLYGON) {
                    currentPoints = update(currentPoints, {
                      [currentPoints.length - 1]: { $set: currentPoints[0].clone() }
                    });
                  }
                } else {
                  // add the shape to the end of the active shape
                  if (activeTool === tools.FREE_HAND) {
                    currentPoints = update(currentPoints, {
                      $push: newPoints
                    });
                  } else if (activeTool === tools.POLYGON) {
                    currentPoints = update(currentPoints, {
                      $splice: [[currentPoints.length - 1, 1, ...newPoints]]
                    });
                  }
                }

                if (state.objectsById[shapeData.UID]) state = removeObject(state, shapeData.UID);
              }
              break;

            default:
              break;
          }
        }

        state = update(state, {
          objectsById: {
            [activeShape]: {
              points: { $set: currentPoints }
            }
          }
        });
      }
      return state;
    }
    default:
      return state;
  }
}