001package gudusoft.gsqlparser.pp2.layout.rules;
002
003import gudusoft.gsqlparser.ETokenType;
004import gudusoft.gsqlparser.pp.para.GFmtOpt;
005import gudusoft.gsqlparser.pp.para.styleenums.TAlignStyle;
006import gudusoft.gsqlparser.pp2.island.ClausePart;
007import gudusoft.gsqlparser.pp2.island.ClauseScopeAnnotator.ClauseScopeResult;
008import gudusoft.gsqlparser.pp2.island.SqlScopeDetector.SqlScopeResult;
009import gudusoft.gsqlparser.pp2.layout.LayoutContext;
010import gudusoft.gsqlparser.pp2.layout.LayoutDecisionView;
011import gudusoft.gsqlparser.pp2.layout.LayoutPriorities;
012import gudusoft.gsqlparser.pp2.layout.LayoutRule;
013import gudusoft.gsqlparser.pp2.token.Pp2Token;
014import gudusoft.gsqlparser.pp2.token.Pp2TokenStream;
015import gudusoft.gsqlparser.pp2.token.TokenRole;
016
017import java.util.List;
018
019/**
020 * When {@link GFmtOpt#alignAliasInSelectList} is set (and the list is stacked,
021 * {@link TAlignStyle#AsStacked}), aligns each stacked SELECT-list continuation
022 * column under the first column (i.e. just past the {@code SELECT} keyword)
023 * rather than at a flat indent:
024 *
025 * <pre>
026 * SELECT a,
027 *        b,
028 *        c
029 * </pre>
030 *
031 * <p>The alignment column is {@code indentOf(SELECT) + len(SELECT) + 1}. This
032 * overrides {@link IndentRules}' flat indent for those columns (priority
033 * {@link LayoutPriorities#ALIGNMENT} &gt; {@link LayoutPriorities#INDENT}).
034 *
035 * <p>Only SELECT-list continuation columns that begin a line are aligned (the
036 * first column stays on the SELECT line). Needs S20 sql + S21 clause analyses.
037 * Iterative; read-only over tokens.
038 *
039 * <p>Plan reference: §7.3/S28, §7.4/S28.
040 */
041public final class AlignmentRules implements LayoutRule {
042
043    @Override
044    public int priority() { return LayoutPriorities.ALIGNMENT; }
045
046    @Override
047    public String name() { return "AlignmentRules"; }
048
049    @Override
050    public void apply(LayoutContext context) {
051        GFmtOpt opt = context.getOptions().toGFmtOpt();
052        if (!opt.alignAliasInSelectList) return;
053        if (opt.selectColumnlistStyle != TAlignStyle.AsStacked) return;
054        ClauseScopeResult clause = context.getClauseScope();
055        SqlScopeResult sql = context.getSqlScope();
056        if (clause == null || sql == null) return;
057
058        int width = indentWidth(opt);
059        Pp2TokenStream stream = context.getStream();
060        List<Integer> masters = clause.getMasterIndices();
061        int n = min(stream.size(), clause.size(), sql.size());
062        for (int i = 0; i < n; i++) {
063            if (clause.partAt(i) != ClausePart.SELECT_LIST) continue;
064            if (effectiveLinebreaks(context, stream, i) <= 0) continue; // first column stays inline
065            if (stream.get(i).hasRole(TokenRole.KEYWORD_MASTER)) continue; // the SELECT keyword itself
066            // Only a genuine stacked column start (the token right after a
067            // top-level list comma) aligns — not CASE/expression continuations
068            // inside a column, which keep IndentRules' indent.
069            int prev = prevSolid(stream, i);
070            if (prev < 0 || stream.get(prev).getSourceToken().tokentype != ETokenType.ttcomma) {
071                continue;
072            }
073            int selectIdx = selectMasterFor(stream, sql, masters, i);
074            if (selectIdx < 0) continue;
075            // Only align under a SELECT that itself begins a line (or is the very
076            // first token); an inline subquery SELECT's real column is unknown
077            // here, so fall back to IndentRules' flat indent for it.
078            if (selectIdx != 0 && effectiveLinebreaks(context, stream, selectIdx) <= 0) {
079                continue;
080            }
081            int selectIndent = sql.levelAt(selectIdx) * width;
082            int alignCol = selectIndent + length(stream.get(selectIdx)) + 1;
083            context.requestBlanksBefore(i, alignCol);
084        }
085    }
086
087    private static int prevSolid(Pp2TokenStream stream, int i) {
088        for (int j = i - 1; j >= 0; j--) {
089            ETokenType type = stream.get(j).getSourceToken().tokentype;
090            if (type == ETokenType.ttsimplecomment
091                || type == ETokenType.ttbracketedcomment
092                || type == ETokenType.ttCPPComment) {
093                continue;
094            }
095            return j;
096        }
097        return -1;
098    }
099
100    /** The nearest preceding SELECT master at the same SQL level as token i. */
101    private static int selectMasterFor(Pp2TokenStream stream, SqlScopeResult sql,
102                                       List<Integer> masters, int i) {
103        int best = -1;
104        for (int m : masters) {
105            if (m > i) break;
106            if (sql.levelAt(m) == sql.levelAt(i) && isSelect(stream.get(m))) best = m;
107        }
108        return best;
109    }
110
111    private static boolean isSelect(Pp2Token t) {
112        String s = t.getText();
113        return s != null && "SELECT".equalsIgnoreCase(s);
114    }
115
116    private static int length(Pp2Token t) {
117        String s = t.getText();
118        return s == null ? 0 : s.length();
119    }
120
121    private static int indentWidth(GFmtOpt opt) {
122        Integer w = opt.indentLen;
123        return (w == null || w.intValue() < 0) ? 2 : w.intValue();
124    }
125
126    private static int effectiveLinebreaks(LayoutContext ctx, Pp2TokenStream stream, int i) {
127        LayoutDecisionView d = ctx.decisionAt(i);
128        return d.isLinebreaksDecided() ? d.getLinebreaksBefore()
129            : stream.get(i).getPrecedingLinebreaks();
130    }
131
132    private static int min(int a, int b, int c) {
133        return Math.min(Math.min(a, b), c);
134    }
135}