package org.geotools.geopkg;

import static java.lang.String.*;

import static org.geotools.geopkg.GeoPackage.*;

import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;

import org.geotools.api.feature.type.AttributeDescriptor;
import org.geotools.api.feature.type.GeometryDescriptor;
import org.geotools.geometry.jts.CircularString;
import org.geotools.geometry.jts.CompoundCurve;
import org.geotools.geometry.jts.CurvePolygon;
import org.geotools.geometry.jts.Geometries;
import org.geotools.geometry.jts.MultiCurve;
import org.geotools.geometry.jts.MultiSurface;
import org.geotools.geopkg.geom.GeoPkgGeomReader;
import org.geotools.geopkg.geom.GeoPkgGeomWriter;
import org.geotools.jdbc.JDBCDataStore;
import org.geotools.util.factory.Hints;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPoint;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;

/**
 * An extension of {@link GeoPkgDialect} to add support for additional geometry types by swapping
 * out the geometry reader.
 *
 * Note: this class needs to live in the org.geotools.geopkg package to make use of package private
 * classes from there.
 */
public class RsGeoPkgDialect extends GeoPkgDialect {

  public RsGeoPkgDialect(JDBCDataStore dataStore, GeoPkgGeomWriter.Configuration writerConfig) {
    super(dataStore, writerConfig);
  }

  @Override
  public Geometry decodeGeometryValue(
      GeometryDescriptor descriptor,
      ResultSet rs,
      String column,
      GeometryFactory factory,
      Connection cx,
      Hints hints)
      throws IOException, SQLException {
    return geometry(descriptor.getType().getBinding(), rs.getBytes(column), factory, hints);
  }

  @Override
  public Geometry decodeGeometryValue(
      GeometryDescriptor descriptor,
      ResultSet rs,
      int column,
      GeometryFactory factory,
      Connection cx,
      Hints hints)
      throws IOException, SQLException {
    return geometry(descriptor.getType().getBinding(), rs.getBytes(column), factory, hints);
  }

  /**
   *
   */
  private Geometry geometry(Class geometryType, byte[] bytes, GeometryFactory factory, Hints hints)
      throws IOException {
    GeoPkgGeomReader geoPkgGeomReader = new RsGeoPkgGeomReader(bytes);
    geoPkgGeomReader.setFactory(factory);
    geoPkgGeomReader.setHints(hints);
    geoPkgGeomReader.setGeometryType(geometryType);
    return bytes != null ? geoPkgGeomReader.get() : null;
  }

  @Override
  Geometry geometry(byte[] b) throws IOException {
    return geometry(null, b, null, null);
  }

  @Override
  public Class<?> getMapping(ResultSet columns, Connection cx) throws SQLException {
    String tbl = columns.getString("TABLE_NAME");
    String col = columns.getString("COLUMN_NAME");
    String typeName = columns.getString("TYPE_NAME");

    String sql = format(
        "SELECT b.geometry_type_name"
        + " FROM %s a, %s b"
        + " WHERE a.table_name = b.table_name"
        + " AND b.table_name = ?"
        + " AND b.column_name = ?",
        GEOPACKAGE_CONTENTS, GEOMETRY_COLUMNS);

    if (LOGGER.isLoggable(Level.FINE)) {
      LOGGER.fine(String.format("%s; 1=%s, 2=%s", sql, tbl, col));
    }

    try (PreparedStatement ps = cx.prepareStatement(sql)) {
      ps.setString(1, tbl);
      ps.setString(2, col);

      ResultSet rs = ps.executeQuery();
      try {
        if (rs.next()) {
          String t = rs.getString(1);
          Geometries g = Geometries.getForName(t);
          if (g != null) {
            return g.getBinding();
          }
        }
      } finally {
        dataStore.closeSafe(rs);
      }
    }

    // if it's an enumeration, switch data type to string
    @SuppressWarnings("PMD.CloseResource") // using the pool of the store, no need to close
    GeoPackage geoPackage = geopkg();
    try {
      GeoPkgSchemaExtension extension = geoPackage.getExtension(GeoPkgSchemaExtension.class);
      List<DataColumn> dataColumns = extension.getDataColumns(tbl, cx);
      for (DataColumn dataColumn : dataColumns) {
        if (col.equals(dataColumn.getColumnName())) {
          DataColumnConstraint constraint = dataColumn.getConstraint();
          // array?
          if ("application/json".equals(dataColumn.getMimeType())) {
            return String[].class;
          }
          // otherwise consider the conversion
          if (constraint instanceof DataColumnConstraint.Enum) {
            return String.class;
          }
        }
      }
    } catch (IOException e) {
      throwSQLException(e);
    }

    // handle GeoPackage integer type expectations
    if ("TINYINT".equals(typeName)) {
      return Byte.class;
    } else if ("SMALLINT".equals(typeName)) {
      return Short.class;
    } else if ("MEDIUMINT".equals(typeName)) {
      return Integer.class;
    } else if ("INT".equals(typeName) || "INTEGER".equals(typeName)) {
      return Long.class;
    } // support for overview tables
    else if ("POINT".equalsIgnoreCase(typeName)) {
      return Point.class;
    } else if ("MULTIPOINT".equalsIgnoreCase(typeName)) {
      return MultiPoint.class;
    } else if ("LINESTRING".equalsIgnoreCase(typeName)) {
      return LineString.class;
    } else if ("MULTILINESTRING".equalsIgnoreCase(typeName)) {
      return MultiLineString.class;
    } else if ("POLYGON".equalsIgnoreCase(typeName)) {
      return Polygon.class;
    } else if ("MULTIPOLYGON".equalsIgnoreCase(typeName)) {
      return MultiPolygon.class;
    } else if ("GEOMETRY".equalsIgnoreCase(typeName)) {
      return Geometry.class;
    } else if ("GEOMETRYCOLLECTION".equalsIgnoreCase(typeName)) {
      return GeometryCollection.class;
    } else if ("CIRCULARSTRING".equalsIgnoreCase(typeName)) {
      return CircularString.class;
    } else if ("COMPOUNDCURVE".equalsIgnoreCase(typeName)) {
      return CompoundCurve.class;
    } else if ("CURVEPOLYGON".equalsIgnoreCase(typeName)) {
      return CurvePolygon.class;
    } else if ("MULTICURVE".equalsIgnoreCase(typeName)) {
      return MultiCurve.class;
    } else if ("MULTISURFACE".equalsIgnoreCase(typeName)) {
      return MultiSurface.class;
    } else if ("CURVE".equalsIgnoreCase(typeName)) {
      return LineString.class;
    } else if ("SURFACE".equalsIgnoreCase(typeName)) {
      return CurvePolygon.class;
    }

    return null;
  }

  private void throwSQLException(IOException e) throws SQLException {
    if (e.getCause() instanceof SQLException) {
      throw (SQLException) e.getCause();
    } else {
      throw new SQLException(e);
    }
  }

  @Override
  public Object convertValue(Object value, AttributeDescriptor ad) {
    Class<?> binding = ad.getType().getBinding();
    if (value instanceof Polygon && CurvePolygon.class.isAssignableFrom(binding)) {
      // GeometryTypeConverterFactory.java (geotools) does not convert polygons to CurvePolygon correctely.
      // We had a patch in that file but we can move the fix here and not need to cargo cult that file around too.
      Polygon poly = (Polygon) value;
      List<LinearRing> holes = new ArrayList<>();
      for (int i = 0; i < poly.getNumInteriorRing(); i++) {
        holes.add(poly.getInteriorRingN(i));
      }
      return new CurvePolygon(poly.getExteriorRing(), holes, poly.getFactory(), Double.MAX_VALUE);
    }
    // this will boil up to SQLDialect.java which does:
    // Class<?> binding = ad.getType().getBinding();
    // return Converters.convert(value, binding);
    return super.convertValue(value, ad);
  }


}
