001package gudusoft.gsqlparser.ir.builder.common;
002
003import gudusoft.gsqlparser.ir.bound.*;
004import gudusoft.gsqlparser.ir.common.Evidence;
005
006import java.util.*;
007
008/**
009 * Shared base class for dialect-specific routine reference resolvers.
010 * <p>
011 * Owns the common resolve() loop: builds uppercase and name-based indexes,
012 * iterates all unresolved refs, delegates per-ref resolution to subclass,
013 * and replaces the ref list on the program.
014 * <p>
015 * Dialect-specific resolution tiers (qualified match, schema inference,
016 * default schema, etc.) belong in the subclass {@link #tryResolve} method.
017 */
018public abstract class AbstractRoutineRefResolver {
019
020    /**
021     * Resolves all unresolved routine refs in the program.
022     * Replaces the program's ref list with updated copies where matches are found.
023     */
024    public final void resolve(BoundProgram program) {
025        if (program == null) return;
026
027        Map<String, BoundRoutineSymbol> upperIndex = buildUpperCaseIndex(program);
028        if (upperIndex.isEmpty()) return;
029
030        Map<String, List<BoundRoutineSymbol>> nameIndex = buildNameIndex(program);
031
032        List<BoundRoutineRef> original = program.getAllRoutineRefs();
033        List<BoundRoutineRef> resolved = new ArrayList<BoundRoutineRef>(original.size());
034
035        for (BoundRoutineRef ref : original) {
036            if (ref.getBindingStatus() == EBindingStatus.EXACT) {
037                resolved.add(ref);
038                continue;
039            }
040            if (Boolean.TRUE.equals(ref.getProperty("externalDependency"))) {
041                resolved.add(ref);
042                continue;
043            }
044
045            BoundRoutineRef resolvedRef = tryResolve(ref, program, upperIndex, nameIndex);
046            if (resolvedRef != null) {
047                resolved.add(resolvedRef);
048            } else {
049                classifyExternalIfKnown(ref);
050                resolved.add(ref);
051            }
052        }
053
054        program.replaceRoutineRefs(resolved);
055    }
056
057    /**
058     * Attempts to resolve a single routine ref against the known routines.
059     *
060     * @param ref        the unresolved ref
061     * @param program    the program containing all routines
062     * @param upperIndex case-insensitive routineId → symbol index
063     * @param nameIndex  case-insensitive name → list of symbols index
064     * @return resolved ref, or null if no match found
065     */
066    protected abstract BoundRoutineRef tryResolve(
067            BoundRoutineRef ref,
068            BoundProgram program,
069            Map<String, BoundRoutineSymbol> upperIndex,
070            Map<String, List<BoundRoutineSymbol>> nameIndex);
071
072    /**
073     * Called for unresolved refs to check if they are known external dependencies.
074     * Subclasses should set properties like "externalDependency", "externalType", etc.
075     */
076    protected abstract void classifyExternalIfKnown(BoundRoutineRef ref);
077
078    // ====== Index Building (shared) ======
079
080    /**
081     * Builds a case-insensitive routineId → symbol index.
082     */
083    protected final Map<String, BoundRoutineSymbol> buildUpperCaseIndex(BoundProgram program) {
084        Map<String, BoundRoutineSymbol> ciIndex =
085                new LinkedHashMap<String, BoundRoutineSymbol>();
086        for (Map.Entry<String, BoundRoutineSymbol> entry : program.getRoutineIndex().entrySet()) {
087            ciIndex.put(entry.getKey().toUpperCase(), entry.getValue());
088        }
089        return ciIndex;
090    }
091
092    /**
093     * Builds a case-insensitive name → list of symbols index.
094     */
095    protected final Map<String, List<BoundRoutineSymbol>> buildNameIndex(BoundProgram program) {
096        Map<String, List<BoundRoutineSymbol>> nameIndex =
097                new LinkedHashMap<String, List<BoundRoutineSymbol>>();
098        for (BoundRoutineSymbol sym : program.getRoutineIndex().values()) {
099            String nameKey = sym.getRoutineName().toUpperCase();
100            List<BoundRoutineSymbol> list = nameIndex.get(nameKey);
101            if (list == null) {
102                list = new ArrayList<BoundRoutineSymbol>();
103                nameIndex.put(nameKey, list);
104            }
105            list.add(sym);
106        }
107        return nameIndex;
108    }
109
110    // ====== Common Resolution Helpers ======
111
112    /**
113     * Tries all KIND code variants for a given base name and argCount.
114     * Returns the first match, or null.
115     */
116    protected static BoundRoutineSymbol tryKindVariants(
117            Map<String, BoundRoutineSymbol> ciIndex,
118            String baseName, int argCount, String[] kindCodes) {
119        // First pass: exact argCount
120        for (String kind : kindCodes) {
121            String candidateId = baseName + "/" + kind + "(" + argCount + ")";
122            BoundRoutineSymbol match = ciIndex.get(candidateId.toUpperCase());
123            if (match != null) {
124                return match;
125            }
126        }
127        // Second pass: default parameter tolerance (paramCount > argCount)
128        String prefix = baseName.toUpperCase() + "/";
129        BoundRoutineSymbol bestMatch = null;
130        int bestParamCount = Integer.MAX_VALUE;
131        for (Map.Entry<String, BoundRoutineSymbol> entry : ciIndex.entrySet()) {
132            String key = entry.getKey();
133            if (!key.startsWith(prefix)) continue;
134            int openParen = key.lastIndexOf('(');
135            int closeParen = key.lastIndexOf(')');
136            if (openParen < 0 || closeParen <= openParen) continue;
137            try {
138                int paramCount = Integer.parseInt(key.substring(openParen + 1, closeParen));
139                if (paramCount > argCount && paramCount < bestParamCount) {
140                    bestMatch = entry.getValue();
141                    bestParamCount = paramCount;
142                }
143            } catch (NumberFormatException e) {
144                // skip
145            }
146        }
147        return bestMatch;
148    }
149
150    /**
151     * Finds candidates matching a name and exact arg count from the name index.
152     */
153    protected static List<BoundRoutineSymbol> findByNameAndArgCount(
154            Map<String, List<BoundRoutineSymbol>> nameIndex,
155            String upperName, int argCount) {
156        List<BoundRoutineSymbol> candidates = nameIndex.get(upperName);
157        if (candidates == null || candidates.isEmpty()) {
158            return Collections.emptyList();
159        }
160        List<BoundRoutineSymbol> matches = new ArrayList<BoundRoutineSymbol>();
161        for (BoundRoutineSymbol sym : candidates) {
162            if (sym.getParameters().size() == argCount) {
163                matches.add(sym);
164            }
165        }
166        return matches;
167    }
168
169    /**
170     * Finds the best default-parameter match (smallest paramCount > argCount).
171     */
172    protected static BoundRoutineSymbol findBestDefaultParamMatch(
173            Map<String, List<BoundRoutineSymbol>> nameIndex,
174            String upperName, int argCount) {
175        List<BoundRoutineSymbol> candidates = nameIndex.get(upperName);
176        if (candidates == null) return null;
177        BoundRoutineSymbol best = null;
178        int bestCount = Integer.MAX_VALUE;
179        for (BoundRoutineSymbol sym : candidates) {
180            int pc = sym.getParameters().size();
181            if (pc > argCount && pc < bestCount) {
182                best = sym;
183                bestCount = pc;
184            }
185        }
186        return best;
187    }
188}