001package gudusoft.gsqlparser.resolver2.iterative;
002
003import java.util.ArrayList;
004import java.util.List;
005
006/**
007 * Detects when iterative resolution has converged.
008 *
009 * <p>Convergence is reached when one of these conditions is met:
010 * 1. No progress in current pass (no new columns resolved)
011 * 2. All columns are resolved
012 * 3. Maximum iterations reached
013 * 4. No change in last N passes (stable state)
014 * 5. Progress rate falls below threshold
015 *
016 * <p>The detector uses multiple strategies to avoid:
017 * - Infinite loops (max iterations)
018 * - Premature stopping (stable state detection)
019 * - Wasted iterations (progress rate threshold)
020 *
021 * <p>Example:
022 * <pre>
023 * Pass 1: 10 columns resolved -> continue
024 * Pass 2: 5 columns resolved  -> continue
025 * Pass 3: 2 columns resolved  -> continue
026 * Pass 4: 0 columns resolved  -> CONVERGED (no progress)
027 * </pre>
028 */
029public class ConvergenceDetector {
030
031    /** Maximum number of iterations allowed */
032    private final int maxIterations;
033
034    /** Number of stable passes required to declare convergence */
035    private final int stablePasses;
036
037    /** Minimum progress rate to continue (0.0 - 1.0) */
038    private final double minProgressRate;
039
040    /** History of passes */
041    private final List<ResolutionPass> passHistory;
042
043    public ConvergenceDetector() {
044        this(10, 2, 0.01);
045    }
046
047    public ConvergenceDetector(int maxIterations, int stablePasses, double minProgressRate) {
048        this.maxIterations = maxIterations;
049        this.stablePasses = stablePasses;
050        this.minProgressRate = minProgressRate;
051        this.passHistory = new ArrayList<>();
052    }
053
054    /**
055     * Record a completed pass.
056     *
057     * @param pass the completed pass
058     */
059    public void recordPass(ResolutionPass pass) {
060        passHistory.add(pass);
061    }
062
063    /**
064     * Check if resolution has converged.
065     *
066     * @return convergence result with reason
067     */
068    public ConvergenceResult checkConvergence() {
069        if (passHistory.isEmpty()) {
070            return new ConvergenceResult(false, null);
071        }
072
073        ResolutionPass lastPass = passHistory.get(passHistory.size() - 1);
074
075        // Check 1: Maximum iterations reached
076        if (lastPass.getPassNumber() >= maxIterations) {
077            return new ConvergenceResult(
078                true,
079                "Maximum iterations reached (" + maxIterations + ")"
080            );
081        }
082
083        // Check 2: No progress in last pass
084        if (!lastPass.madeProgress()) {
085            return new ConvergenceResult(
086                true,
087                "No progress in pass " + lastPass.getPassNumber()
088            );
089        }
090
091        // Check 3: All columns resolved (100% resolution)
092        if (lastPass.getAfterStats() != null) {
093            int total = lastPass.getAfterStats().getTotalReferences();
094            int resolved = lastPass.getAfterStats().getExactMatches();
095            if (total > 0 && resolved == total) {
096                return new ConvergenceResult(
097                    true,
098                    "All columns resolved (" + resolved + "/" + total + ")"
099                );
100            }
101        }
102
103        // Check 4: Stable state (no progress in last N passes)
104        if (passHistory.size() >= stablePasses) {
105            boolean allStable = true;
106            for (int i = passHistory.size() - stablePasses; i < passHistory.size(); i++) {
107                if (passHistory.get(i).madeProgress()) {
108                    allStable = false;
109                    break;
110                }
111            }
112            if (allStable) {
113                return new ConvergenceResult(
114                    true,
115                    "Stable state (no progress in last " + stablePasses + " passes)"
116                );
117            }
118        }
119
120        // Check 5: Progress rate too low
121        if (passHistory.size() >= 3) {
122            double progressRate = calculateProgressRate();
123            if (progressRate < minProgressRate) {
124                return new ConvergenceResult(
125                    true,
126                    String.format("Progress rate too low (%.2f%% < %.2f%%)",
127                        progressRate * 100, minProgressRate * 100)
128                );
129            }
130        }
131
132        // Not converged yet
133        return new ConvergenceResult(false, null);
134    }
135
136    /**
137     * Calculate the progress rate over recent passes.
138     * Returns ratio of columns resolved / total columns.
139     */
140    private double calculateProgressRate() {
141        if (passHistory.isEmpty()) {
142            return 0.0;
143        }
144
145        ResolutionPass lastPass = passHistory.get(passHistory.size() - 1);
146        if (lastPass.getAfterStats() == null) {
147            return 0.0;
148        }
149
150        int totalReferences = lastPass.getAfterStats().getTotalReferences();
151        if (totalReferences == 0) {
152            return 1.0;  // No columns to resolve
153        }
154
155        int resolved = lastPass.getColumnsResolvedInPass();
156        return (double) resolved / totalReferences;
157    }
158
159    /**
160     * Get the total number of passes so far.
161     *
162     * @return pass count
163     */
164    public int getPassCount() {
165        return passHistory.size();
166    }
167
168    /**
169     * Get the pass history.
170     *
171     * @return list of all passes
172     */
173    public List<ResolutionPass> getPassHistory() {
174        return new ArrayList<>(passHistory);
175    }
176
177    /**
178     * Get statistics about the iteration process.
179     *
180     * @return statistics summary
181     */
182    public String getStatistics() {
183        if (passHistory.isEmpty()) {
184            return "No passes yet";
185        }
186
187        int totalResolved = 0;
188        int totalInferences = 0;
189        long totalTime = 0;
190
191        for (ResolutionPass pass : passHistory) {
192            totalResolved += pass.getColumnsResolvedInPass();
193            totalInferences += pass.getNewInferences();
194            totalTime += pass.getTimeTaken();
195        }
196
197        return String.format(
198            "Iterations: %d, Resolved: %d, Inferences: %d, Time: %dms",
199            passHistory.size(), totalResolved, totalInferences, totalTime
200        );
201    }
202
203    /**
204     * Result of convergence check.
205     */
206    public static class ConvergenceResult {
207        private final boolean converged;
208        private final String reason;
209
210        public ConvergenceResult(boolean converged, String reason) {
211            this.converged = converged;
212            this.reason = reason;
213        }
214
215        public boolean hasConverged() {
216            return converged;
217        }
218
219        public String getReason() {
220            return reason;
221        }
222
223        @Override
224        public String toString() {
225            return converged ? "Converged: " + reason : "Not converged";
226        }
227    }
228}