/*
 *
 * This file is part of the iText (R) project.
 * Copyright (c) 2014-2015 iText Group NV
 * Authors: Bruno Lowagie, Paulo Soares, et al.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation with the addition of the
 * following permission added to Section 15 as permitted in Section 7(a):
 * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
 * ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
 * OF THIRD PARTY RIGHTS
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA, 02110-1301 USA, or download the license from the following URL:
 * http://itextpdf.com/terms-of-use/
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License,
 * a covered work must retain the producer line in every PDF that is created
 * or manipulated using iText.
 *
 * You can be released from the requirements of the license by purchasing
 * a commercial license. Buying such a license is mandatory as soon as you
 * develop commercial activities involving the iText software without
 * disclosing the source code of your own applications.
 * These activities include: offering paid services to customers as an ASP,
 * serving PDFs on the fly in a web application, shipping iText with a closed
 * source product.
 *
 * For more information, please contact iText Software Corp. at this
 * address: sales@itextpdf.com
 *
 *
 * This class is based on the C# open source freeware library Clipper:
 * http://www.angusj.com/delphi/clipper.php
 * The original classes were distributed under the Boost Software License:
 *
 * Freeware for both open source and commercial applications
 * Copyright 2010-2014 Angus Johnson
 * Boost Software License - Version 1.0 - August 17th, 2003
 *
 * Permission is hereby granted, free of charge, to any person or organization
 * obtaining a copy of the software and accompanying documentation covered by
 * this license (the "Software") to use, reproduce, display, distribute,
 * execute, and transmit the Software, and to prepare derivative works of the
 * Software, and to permit third-parties to whom the Software is furnished to
 * do so, all subject to the following:
 *
 * The copyright notices in the Software and this entire statement, including
 * the above license grant, this restriction and the following disclaimer,
 * must be included in all copies of the Software, in whole or in part, and
 * all derivative works of the Software, unless such copies or derivative
 * works are solely in the form of machine-executable object code generated by
 * a source language processor.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */
package com.itextpdf.text.pdf.parser.clipper;

import com.itextpdf.text.pdf.parser.clipper.Point.LongPoint;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

/**
 * A pure convenience class to avoid writing List<IntPoint> everywhere.
 *
 * @author Tobias Mahlmann
 *
 */
public class Path extends ArrayList<LongPoint> {
    static class Join {
        Path.OutPt outPt1;
        Path.OutPt outPt2;
        private LongPoint offPt;

        public LongPoint getOffPt() {
            return offPt;
        }

        public void setOffPt( LongPoint offPt ) {
            this.offPt = offPt;
        }

    }

    static class OutPt {
        public static OutRec getLowerMostRec( OutRec outRec1, OutRec outRec2 ) {
            //work out which polygon fragment has the correct hole state ...
            if (outRec1.bottomPt == null) {
                outRec1.bottomPt = outRec1.pts.getBottomPt();
            }
            if (outRec2.bottomPt == null) {
                outRec2.bottomPt = outRec2.pts.getBottomPt();
            }
            final Path.OutPt bPt1 = outRec1.bottomPt;
            final Path.OutPt bPt2 = outRec2.bottomPt;
            if (bPt1.getPt().getY() > bPt2.getPt().getY()) {
                return outRec1;
            }
            else if (bPt1.getPt().getY() < bPt2.getPt().getY()) {
                return outRec2;
            }
            else if (bPt1.getPt().getX() < bPt2.getPt().getX()) {
                return outRec1;
            }
            else if (bPt1.getPt().getX() > bPt2.getPt().getX()) {
                return outRec2;
            }
            else if (bPt1.next == bPt1) {
                return outRec2;
            }
            else if (bPt2.next == bPt2) {
                return outRec1;
            }
            else if (isFirstBottomPt( bPt1, bPt2 )) {
                return outRec1;
            }
            else {
                return outRec2;
            }
        }

        private static boolean isFirstBottomPt( Path.OutPt btmPt1, Path.OutPt btmPt2 ) {
            Path.OutPt p = btmPt1.prev;
            while (p.getPt().equals( btmPt1.getPt() ) && !p.equals( btmPt1 )) {
                p = p.prev;
            }
            final double dx1p = Math.abs( LongPoint.getDeltaX( btmPt1.getPt(), p.getPt() ) );
            p = btmPt1.next;
            while (p.getPt().equals( btmPt1.getPt() ) && !p.equals( btmPt1 )) {
                p = p.next;
            }
            final double dx1n = Math.abs( LongPoint.getDeltaX( btmPt1.getPt(), p.getPt() ) );

            p = btmPt2.prev;
            while (p.getPt().equals( btmPt2.getPt() ) && !p.equals( btmPt2 )) {
                p = p.prev;
            }
            final double dx2p = Math.abs( LongPoint.getDeltaX( btmPt2.getPt(), p.getPt() ) );
            p = btmPt2.next;
            while (p.getPt().equals( btmPt2.getPt() ) && p.equals( btmPt2 )) {
                p = p.next;
            }
            final double dx2n = Math.abs( LongPoint.getDeltaX( btmPt2.getPt(), p.getPt() ) );
            return dx1p >= dx2p && dx1p >= dx2n || dx1n >= dx2p && dx1n >= dx2n;
        }

        int idx;
        protected LongPoint pt;
        OutPt next;

        OutPt prev;

        public Path.OutPt duplicate( boolean InsertAfter ) {
            final Path.OutPt result = new Path.OutPt();
            result.setPt( new LongPoint( getPt() ) );
            result.idx = idx;
            if (InsertAfter) {
                result.next = next;
                result.prev = this;
                next.prev = result;
                next = result;
            }
            else {
                result.prev = prev;
                result.next = this;
                prev.next = result;
                prev = result;
            }
            return result;
        }

        Path.OutPt getBottomPt() {
            Path.OutPt dups = null;
            Path.OutPt p = next;
            Path.OutPt pp = this;
            while (p != pp) {
                if (p.getPt().getY() > pp.getPt().getY()) {
                    pp = p;
                    dups = null;
                }
                else if (p.getPt().getY() == pp.getPt().getY() && p.getPt().getX() <= pp.getPt().getX()) {
                    if (p.getPt().getX() < pp.getPt().getX()) {
                        dups = null;
                        pp = p;
                    }
                    else {
                        if (p.next != pp && p.prev != pp) {
                            dups = p;
                        }
                    }
                }
                p = p.next;
            }
            if (dups != null) {
                //there appears to be at least 2 vertices at bottomPt so ...
                while (dups != p) {
                    if (!isFirstBottomPt( p, dups )) {
                        pp = dups;
                    }
                    dups = dups.next;
                    while (!dups.getPt().equals( pp.getPt() )) {
                        dups = dups.next;
                    }
                }
            }
            return pp;
        }

        public int getPointCount() {

            int result = 0;
            Path.OutPt p = this;
            do {
                result++;
                p = p.next;
            }
            while (p != this && p != null);
            return result;
        }

        public LongPoint getPt() {
            return pt;
        }

        public void reversePolyPtLinks() {

            Path.OutPt pp1;
            Path.OutPt pp2;
            pp1 = this;
            do {
                pp2 = pp1.next;
                pp1.next = pp1.prev;
                pp1.prev = pp2;
                pp1 = pp2;
            }
            while (pp1 != this);
        }

        public void setPt( LongPoint pt ) {
            this.pt = pt;
        }
    }

    static protected class Maxima
    {
        protected long X;
        protected Maxima Next;
        protected Maxima Prev;
    };

    static class OutRec {
        int Idx;

        boolean isHole;

        boolean isOpen;
        OutRec firstLeft; //see comments in clipper.pas
        protected Path.OutPt pts;
        Path.OutPt bottomPt;
        PolyNode polyNode;

        public double area() {
            Path.OutPt op = pts;
            if (op == null) {
                return 0;
            }
            double a = 0;
            do {
                a = a + (double) (op.prev.getPt().getX() + op.getPt().getX()) * (double) (op.prev.getPt().getY() - op.getPt().getY());
                op = op.next;
            }
            while (op != pts);
            return a * 0.5;
        }

        public void fixHoleLinkage() {
            //skip if an outermost polygon or
            //already already points to the correct FirstLeft ...
            if (firstLeft == null || isHole != firstLeft.isHole && firstLeft.pts != null) {
                return;
            }

            OutRec orfl = firstLeft;
            while (orfl != null && (orfl.isHole == isHole || orfl.pts == null)) {
                orfl = orfl.firstLeft;
            }
            firstLeft = orfl;
        }

        public Path.OutPt getPoints() {
            return pts;
        }

        public void setPoints( Path.OutPt pts ) {
            this.pts = pts;
        }
    }

    private static Path.OutPt excludeOp( Path.OutPt op ) {
        final Path.OutPt result = op.prev;
        result.next = op.next;
        op.next.prev = result;
        result.idx = 0;
        return result;
    }

    /**
     *
     */
    private static final long serialVersionUID = -7120161578077546673L;

    public Path() {
        super();
    }

    public Path(Point.LongPoint[] points) {
        this();
        for (LongPoint point : points) {
            add(point);
        }
    }

    public Path( int cnt ) {
        super( cnt );
    }

    public Path(Collection<? extends LongPoint> c) {
        super(c);
    }

    public double area() {
        final int cnt = size();
        if (cnt < 3) {
            return 0;
        }
        double a = 0;
        for (int i = 0, j = cnt - 1; i < cnt; ++i) {
            a += ((double) get( j ).getX() + get( i ).getX()) * ((double) get( j ).getY() - get( i ).getY());
            j = i;
        }
        return -a * 0.5;
    }

    public Path cleanPolygon() {
        return cleanPolygon( 1.415 );
    }

    public Path cleanPolygon( double distance ) {
        //distance = proximity in units/pixels below which vertices will be stripped.
        //Default ~= sqrt(2) so when adjacent vertices or semi-adjacent vertices have
        //both x & y coords within 1 unit, then the second vertex will be stripped.

        int cnt = size();

        if (cnt == 0) {
            return new Path();
        }

        Path.OutPt[] outPts = new Path.OutPt[cnt];
        for (int i = 0; i < cnt; ++i) {
            outPts[i] = new Path.OutPt();
        }

        for (int i = 0; i < cnt; ++i) {
            outPts[i].pt = get( i );
            outPts[i].next = outPts[(i + 1) % cnt];
            outPts[i].next.prev = outPts[i];
            outPts[i].idx = 0;
        }

        final double distSqrd = distance * distance;
        Path.OutPt op = outPts[0];
        while (op.idx == 0 && op.next != op.prev) {
            if (Point.arePointsClose( op.pt, op.prev.pt, distSqrd )) {
                op = excludeOp( op );
                cnt--;
            }
            else if (Point.arePointsClose( op.prev.pt, op.next.pt, distSqrd )) {
                excludeOp( op.next );
                op = excludeOp( op );
                cnt -= 2;
            }
            else if (Point.slopesNearCollinear( op.prev.pt, op.pt, op.next.pt, distSqrd )) {
                op = excludeOp( op );
                cnt--;
            }
            else {
                op.idx = 1;
                op = op.next;
            }
        }

        if (cnt < 3) {
            cnt = 0;
        }
        final Path result = new Path( cnt );
        for (int i = 0; i < cnt; ++i) {
            result.add( op.pt );
            op = op.next;
        }
        outPts = null;
        return result;
    }

    public int isPointInPolygon( LongPoint pt ) {
        //returns 0 if false, +1 if true, -1 if pt ON polygon boundary
        //See "The Point in Polygon Problem for Arbitrary Polygons" by Hormann & Agathos
        //http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf
        int result = 0;
        final int cnt = size();
        if (cnt < 3) {
            return 0;
        }
        LongPoint ip = get( 0 );
        for (int i = 1; i <= cnt; ++i) {
            final LongPoint ipNext = i == cnt ? get( 0 ) : get( i );
            if (ipNext.getY() == pt.getY()) {
                if (ipNext.getX() == pt.getX() || ip.getY() == pt.getY() && ipNext.getX() > pt.getX() == ip.getX() < pt.getX()) {
                    return -1;
                }
            }
            if (ip.getY() < pt.getY() != ipNext.getY() < pt.getY()) {
                if (ip.getX() >= pt.getX()) {
                    if (ipNext.getX() > pt.getX()) {
                        result = 1 - result;
                    }
                    else {
                        final double d = (double) (ip.getX() - pt.getX()) * (ipNext.getY() - pt.getY()) - (double) (ipNext.getX() - pt.getX())
                                        * (ip.getY() - pt.getY());
                        if (d == 0) {
                            return -1;
                        }
                        else if (d > 0 == ipNext.getY() > ip.getY()) {
                            result = 1 - result;
                        }
                    }
                }
                else {
                    if (ipNext.getX() > pt.getX()) {
                        final double d = (double) (ip.getX() - pt.getX()) * (ipNext.getY() - pt.getY()) - (double) (ipNext.getX() - pt.getX())
                                        * (ip.getY() - pt.getY());
                        if (d == 0) {
                            return -1;
                        }
                        else if (d > 0 == ipNext.getY() > ip.getY()) {
                            result = 1 - result;
                        }
                    }
                }
            }
            ip = ipNext;
        }
        return result;
    }

    public boolean orientation() {
        return area() >= 0;
    }

    public void reverse() {
        Collections.reverse( this );
    }

    public Path TranslatePath( LongPoint delta ) {
        final Path outPath = new Path( size() );
        for (int i = 0; i < size(); i++) {
            outPath.add( new LongPoint( get( i ).getX() + delta.getX(), get( i ).getY() + delta.getY() ) );
        }
        return outPath;
    }
}
