Store.java

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

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

import javax.lang.model.type.TypeKind;
import java.util.*;
import java.util.function.BiConsumer;

import static io.kaumei.jdbc.anno.utils.AnnoUtils.requireState;

public class Store<T extends Converter> {
    // ----- services
    protected final Context ctx;
    protected final String name;
    protected final @Nullable Store<T> parent;
    // ----- state
    private final Map<StoreID, Entry<T>> id2converter = new HashMap<>();
    private final Set<SearchKey> pending = new HashSet<>();

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

    public Store(Context ctx, String name) {
        this.ctx = ctx;
        this.name = name;
        this.parent = null;
    }

    public Store(Context ctx, String name, @Nullable Store<T> parent) {
        this.ctx = ctx;
        this.name = name;
        this.parent = parent;
    }

    public void dump(StringBuilder out) {
        out.append(this.name).append(" ----------------------------------------\n");
        if (!id2converter.isEmpty()) {
            var sorted = new TreeMap<>(id2converter);
            for (var entry : sorted.entrySet()) {
                out.append(entry.getKey());
                out.append(": ");
                entry.getValue().append(out);
                out.append("\n");
            }
        }
    }

    public int size() {
        return this.id2converter.size();
    }

    @Override
    public String toString() {
        return "Store{name=" + name + ", size=" + id2converter.size() + (this.parent == null ? "" : ", parent=" + this.parent.name) + "}";
    }

    // collection -------------------------------------------------------------

    public void add(StoreID id, T value) {
        var entry = this.id2converter.get(id);
        if (entry == null) {
            entry = Entry.valid(id, value);
            this.id2converter.put(id, entry);
        } else {
            entry.markAsDuplicate(value.source());
        }
    }

    public void addOpt(@Nullable FactoryResult<T> factoryResult) {
        if (factoryResult != null) {
            add(factoryResult);
        }
    }

    public void add(FactoryResult<T> result) {
        var id = result.id();
        var entry = this.id2converter.get(id);
        if (entry == null) {
            if (result.hasMessages()) {
                entry = Entry.invalid(id, result.source(), result.messages(), result.dependent());
            } else if (result.hasConverter()) {
                entry = Entry.valid(id, result.converter());
            } else if (result.hasPlaceholder()) {
                entry = Entry.valid(id, result.placeholder());
            } else {
                throw new IllegalStateException("Invalid: " + result); // sanity-check
            }
            this.id2converter.put(id, entry);
        } else {
            entry.markAsDuplicate(result.source());
        }
    }

    public SearchResult<T> markInvalid(StoreID storeId, Msg.Messages messages) {
        var entry = requireState(this.id2converter.get(storeId), () -> "Unknown id: " + storeId);
        entry.markAsInvalid(messages, Set.of());
        return entry.toSearchResult(storeId, this);
    }

    public SearchResult<T> markInvalid(StoreID storeId, Msg.Messages messages, Set<StoreID> dependent) {
        var entry = requireState(this.id2converter.get(storeId), () -> "Unknown id: " + storeId);
        entry.markAsInvalid(messages, dependent);
        return entry.toSearchResult(storeId, this);
    }

    public SearchResult<T> markResolve(StoreID storeId, T converter) {
        var entry = requireState(this.id2converter.get(storeId), () -> "Unknown id: " + storeId);
        entry.resolve(converter);
        return entry.toSearchResult(storeId, this);
    }

    public void flagPending(SearchKey key) {
        pending.add(key);
    }

    public void forEachPending(BiConsumer<Store<T>, SearchKey> consumer) {
        for (var nameType : this.pending) {
            consumer.accept(this, nameType);
        }
        this.pending.clear();
    }

    public void forEachPlaceholder(PlaceholderResolver<T> resolver) {
        for (var id : this.id2converter.keySet()) {
            var item = this.id2converter.get(id);
            if (item.placeholder != null) {
                resolver.apply(this, item.id, item.placeholder);
            }
        }
    }

    public SearchResult<T> get(StoreID storeId) {
        var entry = this.id2converter.get(storeId);
        return entry != null
                ? entry.toSearchResult(storeId, this)
                : SearchResult.notFound(storeId);
    }

    public SearchResult<T> search(StoreID storeId, boolean fullMessages) {
        var found = this.searchEntryFound(storeId);
        return found != null
                ? found.store.toSearchResult(storeId, found.entry, fullMessages)
                : SearchResult.notFound(storeId);
    }

    public FactoryResult<T> searchHierarchy(SearchKey searchKey) {
        var storeId = searchKey.toStoreId();
        var found = this.searchEntryFound(storeId);
        if (found != null) {
            return found.entry.toFactoryResult(storeId);
        } else if (searchKey.hasName() || searchKey.type().getKind() != TypeKind.DECLARED) {
            return FactoryResult.of(storeId, SourceDV.empty(), JdbcMsg.NOT_FOUND);
        }
        return searchTypeHierarchy(searchKey);
    }

    private FactoryResult<T> searchTypeHierarchy(SearchKey searchKey) {
        Map<SearchKey, Integer> hierarchy = ctx.collectTypeHierarchy2(searchKey.type());
        int level = -1;
        var candidates = new TreeMap<StoreID, Found<T>>();
        for (var entry : hierarchy.entrySet()) {
            var storeId = entry.getKey().toStoreId();
            var found = this.searchEntryFound(storeId);
            if (found != null && entry.getValue() >= level) {
                if (entry.getValue() > level) {
                    level = entry.getValue();
                    candidates.clear();
                }
                candidates.put(storeId, found);
            }
        }
        var storeId = searchKey.toStoreId();
        if (candidates.isEmpty()) {
            return FactoryResult.of(storeId, SourceDV.empty(), JdbcMsg.NOT_FOUND);
        } else if (candidates.size() > 1) {
            return FactoryResult.of(storeId, SourceDV.empty(), JdbcMsg.ambiguous(candidates.keySet()));
        }
        var found = candidates.values().iterator().next();
        return found.entry.toFactoryResult(storeId);
    }

    private @Nullable Entry<T> searchEntry(StoreID storeId) {
        var found = this.searchEntryFound(storeId);
        return found == null ? null : found.entry;
    }

    private record Found<T extends Converter>(Store<T> store, Entry<T> entry) {
    }

    private @Nullable Found<T> searchEntryFound(StoreID storeId) {
        var entry = this.id2converter.get(storeId);
        if (entry != null) {
            return new Found<>(this, entry);
        } else if (this.parent != null) {
            return parent.searchEntryFound(storeId);
        }
        return null;
    }

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

    SearchResult<T> toSearchResult(StoreID storeId, Entry<T> entry, boolean fullMessages) {
        if (!entry.messages.hasMessages()) {
            return SearchResult.valid(storeId, entry.state.searchState, this, entry.converter, entry.placeholder);
        } else if (!fullMessages) {
            return SearchResult.invalid(storeId, entry.state.searchState, this, entry.messages);
        }
        var sb = new StringBuilder();
        this.dump(sb, entry, 1);
        return SearchResult.invalid(storeId, entry.state.searchState, this, JdbcMsg.converter(sb.toString()));
    }

    private void dump(StringBuilder sb, Entry<T> entry, int indentCount) {
        var indent = "    ".repeat(indentCount);
        if (!sb.isEmpty()) {
            sb.append('\n');
        }
        sb.append(indent).append("source...: ");
        sb.append(entry.source.value());
        sb.append('\n').append(indent).append("id.......: ").append(entry.id());
        if (entry.messages.hasMessages()) {
            var first = true;
            for (var m : entry.messages) {
                if (first) {
                    sb.append('\n').append(indent).append("messages.: ");
                    first = false;
                } else {
                    sb.append('\n').append(indent).append("           ");
                }
                sb.append(m.text().replaceAll("\n", "\n" + indent + "           "));
            }
        }
        if (!entry.dependent.isEmpty()) {
            for (var other : new TreeSet<>(entry.dependent)) {
                var otherEntry = searchEntry(other);
                if (otherEntry == null) {
                    sb.append('\n').append(indent).append("    converter: not found");
                    sb.append('\n').append(indent).append("    id.......: ").append(other);
                } else if (otherEntry.messages.hasMessages()) {
                    // we only print dependent with messages
                    this.dump(sb, otherEntry, indentCount + 1);
                }
            }
        }
    }

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

    enum EntryState {
        VALID(SearchState.VALID),
        INVALID(SearchState.INVALID),
        DUPLICATE(SearchState.INVALID);
        final SearchState searchState;

        EntryState(SearchState searchState) {
            this.searchState = searchState;
        }
    }

    static class Entry<T extends Converter> {
        static <T extends Converter> Entry<T> valid(StoreID id, T value) {
            return new Entry<>(id, EntryState.VALID, value.source(), value, null, Msg.empty());
        }

        static <T extends Converter> Entry<T> valid(StoreID id, Converter.Placeholder value) {
            return new Entry<>(id, EntryState.VALID, value.source(), null, value, Msg.empty());
        }

        static <T extends Converter> Entry<T> invalid(StoreID id, SourceDV source, Msg.Messages messages, Set<StoreID> dependent) {
            if (!messages.hasMessages()) { // sanity-check
                throw new IllegalArgumentException(); // sanity-check
            }
            var entry = new Entry<T>(id, EntryState.INVALID, source, null, null, messages);
            entry.dependent = dependent;
            return entry;
        }

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

        private final StoreID id;
        private final SourceDV source;
        private EntryState state;
        private @Nullable T converter;
        private Converter.@Nullable Placeholder placeholder;
        private Msg.Messages messages;
        private Set<StoreID> dependent;

        private Entry(StoreID id, EntryState state, SourceDV source, @Nullable T converter, Converter.@Nullable Placeholder placeholder, Msg.Messages messages) {
            this.id = id;
            this.state = state;
            this.source = Objects.requireNonNull(source);
            this.converter = converter;
            this.placeholder = placeholder;
            this.messages = messages;
            this.dependent = Set.of();
        }

        SearchResult<T> toSearchResult(StoreID storeId, Store<T> store) {
            if (this.messages.hasMessages()) {
                return SearchResult.invalid(storeId, state.searchState, store, this.messages);
            }
            return SearchResult.valid(storeId, state.searchState, store, this.converter, this.placeholder);
        }

        FactoryResult<T> toFactoryResult(StoreID storeId) {
            if (this.messages.hasMessages()) {
                return FactoryResult.of(storeId, this.source, this.messages, this.dependent);
            } else if (this.converter != null) {
                return FactoryResult.of(storeId, this.converter);
            } else if (this.placeholder != null) {
                return FactoryResult.of(storeId, this.placeholder);
            }
            throw new IllegalStateException(this.toString()); // sanity-check
        }

        void markAsDuplicate(SourceDV source) {
            if (state == EntryState.DUPLICATE) {
                Msg.merge(this.messages, JdbcMsg.duplicateConverter(source));
            } else {
                this.state = EntryState.DUPLICATE;
                this.placeholder = null;
                this.converter = null;
                this.messages = Msg.merge(JdbcMsg.duplicateConverter(this.source),
                        JdbcMsg.duplicateConverter(source));
            }
        }

        void resolve(T converter) {
            // sanity-check:on
            if (this.converter != null || this.placeholder == null || this.messages.hasMessages() || !this.dependent.isEmpty()) {
                throw new IllegalStateException(this.toString());
            }
            // sanity-check:off
            this.converter = converter;
            this.placeholder = null;
            this.state = EntryState.VALID;
        }

        void markAsInvalid(Msg.Messages messages, Set<StoreID> dependent) {
            // sanity-check:on
            if (!messages.hasMessages()) {
                throw new IllegalArgumentException();
            } else if (state == EntryState.DUPLICATE || this.messages.hasMessages() || !this.dependent.isEmpty()) {
                throw new IllegalStateException(this.toString());
            }
            // sanity-check:off
            this.state = EntryState.INVALID;
            this.placeholder = null;
            this.converter = null;
            this.messages = messages;
            this.dependent = dependent;
        }

        void append(StringBuilder sb) {
            sb.append(this.state);
            sb.append("\n     ");
            sb.append("Source.....: ");
            sb.append(source);
            if (this.converter != null) {
                sb.append("\n     ");
                sb.append("Converter..: ");
                sb.append(this.converter);
            }
            if (this.placeholder != null) {
                sb.append("\n     ");
                sb.append("Placeholder: ");
                sb.append(this.placeholder);
            }
            if (messages.hasMessages()) {
                sb.append("\n     ");
                sb.append("Messages...: ");
                sb.append(this.messages.join(","));
            }
            if (!dependent.isEmpty()) {
                sb.append("\n     ");
                sb.append("Dependent..: ");
                sb.append(this.dependent);
            }
        }

        @Override
        public String toString() {
            return "Entry{id=" + id + ", state=" + state  //
                    + (converter != null ? ", converter=" + converter : "") //
                    + (placeholder != null ? ", placeholder=" + placeholder : "") //
                    + (!messages.hasMessages() ? ", messages=" + messages : "") //
                    + (!dependent.isEmpty() ? ", dependent=" + dependent : "") //
                    + '}';
        }

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

        final StoreID id() {
            return this.id;
        }

        EntryState state() {
            return this.state;
        }

        @Nullable T converter() {
            return this.state == EntryState.VALID ? this.converter : null;
        }

        Converter.@Nullable Placeholder placeholder() {
            return this.state == EntryState.VALID ? this.placeholder : null;
        }

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

        Set<StoreID> dependent() {
            return this.dependent;
        }

    }

}