001package gudusoft.gsqlparser.pp2.layout;
002
003import gudusoft.gsqlparser.pp2.Pp2FormatOptions;
004import gudusoft.gsqlparser.pp2.dialect.DialectStrategy;
005import gudusoft.gsqlparser.pp2.island.BlockScopeDetector.BlockScopeResult;
006import gudusoft.gsqlparser.pp2.island.ClauseScopeAnnotator.ClauseScopeResult;
007import gudusoft.gsqlparser.pp2.island.IslandScope;
008import gudusoft.gsqlparser.pp2.island.MultiWordKeywordMerger.Match;
009import gudusoft.gsqlparser.pp2.island.SqlScopeDetector.SqlScopeResult;
010import gudusoft.gsqlparser.pp2.token.Pp2TokenStream;
011
012import java.util.Collections;
013import java.util.List;
014
015/**
016 * Shared, mutable state threaded through the {@link LayoutRulePipeline}. Bundles
017 * the token stream and the upstream analyses (S18 multi-word matches, S19 block
018 * scopes, S20 SQL scopes, S21 clause membership, S22 islands), and holds the
019 * evolving per-token {@link LayoutDecision}s.
020 *
021 * <h2>How rules write decisions</h2>
022 *
023 * <p>Rules never mutate {@link gudusoft.gsqlparser.pp2.token.Pp2Token} (its
024 * {@code precedingBlanks}/{@code precedingLinebreaks} are final). Instead they
025 * call {@link #requestLinebreaksBefore}, {@link #requestBlanksBefore}, and
026 * {@link #requestIndent} with the value they want for a token's leading
027 * whitespace. Each write is mediated by the {@link LayoutConflictResolver} using
028 * the active rule's priority, which {@link LayoutRulePipeline} sets before
029 * invoking each rule. Undecided properties stay {@link LayoutDecision#UNSET} so
030 * the output writer (S31) falls back to the original whitespace — making the
031 * zero-rule pipeline an identity pass.
032 *
033 * <p>Not thread-safe; one context per format call.
034 *
035 * <p>Plan reference: §7.3/S23, §7.4/S23.
036 */
037public final class LayoutContext {
038
039    private final Pp2TokenStream stream;
040    private final Pp2FormatOptions options;
041    private final LayoutConflictResolver resolver;
042
043    private final LayoutDecision[] decisions;
044    private final int[] lbPriority;
045    private final int[] blPriority;
046    private final int[] indPriority;
047    private final int[] txtPriority;
048
049    // Upstream analyses (nullable until populated by the renderer).
050    private List<Match> multiWordMatches = Collections.emptyList();
051    private BlockScopeResult blockScope;
052    private SqlScopeResult sqlScope;
053    private ClauseScopeResult clauseScope;
054    private List<IslandScope> islands = Collections.emptyList();
055    private DialectStrategy dialect;
056
057    // The priority of the rule currently applying; set by the pipeline.
058    private int activePriority = Integer.MIN_VALUE;
059
060    public LayoutContext(Pp2TokenStream stream, Pp2FormatOptions options) {
061        this(stream, options, new LayoutConflictResolver());
062    }
063
064    public LayoutContext(Pp2TokenStream stream, Pp2FormatOptions options,
065                         LayoutConflictResolver resolver) {
066        if (stream == null) throw new NullPointerException("stream");
067        if (options == null) throw new NullPointerException("options");
068        if (resolver == null) throw new NullPointerException("resolver");
069        this.stream = stream;
070        this.options = options;
071        this.resolver = resolver;
072        int n = stream.size();
073        this.decisions = new LayoutDecision[n];
074        this.lbPriority = new int[n];
075        this.blPriority = new int[n];
076        this.indPriority = new int[n];
077        this.txtPriority = new int[n];
078        for (int i = 0; i < n; i++) {
079            decisions[i] = new LayoutDecision();
080            lbPriority[i] = Integer.MIN_VALUE;
081            blPriority[i] = Integer.MIN_VALUE;
082            indPriority[i] = Integer.MIN_VALUE;
083            txtPriority[i] = Integer.MIN_VALUE;
084        }
085    }
086
087    // ---- core accessors -------------------------------------------------
088
089    public Pp2TokenStream getStream() { return stream; }
090    public Pp2FormatOptions getOptions() { return options; }
091    public int size() { return decisions.length; }
092
093    /**
094     * Read-only view of the (possibly still-default) layout decision for the
095     * token at {@code index}. Writes must go through the {@code request*}
096     * methods so the {@link LayoutConflictResolver} precedence is honoured.
097     */
098    public LayoutDecisionView decisionAt(int index) { return decisions[index]; }
099
100    // ---- analyses (fluent setters; nullable getters) --------------------
101
102    public LayoutContext withMultiWordMatches(List<Match> m) {
103        this.multiWordMatches = m == null ? Collections.<Match>emptyList() : m;
104        return this;
105    }
106    public LayoutContext withBlockScope(BlockScopeResult b) { this.blockScope = b; return this; }
107    public LayoutContext withSqlScope(SqlScopeResult s) { this.sqlScope = s; return this; }
108    public LayoutContext withClauseScope(ClauseScopeResult c) { this.clauseScope = c; return this; }
109    public LayoutContext withIslands(List<IslandScope> i) {
110        this.islands = i == null ? Collections.<IslandScope>emptyList() : i;
111        return this;
112    }
113    public LayoutContext withDialect(DialectStrategy d) { this.dialect = d; return this; }
114
115    public List<Match> getMultiWordMatches() { return multiWordMatches; }
116    public BlockScopeResult getBlockScope() { return blockScope; }
117    public SqlScopeResult getSqlScope() { return sqlScope; }
118    public ClauseScopeResult getClauseScope() { return clauseScope; }
119    public List<IslandScope> getIslands() { return islands; }
120    public DialectStrategy getDialect() { return dialect; }
121
122    // ---- pipeline plumbing ----------------------------------------------
123
124    /** Set by {@link LayoutRulePipeline} before each rule's {@code apply}. */
125    void setActivePriority(int priority) { this.activePriority = priority; }
126
127    // ---- rule write API (priority-mediated) -----------------------------
128
129    /** Request {@code count} linebreaks before the token at {@code index}. */
130    public void requestLinebreaksBefore(int index, int count) {
131        checkValue(count);
132        if (resolver.accept(activePriority, lbPriority[index])) {
133            decisions[index].setLinebreaksBefore(count);
134            lbPriority[index] = activePriority;
135        }
136    }
137
138    /** Request {@code count} blank columns before the token at {@code index}. */
139    public void requestBlanksBefore(int index, int count) {
140        checkValue(count);
141        if (resolver.accept(activePriority, blPriority[index])) {
142            decisions[index].setBlanksBefore(count);
143            blPriority[index] = activePriority;
144        }
145    }
146
147    /** Request indent {@code level} for the token at {@code index}. */
148    public void requestIndent(int index, int level) {
149        checkValue(level);
150        if (resolver.accept(activePriority, indPriority[index])) {
151            decisions[index].setIndentLevel(level);
152            indPriority[index] = activePriority;
153        }
154    }
155
156    /**
157     * Request that the token at {@code index} be emitted as {@code text} instead
158     * of its source text (e.g. keyword case modification). The wrapped
159     * {@code TSourceToken} is never changed; this is a render-time override.
160     */
161    public void requestText(int index, String text) {
162        if (text == null) throw new NullPointerException("text");
163        if (resolver.accept(activePriority, txtPriority[index])) {
164            decisions[index].setTextOverride(text);
165            txtPriority[index] = activePriority;
166        }
167    }
168
169    private static void checkValue(int v) {
170        if (v < 0) throw new IllegalArgumentException("value must be >= 0: " + v);
171    }
172}