package com.creditdatamw.zerocell.processor.spec;

import com.creditdatamw.zerocell.converter.NoopConverter;
import com.creditdatamw.zerocell.processor.ZeroCellAnnotationProcessor;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import org.apache.poi.xssf.usermodel.XSSFComment;
import org.slf4j.LoggerFactory;

import javax.lang.model.element.Modifier;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

/**
 * Generates a method that uses a specified convertor
 * for a field in a ZerocellReaderBuilder.
 */
public class CellMethodSpec {

    public static MethodSpec build(List<ColumnInfoType> columns) {
        final CodeBlock.Builder builder = CodeBlock.builder();
        LoggerFactory.getLogger(ZeroCellAnnotationProcessor.class)
                     .info("Found {} columns in source class", columns.size());
        columns.forEach(column -> {
            String staticFieldName = "COL_" + column.getIndex();
            String fieldName = column.getFieldName();

            String beanSetterProperty = beanSetterPropertyName(fieldName);

            builder.beginControlFlow("if ($L == column)", staticFieldName)
                .addStatement("assertColumnName($S, formattedValue)", column.getName());

            converterStatementFor(builder, column, beanSetterProperty);

            builder.addStatement("return").endControlFlow();
        });

        return  MethodSpec.methodBuilder("cell")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(String.class, "cellReference")
                .addParameter(String.class, "formattedValue", Modifier.FINAL)
                .addParameter(XSSFComment.class, "xssfComment", Modifier.FINAL)
                .addStatement("if (java.util.Objects.isNull(cur)) return")
                .addComment("Gracefully handle missing CellRef here in a similar way as XSSFCell does")
                .beginControlFlow("if(cellReference == null)")
                .addStatement("cellReference = new $T(currentRow, currentCol).formatAsString()", org.apache.poi.ss.util.CellAddress.class)
                .endControlFlow()
                .addStatement("int column = new $T(cellReference).getCol()", org.apache.poi.hssf.util.CellReference.class)
                .addStatement("currentCol = column")
                .addCode(builder.build())
                .build();
    }

    private static void converterStatementFor(CodeBlock.Builder builder,
                                              ColumnInfoType column,
                                              String beanSetterProperty) {
        String columnName = column.getName();
        String converterClass = String.format("%s", column.getConverterClass());
        String throwException = "throw new ZeroCellException(\"$L threw an exception while trying to convert value \" + formattedValue, e)";

        CodeBlock converterCodeBlock = CodeBlock.builder()
                .addStatement("//Handle any exceptions thrown by the converter - this stops execution of the whole process")
                .beginControlFlow("try")
                    .addStatement("cur.set$L(new $L().convert(formattedValue, $S, currentRow))",
                            beanSetterProperty,
                            converterClass,
                            columnName)
                .nextControlFlow("catch(Exception e)")
                    .addStatement(throwException, converterClass)
                .endControlFlow()
                .build();
        // Don't use a converter if there isn't a custom one
        if (converterClass.equals(NoopConverter.class.getTypeName())) {
            builder.addStatement(converterStatementFor(column), beanSetterProperty, columnName);
        } else {
            builder.add(converterCodeBlock);
        }
    }

    public static String converterStatementFor(ColumnInfoType column) {
        String fieldType = String.format("%s", column.getType());

        if (fieldType.equals(LocalDateTime.class.getTypeName())) {
            return "cur.set$L(toLocalDateTime.convert(formattedValue, $S, currentRow))";

        } else if (fieldType.equals(LocalDate.class.getTypeName())) {
            return "cur.set$L(toLocalDate.convert(formattedValue, $S, currentRow))";

        } else if (fieldType.equals(java.sql.Date.class.getTypeName())) {
            return "cur.set$L(toSqlDate.convert(formattedValue, $S, currentRow))";

        } else if (fieldType.equals(Timestamp.class.getTypeName())) {
            return "cur.set$L(toSqlTimestamp.convert(formattedValue, $S, currentRow))";

        } else if (fieldType.equals(Integer.class.getTypeName()) ||
                fieldType.equals(int.class.getTypeName())) {
            return "cur.set$L(toInteger.convert(formattedValue, $S, currentRow))";

        } else if (fieldType.equals(Long.class.getTypeName()) ||
                fieldType.equals(long.class.getTypeName())) {
            return "cur.set$L(toLong.convert(formattedValue, $S, currentRow))";

        } else if (fieldType.equals(Double.class.getTypeName()) ||
                fieldType.equals(double.class.getTypeName())) {
            return "cur.set$L(toDouble.convert(formattedValue, $S, currentRow))";

        } else if (fieldType.equals(Float.class.getTypeName()) ||
                fieldType.equals(float.class.getTypeName())) {
            return "cur.set$L(toFloat.convert(formattedValue, $S, currentRow))";

        } else if (fieldType.equals(Boolean.class.getTypeName())) {
            return "cur.set$L(toBoolean.convert(formattedValue, $S, currentRow))";
        }
        // Default to Converters.noop.convert
        return "cur.set$L(noop.convert(formattedValue, $S, currentRow))";
    }

    /**
     * Returns the field name for a "setter" for a POJO, i.e. the part after the "set" text.
     * <br/>
     * Example: <code>"set" + beanSetterPropertyName("name")  -> "setName"</code>
     *
     * @param fieldName
     * @return
     */
    static String beanSetterPropertyName(String fieldName) {
        // TODO(zikani): Find a better way ??
        // Here we're assuming people follow standards of naming POJO fields
        String beanSetterProperty = String.valueOf(fieldName.charAt(0))
                .toUpperCase()
                .concat(fieldName.substring(1));
        return beanSetterProperty;
    }
}