/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library 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
 *    Lesser General Public License for more details.
 */
package org.geotools.data.shapefile.shp;

import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.util.ArrayList;
import java.util.List;

import org.geotools.geometry.jts.coordinatesequence.CoordinateSequences;

import com.vividsolutions.jts.algorithm.CGAlgorithms;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateSequence;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;

/**
 * Wrapper for a Shapefile polygon.
 * 
 * @author aaime
 * @author Ian Schneider
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/plugin/shapefile/src/main/java/org/geotools/data/shapefile/shp/PolygonHandler.java $
 * @version $Id: PolygonHandler.java 37305 2011-05-25 05:57:57Z mbedward $
 */
public class PolygonHandler implements ShapeHandler {
    
    GeometryFactory geometryFactory;

    final ShapeType shapeType;

    public PolygonHandler(ShapeType type, GeometryFactory gf) throws ShapefileException {
        if ((type != ShapeType.POLYGON) && (type != ShapeType.POLYGONM)
                && (type != ShapeType.POLYGONZ)) {
            throw new ShapefileException(
                    "PolygonHandler constructor - expected type to be 5, 15, or 25.");
        }

        shapeType = type;
        this.geometryFactory = gf;
    }
    
    // returns true if testPoint is a point in the pointList list.
    boolean pointInList(Coordinate testPoint, Coordinate[] pointList) {
        Coordinate p;

        for (int t = pointList.length - 1; t >= 0; t--) {
            p = pointList[t];

            // nan test; x!=x iff x is nan
            if ((testPoint.x == p.x)
                    && (testPoint.y == p.y)
                    && ((testPoint.z == p.z) || (!(testPoint.z == testPoint.z))) 
            ) {
                return true;
            }
        }

        return false;
    }

    public Object read(ByteBuffer buffer, ShapeType type, boolean flatFeature) {
        if (type == ShapeType.NULL) {
            return createNull();
        }
        // bounds
        buffer.position(buffer.position() + 4 * 8);

        int[] partOffsets;

        int numParts = buffer.getInt();
        int numPoints = buffer.getInt();
        int dimensions = (shapeType == ShapeType.POLYGONZ) && !flatFeature ? 3 : 2;

        partOffsets = new int[numParts];

        for (int i = 0; i < numParts; i++) {
            partOffsets[i] = buffer.getInt();
        }

        ArrayList shells = new ArrayList();
        ArrayList holes = new ArrayList();
        CoordinateSequence coords = readCoordinates(buffer, numPoints, dimensions);

        int offset = 0;
        int start;
        int finish;
        int length;

        for (int part = 0; part < numParts; part++) {
            start = partOffsets[part];

            if (part == (numParts - 1)) {
                finish = numPoints;
            } else {
                finish = partOffsets[part + 1];
            }

            length = finish - start;
            int close = 0; // '1' if the ring must be closed, '0' otherwise
            if ((coords.getOrdinate(start, 0) != coords.getOrdinate(finish - 1, 0)) 
                    || (coords.getOrdinate(start, 1) != coords.getOrdinate(finish - 1, 1))
            ) {
                close=1;
            }
            if (dimensions == 3) {
                if(coords.getOrdinate(start, 2) != coords.getOrdinate(finish - 1, 2)) {
                    close = 1;
                }
            }

            CoordinateSequence csRing = geometryFactory.getCoordinateSequenceFactory().create(length + close, dimensions);
            // double area = 0;
            // int sx = offset;
            for (int i = 0; i < length; i++) {
                csRing.setOrdinate(i, 0, coords.getOrdinate(offset, 0));
                csRing.setOrdinate(i, 1, coords.getOrdinate(offset, 1));
                if(dimensions == 3) {
                    csRing.setOrdinate(i, 2, coords.getOrdinate(offset, 2));
                }
                offset++;
            }
            if (close == 1) {
                csRing.setOrdinate(length, 0, coords.getOrdinate(start, 0));
                csRing.setOrdinate(length, 1, coords.getOrdinate(start, 1));
                if(dimensions == 3) {
                    csRing.setOrdinate(length, 2, coords.getOrdinate(start, 2));
                }
            }
            // REVISIT: polygons with only 1 or 2 points are not polygons -
            // geometryFactory will bomb so we skip if we find one.
            if (csRing.size() == 0 || csRing.size() > 3) {
                LinearRing ring = geometryFactory.createLinearRing(csRing);

                if (CoordinateSequences.isCCW(csRing)) {
                    // counter-clockwise
                    holes.add(ring);
                } else {
                    // clockwise
                    shells.add(ring);
                }
            }
        }

        // quick optimization: if there's only one shell no need to check
        // for holes inclusion
        if (shells.size() == 1) {
            return createMulti((LinearRing) shells.get(0), holes);
        }
        // if for some reason, there is only one hole, we just reverse it and
        // carry on.
        else if (holes.size() == 1 && shells.size() == 0) {
            return createMulti((LinearRing) holes.get(0));
        } else {

            // build an association between shells and holes
            final ArrayList holesForShells = assignHolesToShells(shells, holes);

            Geometry g = buildGeometries(shells, holes, holesForShells);

            return g;
        }
    }

    /**
     * @param buffer
     * @param numPoints
     */
    private CoordinateSequence readCoordinates(final ByteBuffer buffer,
            final int numPoints, final int dimensions) {
        CoordinateSequence cs = geometryFactory.getCoordinateSequenceFactory().create(numPoints, dimensions);

        DoubleBuffer dbuffer = buffer.asDoubleBuffer();
        double[] ordinates = new double[numPoints * 2];
        dbuffer.get(ordinates);
        for (int t = 0; t < numPoints; t++) {
            cs.setOrdinate(t, 0, ordinates[t * 2]);
            cs.setOrdinate(t, 1, ordinates[t * 2 + 1]);
        }
        
        if (dimensions > 2) {
            // z
            dbuffer.position(dbuffer.position() + 2);
            dbuffer.get(ordinates, 0, numPoints);

            for (int t = 0; t < numPoints; t++) {
                cs.setOrdinate(t, 2, ordinates[t]);
            }
        }

        return cs;
    }

    /**
     * @param shells
     * @param holes
     * @param holesForShells
     */
    private Geometry buildGeometries(final List shells, final List holes,
            final List holesForShells) {
        Polygon[] polygons;

        // if we have shells, lets use them
        if (shells.size() > 0) {
            polygons = new Polygon[shells.size()];
            // oh, this is a bad record with only holes
        } else {
            polygons = new Polygon[holes.size()];
        }

        // this will do nothing for the "only holes case"
        for (int i = 0; i < shells.size(); i++) {
            polygons[i] = geometryFactory.createPolygon((LinearRing) shells
                    .get(i), (LinearRing[]) ((ArrayList) holesForShells.get(i))
                    .toArray(new LinearRing[0]));
        }

        // this will take care of the "only holes case"
        // we just reverse each hole
        if (shells.size() == 0) {
            for (int i = 0, ii = holes.size(); i < ii; i++) {
                LinearRing hole = (LinearRing) holes.get(i);
                polygons[i] = geometryFactory.createPolygon(hole, null);
            }
        }

        Geometry g = geometryFactory.createMultiPolygon(polygons);

        return g;
    }

    /**
     * <b>Package private for testing</b>
     * 
     * @param shells
     * @param holes
     */
    ArrayList assignHolesToShells(final ArrayList shells, final ArrayList holes) {
        ArrayList holesForShells = new ArrayList(shells.size());
        for (int i = 0; i < shells.size(); i++) {
            holesForShells.add(new ArrayList());
        }

        // find homes
        for (int i = 0; i < holes.size(); i++) {
            LinearRing testRing = (LinearRing) holes.get(i);
            LinearRing minShell = null;
            Envelope minEnv = null;
            Envelope testEnv = testRing.getEnvelopeInternal();
            Coordinate testPt = testRing.getCoordinateN(0);
            LinearRing tryRing;

            for (int j = 0; j < shells.size(); j++) {
                tryRing = (LinearRing) shells.get(j);

                Envelope tryEnv = tryRing.getEnvelopeInternal();
                if (minShell != null) {
                    minEnv = minShell.getEnvelopeInternal();
                }

                boolean isContained = false;
                Coordinate[] coordList = tryRing.getCoordinates();

                if (tryEnv.contains(testEnv)
                        && (CGAlgorithms.isPointInRing(testPt, coordList) || (pointInList(
                                testPt, coordList)))) {
                    isContained = true;
                }

                // check if this new containing ring is smaller than the current
                // minimum ring
                if (isContained) {
                    if ((minShell == null) || minEnv.contains(tryEnv)) {
                        minShell = tryRing;
                    }
                }
            }

            if (minShell == null) {
                // now reverse this bad "hole" and turn it into a shell
                shells.add(testRing);
                holesForShells.add(new ArrayList());
            } else {
                ((ArrayList) holesForShells.get(shells.indexOf(minShell)))
                        .add(testRing);
            }
        }

        return holesForShells;
    }

    private MultiPolygon createMulti(LinearRing single) {
        return createMulti(single, java.util.Collections.EMPTY_LIST);
    }

    private MultiPolygon createMulti(LinearRing single, List holes) {
        return geometryFactory
                .createMultiPolygon(new Polygon[] { geometryFactory
                        .createPolygon(single, (LinearRing[]) holes
                                .toArray(new LinearRing[holes.size()])) });
    }

    private MultiPolygon createNull() {
        return geometryFactory.createMultiPolygon(null);
    }
}

/*
 * $Log: PolygonHandler.java,v $ Revision 1.9 2004/02/17 18:10:23 ianschneider
 * changed to use GeometryFactory for Geometry creation
 * 
 * Revision 1.8 2003/07/24 19:10:02 ianschneider *** empty log message ***
 * 
 * Revision 1.7 2003/07/24 18:32:10 ianschneider more test updates, fixed Z type
 * writing
 * 
 * Revision 1.6 2003/07/23 23:41:09 ianschneider more testing updates
 * 
 * Revision 1.5 2003/07/23 00:59:59 ianschneider Lots of PMD fix ups
 * 
 * Revision 1.4 2003/07/21 21:15:29 jmacgill small fix for shapefiles with an
 * invalid hole (only 1 or 2 points)
 * 
 * Revision 1.3 2003/05/19 21:38:55 jmacgill refactored read method to break it
 * up a little
 * 
 * Revision 1.2 2003/05/19 20:51:30 ianschneider removed System.out print
 * statements
 * 
 * Revision 1.1 2003/05/14 17:51:21 ianschneider migrated packages
 * 
 * Revision 1.3 2003/04/30 23:19:46 ianschneider Added construction of multi
 * geometries for default return values, even if only one geometry. This could
 * have effects through system.
 * 
 * Revision 1.2 2003/03/30 20:21:09 ianschneider Moved buffer branch to main
 * 
 * Revision 1.1.2.5 2003/03/29 22:30:09 ianschneider For case of hole without
 * shell - reverse hole, add to shell list
 * 
 * Revision 1.1.2.4 2003/03/26 19:30:30 ianschneider Made hack to reverse
 * polygon records if they contains only holes
 * 
 * Revision 1.1.2.3 2003/03/12 15:30:18 ianschneider made ShapeType final for
 * handlers - once they're created, it won't change.
 * 
 * Revision 1.1.2.2 2003/03/07 00:36:41 ianschneider
 * 
 * Added back the additional ShapeType parameter in ShapeHandler.read.
 * ShapeHandler's need return their own special "null" shape if needed. Fixed
 * the ShapefileReader to not throw exceptions for "null" shapes. Fixed
 * ShapefileReader to accomodate junk after the last valid record. The theory
 * goes, if the shape number is proper, that is, one greater than the previous,
 * we consider that a valid record and attempt to read it. I suppose, by chance,
 * the junk could coincide with the next record number. Stupid ESRI. Fixed some
 * record-length calculations which resulted in writing of bad shapefiles.
 * 
 * Revision 1.1.2.1 2003/03/06 01:16:34 ianschneider
 * 
 * The initial changes for moving to java.nio. Added some documentation and
 * improved exception handling. Works for reading, may work for writing as of
 * now.
 * 
 * Revision 1.1 2003/02/27 22:35:50 aaime New shapefile module, initial commit
 * 
 * Revision 1.2 2003/01/22 18:31:05 jaquino Enh: Make About Box configurable
 * 
 * Revision 1.2 2002/09/09 20:46:22 dblasby Removed LEDatastream refs and
 * replaced with EndianData[in/out]putstream
 * 
 * Revision 1.1 2002/08/27 21:04:58 dblasby orginal
 * 
 * Revision 1.3 2002/03/05 10:51:01 andyt removed use of factory from write
 * method
 * 
 * Revision 1.2 2002/03/05 10:23:59 jmacgill made sure geometries were created
 * using the factory methods
 * 
 * Revision 1.1 2002/02/28 00:38:50 jmacgill Renamed files to more intuitve
 * names
 * 
 * Revision 1.4 2002/02/13 00:23:53 jmacgill First semi working JTS version of
 * Shapefile code
 * 
 * Revision 1.3 2002/02/11 18:44:22 jmacgill replaced geometry constructions
 * with calls to geometryFactory.createX methods
 * 
 * Revision 1.2 2002/02/11 18:28:41 jmacgill rewrote to have static read and
 * write methods
 * 
 * Revision 1.1 2002/02/11 16:54:43 jmacgill added shapefile code and
 * directories
 * 
 */
