/*
 * Copyright (c) 2016 Vivid Solutions.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
 * and the Eclipse Distribution License is available at
 *
 * http://www.eclipse.org/org/documents/edl-v10.php.
 */
package org.locationtech.jts.io.sde;

import org.locationtech.jts.geom.*;

import com.esri.sde.sdk.client.*;

/**
 * Reads a {@link Geometry} from an ESRI SDE Shape.
 * <p>
 * The SDE geometry model differs from the OGC model used by JTS.
 * In particular:
 * <ul>
 * <li>Simple lines are read as {@link LineString}s
 * <li>Inverted Polygons and Exverted Holes are read as is.
 * These create invalid JTS polygons, and must be
 * rectified if further operations are to be performed on them.
 * </ul>
 * <p>
 * This class allows specifying the maximum number of coordinate dimensions to read.
 * If dimensions higher than 2 are not required, it may be more efficient to not read them.
 * <p>
 * To use this class the ESRI SDE Java libraries must be present.
 * <p>
 * Currently reading measure (M) ordinates is not supported.
 * 
 * @author Martin Davis
 *
 */
public class SdeReader 
{
  private GeometryFactory geometryFactory;
  private PrecisionModel precisionModel;
  private CoordinateSequenceFactory coordSeqFact;
  private int maxDimensionToRead = 2;
  
  /**
   * Creates a reader that creates geometries using the default {@link GeometryFactory}.
   */
  public SdeReader() {
    this(new GeometryFactory());
  }
  
  /**
   * Creates a reader that creates geometries using the given {@link GeometryFactory}.
   * @param geometryFactory
   */
  public SdeReader(GeometryFactory geometryFactory) {
    this.geometryFactory = geometryFactory;
    precisionModel = geometryFactory.getPrecisionModel();
    coordSeqFact = geometryFactory.getCoordinateSequenceFactory();
  }

	/**
	 * Gets the maximum number of coordinate dimensions which will be read.
	 * 
	 * @return the dimension which will be read
	 */
	public int getDimension() {
		return maxDimensionToRead;
	}

	/**
	 * Sets the maximum number of coordinate dimensions to read.
	 * If this is larger than the number of dimensions actually 
	 * present in the input geometry, the higher ordinates will not
	 * be read, and NaN will be returned as their value.
	 * <p>
	 * The default is to read only the X and Y ordinates (dimension = 2).
	 * 
	 * @param dimension the dimension to read
	 */
	public void setDimension(int dimension) {
		this.maxDimensionToRead = dimension;
	}

	/**
	 * Reads a {@link Geometry} from a given SDE shape.
	 * 
	 * @param shape the shape to read
	 * @return the geometry which represents the input shape
	 * 
	 * @throws SeException
	 */
  public Geometry read(SeShape shape)
  throws SeException
  {
  	switch (shape.getType()) {
  	case SeShape.TYPE_POINT:
  		return readPoint(shape);

  	case SeShape.TYPE_MULTI_POINT:
  		return readMultiPoint(shape);

  	case SeShape.TYPE_LINE:
  	case SeShape.TYPE_SIMPLE_LINE:
  		return readLine(shape);

  	case SeShape.TYPE_MULTI_LINE:
  	case SeShape.TYPE_MULTI_SIMPLE_LINE:
  		return readMultiLine(shape);

  	case SeShape.TYPE_POLYGON:
  		return readPolygon(shape);

  	case SeShape.TYPE_MULTI_POLYGON:
  		return readMultiPolygon(shape);

  	}
  	throw new IllegalArgumentException("Shapes of type " + shape.getType() + " are not supported");
  }
  
  private Point readPoint(SeShape shape)
  throws SeException
  {
  	java.util.List list = shape.getAllPoints(SeShape.TURN_RIGHT, false);  // get points and offsets
  	SDEPoint[] sePts = (SDEPoint[]) list.get(0);
  	return geometryFactory.createPoint(toCoordinates(sePts, 0, 1));
  }
  
  private MultiPoint readMultiPoint(SeShape shape)
  throws SeException
  {
  	java.util.List list = shape.getAllPoints(SeShape.TURN_RIGHT, false);  // get points and offsets
  	SDEPoint[] sePts = (SDEPoint[]) list.get(0);
  	return geometryFactory.createMultiPoint(toCoordinates(sePts, 0, sePts.length));
  }
  
  private LineString readLine(SeShape shape)
  throws SeException
  {
  	java.util.List list = shape.getAllPoints(SeShape.TURN_RIGHT, false);  // get points and offsets
  	SDEPoint[] sePts = (SDEPoint[]) list.get(0);
  	return geometryFactory.createLineString(toCoordinates(sePts, 0, sePts.length));
  }
  
  private MultiLineString readMultiLine(SeShape shape)
  throws SeException
  {
  	java.util.List list = shape.getAllPoints(SeShape.TURN_RIGHT, false);  // get points and offsets
  	SDEPoint[] sePts = (SDEPoint[]) list.get(0);
  	int[] partOffset = (int[]) list.get(1);
  	
  	LineString[] lines = new LineString[partOffset.length];
  	for (int i = 0; i < partOffset.length; i++) {
  		int end = sePts.length;
  		if (i < partOffset.length - 1) 
  			end = partOffset[i + 1];
  		lines[i] = geometryFactory.createLineString(toCoordinates(sePts, partOffset[i], end));
  	}
  	return geometryFactory.createMultiLineString(lines);
  }
  
  private Polygon readPolygon(SeShape shape)
  throws SeException
  {
  	java.util.List list = shape.getAllPoints(SeShape.TURN_RIGHT, true);  // get points and offsets
  	SDEPoint[] sePts = (SDEPoint[]) list.get(0);
  	// a polygon has only one part
  	int[] subPartOffset = (int[]) list.get(2);
  	
  	return readPolygon(sePts, subPartOffset, 0, subPartOffset.length);
  }
  
  private Polygon readPolygon(SDEPoint[] sePts, int[] subPartOffset, int subPartStart, int subPartEnd)
  throws SeException
  {  	
  	int numSubParts = subPartEnd - subPartStart;
  	int nHoles = numSubParts - 1;
  	if (nHoles < 0)
  		nHoles = 0;
  	LinearRing shell = null;
  	LinearRing[] holes = new LinearRing[nHoles];
  	int holeIndex = 0;
  	
  	for (int i = subPartStart; i < subPartEnd; i++) {
  		int end = sePts.length;
  		if (i < subPartEnd - 1) 
  			end = subPartOffset[i + 1];
  		LinearRing ring = geometryFactory.createLinearRing(toCoordinates(sePts, subPartOffset[i], end));
  		
  		if (shell == null) {
  			shell = ring;
  		}
  		else {
  			holes[holeIndex++] = ring;
  		}
  	}
  	return geometryFactory.createPolygon(shell, holes);
  }
  
  private MultiPolygon readMultiPolygon(SeShape shape)
  throws SeException
  {
  	java.util.List list = shape.getAllPoints(SeShape.TURN_RIGHT, true);  // get points and offsets
  	SDEPoint[] sePts = (SDEPoint[]) list.get(0);
  	int[] partOffset = (int[]) list.get(1);
  	int[] subPartOffset = (int[]) list.get(2);
  	
  	Polygon[] polys = new Polygon[partOffset.length];
  	for (int i = 0; i < partOffset.length; i++) {
  		int subPartEnd = subPartOffset.length;
  		if (i + 1 < partOffset.length) {
  			subPartEnd = partOffset[i + 1];
  		}
  		polys[i] = readPolygon(sePts, subPartOffset, partOffset[i], subPartEnd);
  	}
  	return geometryFactory.createMultiPolygon(polys);
  }
  
  private void readCoordinate(SDEPoint p, CoordinateSequence seq, int index)
  throws SeException
  {
  	seq.setOrdinate(index, 0, precisionModel.makePrecise(p.getX()));
  	seq.setOrdinate(index, 1, precisionModel.makePrecise(p.getY()));
  	// only read the Z dim if requested and present
  	if (maxDimensionToRead >= 3 && p.is3D()) {
  		seq.setOrdinate(index, 2, p.getZ());
  	}
  }
  
  private CoordinateSequence toCoordinates(SDEPoint[] sePts, int start, int end)
  throws SeException
  {
  	int size = end - start;
  	CoordinateSequence seq = coordSeqFact.create(size, maxDimensionToRead);
  	int index = 0;
  	for (int i = start; i < end; i++) {
  		readCoordinate(sePts[i], seq, index);
  		index++;
  	}
  	return seq;
  }
}
