package org.poly2tri.geometry.polygon;

import org.poly2tri.triangulation.Triangulatable;
import org.poly2tri.triangulation.TriangulationContext;
import org.poly2tri.triangulation.TriangulationMode;
import org.poly2tri.triangulation.TriangulationPoint;
import org.poly2tri.triangulation.delaunay.DelaunayTriangle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

public class Polygon implements Triangulatable {
    private final static Logger logger = LoggerFactory.getLogger(Polygon.class);

    protected ArrayList<TriangulationPoint> _points = new ArrayList<TriangulationPoint>();
    protected ArrayList<TriangulationPoint> _steinerPoints;
    protected ArrayList<Polygon> _holes;

    protected List<DelaunayTriangle> m_triangles;

    protected PolygonPoint _last;

    /**
     * To create a polygon we need atleast 3 separate points
     *
     * @param p1
     * @param p2
     * @param p3
     */
    public Polygon(PolygonPoint p1, PolygonPoint p2, PolygonPoint p3) {
        p1._next = p2;
        p2._next = p3;
        p3._next = p1;
        p1._previous = p3;
        p2._previous = p1;
        p3._previous = p2;
        _points.add(p1);
        _points.add(p2);
        _points.add(p3);
    }

    /**
     * Requires atleast 3 points
     *
     * @param points - ordered list of points forming the polygon.
     *               No duplicates are allowed
     */
    public Polygon(List<PolygonPoint> points) {
        // Lets do one sanity check that first and last point hasn't got same position
        // Its something that often happen when importing polygon data from other formats
        int size = points.size();
        if (points.get(0).equals(points.get(size - 1))) {
            logger.warn("Removed duplicate point");
            points.remove(size - 1);
        }
        _points.addAll(points);
    }

    /**
     * Requires atleast 3 points
     *
     * @param points
     */
    public Polygon(PolygonPoint[] points) {
        this(Arrays.asList(points));
    }

    public TriangulationMode getTriangulationMode() {
        return TriangulationMode.POLYGON;
    }

    public int pointCount() {
        int count = _points.size();
        if (_steinerPoints != null) {
            count += _steinerPoints.size();
        }
        return count;
    }

    public void addSteinerPoint(TriangulationPoint point) {
        if (_steinerPoints == null) {
            _steinerPoints = new ArrayList<TriangulationPoint>();
        }
        _steinerPoints.add(point);
    }

    public void addSteinerPoints(List<TriangulationPoint> points) {
        if (_steinerPoints == null) {
            _steinerPoints = new ArrayList<TriangulationPoint>();
        }
        _steinerPoints.addAll(points);
    }

    public void clearSteinerPoints() {
        if (_steinerPoints != null) {
            _steinerPoints.clear();
        }
    }

    /**
     * Assumes: that given polygon is fully inside the current polygon
     *
     * @param poly - a subtraction polygon
     */
    public void addHole(Polygon poly) {
        if (_holes == null) {
            _holes = new ArrayList<Polygon>();
        }
        _holes.add(poly);
        // XXX: tests could be made here to be sure it is fully inside
//        addSubtraction( poly.getPoints() );
    }

    /**
     * Will insert a point in the polygon after given point
     */
    public void insertPointAfter(PolygonPoint a, PolygonPoint newPoint) {
        // Validate that 
        int index = _points.indexOf(a);
        if (index != -1) {
            newPoint.setNext(a.getNext());
            newPoint.setPrevious(a);
            a.getNext().setPrevious(newPoint);
            a.setNext(newPoint);
            _points.add(index + 1, newPoint);
        } else {
            throw new RuntimeException("Tried to insert a point into a Polygon after a point not belonging to the Polygon");
        }
    }

    public void addPoints(List<PolygonPoint> list) {
        PolygonPoint first;
        for (PolygonPoint p : list) {
            p.setPrevious(_last);
            if (_last != null) {
                p.setNext(_last.getNext());
                _last.setNext(p);
            }
            _last = p;
            _points.add(p);
        }
        first = (PolygonPoint) _points.get(0);
        _last.setNext(first);
        first.setPrevious(_last);
    }

    /**
     * Will add a point after the last point added
     *
     * @param p
     */
    public void addPoint(PolygonPoint p) {
        p.setPrevious(_last);
        p.setNext(_last.getNext());
        _last.setNext(p);
        _points.add(p);
    }

    public void removePoint(PolygonPoint p) {
        PolygonPoint next, prev;

        next = p.getNext();
        prev = p.getPrevious();
        prev.setNext(next);
        next.setPrevious(prev);
        _points.remove(p);
    }

    public PolygonPoint getPoint() {
        return _last;
    }

    public List<TriangulationPoint> getPoints() {
        return _points;
    }

    /**
     * @return Polygon holes
     */
    public ArrayList<Polygon> getHoles() {
        return _holes;
    }

    public List<DelaunayTriangle> getTriangles() {
        return m_triangles;
    }

    public void addTriangle(DelaunayTriangle t) {
        m_triangles.add(t);
    }

    public void addTriangles(List<DelaunayTriangle> list) {
        m_triangles.addAll(list);
    }

    public void clearTriangulation() {
        if (m_triangles != null) {
            m_triangles.clear();
        }
    }

    /**
     * Merge equals points.
     * Creates constraints and populates the context with points.
     */
    public void prepareTriangulation(TriangulationContext<?> tcx) {
        int hint = _points.size();
        if (_steinerPoints != null) {
            hint += _steinerPoints.size();
        }
        if (_holes != null) {
            for (Polygon p : _holes) {
                hint += p.pointCount();
            }
        }
        HashMap<TriangulationPoint, TriangulationPoint> uniquePts = new HashMap<TriangulationPoint, TriangulationPoint>(hint);
        TriangulationPoint.mergeInstances(uniquePts, _points);
        if (_steinerPoints != null) {
            TriangulationPoint.mergeInstances(uniquePts, _steinerPoints);
        }
        if (_holes != null) {
            for (Polygon p : _holes) {
                TriangulationPoint.mergeInstances(uniquePts, p._points);
            }
        }
        int size = _points.size();
        if (m_triangles == null) {
            m_triangles = new ArrayList<DelaunayTriangle>(size);
        } else {
            m_triangles.clear();
        }

        // Outer constraints
        for (int i = 0; i < size - 1; i++) {
            tcx.newConstraint(_points.get(i), _points.get(i + 1));
        }
        tcx.newConstraint(_points.get(0), _points.get(size - 1));

        // Hole constraints
        if (_holes != null) {
            for (Polygon p : _holes) {
                for (int i = 0; i < p._points.size() - 1; i++) {
                    tcx.newConstraint(p._points.get(i), p._points.get(i + 1));
                }
                tcx.newConstraint(p._points.get(0), p._points.get(p._points.size() - 1));
            }
        }
        tcx.addPoints(uniquePts.keySet());
    }

}
