import update from 'react-addons-update';
import { Matrix } from 'cal';
import ClipperShape from 'clipper-js';
import { recursiveClone } from './clone.js';
import { addObject, removeObject } from '../reducer/objectReducers.js';
import { SHAPE_TYPE_PROPERTIES } from '../constants/shapeTypeProperties.js';
import { shapeToPoints } from '../shape/shapeToPoints.js';
import { applyMatrixOnShape, pathToVectorPath } from './vectorUtils.js';
import { findEndPointIndexesOfPaths, mergePaths } from '../reducer/pointsReducers.js';
import R from 'ramda';
// import createDebug from 'debug';
// const debug = createDebug('d3d:util:substractShapeFromState');

function getShapePaths(shapeData, matrix) {
  matrix = shapeData.transform.multiplyMatrix(matrix);
  const shapes = shapeToPoints(shapeData)
    .reduce((a, { points, holes }) => a.concat([points, ...holes]), []);
  // collect all paths (transform to Scene Space)
  return applyMatrixOnShape(shapes, matrix);
}

// Does the last point of the outline equal the last point?
// TODO move to pointsReducer
function isClosed(paths) {
  const outline = paths[0];
  const firstPoint = outline[0];
  const lastPoint = outline[outline.length - 1];
  return firstPoint.equals(lastPoint);
}

export default function subtractShapeFromState(state, differenceShape, tool, options = {}) {
  const { matrix = new Matrix(), skipCompoundPath = false, scale, skip } = options;
  const objectsToAdd = [];

  if (scale !== undefined) differenceShape.scaleUp(scale);

  for (const id of [...state.spaces[state.activeSpace].objectIds]) {
    const shapeData = state.objectsById[id];

    if (skip && skip.includes(id)) continue;

    // if shape doens't interact with tool go to next shape
    if (!SHAPE_TYPE_PROPERTIES[shapeData.type].tools[tool]) continue;

    const paths = getShapePaths(shapeData, matrix);

    if (skipCompoundPath && shapeData.fill) continue;

    let shape = new ClipperShape(paths, shapeData.fill, true, false, false);
    if (scale !== undefined) shape = shape.scaleUp(scale);
    shape = shape.round().removeDuplicates();

    // shape touched differenceShape ?
    // TODO optimize: can we find a cheaper check?
    const hasCollsion = shape.intersect(differenceShape).paths.length > 0;

    if (!hasCollsion) continue;

    const resultShape = shape.difference(differenceShape);

    // Nothing left?
    if (resultShape.paths.length === 0) {
      state = removeObject(state, shapeData.UID);
      continue;
    }
    // Seperate shapes, group hole shapes with their outline (shape they're a hole in).
    // Returns two dimentional array, with per shape an array of paths.
    // The first item of each path is the outline, the others are holes, if there are any.
    let resultShapes = resultShape
      .seperateShapes()
      .map(resultPaths => { // go through all created shapes (1 shape can be a combination of outline+holes)
        if (scale !== undefined) resultPaths.scaleDown(scale);
        resultPaths = resultPaths
          .removeDuplicates()
          .mapToLower()
          .filter(path => path.length > 1)
          .map(pathToVectorPath);

        // Clipper never adds a line from the last point to first point,
        // so we add these lines for all paths of this shape manually.
        // But only when this was a filled shape.
        if (shapeData.fill) {
          resultPaths.forEach(path => path.push(path[0].clone()));
        }

        return resultPaths;
      })
      .filter(resultPaths => resultPaths.length > 0);

    if (resultShapes.length === 0) {
      state = removeObject(state, shapeData.UID);
      continue;
    }

    if (!shapeData.fill && isClosed(paths)) {
      resultShapes = mergeShapes(resultShapes);
    }

    // go through all (merged) shapes
    for (let i = 0; i < resultShapes.length; i ++) {
      const [points, ...holes] = resultShapes[i];

      // try updating shape with first path.
      // only possible if compount path or free hand shape
      if (i === 0 && ['COMPOUND_PATH', 'FREE_HAND', 'POLYGON'].includes(shapeData.type)) {
        state = update(state, {
          objectsById: {
            [shapeData.UID]: {
              points: { $set: points },
              holes: { $set: holes },
              transform: { $set: matrix.inverseMatrix() }
            }
          }
        });
      } else {
        // if first shape but not a compount path or free hand shape,
        // we can't update so we have to remove
        if (i === 0) {
          state = removeObject(state, shapeData.UID);
        }

        objectsToAdd.push({
          ...recursiveClone(shapeData),
          type: shapeData.fill ? 'COMPOUND_PATH' : 'FREE_HAND',
          transform: matrix.inverseMatrix(),
          points,
          holes
        });
      }
    }
  }

  for (let i = 0; i < objectsToAdd.length; i ++) {
    const object = objectsToAdd[i];
    state = addObject(state, object);
  }

  return state;
}

const getOutline = R.head();

function mergeShapes(shapes) {
  // debug('mergeShapes');
  // get outlines from shapes
  const paths = R.map(getOutline, shapes);
  // debug('    paths: ', toString(paths));
  const match = findEndPointIndexesOfPaths(paths);
  // debug('    match: ', toString(match));
  if (match) {
    const [pathAIndex, pathBIndex, matchingIndexes] = match;
    const mergedPath = mergePaths(paths[pathAIndex], paths[pathBIndex], matchingIndexes); // merge path
    // debug('    mergedPath: ', toString(mergedPath));
    const mergedShape = [mergedPath]; // turn path into shape
    // update shapes
    const newShapes = R.pipe(
      R.update(pathAIndex, mergedShape), // update one shape with merged shape
      R.remove(pathBIndex, 1) // remove other path
    )(shapes);
    // if merge was found we start over
    return mergeShapes(newShapes);
  } else { // no merge, just return same shapes
    return shapes;
  }
}
