/*
 *    Geotools2 - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2002, Geotools Project Managment Committee (PMC)
 *
 *    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.renderer.shape.shapehandler.simple;

import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.nio.ByteBuffer;

import org.geotools.data.shapefile.shp.ShapeHandler;
import org.geotools.data.shapefile.shp.ShapeType;
import org.geotools.renderer.shape.GeometryHandlerUtilities;
import org.geotools.renderer.shape.ScreenMap;
import org.geotools.renderer.shape.ShapefileRenderer;
import org.geotools.renderer.shape.SimpleGeometry;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;

import com.vividsolutions.jts.geom.Envelope;


/**
 * Creates Geometry line objects for use by the ShapeRenderer.
 *
 * @author jeichar
 *
 * @since 2.1.x
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/shapefile-renderer/src/main/java/org/geotools/renderer/shape/shapehandler/simple/MultiLineHandler.java $
 */
public class MultiLineHandler implements ShapeHandler {
    static final int TOP = 0x8;
    static final int BOTTOM = 0x4;
    static final int RIGHT = 0x2;
    static final int LEFT = 0x1;
    private ShapeType type;
    private Envelope bbox;
    private MathTransform mt;
    ScreenMap screenMap;
    private Point2D span;

    /**
     * Create new instance
     *
     * @param type the type of shape.
     * @param env the area that is visible. If shape is not in area then skip.
     * @param mt the transform to go from data to the envelope (and that should
     *        be used to transform the shape coords)
     * @param hasOpacity
     * @param screenSize 
     *
     * @throws TransformException
     */
    public MultiLineHandler(ShapeType type, Envelope env, MathTransform mt,
        boolean hasOpacity, Rectangle screenSize) throws TransformException {
        if (mt == null) {
            throw new NullPointerException();
        }
        this.type = type;
        this.bbox = env;
        this.mt = mt;
        screenMap = GeometryHandlerUtilities.calculateScreenSize(screenSize,
                hasOpacity);
        span = GeometryHandlerUtilities.calculateSpan(mt,0,0);

    }

    /**
     * @see org.geotools.data.shapefile.shp.ShapeHandler#getShapeType()
     */
    public ShapeType getShapeType() {
        return type;
    }

    /**
     * @see org.geotools.data.shapefile.shp.ShapeHandler#read(java.nio.ByteBuffer,
     *      org.geotools.data.shapefile.shp.ShapeType)
     */
    public Object read(ByteBuffer buffer, ShapeType type) {
        if (type == ShapeType.NULL) {
            return null;
        }

        // read bounding box
        Envelope geomBBox = GeometryHandlerUtilities.readBounds(buffer);

        if (!bbox.intersects(geomBBox)) {
            return null;
        }

        boolean bboxdecimate = (geomBBox.getWidth() <= span.getX())
            && (geomBBox.getHeight() <= span.getY());
        int numParts = buffer.getInt();
        int numPoints = buffer.getInt(); // total number of points

        int[] partOffsets = new int[numParts];

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

        double[][] coords = new double[numParts][];
        double[][] transformed = new double[numParts][];

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

        // if bbox is less than a pixel then decimate the geometry.  But orientation must
        // remain the same so geometry data must be parsed.
        if (bboxdecimate) {
            coords = new double[1][];
            coords[0] = new double[4];
            transformed = new double[1][];
            transformed[0] = new double[4];
            coords[0][0] = buffer.getDouble();
            coords[0][1] = buffer.getDouble();
            buffer.position((buffer.position() + ((numPoints - 2) * 16)));
            coords[0][2] = buffer.getDouble();
            coords[0][3] = buffer.getDouble();

            if (!bbox.contains(coords[0][0], coords[0][1])
                    && !bbox.contains(coords[0][2], coords[0][3])) {
                return null;
            }

            try {
                mt.transform(coords[0], 0, transformed[0], 0, 1);

                if (screenMap.get((int) transformed[0][0],
                            (int) transformed[0][1])) {
                    return null;
                }

                screenMap.set((int) transformed[0][0], (int) transformed[0][1],
                    true);
                mt.transform(coords[0], 2, transformed[0], 2, 1);

                double minx = (int) transformed[0][0];
                double miny = (int) transformed[0][1];
                double maxx = minx + 1;
                double maxy = transformed[0][3];
                transformed[0][0] = minx;
                transformed[0][1] = miny;
                transformed[0][2] = maxx;
                transformed[0][3] = maxy;
            } catch (Exception e) {
                ShapefileRenderer.LOGGER.severe(
                    "could not transform coordinates "
                    + e.getLocalizedMessage());
                transformed[0] = coords[0];
            }
        } else {
            boolean intersection = false;
            int partsInBBox = 0;

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

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

                length = finish - start;
                coords[part] = new double[length * 2];

                int readDoubles = 0;
                int currentDoubles = 0;
                int totalDoubles = length * 2;

                while (currentDoubles < totalDoubles) {
                    coords[part][readDoubles] = buffer.getDouble();
                    readDoubles++;
                    currentDoubles++;
                    coords[part][readDoubles] = buffer.getDouble();
                    readDoubles++;
                    currentDoubles++;
                    intersection = bboxIntersectSegment(intersection,
                            coords[part], readDoubles);

                    if ((readDoubles > 3)
                            && (currentDoubles < (totalDoubles - 1))) {
                        if ((Math.abs(coords[part][readDoubles - 4]
                                    - coords[part][readDoubles - 2]) <= span
                                .getX())
                                && (Math.abs(coords[part][readDoubles - 3]
                                    - coords[part][readDoubles - 1]) <= span
                                .getY())) {
                            readDoubles -= 2;
                        } 
                    }
                }

                if (!intersection) {
                    continue;
                }

                if (!mt.isIdentity()) {
                    try {
                        transformed[partsInBBox] = new double[readDoubles];
                        GeometryHandlerUtilities.transform(type, mt,
                            coords[part], transformed[partsInBBox]);
                    } catch (Exception e) {
                        ShapefileRenderer.LOGGER.severe(
                            "could not transform coordinates "
                            + e.getLocalizedMessage());
                        transformed[partsInBBox] = coords[part];
                    }
                } else {
                    transformed[partsInBBox] = new double[readDoubles];
                    System.arraycopy(coords[part], 0, transformed[partsInBBox],
                        0, readDoubles / 2);
                }

                partsInBBox++;
            }

            if (partsInBBox == 0) {
                return null;
            }

            if (partsInBBox != numParts) {
                double[][] tmp = new double[partsInBBox][];
                System.arraycopy(transformed, 0, tmp, 0, partsInBBox);
                transformed = tmp;
            }
        }

        return createGeometry(type, geomBBox, transformed);
    }

    protected Object createGeometry(ShapeType type, Envelope geomBBox, double[][] transformed) {
        return new SimpleGeometry(type, transformed, geomBBox);
    }

    public boolean bboxIntersectSegment(boolean intersection, double[] coords,
        int index) {
        if (intersection) {
            return true;
        }

        if (bbox.contains(coords[index - 2], coords[index - 1])) {
            return true;
        }

        if (index < 4) {
            return false;
        }

        return intersect(coords[index - 4],
                    coords[index - 3], coords[index - 2],
                    coords[index - 1], bbox.getMinX(), bbox.getMinY(),
                    bbox.getMaxX(), bbox.getMaxY());
    }

    /**
     * Uses Cohen Sutherland line clipping algorithm to determine intersection.
     * See  Computer Graphics: Principles and Practice Foley, van Dam,
     * Feiner, Hughes
     *
     * @param x0 DOCUMENT ME!
     * @param y0 DOCUMENT ME!
     * @param x1 DOCUMENT ME!
     * @param y1 DOCUMENT ME!
     * @param xmin DOCUMENT ME!
     * @param ymin DOCUMENT ME!
     * @param xmax DOCUMENT ME!
     * @param ymax DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public static boolean intersect(double x0, double y0, double x1, double y1,
        double xmin, double ymin, double xmax, double ymax) {
        boolean accept = false;
        boolean done = false;
        int outcode0 = compOutCode(x0, y0, xmin, xmax, ymin, ymax);
        int outcode1 = compOutCode(x1, y1, xmin, xmax, ymin, ymax);

        do {
            if ((outcode0 == 0) || (outcode1 == 0)) { // trivial accept and exit
                accept = done = true;
            } else if ((outcode0 & outcode1) != 0) { // logical and is true so trivial reject and exit
                done = true;
            } else {
                // failed both tests so calculate the line segment to clip:
                //from an otside poin to the intersectin with clip edge.
                double x;

                // failed both tests so calculate the line segment to clip:
                //from an otside poin to the intersectin with clip edge.
                double y;

                //at least one endpoint is outside the clip rectangle so pick it.
                int outcodeOut = (outcode0 > 0) ? outcode0 : outcode1;

                //Now find intersection point 
                if ((outcodeOut & TOP) > 0) {
                    x = x0 + (((x1 - x0) * (ymax - y0)) / (y1 - y0));
                    y = ymax;
                } else if ((outcodeOut & BOTTOM) > 0) {
                    x = x0 + (((x1 - x0) * (ymin - y0)) / (y1 - y0));
                    y = ymin;
                } else if ((outcodeOut & RIGHT) > 0) {
                    y = y0 + (((y1 - y0) * (xmax - x0)) / (x1 - x0));
                    x = xmax;
                } else {
                    y = y0 + (((y1 - y0) * (xmin - x0)) / (x1 - x0));
                    x = xmin;
                }

                if (outcodeOut == outcode0) {
                    x0 = x;
                    y0 = y;
                    outcode0 = compOutCode(x0, y0, xmin, xmax, ymin, ymax);
                } else {
                    x1 = x;
                    y1 = y;
                    outcode1 = compOutCode(x1, y1, xmin, xmax, ymin, ymax);
                }
            }
        } while (done == false);

        return accept;
    }

    private static int compOutCode(double x, double y, double xmin, double xmax,
        double ymin, double ymax) {
        int outcode = 0;

        if (y > ymax) {
            outcode |= TOP;
        }

        if (y < ymin) {
            outcode |= BOTTOM;
        }

        if (x > xmax) {
            outcode |= RIGHT;
        }

        if (x < xmin) {
            outcode |= LEFT;
        }

        return outcode;
    }

    //	/**
    //	 * @return
    //	 */
    //	private SimpleGeometry decimateBasedOnEnvelope(Envelope geomBBox) {
    //		if (geomBBox.getWidth() <= span.getX() && geomBBox.getHeight() <= span.getY()) {
    //			double[][] coords = new double[1][];
    //			coords[0] = new double[] { geomBBox.getMinX(), geomBBox.getMinY() };
    //			double[][] transformed = new double[1][];
    //			transformed[0] = new double[4];
    //			try {
    //				mt.transform(coords[0], 0, transformed[0], 0, 1);
    //			} catch (Exception e) {
    //				ShapefileRenderer.LOGGER.severe("could not transform coordinates "
    //						+ e.getLocalizedMessage());
    //				transformed = coords;
    //			}
    //			transformed[0][2]=transformed[0][0];
    //			transformed[0][3]=transformed[0][1];
    //			return new SimpleGeometry(type, transformed, geomBBox);
    //		}
    //		return null;
    //	}

    /**
     * @see org.geotools.data.shapefile.shp.ShapeHandler#write(java.nio.ByteBuffer,
     *      java.lang.Object)
     */
    public void write(ByteBuffer buffer, Object geometry) {
        // This handler doesnt write
        throw new UnsupportedOperationException(
            "This handler is only for reading");
    }

    /**
     * @see org.geotools.data.shapefile.shp.ShapeHandler#getLength(java.lang.Object)
     */
    public int getLength(Object geometry) {
        // TODO Auto-generated method stub
        return 0;
    }
}
