001package gudusoft.gsqlparser.pp2;
002
003import gudusoft.gsqlparser.pp.para.GFmtOpt;
004import gudusoft.gsqlparser.pp.para.GFmtOptFactory;
005import gudusoft.gsqlparser.pp2.zone.CommentPolicy;
006
007import java.lang.reflect.Field;
008import java.lang.reflect.Modifier;
009import java.util.ArrayList;
010import java.util.List;
011
012/**
013 * Configuration for the pp2 fault-tolerant SQL formatter.
014 *
015 * <p>Composes — does not extend — {@link GFmtOpt}. Composition is deliberate
016 * (plan §5.5): inheritance would couple pp2's option surface to every future
017 * change to {@code GFmtOpt} and force {@code protected} shims. The wrapped
018 * {@code GFmtOpt} carries all of pp's options (case rules, alignment, indent,
019 * etc.); the pp2-specific fields below extend that surface with the knobs
020 * pp2's region/recovery/fallback pipeline needs.
021 *
022 * <p>The {@link #from(GFmtOpt)} factory <b>copies field values by reflection</b>
023 * into a freshly-allocated {@code GFmtOpt} so that:
024 * <ul>
025 *   <li>pp2 tuning never leaks back to the caller's {@code GFmtOpt}, and</li>
026 *   <li>each {@code Pp2FormatOptions} carries its own {@code sessionId} so
027 *       pp's session-keyed caches in {@code FormatterFactory},
028 *       {@code ProcessorFactory}, and {@code MediatorFactory} are not poisoned
029 *       by pp2's invocations.</li>
030 * </ul>
031 *
032 * <p>The reflection copier is risk R17's mitigation: when {@code GFmtOpt} gains
033 * a new public instance field, the copier picks it up automatically. The S2
034 * test {@code Pp2FormatOptionsTest} verifies field-by-field equality after
035 * {@code from()}.
036 *
037 * <h2>Field semantics</h2>
038 *
039 * <ul>
040 *   <li><b>{@link #tolerantMode}</b> ({@code true} by default) — reserved for
041 *       a future strict-mode switch. The current engine does not read this
042 *       field and always recovers ordinary parse failures.</li>
043 *   <li><b>{@link #maxLineWidth}</b> (default 120) — reserved soft-wrap target.
044 *       The current pp2 renderers do not read this field.</li>
045 *   <li><b>{@link #errorRegionStrategy}</b> (default {@link ErrorRegionStrategy#PRESERVE}) —
046 *       reserved strategy selector for lexical {@code ERROR_REGION}s. The
047 *       current engine does not read this field.</li>
048 *   <li><b>{@link #maxErrorRegionSize}</b> (default 10000 chars) — caps the
049 *       token span the island recognizer annotates as an {@code ERROR_REGION}.
050 *       The current layout pipeline records that boundary but does not use it
051 *       to select a different renderer.</li>
052 *   <li><b>{@link #maxRegionParseChars}</b> (default 200000 chars) — safety
053 *       valve: regions whose source span exceeds this length skip per-region
054 *       parsing entirely and go straight to the fallback renderer.</li>
055 *   <li><b>{@link #commentPolicy}</b> (default {@link CommentPolicy#PRESERVE}) —
056 *       preserves comments across region boundaries. The alternative
057 *       re-anchor/reflow policies remain experimental and are not fully
058 *       implemented by the region assembler.</li>
059 *   <li><b>{@link #showIndentMarkers}</b> (default {@code false}) — reserved
060 *       diagnostic option. The current pp2 renderers do not read this field.</li>
061 *   <li><b>{@link #astOverlayEnabled}</b> (default {@code false}) — feature flag
062 *       for the v3 AST overlay annotator. Off in v2; flipping it on in v2
063 *       activates the annotator scaffold from slice S33 but does not change
064 *       rendering output.</li>
065 * </ul>
066 *
067 * <p>Mutability matches {@code GFmtOpt}'s convention: fields are public and
068 * mutable so existing callers can tune options ergonomically. Concurrent
069 * mutation while a {@code Pp2Formatter} call is in flight is undefined.
070 */
071public class Pp2FormatOptions {
072
073    /** Reserved strategies for rendering parse-failed lexical regions. */
074    public enum ErrorRegionStrategy {
075        /** Planned raw-source preservation strategy. Default reserved value. */
076        PRESERVE,
077        /** Planned light token-spacing strategy. */
078        LIGHT_FORMAT,
079        /** Planned full lexical-island strategy. */
080        BEST_EFFORT
081    }
082
083    // ---- pp options surface (composed, not inherited) -------------------
084
085    private final GFmtOpt gfmtOpt;
086
087    // ---- pp2 knobs ------------------------------------------------------
088
089    /** Reserved strict-mode switch; the current engine always recovers parse failures. */
090    public boolean tolerantMode = true;
091
092    /** Reserved soft line-wrap target; currently has no effect. */
093    public int maxLineWidth = 120;
094
095    /** Reserved error-region strategy selector; currently has no effect. */
096    public ErrorRegionStrategy errorRegionStrategy = ErrorRegionStrategy.PRESERVE;
097
098    /** Maximum character span annotated as an ERROR_REGION by island recognition. */
099    public int maxErrorRegionSize = 10000;
100
101    /** Regions larger than this skip per-region parsing entirely. */
102    public int maxRegionParseChars = 200000;
103
104    /** Comment policy; PRESERVE is implemented, alternatives remain experimental. */
105    public CommentPolicy commentPolicy = CommentPolicy.PRESERVE;
106
107    /** Reserved indent-marker option; currently has no effect. */
108    public boolean showIndentMarkers = false;
109
110    /** Feature flag for the v3 AST overlay annotator. */
111    public boolean astOverlayEnabled = false;
112
113    /**
114     * Whether a parseable input is handed straight to {@code pp}.
115     *
116     * <p>When {@code true} (the default) and the whole input parses, pp2 first
117     * formats the whole document with {@code FormatterFactory.pp} using these
118     * same options, and returns <b>that text unchanged</b> as an {@code OK}
119     * result provided it is token-equivalent to the input. Only when the
120     * whole-document parse fails, or pp throws, or pp's output drops or
121     * rewrites a token, does the regional pipeline run.
122     *
123     * <p>This is what makes pp2 byte-identical to pp on parseable SQL: not an
124     * imitation of pp's layout rules, but pp itself. Set it to {@code false}
125     * only to exercise the regional pipeline directly (the regional pipeline
126     * reparses statement ranges independently, and its reassembled text was
127     * not byte-identical to whole-document pp on 60 of 1000 parseable corpus
128     * inputs — which is why this path exists).
129     */
130    public boolean wholeDocumentFastPath = true;
131
132    /**
133     * When {@code true} (default), consecutive top-level statements that share
134     * a physical line in the source (separated only by horizontal whitespace,
135     * with no line break) are placed on their own lines in the output. When
136     * {@code false}, the original inter-statement whitespace is preserved
137     * verbatim — the legacy behaviour, which could leave several statements
138     * running together on one line. Gaps that already contain a line break, or
139     * that contain comments / non-whitespace trivia, are always preserved
140     * verbatim regardless of this flag.
141     */
142    public boolean breakStatementsOnNewLine = true;
143
144    /**
145     * Transformations of tokens or comments that the caller asked for and that
146     * pp2's content guards must therefore accept, in their exact expected form
147     * (see {@link gudusoft.gsqlparser.pp2.token.TransformPolicy}). Default:
148     * none, the plain content check.
149     */
150    public gudusoft.gsqlparser.pp2.token.TransformPolicy transformPolicy =
151        gudusoft.gsqlparser.pp2.token.TransformPolicy.NONE;
152
153    // ---- construction ---------------------------------------------------
154
155    /**
156     * Internal constructor used by {@link #defaults()} and {@link #from(GFmtOpt)}.
157     * Both factories supply a freshly-allocated {@code GFmtOpt}.
158     *
159     * @throws NullPointerException if {@code gfmtOpt} is null
160     */
161    Pp2FormatOptions(GFmtOpt gfmtOpt) {
162        if (gfmtOpt == null) {
163            throw new NullPointerException("gfmtOpt");
164        }
165        this.gfmtOpt = gfmtOpt;
166    }
167
168    /**
169     * Construct with a fresh {@code GFmtOpt} carrying default values. The
170     * underlying {@code GFmtOpt} is allocated via
171     * {@link GFmtOptFactory#newInstance()} so it gets a unique
172     * {@code sessionId} (used by pp's {@code FormatterFactory} caches).
173     */
174    public static Pp2FormatOptions defaults() {
175        return new Pp2FormatOptions(GFmtOptFactory.newInstance());
176    }
177
178    /**
179     * Construct from an existing {@code GFmtOpt} by <b>copying its public
180     * instance fields</b> into a freshly-allocated {@code GFmtOpt}. The
181     * caller's instance is not retained; subsequent mutations to it are
182     * invisible to pp2, and pp2's mutations are invisible to the caller.
183     *
184     * <p>The {@code sessionId} field is {@code final} on {@code GFmtOpt}, so
185     * the copy carries the new fresh {@code sessionId} from
186     * {@link GFmtOptFactory#newInstance()} — not the source's.
187     *
188     * @param gfmtOpt must not be {@code null}
189     * @throws NullPointerException if {@code gfmtOpt} is null
190     */
191    /**
192     * Wrap an existing {@code GFmtOpt} <b>without copying</b>: pp2 formats
193     * with this very instance and its {@code sessionId}. Use it when the
194     * options must stay registered in {@link GFmtOptFactory} under that id,
195     * because parts of pp re-read their options by session id; the caller
196     * owns the instance and releases it ({@link GFmtOptFactory#remove}).
197     *
198     * @param gfmtOpt must not be {@code null}
199     * @throws NullPointerException if {@code gfmtOpt} is null
200     */
201    public static Pp2FormatOptions wrap(GFmtOpt gfmtOpt) {
202        return new Pp2FormatOptions(gfmtOpt);
203    }
204
205    public static Pp2FormatOptions from(GFmtOpt gfmtOpt) {
206        if (gfmtOpt == null) {
207            throw new NullPointerException("gfmtOpt");
208        }
209        GFmtOpt copy = GFmtOptFactory.newInstance();
210        copyPublicFields(gfmtOpt, copy);
211        return new Pp2FormatOptions(copy);
212    }
213
214    /**
215     * Reflection-based field copier. Walks every public, non-static,
216     * non-{@code final} field on {@link GFmtOpt} and copies its value from
217     * {@code src} to {@code dst}. Skipping {@code final} fields means
218     * {@code sessionId} (which is {@code final}) is preserved from {@code dst}.
219     *
220     * <p>This is the R17 mitigation: a new public field on {@code GFmtOpt} is
221     * picked up automatically. The S2 test verifies field-by-field equality.
222     */
223    private static void copyPublicFields(GFmtOpt src, GFmtOpt dst) {
224        List<String> skipped = null;
225        for (Field f : GFmtOpt.class.getFields()) {
226            int mod = f.getModifiers();
227            if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) continue;
228            try {
229                f.set(dst, f.get(src));
230            } catch (IllegalAccessException e) {
231                // Should not happen for public fields, but record any holes
232                // rather than swallow them silently.
233                if (skipped == null) skipped = new ArrayList<String>();
234                skipped.add(f.getName());
235            }
236        }
237        if (skipped != null && !skipped.isEmpty()) {
238            throw new IllegalStateException(
239                "Pp2FormatOptions.from(): could not copy GFmtOpt fields: " + skipped);
240        }
241    }
242
243    // ---- accessors ------------------------------------------------------
244
245    /**
246     * Return the wrapped {@code GFmtOpt}. pp2 hands this directly to
247     * {@code FormatterFactory.pp()} when the
248     * {@code gudusoft.gsqlparser.pp2.engine.Pp2Engine} (S16) dispatches a
249     * parseable region to the AST delegate.
250     */
251    public GFmtOpt toGFmtOpt() {
252        return gfmtOpt;
253    }
254}