001package gudusoft.gsqlparser.pp2.token;
002
003import gudusoft.gsqlparser.TSourceToken;
004
005import java.util.Collections;
006import java.util.EnumSet;
007import java.util.Set;
008
009/**
010 * Lightweight wrapper around a {@link TSourceToken}, owned by pp2.
011 *
012 * <p>Carries:
013 * <ul>
014 *   <li>A reference to the underlying {@link TSourceToken} — pp2 <b>never
015 *       mutates</b> it. This is the cache-safety invariant from plan §5.3.
016 *       Probe A7 in slice S1 confirmed {@code TGSqlParser} reuses the same
017 *       {@code TSourceTokenList} reference across tokenizations, which makes
018 *       this non-mutation contract mandatory. <b>Callers must build the pp2
019 *       token stream and copy any per-token data they need before reusing
020 *       the parser instance</b>; holding {@code Pp2Token} references across
021 *       a re-tokenization of the same parser will silently observe
022 *       post-reuse data.</li>
023 *   <li>A mutable {@link EnumSet} of {@link TokenRole}s — pp2's side-channel
024 *       annotations. Set during token-stream construction (S7), by
025 *       designated detector/annotator stages (S9 protected zones, S21
026 *       clause scopes, S33 AST overlay), and by layout/island stages
027 *       (S22 onward).</li>
028 *   <li>{@code precedingBlanks} — number of whitespace columns immediately
029 *       preceding this token in the source (e.g., 4 for {@code "    SELECT"}
030 *       at line start after the linebreak).</li>
031 *   <li>{@code precedingLinebreaks} — number of newline characters between
032 *       the previous solid token and this one.</li>
033 * </ul>
034 *
035 * <p>The {@code precedingBlanks} / {@code precedingLinebreaks} model
036 * mirrors the Delphi {@code initTokenArray} reconstruction property
037 * (plan §7.4 S7): a stream of {@code Pp2Token}s reproduces the original
038 * input byte-for-byte by concatenating
039 * {@code Σ(linebreaks + blanks + token.text)}.
040 *
041 * <p>Construction does not snapshot the wrapped token's text — it stores
042 * the reference and exposes {@link #getText()} which delegates to
043 * {@code TSourceToken.toString()}. Pp2 callers must not rely on
044 * {@code TSourceToken} field stability across tokenization reuse.
045 */
046public final class Pp2Token {
047
048    private final TSourceToken sourceToken;
049    private final EnumSet<TokenRole> roles;
050    private final int precedingBlanks;
051    private final int precedingLinebreaks;
052
053    /**
054     * Construct with no roles, zero preceding whitespace.
055     *
056     * @param sourceToken non-null reference to the underlying source token
057     */
058    public Pp2Token(TSourceToken sourceToken) {
059        this(sourceToken, 0, 0, EnumSet.noneOf(TokenRole.class));
060    }
061
062    /**
063     * Construct with the given roles and preceding whitespace counts.
064     *
065     * @param sourceToken non-null reference; never mutated
066     * @param precedingBlanks count of whitespace columns immediately before
067     *                        this token; must be {@code >= 0}
068     * @param precedingLinebreaks count of linebreaks immediately before this
069     *                            token; must be {@code >= 0}
070     * @param roles initial role set; copied into an {@link EnumSet} owned by
071     *              this token; may be {@code null} or empty
072     */
073    public Pp2Token(TSourceToken sourceToken,
074                    int precedingBlanks,
075                    int precedingLinebreaks,
076                    Set<TokenRole> roles) {
077        if (sourceToken == null) throw new NullPointerException("sourceToken");
078        if (precedingBlanks < 0) {
079            throw new IllegalArgumentException(
080                "precedingBlanks < 0: " + precedingBlanks);
081        }
082        if (precedingLinebreaks < 0) {
083            throw new IllegalArgumentException(
084                "precedingLinebreaks < 0: " + precedingLinebreaks);
085        }
086        this.sourceToken = sourceToken;
087        this.precedingBlanks = precedingBlanks;
088        this.precedingLinebreaks = precedingLinebreaks;
089        this.roles = (roles == null || roles.isEmpty())
090            ? EnumSet.noneOf(TokenRole.class)
091            : EnumSet.copyOf(roles);
092    }
093
094    /** Reference to the wrapped {@link TSourceToken}. Never mutated by pp2. */
095    public TSourceToken getSourceToken() {
096        return sourceToken;
097    }
098
099    /**
100     * Text of the wrapped token. Delegates to
101     * {@link TSourceToken#toString()} — pp2 never caches or modifies it.
102     */
103    public String getText() {
104        return sourceToken.toString();
105    }
106
107    /**
108     * Read-only view of the role set. Use {@link #addRole(TokenRole)} and
109     * {@link #removeRole(TokenRole)} to mutate; bulk replacement is not
110     * supported by design (the invariants live in the mutators).
111     */
112    public Set<TokenRole> getRoles() {
113        return Collections.unmodifiableSet(roles);
114    }
115
116    public boolean hasRole(TokenRole role) {
117        return roles.contains(role);
118    }
119
120    public void addRole(TokenRole role) {
121        if (role == null) throw new NullPointerException("role");
122        roles.add(role);
123    }
124
125    public void removeRole(TokenRole role) {
126        if (role == null) throw new NullPointerException("role");
127        roles.remove(role);
128    }
129
130    public int getPrecedingBlanks() {
131        return precedingBlanks;
132    }
133
134    public int getPrecedingLinebreaks() {
135        return precedingLinebreaks;
136    }
137
138    @Override
139    public String toString() {
140        return "Pp2Token{'" + getText() + "' roles=" + roles
141            + " blanks=" + precedingBlanks
142            + " breaks=" + precedingLinebreaks + "}";
143    }
144}