package org.gcube.application.geoportal.model.db;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.gcube.application.geoportal.model.fault.DataParsingException;
import org.gcube.application.geoportal.model.gis.BBOX;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
@Getter
@ToString
public class PostgisTable {


	@Getter
	@AllArgsConstructor
	public static enum GeometryType{
		MULTIPOINT("4326","geometry (MULTIPOINT,4326)","",""),
		POINT("4326","geometry (POINT,4326)","",""),
		LINE("4326","geometry (MULTILINESTRING,4326)","",""),
		POLYGON("4326","geometry (MULTIPOLYGON,4326)","","");
		private final String SRID;		
		private final String definition;
		private final String InsertWKT;
		private final String insertWKB;


	}

	@RequiredArgsConstructor
	@Getter
	@ToString
	public static class Field{
		@NonNull
		private String name;
		@NonNull
		private FieldType type;
		private Boolean isIndexed;
		private Object constantValue;
	}

	@Getter
	@AllArgsConstructor
	public enum FieldType{
		INT("int",java.sql.Types.INTEGER),
		TEXT("text",java.sql.Types.LONGVARCHAR),
		FLOAT("float",java.sql.Types.FLOAT),
		GEOMETRY("",0),
		AUTOINCREMENT("BIGSERIAL PRIMARY KEY",java.sql.Types.BIGINT);

		private String definition;
		private int sqlType;
	}


	@RequiredArgsConstructor
	@Getter
	@ToString
	public static class POINT{

		private static Pattern pattern = Pattern.compile("(?!=\\d\\.\\d\\.)([\\d.]+)");

		public static POINT parsePOINT(String point) throws DataParsingException {
			//POINT(8.30230113965909 44.8011688237011)
			// x,y
			try {
				log.debug("Parsing POINT "+point);
				Matcher m=pattern.matcher(point);

				if(!m.find()) throw new DataParsingException("Unable to get x ");
				Double x=Double.parseDouble(m.group(1));

				if(!m.find()) throw new DataParsingException("Unable to get y ");				
				Double y=Double.parseDouble(m.group(1));

				return new POINT(x,y);
			}catch(Throwable t) {
				throw new DataParsingException("Invalid POINT "+point,t);
			}
		}

		@NonNull
		private Double x;
		@NonNull
		private Double y;
		
	}
	
	
	private static final NumberFormat DECIMAL_FORMAT=NumberFormat.getInstance(Locale.US);

	static {
		((DecimalFormat) DECIMAL_FORMAT).setGroupingUsed(false);
	}


	public String getGeometryColumn() {
		for(Field f:fields)
			if(f.getType().equals(FieldType.GEOMETRY)) return f.getName();
		return null;
	}
	

	@NonNull	
	private String tablename;

	@NonNull
	private List<Field> fields;


	@NonNull
	private GeometryType geometryColumnType;

	@Setter
	private BBOX boundingBox=null;

	@Setter
	private POINT centroid=null;

	public void setTablename(String tablename) {
		this.tablename = sanitizeFieldName(tablename);
	}

	public String getCreateStatement() {
		StringBuilder stmt=new StringBuilder();
		stmt.append("CREATE TABLE IF NOT EXISTS "+tablename+"( ");
		for(Field field:fields){

			String fieldDefinition=field.getType().getDefinition();
			if(field.getType().equals(FieldType.GEOMETRY))
				fieldDefinition=this.getGeometryColumnType().definition;

			stmt.append(field.getName()+" "+fieldDefinition+",");
		}
		stmt.deleteCharAt(stmt.lastIndexOf(","));
		stmt.append(")");
		return stmt.toString();
	}

	public String getDeleteByFieldStatement(Field field) {
		return "DELETE FROM "+tablename+" WHERE "+field.getName()+" =  ? ";
	}
	
	
	public String getInsertionStatement(boolean geometryText) {
		StringBuilder fieldList=new StringBuilder();
		StringBuilder fieldInsertion=new StringBuilder();

		for(Field field:fields) {
			switch(field.getType()) {
			case AUTOINCREMENT : break;
			case GEOMETRY : {
				fieldList.append(field.getName()+",");
				if(geometryText)
					fieldInsertion.append("ST_GeomFromText(?, 4326),");
				else
					fieldInsertion.append("ST_GeomFromWKB(?, 4326),");
				break;
			}
			default : {
				fieldList.append(field.getName()+",");
				fieldInsertion.append("?,");
			}
			}
		}

		fieldList.deleteCharAt(fieldList.lastIndexOf(","));
		fieldInsertion.deleteCharAt(fieldInsertion.lastIndexOf(","));



		return "Insert into "+tablename+" ("+fieldList+") VALUES ("+fieldInsertion+")";
	}


	public void fillObjectsPreparedStatement(Map<String,Object> row, PreparedStatement toFill) throws SQLException {
		int psFieldIndex=0;
		HashMap<String,Object> rowValues=new HashMap<String,Object>();
		for(Entry<String,Object> entry:row.entrySet())
			rowValues.put(sanitizeFieldName(entry.getKey()), entry.getValue());


		for(Field field:fields) {
			if(!field.getType().equals(FieldType.AUTOINCREMENT)) {
				psFieldIndex++;

				Object value=rowValues.get(field.getName());
				setObjectInPreparedStatement(field,value,toFill,psFieldIndex);
				
			}


		}
	}

	
	public void setObjectInPreparedStatement(Field field,Object value, PreparedStatement toFill, int psFieldIndex) throws SQLException {
		if(value==null) {
			try{
				toFill.setNull(psFieldIndex, field.getType().sqlType);
			}catch(SQLException e) {
				log.error("Unable to set null for field "+field);
				throw e;
			}
		}
		else 
			switch(field.getType()) {
			case FLOAT :{
				toFill.setFloat(psFieldIndex, (Float)value);
				break;
			}
			case INT : {
				toFill.setInt(psFieldIndex, (Integer)value);
				break;
			}
			case TEXT : {
				toFill.setString(psFieldIndex, value.toString());
				break;
			}					
			case GEOMETRY : {
				toFill.setBytes(psFieldIndex, (byte[])value);
			}
			}
	}
	
	public void fillCSVPreparedStatament(Map<String,String> row, PreparedStatement toFill,boolean explicitGeometry) throws SQLException {
		int psFieldIndex=0;

		HashMap<String,String> rowValues=new HashMap<String,String>();
		for(Entry<String,String> entry:row.entrySet())
			rowValues.put(sanitizeFieldName(entry.getKey()), entry.getValue());

		for(Field field:fields) {


			if(!field.getType().equals(FieldType.AUTOINCREMENT)) {
				psFieldIndex++;

				String value=rowValues.get(field.getName());

//			if(value==null||value.equalsIgnoreCase("null")) toFill.setNull(psFieldIndex, field.getType().sqlType);
//				else 
					switch(field.getType()) {
					case FLOAT :{
						try{
							toFill.setFloat(psFieldIndex, Float.parseFloat(value));
						}catch(NumberFormatException e) {
							throw new SQLException(field+" cannot be null.  CSV Row is "+rowValues,e);
						}
						break;
					}
					case INT : {
						try{
							toFill.setInt(psFieldIndex, Integer.parseInt(value));
						}catch(NumberFormatException e) {
							log.warn("Skipping value for "+field+" row was "+rowValues,e);
							toFill.setNull(psFieldIndex, java.sql.Types.INTEGER);
						}
						break;
					}
					case TEXT : {
						toFill.setString(psFieldIndex, value.toString());
						break;
					}					
					case GEOMETRY : {
						if(explicitGeometry) {
							toFill.setString(psFieldIndex,value);
						}else {
							switch(geometryColumnType){
							case POINT: {
								String xRepresentation=DECIMAL_FORMAT.format(Double.parseDouble(rowValues.get(DBConstants.Defaults.XCOORD_FIELD)));
								String yRepresentation=DECIMAL_FORMAT.format(Double.parseDouble(rowValues.get(DBConstants.Defaults.YCOORD_FIELD)));

								toFill.setString(psFieldIndex, "POINT("+xRepresentation+" "+
										yRepresentation+")");
								break;
							}
							default :{
								toFill.setString(psFieldIndex,rowValues.get("wkt"));
								break;
							}
							}
						}
						break;
					}
					}
			}




		}
	}

	public static String sanitizeFieldName(String fieldName) {
//		return fieldName.toLowerCase().replaceAll(" ", "_").replaceAll("\\.", "").replaceAll("-", "_").replaceAll("////","_");
		return fieldName.toLowerCase().replaceAll("[^a-z0-9_\\\\]", "_");
	}

	
	
	
}
