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.pp.para.styleenums.TLinefeedsCommaOption;
007import gudusoft.gsqlparser.pp2.island.BlockScopeDetector.BlockScopeResult;
008import gudusoft.gsqlparser.pp2.island.ClausePart;
009import gudusoft.gsqlparser.pp2.island.ClauseScopeAnnotator.ClauseScopeResult;
010import gudusoft.gsqlparser.pp2.island.SqlScopeDetector.SqlScopeResult;
011import gudusoft.gsqlparser.pp2.layout.LayoutContext;
012import gudusoft.gsqlparser.pp2.layout.LayoutPriorities;
013import gudusoft.gsqlparser.pp2.layout.LayoutRule;
014import gudusoft.gsqlparser.pp2.token.Pp2TokenStream;
015
016import java.util.EnumSet;
017import java.util.Set;
018
019/**
020 * Stacks comma-separated list items (SELECT list, GROUP BY, ORDER BY) one per
021 * line, placing the linebreak relative to the comma per
022 * {@link GFmtOpt#selectColumnlistComma}.
023 *
024 * <h2>Which commas</h2>
025 *
026 * <p>Only <i>top-level</i> list commas are stacked: a comma whose paren (block)
027 * depth equals the depth of its clause run's first token. This excludes commas
028 * inside function arguments ({@code count(a, b)}) or nested parens — the
029 * "nested paren handling" of S26.
030 *
031 * <h2>Style</h2>
032 *
033 * <p>Stacking only happens when {@link GFmtOpt#selectColumnlistStyle} is
034 * {@link TAlignStyle#AsStacked} (the default). {@link TAlignStyle#AsWrapped}
035 * (width-based wrapping) is deferred. Comma placement:
036 * <ul>
037 *   <li>{@code LfAfterComma} — break before the next item (comma trails the line);</li>
038 *   <li>{@code LfBeforeComma} — break before the comma (comma leads the next line);</li>
039 *   <li>{@code LfbeforeCommaWithSpace} — break before the comma, with a leading space.</li>
040 * </ul>
041 *
042 * <p>Priority {@link LayoutPriorities#JOIN_PAREN_COMMA}. Needs the S19 block,
043 * S20 sql, and S21 clause analyses attached to the context. Iterative; read-only.
044 *
045 * <p>Plan reference: §7.3/S26, §7.4/S26.
046 */
047public final class ParenAndCommaRules implements LayoutRule {
048
049    private static final Set<ClausePart> LIST_CLAUSES = EnumSet.of(
050        ClausePart.SELECT_LIST, ClausePart.GROUP_BY, ClausePart.ORDER_BY);
051
052    @Override
053    public int priority() { return LayoutPriorities.JOIN_PAREN_COMMA; }
054
055    @Override
056    public String name() { return "ParenAndCommaRules"; }
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
065        GFmtOpt opt = context.getOptions().toGFmtOpt();
066        if (opt.selectColumnlistStyle != TAlignStyle.AsStacked) return; // AsWrapped deferred
067        TLinefeedsCommaOption commaOpt = opt.selectColumnlistComma;
068
069        Pp2TokenStream stream = context.getStream();
070        int n = min(stream.size(), clause.size(), sql.size(), block.size());
071        for (int i = 0; i < n; i++) {
072            if (stream.get(i).getSourceToken().tokentype != ETokenType.ttcomma) continue;
073            if (!LIST_CLAUSES.contains(clause.partAt(i))) continue;
074            int runStart = clauseRunStart(clause, sql, i);
075            // Only stack a comma that is a top-level separator of its clause run,
076            // not one nested inside a function call / paren group.
077            if (block.depthAt(i) != block.depthAt(runStart)) continue;
078            applyComma(context, stream, i, commaOpt);
079        }
080    }
081
082    private static void applyComma(LayoutContext ctx, Pp2TokenStream stream, int comma,
083                                   TLinefeedsCommaOption commaOpt) {
084        if (commaOpt == TLinefeedsCommaOption.LfAfterComma) {
085            int next = nextSolid(stream, comma);
086            if (next >= 0) {
087                ctx.requestLinebreaksBefore(next, 1);
088                ctx.requestBlanksBefore(next, 0);
089            }
090        } else if (commaOpt == TLinefeedsCommaOption.LfBeforeComma) {
091            ctx.requestLinebreaksBefore(comma, 1);
092            ctx.requestBlanksBefore(comma, 0);
093        } else { // LfbeforeCommaWithSpace
094            ctx.requestLinebreaksBefore(comma, 1);
095            ctx.requestBlanksBefore(comma, 1);
096        }
097    }
098
099    /** First index of the contiguous run with the same clause part and SQL level as i. */
100    private static int clauseRunStart(ClauseScopeResult clause, SqlScopeResult sql, int i) {
101        ClausePart part = clause.partAt(i);
102        int level = sql.levelAt(i);
103        int s = i;
104        while (s - 1 >= 0 && clause.partAt(s - 1) == part && sql.levelAt(s - 1) == level) {
105            s--;
106        }
107        return s;
108    }
109
110    private static int nextSolid(Pp2TokenStream stream, int from) {
111        for (int j = from + 1; j < stream.size(); j++) {
112            ETokenType type = stream.get(j).getSourceToken().tokentype;
113            if (type == ETokenType.ttsimplecomment
114                || type == ETokenType.ttbracketedcomment
115                || type == ETokenType.ttCPPComment) {
116                continue;
117            }
118            return j;
119        }
120        return -1;
121    }
122
123    private static int min(int a, int b, int c, int d) {
124        return Math.min(Math.min(a, b), Math.min(c, d));
125    }
126}