001package gudusoft.gsqlparser.pp2.token;
002
003import gudusoft.gsqlparser.TSourceToken;
004import gudusoft.gsqlparser.TSourceTokenList;
005
006import java.util.ArrayList;
007import java.util.Collections;
008import java.util.Iterator;
009import java.util.List;
010import java.util.NoSuchElementException;
011
012/**
013 * Immutable, indexed view of a {@link TSourceTokenList} as a sequence of
014 * {@link Pp2Token}s.
015 *
016 * <p>The constructor copies token <i>references</i> from the input
017 * {@code TSourceTokenList} into a private {@link ArrayList} — the source
018 * list is never mutated, and the resulting stream cannot be structurally
019 * mutated (no add/remove/clear). Per-token role bookkeeping on individual
020 * {@link Pp2Token}s remains writable because pp2 stages annotate roles
021 * incrementally (see {@code TokenRole}).
022 *
023 * <p>Two factory paths are supported:
024 * <ul>
025 *   <li>{@link #fromSourceTokenList(TSourceTokenList)} — basic wrap with
026 *       zero {@code precedingBlanks}/{@code precedingLinebreaks}. Used by
027 *       early Phase-1 callers and tests.</li>
028 *   <li>{@link #ofTokens(List)} — internal entry point used by
029 *       {@code Pp2TokenStreamBuilder} (S7), which folds whitespace into the
030 *       next solid token's preceding-whitespace counts before constructing
031 *       the stream.</li>
032 * </ul>
033 *
034 * <p>Probe A7 (slice S1) established that {@code TGSqlParser} reuses the
035 * same {@code TSourceTokenList} reference across calls. This stream's
036 * copy-on-construct contract is the reason that finding is not a footgun:
037 * the next re-tokenization cannot invalidate an already-built stream's
038 * <i>structure</i>.
039 *
040 * <h2>Two-tier stability contract</h2>
041 *
042 * <ul>
043 *   <li><b>Structural stability</b> (guaranteed): after a stream is built,
044 *       {@link #size()} and the per-index {@link Pp2Token} wrapper
045 *       references are stable. Re-tokenizing the source parser does not
046 *       grow, shrink, or re-shuffle the stream, and never replaces a
047 *       wrapper reference at a given index. {@link #toReadOnlyList()}
048 *       returns this stable structure.</li>
049 *   <li><b>Text stability</b> (NOT guaranteed): {@code Pp2Token.getText()}
050 *       delegates live to the wrapped {@code TSourceToken}. If the parser
051 *       re-tokenizes a different SQL and re-uses the underlying
052 *       {@code TSourceToken} fields, the live text returned by an existing
053 *       wrapper changes. Neither {@link #toReadOnlyList()} nor any other
054 *       method in this class snapshots text. Callers that must outlive a
055 *       parser re-tokenization MUST snapshot text into their own
056 *       {@code List<String>} before the second tokenize call (see
057 *       {@code Pp2TokenStreamTest#textSnapshotSurvivesParserReuse}).</li>
058 * </ul>
059 */
060public final class Pp2TokenStream implements Iterable<Pp2Token> {
061
062    private final List<Pp2Token> tokens;
063
064    /** Internal constructor. Use {@link #ofTokens(List)} or factories. */
065    private Pp2TokenStream(List<Pp2Token> tokens) {
066        // Defensive copy into a fresh ArrayList so the caller cannot
067        // structurally mutate the stream after construction.
068        this.tokens = new ArrayList<Pp2Token>(tokens);
069    }
070
071    /**
072     * Build a stream from a previously-prepared list of {@link Pp2Token}s.
073     * The supplied list is copied; subsequent caller mutations do not leak.
074     *
075     * @throws NullPointerException if {@code tokens} or any element is null
076     */
077    public static Pp2TokenStream ofTokens(List<Pp2Token> tokens) {
078        if (tokens == null) throw new NullPointerException("tokens");
079        for (int i = 0; i < tokens.size(); i++) {
080            if (tokens.get(i) == null) {
081                throw new NullPointerException("tokens[" + i + "]");
082            }
083        }
084        return new Pp2TokenStream(tokens);
085    }
086
087    /**
088     * Build a stream by wrapping each {@link TSourceToken} in the input
089     * list as a {@link Pp2Token} with zero preceding whitespace.
090     *
091     * <p>Smarter folding of whitespace into {@code precedingBlanks}/
092     * {@code precedingLinebreaks} is the job of
093     * {@code Pp2TokenStreamBuilder} (S7); this factory is the minimal
094     * Phase-1 wrap.
095     *
096     * <p>Contract: the input {@code TSourceTokenList} is not mutated. The
097     * stream holds the same {@code TSourceToken} references as the input,
098     * verifiable in tests by reference + text equality before vs after
099     * construction.
100     *
101     * <p>A null element in the source list is treated as a parser-side bug
102     * and triggers a {@link NullPointerException} — silently skipping would
103     * shift the per-index mapping and hide corruption.
104     *
105     * @throws NullPointerException if {@code source} is null or contains a
106     *     null element
107     */
108    public static Pp2TokenStream fromSourceTokenList(TSourceTokenList source) {
109        if (source == null) throw new NullPointerException("source");
110        List<Pp2Token> wrapped = new ArrayList<Pp2Token>(source.size());
111        for (int i = 0; i < source.size(); i++) {
112            TSourceToken t = source.get(i);
113            if (t == null) {
114                throw new NullPointerException("source[" + i + "] — "
115                    + "null entry in TSourceTokenList; pp2 treats this as "
116                    + "parser corruption rather than silently shrinking the stream");
117            }
118            wrapped.add(new Pp2Token(t));
119        }
120        return new Pp2TokenStream(wrapped);
121    }
122
123    public int size() {
124        return tokens.size();
125    }
126
127    public boolean isEmpty() {
128        return tokens.isEmpty();
129    }
130
131    /**
132     * @throws IndexOutOfBoundsException if {@code index} is out of range
133     */
134    public Pp2Token get(int index) {
135        return tokens.get(index);
136    }
137
138    /** Unmodifiable view of the underlying token list. */
139    public List<Pp2Token> toReadOnlyList() {
140        return Collections.unmodifiableList(tokens);
141    }
142
143    @Override
144    public Iterator<Pp2Token> iterator() {
145        return new Iterator<Pp2Token>() {
146            int idx = 0;
147            @Override public boolean hasNext() { return idx < tokens.size(); }
148            @Override public Pp2Token next() {
149                if (idx >= tokens.size()) throw new NoSuchElementException();
150                return tokens.get(idx++);
151            }
152            @Override public void remove() {
153                throw new UnsupportedOperationException("Pp2TokenStream is immutable");
154            }
155        };
156    }
157
158    @Override
159    public String toString() {
160        return "Pp2TokenStream{size=" + tokens.size() + "}";
161    }
162}