001package gudusoft.gsqlparser.pp2.layout.rules;
002
003import gudusoft.gsqlparser.ETokenType;
004import gudusoft.gsqlparser.pp2.island.BlockScopeDetector.BlockScopeResult;
005import gudusoft.gsqlparser.pp2.island.ClausePart;
006import gudusoft.gsqlparser.pp2.island.ClauseScopeAnnotator.ClauseScopeResult;
007import gudusoft.gsqlparser.pp2.island.SqlScopeDetector.SqlScopeResult;
008import gudusoft.gsqlparser.pp2.layout.LayoutContext;
009import gudusoft.gsqlparser.pp2.layout.LayoutPriorities;
010import gudusoft.gsqlparser.pp2.layout.LayoutRule;
011import gudusoft.gsqlparser.pp2.token.Pp2Token;
012import gudusoft.gsqlparser.pp2.token.Pp2TokenStream;
013
014import java.util.EnumSet;
015import java.util.Locale;
016import java.util.Set;
017
018/**
019 * Breaks a predicate before each top-level {@code AND} / {@code OR} so a long
020 * WHERE/HAVING/ON condition lays out one conjunct per line:
021 *
022 * <pre>
023 * WHERE a = 1
024 * AND b = 2
025 * OR c = 3
026 * </pre>
027 *
028 * <p>Only top-level boolean operators break: an {@code AND}/{@code OR} whose
029 * block (paren) depth equals its clause run's base depth. Operators inside a
030 * parenthesised sub-predicate ({@code (a AND b)}) or a function stay inline.
031 * Limited to predicate clauses ({@code WHERE}/{@code HAVING}/{@code JOIN}).
032 *
033 * <p>Concatenation operators ({@code ||}, {@code +}) are left to the spacing
034 * rule (no break) — keeping concatenated expressions on one line.
035 *
036 * <h2>Deep input safety</h2>
037 *
038 * <p>This is a single linear pass over the token stream — there is no recursion
039 * over the boolean expression tree — so a 2000-deep {@code AND}/{@code OR} chain
040 * is handled in O(n) without {@code StackOverflowError} (plan §13/R3).
041 *
042 * <p>Priority {@link LayoutPriorities#CASE_ANDOR}. Needs S19 block + S20 sql +
043 * S21 clause analyses. Read-only.
044 *
045 * <p>Plan reference: §7.3/S27, §7.4/S27.
046 */
047public final class AndOrConcatRules implements LayoutRule {
048
049    private static final Set<ClausePart> PREDICATE_CLAUSES = EnumSet.of(
050        ClausePart.WHERE, ClausePart.HAVING, ClausePart.JOIN);
051
052    @Override
053    public int priority() { return LayoutPriorities.CASE_ANDOR; }
054
055    @Override
056    public String name() { return "AndOrConcatRules"; }
057
058    @Override
059    public void apply(LayoutContext context) {
060        ClauseScopeResult clause = context.getClauseScope();
061        SqlScopeResult sql = context.getSqlScope();
062        BlockScopeResult block = context.getBlockScope();
063        if (clause == null || sql == null || block == null) return;
064        Pp2TokenStream stream = context.getStream();
065        int n = min(stream.size(), clause.size(), sql.size(), block.size());
066
067        // Single O(n) forward pass: track the current clause run's base paren
068        // depth (the depth at the run's first token) instead of re-scanning back
069        // for every AND/OR, and a BETWEEN "debt" so the AND of a BETWEEN ... AND
070        // is not mistaken for a boolean operator.
071        ClausePart prevPart = null;
072        int prevLevel = -1;
073        int runBaseDepth = 0;
074        int betweenDebt = 0;
075        for (int i = 0; i < n; i++) {
076            ClausePart part = clause.partAt(i);
077            int level = sql.levelAt(i);
078            if (part != prevPart || level != prevLevel) {
079                runBaseDepth = block.depthAt(i); // new clause run starts here
080                betweenDebt = 0;
081            }
082            prevPart = part;
083            prevLevel = level;
084
085            if (!PREDICATE_CLAUSES.contains(part)) continue;
086            boolean atBase = block.depthAt(i) == runBaseDepth;
087            if (!atBase) continue;
088
089            String u = upper(stream.get(i));
090            if ("BETWEEN".equals(u)) {
091                betweenDebt++;
092            } else if ("AND".equals(u)) {
093                if (betweenDebt > 0) {
094                    betweenDebt--; // this AND belongs to a BETWEEN ... AND
095                } else if (i > 0) {
096                    context.requestLinebreaksBefore(i, 1);
097                    context.requestBlanksBefore(i, 0);
098                }
099            } else if ("OR".equals(u) && i > 0) {
100                context.requestLinebreaksBefore(i, 1);
101                context.requestBlanksBefore(i, 0);
102            }
103        }
104    }
105
106    private static String upper(Pp2Token t) {
107        if (t.getSourceToken().tokentype != ETokenType.ttkeyword) return "";
108        String s = t.getText();
109        return s == null ? "" : s.toUpperCase(Locale.ROOT);
110    }
111
112    private static int min(int a, int b, int c, int d) {
113        return Math.min(Math.min(a, b), Math.min(c, d));
114    }
115}