Java2JdbcFactory.java

/*
 * SPDX-FileCopyrightText: 2025 kaumei.io
 * SPDX-License-Identifier: Apache-2.0
 */
package io.kaumei.jdbc.anno.java2jdbc;

import io.kaumei.jdbc.anno.ctx.Context;
import io.kaumei.jdbc.anno.model.ConverterNameDV;
import io.kaumei.jdbc.anno.msg.JdbcMsg;
import io.kaumei.jdbc.anno.msg.Msg;
import io.kaumei.jdbc.anno.store.FactoryResult;
import io.kaumei.jdbc.anno.store.SearchKey;
import io.kaumei.jdbc.anno.store.SourceDV;
import io.kaumei.jdbc.anno.store.StoreID;
import org.jspecify.annotations.Nullable;

import javax.lang.model.element.*;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import java.util.List;

import static io.kaumei.jdbc.anno.ctx.JavaModelUtils.isStatic;
import static io.kaumei.jdbc.anno.ctx.JavaModelUtils.isVisible;
import static javax.lang.model.element.ElementKind.RECORD;

class Java2JdbcFactory {

    private static final Msg.Message INVALID_STATIC_PARAMETER_COUNT =
            JdbcMsg.JAVA_TO_JDBC_METHOD_REQUIRES_ONE_OR_THREE_PARAMETERS;

    private final Context ctx;

    Java2JdbcFactory(Context ctx) {
        this.ctx = ctx;
    }

    // ------------------------------------------------------------------------

    FactoryResult<Java2JdbcConverter> converterEnum(Element elem) {
        if (elem.getKind() != ElementKind.ENUM) { // sanity-check
            throw new IllegalArgumentException("Element must be a record."); // sanity-check
        }
        var storeId = StoreID.of(elem.asType());
        var other = SearchKey.of(this.ctx.JAVA_String.typeMirror());
        var source = SourceDV.converter(ctx, elem);
        return FactoryResult.of(storeId, new ConverterEnum.Placeholder(elem.asType(), source, other));
    }

    // ------------------------------------------------------------------------

    private FactoryResult<Java2JdbcConverter> converterObjectSimple(ExecutableElement method) {
        if (isStatic(method)) { // sanity-check
            throw new IllegalArgumentException("method must not be static"); // sanity-check
        }

        var type = method.getEnclosingElement().asType();
        var messages = Msg.builder();
        if (!isVisible(method)) {
            messages.add(JdbcMsg.JAVA_TO_JDBC_METHOD_MUST_BE_VISIBLE);
        }

        // ----- check return type
        var returnType = method.getReturnType();
        if (returnType.getKind() == TypeKind.VOID) {
            messages.add(JdbcMsg.INVALID_RETURN_TYPE);
        } else if (!this.ctx.optionalFlag(method, returnType).isNonNullOrUnspecified()) {
            messages.add(JdbcMsg.JAVA_TO_JDBC_RETURN_TYPE_MUST_BE_NON_NULL_OR_UNSPECIFIED);
        }
        // ----- check parameter
        if (!method.getParameters().isEmpty()) {
            messages.add(JdbcMsg.JAVA_TO_JDBC_METHOD_MUST_HAVE_NO_PARAMETERS);
        }
        // ----- check throws
        if (!this.ctx.hasValidSqlExceptions(method)) {
            messages.add(JdbcMsg.JAVA_TO_JDBC_HAS_INCOMPATIBLE_EXCEPTIONS);
        }
        var name = ctx.JAVA_TO_JDBC.getAnno(method, ConverterNameDV.unnamed());
        if (name.isBlankName()) {
            messages.add(JdbcMsg.annotationValueMustNotBeBlank("@JavaToJdbc"));
        }
        var storeId = StoreID.of(type, name);
        var source = SourceDV.converter(ctx, method);
        return messages.hasMessages()
                ? FactoryResult.of(storeId, source, messages.build())
                : FactoryResult.of(storeId, new ConverterObjectSimple.Placeholder(type, source, method, SearchKey.of(returnType)));
    }

    // ------------------------------------------------------------------------

    FactoryResult<Java2JdbcConverter> converterRecord(TypeElement elem) {
        if (elem.getKind() != ElementKind.RECORD) { // sanity-check
            throw new IllegalArgumentException("Element must be a record."); // sanity-check
        }

        var type = elem.asType();
        var storeId = StoreID.of(type);
        var source = SourceDV.converter(ctx, elem);

        List<? extends RecordComponentElement> components = elem.getRecordComponents();
        if (components.size() != 1) {
            return FactoryResult.of(storeId, source, JdbcMsg.JAVA_TO_JDBC_RECORD_MUST_HAVE_ONE_COMPONENT);
        }

        var component = components.get(0);
        var cType = component.asType();
        if (!this.ctx.optionalFlag(elem, cType).isNonNullOrUnspecified()) {
            return FactoryResult.of(storeId, source, JdbcMsg.JAVA_TO_JDBC_RECORD_COMPONENT_MUST_BE_NON_NULL_OR_UNSPECIFIED);
        }
        return FactoryResult.of(storeId, new ConverterRecord.Placeholder(type, source, component.getAccessor(), SearchKey.of(cType)));
    }

    // ------------------------------------------------------------------------

    @Nullable
    FactoryResult<Java2JdbcConverter> converterMethod(ExecutableElement elem) {
        if (isStatic(elem)) {
            return converterStatic(elem);
        } else {
            return converterObjectSimple(elem);
        }
    }

    // ------------------------------------------------------------------------

    @Nullable FactoryResult<Java2JdbcConverter> converterStatic(ExecutableElement method) {
        if (!isStatic(method)) { // sanity-check
            throw new IllegalArgumentException("method must be static"); // sanity-check
        }

        var messages = Msg.builder();
        if (!isVisible(method)) {
            messages.add(JdbcMsg.JAVA_TO_JDBC_METHOD_MUST_BE_VISIBLE);
        }
        var name = ctx.JAVA_TO_JDBC.getAnno(method, ConverterNameDV.unnamed());
        if (name.isBlankName()) {
            messages.add(JdbcMsg.annotationValueMustNotBeBlank("@JavaToJdbc"));
        }
        var paramSize = method.getParameters().size();
        if (paramSize == 1) {
            return converterStaticSimple(messages, method, name);
        } else if (paramSize == 3) {
            return converterStaticStatement(messages, method, name);
        } else if (name.hasName()) {
            var storeId = StoreID.of(ctx.JAVA_Void.typeMirror(), name);
            return FactoryResult.of(storeId, SourceDV.converter(ctx, method), JdbcMsg.JAVA_TO_JDBC_NO_TYPE);
        }
        this.ctx.logger.warn(method, INVALID_STATIC_PARAMETER_COUNT.text());
        return null;
    }

    // ------------------------------------------------------------------------

    private FactoryResult<Java2JdbcConverter> converterStaticSimple(Msg.Builder messages,
                                                                    ExecutableElement method,
                                                                    ConverterNameDV name) {
        if (method.getParameters().size() != 1) { // sanity-check
            throw new IllegalArgumentException("Invalid parameter count: " + method.getParameters().size()); // sanity-check
        }

        // ----- check return type
        var returnType = method.getReturnType();
        if (returnType.getKind() == TypeKind.VOID) {
            messages.add(JdbcMsg.INVALID_RETURN_TYPE);
        } else if (!this.ctx.optionalFlag(method, returnType).isNonNullOrUnspecified()) {
            messages.add(JdbcMsg.JAVA_TO_JDBC_RETURN_TYPE_MUST_BE_NON_NULL_OR_UNSPECIFIED);
        }

        // ----- check throws
        if (!this.ctx.hasValidSqlExceptions(method)) {
            messages.add(JdbcMsg.JAVA_TO_JDBC_HAS_INCOMPATIBLE_EXCEPTIONS);
        }

        var type = method.getParameters().get(0).asType();
        var storeId = StoreID.of(method.getParameters().get(0).asType(), name);
        var source = SourceDV.converter(ctx, method);
        return messages.hasMessages()
                ? FactoryResult.of(storeId, source, messages.build())
                : FactoryResult.of(storeId, new ConverterStaticSimple.Placeholder(type, source, method, SearchKey.of(method.getReturnType())));
    }

    // ------------------------------------------------------------------------

    private FactoryResult<Java2JdbcConverter> converterStaticStatement(Msg.Builder messages,
                                                                       ExecutableElement method,
                                                                       ConverterNameDV name) {
        if (method.getParameters().size() != 3) { // sanity-check
            throw new IllegalArgumentException("Invalid parameter count: " + method.getParameters().size()); // sanity-check
        }

        // ----- check return type
        var returnType = method.getReturnType();
        if (returnType.getKind() != TypeKind.VOID) {
            messages.add(JdbcMsg.INVALID_RETURN_TYPE);
        }

        // ----- check throws
        if (!this.ctx.hasValidSqlExceptions(method)) {
            messages.add(JdbcMsg.JAVA_TO_JDBC_HAS_INCOMPATIBLE_EXCEPTIONS);
        }

        // ----- check parameter statement
        var paramStmt = method.getParameters().get(0);
        if (!this.ctx.JAVA_SQL_PreparedStatement.isSameType(paramStmt.asType())) {
            messages.add(JdbcMsg.JAVA_TO_JDBC_FIRST_PARAMETER_MUST_BE_PREPARED_STATEMENT);
        } else if (!this.ctx.optionalFlag(method, paramStmt.asType()).isNonNullOrUnspecified()) {
            messages.add(JdbcMsg.JAVA_TO_JDBC_FIRST_PARAMETER_MUST_BE_NON_NULL_OR_UNSPECIFIED);
        }

        // ----- check parameter: index
        var paramIndex = method.getParameters().get(1);
        if (paramIndex.asType().getKind() != TypeKind.INT) {
            messages.add(JdbcMsg.JAVA_TO_JDBC_SECOND_PARAMETER_MUST_BE_INT);
        }

        // ----- check parameter: value
        var paramValue = method.getParameters().get(2);
        var paramValueType = paramValue.asType();
        if (!paramValueType.getKind().isPrimitive()
                && !this.ctx.optionalFlag(method, paramValueType).isNullableOrUnspecified()) {
            messages.add(JdbcMsg.JAVA_TO_JDBC_THIRD_PARAMETER_MUST_BE_NULLABLE_OR_UNSPECIFIED);
        }

        var storeId = StoreID.of(paramValueType, name);
        var source = SourceDV.converter(ctx, method);
        return messages.hasMessages()
                ? FactoryResult.of(storeId, source, messages.build())
                : FactoryResult.of(storeId, new ConverterStaticStatement(paramValueType, source, method));
    }

    // ----------------------------------------------------------------

    @Nullable FactoryResult<Java2JdbcConverter> tryToCreate(TypeMirror typeMirror) {
        var storeId = StoreID.of(typeMirror);
        var elem = this.ctx.asElementOpt(typeMirror);
        if (elem == null) {
            return FactoryResult.of(storeId, SourceDV.empty(), JdbcMsg.javaToJdbcElementTypeNotFound(typeMirror));
        }

        var source = SourceDV.converter(ctx, elem);
        var messages = Msg.builder();
        FactoryResult<Java2JdbcConverter> annotatedConverter = null;

        for (var child : elem.getEnclosedElements()) {
            if (!(child instanceof ExecutableElement executable)) {
                continue;
            } else if (!ctx.JAVA_TO_JDBC.hasAnno(executable)) {
                continue;
            }
            var javaToJdbcAnno = ctx.JAVA_TO_JDBC.getAnno(executable, ConverterNameDV.unnamed());
            if (javaToJdbcAnno.isBlankName()) {
                messages.add(JdbcMsg.annotationValueMustNotBeBlank("@JavaToJdbc"));
            } else if (javaToJdbcAnno.hasName()) {
                messages.add(JdbcMsg.JAVA_TO_JDBC_ANNOTATION_NAME_UNSUPPORTED);
            } else {
                var converter = this.converterMethod(executable);
                if (converter != null) {
                    if (converter.hasMessages()) {
                        messages.add(converter.messages());
                    } else if (!this.ctx.isSubtype(typeMirror, converter.type())) {
                        messages.add(JdbcMsg.javaToJdbcAnnotationHasWrongType(converter.type()));
                    } else if (annotatedConverter == null) {
                        annotatedConverter = converter;
                    } else {
                        messages.add(JdbcMsg.JAVA_TO_JDBC_TOO_MANY_ANNOTATIONS);
                    }
                } else {
                    messages.add(INVALID_STATIC_PARAMETER_COUNT);
                }
            }
        }
        if (messages.hasMessages()) {
            return FactoryResult.of(storeId, source, messages.build());
        } else if (annotatedConverter != null) {
            return annotatedConverter;
        } else if (elem instanceof TypeElement type) {
            if (type.getKind() == RECORD) {
                return this.converterRecord(type);
            } else if (type.getKind() == ElementKind.ENUM) {
                return this.converterEnum(type);
            }
        }
        return null;
    }

}