/*
 *
 * 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.List;
import java.util.logging.Logger;

public abstract class ClipperBase implements Clipper {
    protected class LocalMinima {
        long y;
        Edge leftBound;
        Edge rightBound;
        LocalMinima next;
    };

    protected class Scanbeam {
        long y;
        Scanbeam next;
    };

    private static void initEdge( Edge e, Edge eNext, Edge ePrev, LongPoint pt ) {
        e.next = eNext;
        e.prev = ePrev;
        e.setCurrent( new LongPoint( pt ) );
        e.outIdx = Edge.UNASSIGNED;
    }

    private static void initEdge2( Edge e, PolyType polyType ) {
        if (e.getCurrent().getY() >= e.next.getCurrent().getY()) {
            e.setBot( new LongPoint( e.getCurrent() ) );
            e.setTop( new LongPoint( e.next.getCurrent() ) );
        }
        else {
            e.setTop( new LongPoint( e.getCurrent() ) );
            e.setBot( new LongPoint( e.next.getCurrent() ) );
        }
        e.updateDeltaX();
        e.polyTyp = polyType;
    }

    private static boolean rangeTest( LongPoint Pt, boolean useFullRange ) {
        if (useFullRange) {
            if (Pt.getX() > HI_RANGE || Pt.getY() > HI_RANGE || -Pt.getX() > HI_RANGE || -Pt.getY() > HI_RANGE)
                throw new IllegalStateException("Coordinate outside allowed range");
        } else if (Pt.getX() > LOW_RANGE || Pt.getY() > LOW_RANGE || -Pt.getX() > LOW_RANGE || -Pt.getY() > LOW_RANGE) {
            return rangeTest(Pt, true);
        }

        return useFullRange;
    }

    private static Edge removeEdge( Edge e ) {
        //removes e from double_linked_list (but without removing from memory)
        e.prev.next = e.next;
        e.next.prev = e.prev;
        final Edge result = e.next;
        e.prev = null; //flag as removed (see ClipperBase.Clear)
        return result;
    }

    private final static long LOW_RANGE = 0x3FFFFFFF;

    private final static long HI_RANGE = 0x3FFFFFFFFFFFFFFFL;

    protected LocalMinima minimaList;

    protected LocalMinima currentLM;

    private final List<List<Edge>> edges;

    protected boolean useFullRange;

    protected boolean hasOpenPaths;

    protected final boolean preserveCollinear;

    private final static Logger LOGGER = Logger.getLogger( Clipper.class.getName() );

    protected ClipperBase( boolean preserveCollinear ) //constructor (nb: no external instantiation)
    {
        this.preserveCollinear = preserveCollinear;
        minimaList = null;
        currentLM = null;
        hasOpenPaths = false;
        edges = new ArrayList<List<Edge>>();
    }

    public boolean addPath( Path pg, PolyType polyType, boolean Closed ) {

        if (!Closed && polyType == PolyType.CLIP) {
            throw new IllegalStateException( "AddPath: Open paths must be subject." );
        }

        int highI = pg.size() - 1;
        if (Closed) {
            while (highI > 0 && pg.get( highI ).equals( pg.get( 0 ) )) {
                --highI;
            }
        }
        while (highI > 0 && pg.get( highI ).equals( pg.get( highI - 1 ) )) {
            --highI;
        }
        if (Closed && highI < 2 || !Closed && highI < 1) {
            return false;
        }

        //create a new edge array ...
        final List<Edge> edges = new ArrayList<Edge>( highI + 1 );
        for (int i = 0; i <= highI; i++) {
            edges.add( new Edge() );
        }

        boolean IsFlat = true;

        //1. Basic (first) edge initialization ...
        edges.get( 1 ).setCurrent( new LongPoint( pg.get( 1 ) ) );
        useFullRange = rangeTest( pg.get( 0 ), useFullRange );
        useFullRange = rangeTest( pg.get( highI ), useFullRange );
        initEdge( edges.get( 0 ), edges.get( 1 ), edges.get( highI ), pg.get( 0 ) );
        initEdge( edges.get( highI ), edges.get( 0 ), edges.get( highI - 1 ), pg.get( highI ) );
        for (int i = highI - 1; i >= 1; --i) {
            useFullRange = rangeTest( pg.get( i ), useFullRange );
            initEdge( edges.get( i ), edges.get( i + 1 ), edges.get( i - 1 ), pg.get( i ) );
        }
        Edge eStart = edges.get( 0 );

        //2. Remove duplicate vertices, and (when closed) collinear edges ...
        Edge e = eStart, eLoopStop = eStart;
        for (;;) {
            //nb: allows matching start and end points when not Closed ...
            if (e.getCurrent().equals( e.next.getCurrent() ) && (Closed || !e.next.equals( eStart ))) {
                if (e == e.next) {
                    break;
                }
                if (e == eStart) {
                    eStart = e.next;
                }
                e = removeEdge( e );
                eLoopStop = e;
                continue;
            }
            if (e.prev == e.next) {
                break; //only two vertices
            }
            else if (Closed && Point.slopesEqual( e.prev.getCurrent(), e.getCurrent(), e.next.getCurrent(), useFullRange )
                            && (!isPreserveCollinear() || !Point.isPt2BetweenPt1AndPt3( e.prev.getCurrent(), e.getCurrent(), e.next.getCurrent() ))) {
                //Collinear edges are allowed for open paths but in closed paths
                //the default is to merge adjacent collinear edges into a single edge.
                //However, if the PreserveCollinear property is enabled, only overlapping
                //collinear edges (ie spikes) will be removed from closed paths.
                if (e == eStart) {
                    eStart = e.next;
                }
                e = removeEdge( e );
                e = e.prev;
                eLoopStop = e;
                continue;
            }
            e = e.next;
            if (e == eLoopStop || !Closed && e.next == eStart) {
                break;
            }
        }

        if (!Closed && e == e.next || Closed && e.prev == e.next) {
            return false;
        }

        if (!Closed) {
            hasOpenPaths = true;
            eStart.prev.outIdx = Edge.SKIP;
        }

        //3. Do second stage of edge initialization ...
        e = eStart;
        do {
            initEdge2( e, polyType );
            e = e.next;
            if (IsFlat && e.getCurrent().getY() != eStart.getCurrent().getY()) {
                IsFlat = false;
            }
        }
        while (e != eStart);

        //4. Finally, add edge bounds to LocalMinima list ...

        //Totally flat paths must be handled differently when adding them
        //to LocalMinima list to avoid endless loops etc ...
        if (IsFlat) {
            if (Closed) {
                return false;
            }
            e.prev.outIdx = Edge.SKIP;
            final LocalMinima locMin = new LocalMinima();
            locMin.next = null;
            locMin.y = e.getBot().getY();
            locMin.leftBound = null;
            locMin.rightBound = e;
            locMin.rightBound.side = Edge.Side.RIGHT;
            locMin.rightBound.windDelta = 0;
            for ( ; ; )
            {
                if (e.getBot().getX() != e.prev.getTop().getX()) e.reverseHorizontal();
                if (e.next.outIdx == Edge.SKIP) break;
                e.nextInLML = e.next;
                e = e.next;
            }
            insertLocalMinima( locMin );
            this.edges.add( edges );
            return true;
        }

        this.edges.add( edges );
        boolean leftBoundIsForward;
        Edge EMin = null;

        //workaround to avoid an endless loop in the while loop below when
        //open paths have matching start and end points ...
        if (e.prev.getBot().equals( e.prev.getTop() )) {
            e = e.next;
        }

        for (;;) {
            e = e.findNextLocMin();
            if (e == EMin) {
                break;
            }
            else if (EMin == null) {
                EMin = e;
            }

            //E and E.Prev now share a local minima (left aligned if horizontal).
            //Compare their slopes to find which starts which bound ...
            final LocalMinima locMin = new LocalMinima();
            locMin.next = null;
            locMin.y = e.getBot().getY();
            if (e.deltaX < e.prev.deltaX) {
                locMin.leftBound = e.prev;
                locMin.rightBound = e;
                leftBoundIsForward = false; //Q.nextInLML = Q.prev
            }
            else {
                locMin.leftBound = e;
                locMin.rightBound = e.prev;
                leftBoundIsForward = true; //Q.nextInLML = Q.next
            }
            locMin.leftBound.side = Edge.Side.LEFT;
            locMin.rightBound.side = Edge.Side.RIGHT;

            if (!Closed) {
                locMin.leftBound.windDelta = 0;
            }
            else if (locMin.leftBound.next == locMin.rightBound) {
                locMin.leftBound.windDelta = -1;
            }
            else {
                locMin.leftBound.windDelta = 1;
            }
            locMin.rightBound.windDelta = -locMin.leftBound.windDelta;

            e = processBound( locMin.leftBound, leftBoundIsForward );
            if (e.outIdx == Edge.SKIP) {
                e = processBound( e, leftBoundIsForward );
            }

            Edge E2 = processBound( locMin.rightBound, !leftBoundIsForward );
            if (E2.outIdx == Edge.SKIP) {
                E2 = processBound( E2, !leftBoundIsForward );
            }

            if (locMin.leftBound.outIdx == Edge.SKIP) {
                locMin.leftBound = null;
            }
            else if (locMin.rightBound.outIdx == Edge.SKIP) {
                locMin.rightBound = null;
            }
            insertLocalMinima( locMin );
            if (!leftBoundIsForward) {
                e = E2;
            }
        }
        return true;

    }

    public boolean addPaths( Paths ppg, PolyType polyType, boolean closed ) {
        boolean result = false;
        for (int i = 0; i < ppg.size(); ++i) {
            if (addPath( ppg.get( i ), polyType, closed )) {
                result = true;
            }
        }
        return result;
    }

    public void clear() {
        disposeLocalMinimaList();
        edges.clear();
        useFullRange = false;
        hasOpenPaths = false;
    }

    private void disposeLocalMinimaList() {
        while (minimaList != null) {
            final LocalMinima tmpLm = minimaList.next;
            minimaList = null;
            minimaList = tmpLm;
        }
        currentLM = null;
    }

    private void insertLocalMinima( LocalMinima newLm ) {
        if (minimaList == null) {
            minimaList = newLm;
        }
        else if (newLm.y >= minimaList.y) {
            newLm.next = minimaList;
            minimaList = newLm;
        }
        else {
            LocalMinima tmpLm = minimaList;
            while (tmpLm.next != null && newLm.y < tmpLm.next.y) {
                tmpLm = tmpLm.next;
            }
            newLm.next = tmpLm.next;
            tmpLm.next = newLm;
        }
    }

    public boolean isPreserveCollinear() {
        return preserveCollinear;
    }

    protected void popLocalMinima() {
        LOGGER.entering( ClipperBase.class.getName(), "popLocalMinima" );
        if (currentLM == null) {
            return;
        }
        currentLM = currentLM.next;
    }

    private Edge processBound( Edge e, boolean LeftBoundIsForward ) {
        Edge EStart, result = e;
        Edge Horz;

        if (result.outIdx == Edge.SKIP) {
            //check if there are edges beyond the skip edge in the bound and if so
            //create another LocMin and calling ProcessBound once more ...
            e = result;
            if (LeftBoundIsForward) {
                while (e.getTop().getY() == e.next.getBot().getY()) {
                    e = e.next;
                }
                while (e != result && e.deltaX == Edge.HORIZONTAL) {
                    e = e.prev;
                }
            }
            else {
                while (e.getTop().getY() == e.prev.getBot().getY()) {
                    e = e.prev;
                }
                while (e != result && e.deltaX == Edge.HORIZONTAL) {
                    e = e.next;
                }
            }
            if (e == result) {
                if (LeftBoundIsForward) {
                    result = e.next;
                }
                else {
                    result = e.prev;
                }
            }
            else {
                //there are more edges in the bound beyond result starting with E
                if (LeftBoundIsForward) {
                    e = result.next;
                }
                else {
                    e = result.prev;
                }
                final LocalMinima locMin = new LocalMinima();
                locMin.next = null;
                locMin.y = e.getBot().getY();
                locMin.leftBound = null;
                locMin.rightBound = e;
                e.windDelta = 0;
                result = processBound( e, LeftBoundIsForward );
                insertLocalMinima( locMin );
            }
            return result;
        }

        if (e.deltaX == Edge.HORIZONTAL) {
            //We need to be careful with open paths because this may not be a
            //true local minima (ie E may be following a skip edge).
            //Also, consecutive horz. edges may start heading left before going right.
            if (LeftBoundIsForward) {
                EStart = e.prev;
            }
            else {
                EStart = e.next;
            }
            if (EStart.deltaX == Edge.HORIZONTAL) //ie an adjoining horizontal skip edge
            {
                if (EStart.getBot().getX() != e.getBot().getX() && EStart.getTop().getX() != e.getBot().getX())
                    e.reverseHorizontal();
            }
            else if (EStart.getBot().getX() != e.getBot().getX())
                e.reverseHorizontal();
        }

        EStart = e;
        if (LeftBoundIsForward) {
            while (result.getTop().getY() == result.next.getBot().getY() && result.next.outIdx != Edge.SKIP) {
                result = result.next;
            }
            if (result.deltaX == Edge.HORIZONTAL && result.next.outIdx != Edge.SKIP) {
                //nb: at the top of a bound, horizontals are added to the bound
                //only when the preceding edge attaches to the horizontal's left vertex
                //unless a Skip edge is encountered when that becomes the top divide
                Horz = result;
                while (Horz.prev.deltaX == Edge.HORIZONTAL) {
                    Horz = Horz.prev;
                }
                if (Horz.prev.getTop().getX() > result.next.getTop().getX()) result = Horz.prev;
            }
            while (e != result) {
                e.nextInLML = e.next;
                if (e.deltaX == Edge.HORIZONTAL && e != EStart && e.getBot().getX() != e.prev.getTop().getX()) {
                    e.reverseHorizontal();
                }
                e = e.next;
            }
            if (e.deltaX == Edge.HORIZONTAL && e != EStart && e.getBot().getX() != e.prev.getTop().getX()) {
                e.reverseHorizontal();
            }
            result = result.next; //move to the edge just beyond current bound
        }
        else {
            while (result.getTop().getY() == result.prev.getBot().getY() && result.prev.outIdx != Edge.SKIP) {
                result = result.prev;
            }
            if (result.deltaX == Edge.HORIZONTAL && result.prev.outIdx != Edge.SKIP) {
                Horz = result;
                while (Horz.next.deltaX == Edge.HORIZONTAL) {
                    Horz = Horz.next;
                }
                if (Horz.next.getTop().getX() == result.prev.getTop().getX() ||
                        Horz.next.getTop().getX() > result.prev.getTop().getX()) result = Horz.next;
            }

            while (e != result) {
                e.nextInLML = e.prev;
                if (e.deltaX == Edge.HORIZONTAL && e != EStart && e.getBot().getX() != e.next.getTop().getX()) {
                    e.reverseHorizontal();
                }
                e = e.prev;
            }
            if (e.deltaX == Edge.HORIZONTAL && e != EStart && e.getBot().getX() != e.next.getTop().getX()) {
                e.reverseHorizontal();
            }
            result = result.prev; //move to the edge just beyond current bound
        }
        return result;
    }

    protected static Path.OutRec parseFirstLeft(Path.OutRec FirstLeft) {
        while (FirstLeft != null && FirstLeft.getPoints() == null)
            FirstLeft = FirstLeft.firstLeft;
        return FirstLeft;
    }

    protected void reset() {
        currentLM = minimaList;
        if (currentLM == null) {
            return; //ie nothing to process
        }

        //reset all edges ...
        LocalMinima lm = minimaList;
        while (lm != null) {
            Edge e = lm.leftBound;
            if (e != null) {
                e.setCurrent( new LongPoint( e.getBot() ) );
                e.side = Edge.Side.LEFT;
                e.outIdx = Edge.UNASSIGNED;
            }
            e = lm.rightBound;
            if (e != null) {
                e.setCurrent( new LongPoint( e.getBot() ) );
                e.side = Edge.Side.RIGHT;
                e.outIdx = Edge.UNASSIGNED;
            }
            lm = lm.next;
        }
    }

}