package net.jonh.mazeharvester;

import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.Dimension;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeSet;
import java.util.Queue;
import java.util.Random;
import java.awt.geom.Line2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;

/**
 * Collects line segments, sorts by color, and then sorts by proximity to reduce plot time.
 */
class SegmentSorter {
  // Two points are the same point if they're within a millionth of a room-width.
  // (Avoid missing an equality due to double precision limitations.)
  static final double COLLAPSE_THRESH = 1e-6;

  // Comically-slow sort, because mazes big enough for O(n^2) are too big for my kids to solve,
  // and I don't have a QuadTree lying around to make this efficient.
  private static List<Line2D> quadraticSort(ImmutableList<Line2D> input) {
    ArrayList<Line2D> segments = new ArrayList<>(input);
    ArrayList<Line2D> output = new ArrayList<>();

    // Pick the first segment arbitrarily.
    Line2D nextSegment = segments.remove(0);
    output.add(nextSegment);

    // Now find nearby segments with scans of the segments array. O(n^2)!
    while (segments.size() > 0) {
      Point2D currentPen = nextSegment.getP2();

      int bestIndex = 0;
      int bestDirection = 0;
      double bestDistance = Double.MAX_VALUE;
      
      for (int i=0; i<segments.size(); i++) {
        Line2D thisLine = segments.get(i);
        for (int direction = 0; direction < 2; direction++) {
          Point2D thisPoint = direction==0 ? thisLine.getP1() : thisLine.getP2();
          double thisDistance = currentPen.distance(thisPoint);
          if (thisDistance < bestDistance) {
            bestIndex = i;
            bestDirection = direction;
            bestDistance = thisDistance;
          }
        }
      }

      nextSegment = segments.remove(bestIndex);
      if (bestDirection == 1) {
        nextSegment = new Line2D.Double(nextSegment.getP2(), nextSegment.getP1());
      }
      output.add(nextSegment);
    }
    return output;
  }

  private static class SegmentCollector implements SegmentGraphics {
    Map<Color, List<Line2D>> segsByColor = new HashMap<>();
    List<Line2D> curColorList;

    void add(Line2D seg) {
      curColorList.add(seg);
    }

    public Iterable<Color> getColors() {
      return segsByColor.keySet();
    }

    public ImmutableList<Line2D> getSegments(Color color) {
      return ImmutableList.copyOf(segsByColor.get(color));
    }

    // Implements SegmentGraphics
    public void setColor(Color color) {
      List<Line2D> list = segsByColor.get(color);
      if (list == null) {
        list = new ArrayList<>();
        segsByColor.put(color, list);
      }
      curColorList = list;
    }

    public void draw(Line2D line2d) {
      if (curColorList == null) {
        setColor(Color.BLACK);
      }
      curColorList.add(line2d);
    }
  }

  SegmentCollector collector = new SegmentCollector();

  public void collect(SegmentPainter painter) {
    painter.paint(collector);
  }

  private List<Line2D> emitCurrentPolyLine(Graphics2D g2d, List<Line2D> segments) {
    if (segments.size() == 0) {
      return segments;
    }

    GeneralPath path = new GeneralPath();
    Point2D p = segments.get(0).getP1();
    path.moveTo(p.getX(), p.getY());
    for (int i=0; i<segments.size(); i++) {
      p = segments.get(i).getP2();
      path.lineTo(p.getX(), p.getY());
    }
    g2d.draw(path);
    return new ArrayList<>();
  }

  public Rectangle2D getBounds() {
    Rectangle2D boundingBox = null;
    for (Color color : collector.getColors()) {
      for (Line2D seg : collector.getSegments(color)) {
        Rectangle2D shapeBox = seg.getBounds2D();
        if (boundingBox == null) {
          boundingBox = shapeBox;
        }
        boundingBox = boundingBox.createUnion(shapeBox);
      }
    }
    return boundingBox;
  }

  public void paint(Graphics2D g2d) {
    g2d.setStroke(new BasicStroke(0.16f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));

    // Greedily gather segments into polyline paths.
    List<Line2D> polyLine = new ArrayList<>();
    Point2D lastPoint = null;
    for (Color color : collector.getColors()) {
      g2d.setPaint(color);
      List<Line2D> sortedSegments = quadraticSort(collector.getSegments(color));
      for (Line2D seg : sortedSegments) {
        if (lastPoint == null || lastPoint.distance(seg.getP1()) > COLLAPSE_THRESH) {
          polyLine = emitCurrentPolyLine(g2d, polyLine);
        }
        polyLine.add(seg);
        lastPoint = seg.getP2();
      }
      polyLine = emitCurrentPolyLine(g2d, polyLine);
    }
  }
}