ConverterColumnObject.java

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

import com.palantir.javapoet.CodeBlock;
import io.kaumei.jdbc.anno.gen.TargetMethod;
import io.kaumei.jdbc.anno.model.*;
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.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeMirror;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

final class ConverterColumnObject extends Jdbc2JavaConverter.ColumnConverter implements Jdbc2JavaConverter.Callable {

    record Placeholder(TypeMirror type, SourceDV source, Visibility visibility,
                       ExecutableElement factoryMethod,
                       SearchKey other,
                       @Nullable SQLTypeDV sqlType) implements Jdbc2JavaConverter.Placeholder {
        @Override
        public Jdbc2JavaConverter resolve(Map<SearchKey, Jdbc2JavaConverter> converter) {
            return new ConverterColumnObject(this, Objects.requireNonNull(converter.get(other)), sqlType);
        }

        @Override
        public Set<SearchKey> otherPlaceholders() {
            return Set.of(other);
        }

        @Nullable StoreID invalidDependentConverter(Map<SearchKey, Jdbc2JavaConverter> converters) {
            var converter = Objects.requireNonNull(converters.get(other));
            return converter instanceof Jdbc2JavaConverter.RowConverter
                    ? other.toStoreId()
                    : null;
        }

        @Nullable StoreID invalidSqlTypeOverwrite(Map<SearchKey, Jdbc2JavaConverter> converters) {
            if (sqlType == null) {
                return null;
            }
            var converter = Objects.requireNonNull(converters.get(other));
            return converter instanceof Jdbc2JavaConverter.ColumnConverter column && column.hasSqlType()
                    ? null
                    : other.toStoreId();
        }
    }

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

    private final ExecutableElement factoryMethod;
    private final Element methodParent;
    private final Jdbc2JavaConverter.ColumnConverter converter;
    private final @Nullable SQLTypeDV sqlType;

    private ConverterColumnObject(Placeholder placeholder, Jdbc2JavaConverter converter,
                                  @Nullable SQLTypeDV sqlTypeOverwrite) {
        super(placeholder.type, placeholder.source, placeholder.visibility.and(converter.visibility()));
        this.factoryMethod = placeholder.factoryMethod;
        this.methodParent = factoryMethod.getEnclosingElement();
        this.converter = (Jdbc2JavaConverter.ColumnConverter) converter;
        var converterSqlType = this.converter.hasSqlType() ? this.converter.sqlType() : null;
        if (sqlTypeOverwrite != null && converterSqlType == null) {
            throw new IllegalArgumentException("SQL type override requires a OneValue converter");
        }
        this.sqlType = sqlTypeOverwrite != null ? sqlTypeOverwrite : converterSqlType;
    }

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

    @Override
    public boolean hasSqlType() {
        return this.sqlType != null;
    }

    @Override
    public SQLTypeDV sqlType() {
        if (this.hasSqlType()) {
            return Objects.requireNonNull(this.sqlType);
        }
        throw new IllegalStateException("Column converter does not provide an SQL type: " + converter);
    }

    @Override
    public CodeBlock columnNullCheck(String varName) {
        return CodeBlock.of("$L == null", varName);
    }

    @Override
    public CodeBlock callNullCheck(String varName) {
        return CodeBlock.of("$L == null", varName);
    }

    @Override
    public void addColumnByIndex(TargetMethod builder, String localVarName, ColumnIndex index, OptionalFlag optional) {
        builder.addComment("ConverterColumnObject.addColumnByIndex", "type", type(), "optional", optional);
        var tempVarName = builder.tempVarName(localVarName);
        converter.addColumnByIndex(builder, tempVarName, index, OptionalFlag.NULLABLE);
        if (optional.isNonNull()) {
            builder.beginControlFlow("if($L)", converter.columnNullCheck(tempVarName));
            builder.addThrowColumnWasNull(index);
            builder.endControlFlow();
            builder.addStatement("var $L = $L", localVarName, createValue(tempVarName));
        } else if (optional.isOptionalType()) {
            builder.addStatement("$T<$T> $L = $L ? $T.empty() : $T.of($L)",
                    Optional.class, this.type(),
                    localVarName, converter.columnNullCheck(tempVarName),
                    Optional.class, Optional.class, createValue(tempVarName));
        } else {
            builder.addStatement("var $L = $L ? null : $L", localVarName, converter.columnNullCheck(tempVarName), createValue(tempVarName));
        }
    }

    @Override
    public void addColumnByName(TargetMethod builder, String localVarName, SQLNameDV columnName, OptionalFlag optional) {
        builder.addComment("ConverterColumnObject.addColumnByName", "type", type(), "optional", optional);
        var index = ColumnIndex.ofVariable(builder.tempVarName(localVarName), columnName);
        builder.addStatement("var $N = rs.findColumn($S)", index.columnIndexVar(), index.columnName());
        addColumnByIndex(builder, localVarName, index, optional);
    }

    @Override
    public void addCallToRow(TargetMethod builder, String localVarName, OptionalFlag optional, Map<String, ColumnIndex> map) {
        builder.addComment("ConverterColumnObject.addCallableOutput", "type", type(), "map", map, "optional", optional);
        var index = map.values().iterator().next();
        if( !(converter instanceof Jdbc2JavaConverter.Callable c)) {
            throw new RuntimeException("!!!");
        }

        var tempVarName = builder.tempVarName(localVarName);
        c.addCallToRow(builder, tempVarName,  OptionalFlag.NULLABLE,map);
        if (optional.isNonNull()) {
            builder.beginControlFlow("if($L)", converter.callNullCheck(tempVarName));
            builder.addThrowColumnWasNull(index);
            builder.endControlFlow();
            builder.addStatement("var $L = $L", localVarName, createValue(tempVarName));
        } else if (optional.isOptionalType()) {
            builder.addStatement("$T<$T> $L = $L ? $T.empty() : $T.of($L)",
                    Optional.class, this.type(),
                    localVarName, converter.callNullCheck(tempVarName),
                    Optional.class, Optional.class, createValue(tempVarName));
        } else {
            builder.addStatement("var $L = $L ? null : $L", localVarName, converter.callNullCheck(tempVarName), createValue(tempVarName));
        }
    }

    private CodeBlock createValue(String varName) {
        return switch (this.factoryMethod.getKind()) {
            case METHOD ->
                    CodeBlock.of("$T.$N($L)", this.methodParent, this.factoryMethod.getSimpleName(), varName);
            case CONSTRUCTOR -> CodeBlock.of("new $L($L)", this.methodParent, varName);
            default ->
                    throw new IllegalStateException("Unexpected kind: " + factoryMethod.getKind()); // sanity-check
        };
    }

}