ConverterService.java

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

import io.kaumei.jdbc.anno.ProcessorEnvironment;
import io.kaumei.jdbc.anno.ctx.Context;
import io.kaumei.jdbc.anno.msg.JdbcMsg;
import org.jspecify.annotations.Nullable;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.type.TypeMirror;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;

import static java.util.Objects.requireNonNull;

public class ConverterService<T extends Converter> {
    // ----- services
    private final Context ctx;
    private final String prefix;
    // ----- state
    private final Store<T> basic;
    private final Store<T> global;
    private final Map<Element, Store<T>> local = new HashMap<>();

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

    public ConverterService(Context ctx, String prefix) {
        this.ctx = ctx;
        this.prefix = prefix;
        this.basic = new Store<>(this.ctx, prefix + ".basic");
        this.global = new Store<>(this.ctx, prefix + ".global", this.basic);
    }

    public void init(ProcessorEnvironment roundEnv) {
        for (var element : roundEnv.jdbcInterfaces()) {
            var store = new Store<>(this.ctx, prefix + ".local." + element.getQualifiedName(), this.global);
            this.local.put(element, store);
        }
    }

    public Store<T> basic() {
        return this.basic;
    }

    public Store<T> global() {
        return this.global;
    }

    public Store<T> local(Element element) {
        return requireNonNull(local.get(element));
    }

    public Store<T> getStoreFor(@Nullable Element element) {
        while (element != null && element.getKind() != ElementKind.PACKAGE) {
            var store = local.get(element);
            if (store != null) {
                return store;
            }
            element = element.getEnclosingElement();
        }
        return this.global;
    }

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

    public void forEachPending(BiConsumer<Store<T>, SearchKey> consumer) {
        this.basic.forEachPending(consumer);
        this.global.forEachPending(consumer);
        local.values().forEach((store) -> store.forEachPending(consumer));
    }

    public void forEachPlaceholder(PlaceholderResolver<T> resolver) {
        this.basic.forEachPlaceholder(resolver);
        this.global.forEachPlaceholder(resolver);
        local.values().forEach((store) -> store.forEachPlaceholder(resolver));
    }

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


    private enum Result {OK, INVALID, CYCLE}

    private record ResultAndCycle(Result result, List<SearchKey> cycle, @Nullable SearchKey start) {
        static final ResultAndCycle RESULT_OK = new ResultAndCycle(Result.OK, List.of(), null);
        static final ResultAndCycle RESULT_INVALID = new ResultAndCycle(Result.INVALID, List.of(), null);

        static ResultAndCycle cycle(List<SearchKey> cycle, SearchKey start) {
            return new ResultAndCycle(Result.CYCLE, cycle, start);
        }

        ResultAndCycle combine(ResultAndCycle other) {
            if (this.result == Result.INVALID) {
                return this;
            }
            // this in (OK,CYCLE)
            return switch (other.result) {
                case OK -> this;
                case INVALID, CYCLE -> other;
            };
        }
    }

    public void process(Store<T> store, SearchKey searchKey, Function<TypeMirror, @Nullable FactoryResult<T>> func) {
        final var cycle = new LinkedCycle<SearchKey>();
        process(cycle, store, searchKey, func);
    }

    private ResultAndCycle process(LinkedCycle<SearchKey> cycle,
                                   Store<T> store,
                                   SearchKey searchKey,
                                   Function<TypeMirror, @Nullable FactoryResult<T>> func) {
        if (!cycle.push(searchKey)) {
            return ResultAndCycle.cycle(cycle.asList(), searchKey);
        }
        try {
            // ----- check if we already have a result for this search key
            var result = store.search(searchKey.toStoreId(), false);

            if (result.hasPlaceholder()) {
                var otherResult = ResultAndCycle.RESULT_OK;
                var dependent = new LinkedHashSet<StoreID>();
                for (var other : result.placeholder().otherPlaceholders()) {
                    var current = process(cycle, store, other, func);
                    if (current.result == Result.INVALID) {
                        dependent.add(other.toStoreId());
                    }
                    otherResult = otherResult.combine(current);
                }
                if (otherResult.result == Result.INVALID) {
                    result.store().markInvalid(result.storeId(), JdbcMsg.INVALID_DEPENDENT_CONVERTER, dependent);
                    return ResultAndCycle.RESULT_INVALID;
                } else if (otherResult.result == Result.CYCLE && cycle.hasLast(otherResult.start)) {
                    var currentCycle = cycle.calculateCycle(otherResult.cycle);
                    result.store().markInvalid(result.storeId(), JdbcMsg.cycle(currentCycle));
                    return ResultAndCycle.RESULT_INVALID;
                }
                return otherResult;

            } else if (result.state() != SearchState.NOT_FOUND) {
                return result.hasMessages() ? ResultAndCycle.RESULT_INVALID : ResultAndCycle.RESULT_OK;
            }
            // ----- try to generate dynamic converter
            FactoryResult<T> factoryResult = func.apply(searchKey.type());
            if (factoryResult == null) {
                store.flagPending(searchKey);
                return ResultAndCycle.RESULT_OK; // this is handled as ok
            }
            store = this.global(); // used-type converter owns global

            // -----  placeholder, check dependencies
            var otherResult = ResultAndCycle.RESULT_OK;
            var dependent = new LinkedHashSet<StoreID>();
            if (factoryResult.hasPlaceholder()) {
                for (var other : factoryResult.placeholder().otherPlaceholders()) {
                    var current = process(cycle, store, other, func);
                    if (current.result == Result.INVALID) {
                        dependent.add(other.toStoreId());
                    }
                    otherResult = otherResult.combine(current);
                }
            }

            if (otherResult.result == Result.INVALID) {
                factoryResult = factoryResult.newWithMessages(JdbcMsg.INVALID_DEPENDENT_CONVERTER, dependent);
            } else if (otherResult.result == Result.CYCLE && cycle.hasLast(otherResult.start)) {
                var currentCycle = cycle.calculateCycle(otherResult.cycle);
                factoryResult = factoryResult.newWithMessages(JdbcMsg.cycle(currentCycle));
                otherResult = ResultAndCycle.RESULT_INVALID;
            }

            store.add(factoryResult);
            return otherResult;

        } finally {
            cycle.pop();
        }
    }

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

    public String csvStats() {
        var count = 0;
        for (var repo : local.values()) {
            count += repo.size();
        }
        //  basic, global, local
        return basic.size() + "," + global.size() + "," + count;
    }

    public void dump(StringBuilder out) {
        basic.dump(out);
        global.dump(out);
        var t = new TreeMap<String, Store<T>>();
        local.forEach((element, store) -> t.put(element.toString(), store));
        for (var entry : t.entrySet()) {
            var store = entry.getValue();
            if (store.size() != 0) {
                store.dump(out);
            }
        }
    }
}