SourceMethod.java

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

import io.kaumei.jdbc.anno.ctx.ConfigService;
import io.kaumei.jdbc.anno.ctx.Context;
import io.kaumei.jdbc.anno.msg.JdbcMsg;
import io.kaumei.jdbc.anno.msg.Msg;
import io.kaumei.jdbc.annotation.JdbcUpdate;
import io.kaumei.jdbc.annotation.config.*;
import org.jspecify.annotations.Nullable;

import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;

public abstract sealed class SourceMethod permits
        SourceMethod.Select, SourceMethod.Update, SourceMethod.BatchUpdate,
        SourceMethod.Native, SourceMethod.CallMethod,
        SourceMethod.InvalidMethod {

    public static SourceMethod of(Context ctx, ExecutableElement method) {
        var messages = Msg.builder();
        var annoMap = AnnoMap.of(ctx, method);

        // ----- check entry points
        var entryPoint = annoMap.entryPoints();

        if (entryPoint.count() == 0) {
            messages.add(JdbcMsg.jdbcMethodRequiresEntryPointAnnotation());
            return InvalidMethod.of(ctx, method, annoMap, messages);
        } else if (entryPoint.count() > 1) {
            messages.add(JdbcMsg.jdbcMethodRequiresSingleEntryPointAnnotation());
            return InvalidMethod.of(ctx, method, annoMap, messages);
        }

        var jdbcName = ctx.JDBC_NAME.getAnnoOpt(method);
        messages.add(jdbcName);
        var jdbcConverterName = ctx.JDBC_CONVERTER_NAME.getAnnoOpt(method);
        if (jdbcConverterName != null && jdbcConverterName.isBlankName()) {
            messages.add(JdbcMsg.annotationValueMustNotBeBlank("@JdbcConverterName"));
        }

        if (entryPoint.batchUpdate() != null) {
            var parameter = SourceMethodParameter.of(ctx, messages, method);
            return new BatchUpdate(ctx, method, annoMap, parameter, messages.build());
        } else if (entryPoint.call() != null) {
            SourceMethodParameter parameter;
            if (entryPoint.call().sql().value().isBlank()) {
                messages.add(JdbcMsg.jdbcCallRequiresSqlString());
                parameter = SourceMethodParameter.of(ctx, messages, method);
            } else {
                parameter = SourceMethodParameter.of(ctx, messages, method, entryPoint.call().sql());
            }
            var returnType = ctx.jdbcTypeMirror(method);
            if (returnType.kind() == JdbcTypeKind.RECORD
                    && !returnType.optional().isNonNullOrUnspecified()
                    && ctx.asElementOpt(returnType.type()) instanceof TypeElement record
                    && record.getRecordComponents().size() > 1) {
                messages.add(JdbcMsg.jdbcCallMultipleOutputsRequireNonNullReturnType());
            }
            return new CallMethod(ctx, method, annoMap, parameter, jdbcName, jdbcConverterName, messages.build());
        } else if (entryPoint.nativeJdbc() != null) {
            var parameter = SourceMethodParameter.of(ctx, messages, method);
            return new Native(ctx, method, annoMap, parameter, entryPoint.nativeJdbc(), messages.build());
        } else if (entryPoint.select() != null) {
            SourceMethodParameter parameter;
            if (entryPoint.select().sql().value().isBlank()) {
                messages.add(JdbcMsg.jdbcSelectRequiresSqlString());
                parameter = SourceMethodParameter.of(ctx, messages, method);
            } else {
                parameter = SourceMethodParameter.of(ctx, messages, method, entryPoint.select().sql());
                parameter.selectUpdateValidation(messages);
            }
            return new Select(ctx, method, annoMap, parameter, messages.build());
        } else if (entryPoint.update() != null) {
            SourceMethodParameter parameter;
            if (entryPoint.update().sql().value().isBlank()) {
                messages.add(JdbcMsg.jdbcUpdateRequiresSqlString());
                parameter = SourceMethodParameter.of(ctx, messages, method);
            } else {
                parameter = SourceMethodParameter.of(ctx, messages, method, entryPoint.update().sql());
                parameter.selectUpdateValidation(messages);
            }
            return new Update(ctx, method, annoMap, parameter, entryPoint.update(), messages.build());
        }
        throw new IllegalStateException("Illegal state: entry point is not supported yet: " + entryPoint);
    }


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

    protected final Context ctx;
    protected final ExecutableElement method;
    protected final AnnoMap annoMap;
    protected final Msg.Messages messages;
    protected final SourceMethodParameter parameter;

    protected SourceMethod(Context ctx,
                           ExecutableElement method,
                           AnnoMap annoMap, SourceMethodParameter parameter,
                           Msg.Messages messages) {
        this.ctx = ctx;
        this.method = method;
        this.annoMap = annoMap;
        this.parameter = parameter;
        this.messages = messages;
    }

    public ExecutableElement method() {
        return method;
    }

    public AnnoMap annoMap() {
        return annoMap;
    }

    public TypeElement parent() {
        return (TypeElement) this.method.getEnclosingElement();
    }

    public SourceMethodParameter parameter() {
        return parameter;
    }

    public boolean hasMessages() {
        return messages.hasMessages();
    }

    public Msg.Messages messages() {
        return messages;
    }

    public SQLNameDV jdbcName() {
        return annoMap.get(ctx.JDBC_NAME, SQLNameDV.noName());
    }

    public ConverterNameDV jdbcConverterName() {
        return annoMap.get(ctx.JDBC_CONVERTER_NAME, ConverterNameDV.unnamed());
    }

    public JdbcNoMoreRows.Kind jdbcNoMoreRows() {
        return this.ctx.kaumeiConfig.searchOnMethod(this.ctx.kaumeiConfig.NO_MORE_ROWS, this);
    }

    public JdbcNoRows.Kind jdbcNoRows() {
        return this.ctx.kaumeiConfig.searchOnMethod(this.ctx.kaumeiConfig.NO_ROWS, this);
    }

    public ConfigService.@Nullable OptionValue<Integer> jdbcFetchSize() {
        return this.ctx.kaumeiConfig.search(this.ctx.kaumeiConfig.FETCH_SIZE, this);
    }

    public ConfigService.@Nullable OptionValue<Integer> jdbcMaxRows() {
        return this.ctx.kaumeiConfig.search(this.ctx.kaumeiConfig.MAX_ROWS, this);
    }

    public ConfigService.@Nullable OptionValue<JdbcResultSetConcurrency.Kind> jdbcResultSetConcurrency() {
        return this.ctx.kaumeiConfig.search(this.ctx.kaumeiConfig.RESULT_SET_CONCURRENCY, this);
    }

    public ConfigService.@Nullable OptionValue<JdbcResultSetType.Kind> jdbcResultSetType() {
        return this.ctx.kaumeiConfig.search(this.ctx.kaumeiConfig.RESULT_SET_TYPE, this);
    }

    public ConfigService.@Nullable OptionValue<JdbcFetchDirection.Kind> jdbcFetchDirection() {
        return this.ctx.kaumeiConfig.search(this.ctx.kaumeiConfig.FETCH_DIRECTION, this);
    }

    public ConfigService.@Nullable OptionValue<Integer> jdbcQueryTimeout() {
        return this.ctx.kaumeiConfig.search(this.ctx.kaumeiConfig.QUERY_TIMEOUT, this);
    }

    public ConfigService.@Nullable OptionValue<Integer> jdbcBatchSize() {
        return this.ctx.kaumeiConfig.search(this.ctx.kaumeiConfig.BATCH_SIZE, this);
    }

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

    public Msg.Messages unusedAnno() {
        var messages = Msg.builder();
        if (annoMap.hasUnused()) {
            messages.add(JdbcMsg.javaMethodHasUnusedAnnotations(annoMap.unused()));
        }
        parameter.addUnusedAnno(messages);
        return messages.build();
    }

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

    public JdbcTypeMirror<ExecutableElement> returnType() {
        return ctx.jdbcTypeMirror(this.method);
    }

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

    public static final class Select extends SourceMethod {
        Select(Context ctx, ExecutableElement method, AnnoMap annoMap, SourceMethodParameter parameter,
               Msg.Messages messages) {
            super(ctx, method, annoMap, parameter, messages);
        }

        @Override
        public String toString() {
            return "@JdbcSelect";
        }
    }

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

    public static final class Update extends SourceMethod {
        private final JavaAnnoType.JdbcUpdateRecord annotation;

        Update(Context ctx, ExecutableElement method, AnnoMap annoMap, SourceMethodParameter parameter,
               JavaAnnoType.JdbcUpdateRecord annotation, Msg.Messages messages) {
            super(ctx, method, annoMap, parameter, messages);
            this.annotation = annotation;
        }

        public JdbcUpdate.GeneratedValues jdbcReturnGeneratedValues() {
            var value = this.annotation.returnGeneratedValues();
            if (value == JdbcUpdate.GeneratedValues.DEFAULT) {
                value = this.ctx.kaumeiConfig.jdbcReturnGeneratedValues();
            }
            return value;
        }

        public String[] returnGeneratedColumns() {
            return this.annotation.returnGeneratedColumns();
        }

        @Override
        public String toString() {
            return "@JdbcUpdate";
        }
    }

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

    public static final class BatchUpdate extends SourceMethod {

        BatchUpdate(Context ctx, ExecutableElement method, AnnoMap annoMap, SourceMethodParameter parameter, Msg.Messages messages) {
            super(ctx, method, annoMap, parameter, messages);

        }

        @Override
        public String toString() {
            return "@JdbcBatchUpdate";
        }
    }

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

    public static final class Native extends SourceMethod {
        private final JavaAnnoType.JdbcNativeRecord annotation;

        Native(Context ctx, ExecutableElement method, AnnoMap annoMap, SourceMethodParameter parameter,
               JavaAnnoType.JdbcNativeRecord annotation, Msg.Messages messages) {
            super(ctx, method, annoMap, parameter, messages);
            this.annotation = annotation;
        }

        public JavaAnnoType.JdbcNativeRecord annotation() {
            return this.annotation;
        }

        @Override
        public String toString() {
            return "@JdbcNative";
        }
    }

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

    public static final class CallMethod extends SourceMethod {
        private final @Nullable SQLNameDV returnJdbcName; // FIXME hier oder doch im generate holen
        private final @Nullable ConverterNameDV returnConverterName; // FIXME hier oder doch im generate holen

        CallMethod(Context ctx, ExecutableElement method, AnnoMap annoMap, SourceMethodParameter parameter,
                   @Nullable SQLNameDV returnJdbcName, @Nullable ConverterNameDV returnConverterName,
                   Msg.Messages messages) {
            super(ctx, method, annoMap, parameter, messages);
            this.returnJdbcName = returnJdbcName;
            this.returnConverterName = returnConverterName;
        }

        public @Nullable SQLNameDV returnJdbcName() {
            return returnJdbcName;
        }

        public @Nullable ConverterNameDV returnConverterName() {
            return returnConverterName;
        }

        @Override
        public String toString() {
            return "@JdbcCall";
        }
    }

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

    public static final class InvalidMethod extends SourceMethod {

        InvalidMethod(Context ctx, ExecutableElement method, AnnoMap annoMap, SourceMethodParameter parameter, Msg.Messages messages) {
            super(ctx, method, annoMap, parameter, messages);
        }

        static InvalidMethod of(Context ctx, ExecutableElement method, AnnoMap annoMap, Msg.Builder messages) {
            var parameter = SourceMethodParameter.of(ctx, messages, method);
            return new InvalidMethod(ctx, method, annoMap, parameter, messages.build());
        }

        @Override
        public String toString() {
            return "invalid";
        }

    }

}