package com.creditdatamw.zerocell.processor.spec;

import com.creditdatamw.zerocell.ZeroCellException;
import com.creditdatamw.zerocell.ZeroCellReader;
import com.creditdatamw.zerocell.annotation.RowNumber;
import com.creditdatamw.zerocell.processor.ZeroCellAnnotationProcessor;
import com.squareup.javapoet.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeMirror;
import java.util.*;
import java.util.regex.Pattern;

import static com.creditdatamw.zerocell.processor.spec.CellMethodSpec.beanSetterPropertyName;
import static com.creditdatamw.zerocell.processor.spec.ColumnInfoType.columnsOf;

public class ReaderTypeSpec {
    private final TypeElement typeElement;
    private final String readerClassName;

    private static final String INVALID_CHARS_REGEX = "\\s";

    public ReaderTypeSpec(TypeElement typeElement, Optional<String> customReaderName) {
        Objects.requireNonNull(typeElement);
        this.typeElement = typeElement;
        this.readerClassName = customReaderName.orElse(
                String.format("%sReader", typeElement.getSimpleName()));
    }

    public JavaFile build() throws java.io.IOException {
        assertReaderName();
        LoggerFactory.getLogger(ZeroCellAnnotationProcessor.class)
                .info("Processing class: {}", typeElement);
        ClassName dataClass = ClassName.get(typeElement);
        ClassName list = ClassName.get("java.util", "List");
        ClassName readerUtil = ClassName.get("com.creditdatamw.zerocell", "ReaderUtil");
        ClassName arrayList = ClassName.get("java.util", "ArrayList");
        ClassName convertersClass = ClassName.get("com.creditdatamw.zerocell.converter", "Converters");

        TypeName listOfData = ParameterizedTypeName.get(list, dataClass);
        TypeName zeroCellReader = ParameterizedTypeName.get(ClassName.get(ZeroCellReader.class), dataClass);

        MethodSpec reset = MethodSpec.methodBuilder("reset")
                .addModifiers(Modifier.PRIVATE)
                .addStatement("this.currentRow = -1")
                .addStatement("this.currentCol = -1")
                .addStatement("this.cur = null")
                .addStatement("this.data.clear()")
                .build();

        MethodSpec read = MethodSpec.methodBuilder("read")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(java.io.File.class, "file", Modifier.FINAL)
                .addParameter(String.class, "sheet", Modifier.FINAL)
                .returns(listOfData)
                .addStatement("this.reset()")
                .addStatement("$T.process(file, sheet, this)", readerUtil)
                .addStatement("List<$T> dataList", typeElement)
                .addStatement("dataList = $T.unmodifiableList(this.data)", Collections.class)
                .addStatement("return dataList")
                .build();

        MethodSpec.Builder startRowBuilder = MethodSpec.methodBuilder("startRow")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(Integer.TYPE, "i", Modifier.FINAL)
                .addStatement("currentRow = i")
                .addStatement("isHeaderRow = false")
                .addComment("Skip header row")
                .beginControlFlow("if (currentRow == 0)")
                .addStatement("isHeaderRow=true")
                .addStatement("return")
                .endControlFlow()
                .addStatement("cur = new $T()", dataClass);

        checkRowNumberField().ifPresent(fieldName -> {
            //we'll get something like startRowBuilder.addStatement("cur.setRowNumber(currentRow)");
            startRowBuilder.addStatement("cur.set$L(currentRow)", fieldName);
        });

        MethodSpec startRow = startRowBuilder.build();

        MethodSpec endRow = MethodSpec.methodBuilder("endRow")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(Integer.TYPE, "i", Modifier.FINAL)
                .beginControlFlow("if (! java.util.Objects.isNull(cur))")
                .addStatement("this.data.add(cur)")
                .addStatement("this.cur = null")
                .endControlFlow()
                .build();

        MethodSpec headerFooter = MethodSpec.methodBuilder("headerFooter")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(String.class, "text", Modifier.FINAL)
                .addParameter(boolean.class, "b", Modifier.FINAL)
                .addParameter(String.class, "tagName", Modifier.FINAL)
                .addComment("Skip, not processing headers or footers here")
                .build();

        // fields to spec
        List<FieldSpec> columnIndexFields = new ArrayList<>();

        List<ColumnInfoType> columnInfoList = columnsOf(typeElement);

        columnInfoList.forEach(column -> {
            int idx= column.getIndex();
            String name = column.getName();
            String normalizedName = String.format("COL_%s", column.getIndex()).toUpperCase();
            columnIndexFields.add(FieldSpec.builder(
                    Integer.TYPE,
                    normalizedName,
                    Modifier.STATIC, Modifier.FINAL)
                    .initializer("$L", idx)
                    .build());
        });

        MethodSpec constructor = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addStatement("this.data = new $T<>()", arrayList)
                .build();

        MethodSpec cell = CellMethodSpec.build(columnInfoList);

        MethodSpec assertColumnName = MethodSpec.methodBuilder("assertColumnName")
                .addParameter(String.class, "columnName", Modifier.FINAL)
                .addParameter(String.class, "value", Modifier.FINAL)
                .beginControlFlow("if (validateHeaders && isHeaderRow)")
                .beginControlFlow("if (! columnName.equalsIgnoreCase(value))")
                .addStatement("throw new $T(String.format($S, columnName, value))", ZeroCellException.class, "Expected Column '%s' but found '%s'")
                .endControlFlow()
                .endControlFlow()
                .build();

        TypeSpec readerTypeSpec = TypeSpec.classBuilder(readerClassName)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addSuperinterface(zeroCellReader)
                .addField(FieldSpec.builder(Logger.class, "LOGGER", Modifier.PRIVATE, Modifier.STATIC)
                        .initializer("$T.getLogger($L.class)", LoggerFactory.class, typeElement.getQualifiedName())
                        .build())
                .addFields(columnIndexFields)
                .addField(FieldSpec.builder(Boolean.TYPE, "validateHeaders", Modifier.PRIVATE).build())
                .addField(FieldSpec.builder(Boolean.TYPE, "isHeaderRow", Modifier.PRIVATE).build())
                .addField(FieldSpec.builder(Integer.TYPE, "currentRow", Modifier.PRIVATE).build())
                .addField(FieldSpec.builder(Integer.TYPE, "currentCol", Modifier.PRIVATE).build())
                .addField(FieldSpec.builder(dataClass, "cur", Modifier.PRIVATE).build())
                .addField(FieldSpec.builder(listOfData, "data", Modifier.PRIVATE).build())
                .addMethod(read)
                .addMethod(reset)
                .addMethod(constructor)
                .addMethod(headerFooter)
                .addMethod(startRow)
                .addMethod(cell)
                .addMethod(endRow)
                .addMethod(assertColumnName)
                .build();

        final JavaFile javaFile = JavaFile.builder(dataClass.packageName(), readerTypeSpec )
                .addStaticImport(convertersClass, "*")
                .build();
        LoggerFactory.getLogger(ZeroCellAnnotationProcessor.class)
                .info("Generated reader class: {}", readerTypeSpec.name);
        return javaFile;
    }

    private void assertReaderName() {
        if (! Pattern.matches("[A-Za-z]+\\d*[A-Za-z]", readerClassName)) {
            throw new IllegalArgumentException("Invalid name for the reader Class: " + readerClassName);
        }
    }

    private Optional<String> checkRowNumberField() {
        for(Element element: typeElement.getEnclosedElements()) {
            if (! element.getKind().isField()) {
                continue;
            }
            RowNumber annotation = element.getAnnotation(RowNumber.class);
            if (! Objects.isNull(annotation)) {
                TypeMirror type;
                try {
                    type = element.asType();
                } catch (MirroredTypeException mte) {
                    type = mte.getTypeMirror();
                }
                final String fieldType = String.format("%s", type);
                final String fieldName = element.getSimpleName().toString();
                if (fieldType.equals(int.class.getTypeName())     ||
                    fieldType.equals(long.class.getTypeName())    ||
                    fieldType.equals(Integer.class.getTypeName()) ||
                    fieldType.equals(Long.class.getTypeName())
                ) {
                    return Optional.of(beanSetterPropertyName(fieldName));
                } else {
                    // Must be one of the integer classes or bust!
                    throw new IllegalArgumentException(
                            String.format("Invalid type (%s) for @RowNumber field (%s). Only java.lang.Integer and java.lang.Long are allowed", fieldType, fieldName));
                }
            }
        }
        return Optional.empty();
    }
}