001package gudusoft.gsqlparser.pp2.render;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.pp.logger.PPLogger;
005import gudusoft.gsqlparser.pp2.Pp2FormatOptions;
006import gudusoft.gsqlparser.pp2.RendererId;
007import gudusoft.gsqlparser.pp2.dialect.DialectRegistry;
008import gudusoft.gsqlparser.pp2.island.BlockScopeDetector;
009import gudusoft.gsqlparser.pp2.island.ClauseScopeAnnotator;
010import gudusoft.gsqlparser.pp2.island.ClauseScopeAnnotator.ClauseScopeResult;
011import gudusoft.gsqlparser.pp2.island.IslandRecognizer;
012import gudusoft.gsqlparser.pp2.island.SqlScopeDetector;
013import gudusoft.gsqlparser.pp2.island.SqlScopeDetector.SqlScopeResult;
014import gudusoft.gsqlparser.pp2.layout.LayoutContext;
015import gudusoft.gsqlparser.pp2.layout.LayoutRulePipeline;
016import gudusoft.gsqlparser.pp2.layout.rules.AlignmentRules;
017import gudusoft.gsqlparser.pp2.layout.rules.AndOrConcatRules;
018import gudusoft.gsqlparser.pp2.layout.rules.CaseModificationRules;
019import gudusoft.gsqlparser.pp2.layout.rules.CaseRules;
020import gudusoft.gsqlparser.pp2.layout.rules.ClauseLinebreakRules;
021import gudusoft.gsqlparser.pp2.layout.rules.IndentRules;
022import gudusoft.gsqlparser.pp2.layout.rules.JoinRules;
023import gudusoft.gsqlparser.pp2.layout.rules.ParenAndCommaRules;
024import gudusoft.gsqlparser.pp2.layout.rules.SetOperatorRules;
025import gudusoft.gsqlparser.pp2.layout.rules.SpacingRules;
026import gudusoft.gsqlparser.pp2.region.RegionParseOutcome;
027import gudusoft.gsqlparser.pp2.region.StatementRange;
028import gudusoft.gsqlparser.pp2.token.Pp2Token;
029import gudusoft.gsqlparser.pp2.token.Pp2TokenStream;
030import gudusoft.gsqlparser.pp2.token.TokenEquivalence;
031
032import java.util.ArrayList;
033import java.util.List;
034
035/**
036 * The full lexical island renderer: runs the S18-S29 analysis + layout pipeline
037 * over a region's tokens and emits the formatted text via {@link Pp2OutputWriter}.
038 *
039 * <h2>Pipeline</h2>
040 *
041 * <ol>
042 *   <li>Slice a region-local {@link Pp2TokenStream} from the region's token range.</li>
043 *   <li>Run S20 SQL scope, S19 block scope, S21 clause annotation, and S22 island
044 *       recognition over the slice.</li>
045 *   <li>Build a {@link LayoutContext} (with those analyses + the vendor's
046 *       {@link gudusoft.gsqlparser.pp2.dialect.DialectStrategy}) and run the full
047 *       {@link LayoutRulePipeline}: spacing, clause/set-operator linebreaks,
048 *       JOIN/paren/comma, CASE/AND-OR, indent/alignment, and case modification.</li>
049 *   <li>Render with {@link Pp2OutputWriter}.</li>
050 * </ol>
051 *
052 * <h2>Fall-through contract</h2>
053 *
054 * <p>Per {@link RegionRenderer}, this returns {@code null} (so the engine falls
055 * back to the conservative renderer) on any {@link Throwable}, or if a
056 * {@link TokenEquivalence} content guard detects that the formatted output lost
057 * a solid token. Content preservation and never-throw are thus upheld even if a
058 * layout rule misbehaves — the conservative renderer remains the safety net.
059 *
060 * <p>Plan reference: §5.2, §7.3/S31, §7.4/S31, §10.4.
061 */
062public final class LexicalIslandRenderer implements RegionRenderer {
063
064    private final EDbVendor vendor;
065    private final LayoutRulePipeline pipeline;
066    private final Pp2OutputWriter writer = new Pp2OutputWriter();
067    private final SqlScopeDetector sqlScope = new SqlScopeDetector();
068    private final BlockScopeDetector blockScope = new BlockScopeDetector();
069    private final ClauseScopeAnnotator clauseScope = new ClauseScopeAnnotator();
070    private final IslandRecognizer islands = new IslandRecognizer();
071
072    private long successCount;
073    private long fallthroughCount;
074
075    public LexicalIslandRenderer(EDbVendor vendor) {
076        if (vendor == null) throw new NullPointerException("vendor");
077        this.vendor = vendor;
078        // Stateless rules, registered low-to-high priority (the conflict
079        // resolver enforces precedence regardless, but this reads naturally).
080        this.pipeline = new LayoutRulePipeline()
081            .register(new SpacingRules())
082            .register(new ClauseLinebreakRules())
083            .register(new SetOperatorRules())
084            .register(new JoinRules())
085            .register(new ParenAndCommaRules())
086            .register(new CaseRules())
087            .register(new AndOrConcatRules())
088            .register(new IndentRules())
089            .register(new AlignmentRules())
090            .register(new CaseModificationRules());
091    }
092
093    public long getSuccessCount() { return successCount; }
094    public long getFallthroughCount() { return fallthroughCount; }
095
096    @Override
097    public RendererId id() { return RendererId.LEXICAL_ISLAND; }
098
099    @Override
100    public String render(RegionParseOutcome outcome, Pp2TokenStream stream,
101                         Pp2FormatOptions opts) {
102        if (outcome == null) throw new NullPointerException("outcome");
103        if (opts == null) throw new NullPointerException("opts");
104        try {
105            return renderInternal(outcome, stream, opts);
106        } catch (Throwable t) {
107            fallthroughCount++;
108            PPLogger.error(t);
109            PPLogger.info("LexicalIslandRenderer: threw; falling through to "
110                + "conservative renderer. range=" + outcome.getRange());
111            return null;
112        }
113    }
114
115    private String renderInternal(RegionParseOutcome outcome, Pp2TokenStream stream,
116                                  Pp2FormatOptions opts) {
117        StatementRange range = outcome.getRange();
118        int start = range.getStartTokenIndex();
119        int end = Math.min(range.getEndTokenIndex(), stream.size());
120        if (start >= end) {
121            // A collapsed range is only a valid empty render when the region's
122            // source is genuinely empty; otherwise fall through so the
123            // conservative renderer preserves the region's content.
124            String original = outcome.getParsedSql();
125            return (original == null || original.trim().isEmpty()) ? "" : null;
126        }
127
128        // Copy the region's tokens (same TSourceToken reference, copied roles +
129        // whitespace) into fresh Pp2Token wrappers, so the S19-S22 analyses and
130        // layout rules annotate these copies — never the engine's shared token
131        // objects. This keeps island annotations from leaking into the
132        // conservative fallback (on a guard failure) or into other regions.
133        List<Pp2Token> source = stream.toReadOnlyList();
134        List<Pp2Token> regionTokens = new ArrayList<Pp2Token>(end - start);
135        for (int i = start; i < end; i++) {
136            Pp2Token o = source.get(i);
137            regionTokens.add(new Pp2Token(o.getSourceToken(),
138                o.getPrecedingBlanks(), o.getPrecedingLinebreaks(), o.getRoles()));
139        }
140        Pp2TokenStream sub = Pp2TokenStream.ofTokens(regionTokens);
141
142        SqlScopeResult sql = sqlScope.detect(sub);
143        ClauseScopeResult clause = clauseScope.annotate(sub, sql);
144        LayoutContext ctx = new LayoutContext(sub, opts)
145            .withSqlScope(sql)
146            .withBlockScope(blockScope.detect(sub))
147            .withClauseScope(clause)
148            .withIslands(islands.recognize(sub, sql, clause, opts))
149            .withDialect(DialectRegistry.forVendor(vendor));
150        pipeline.run(ctx);
151        String output = writer.write(ctx);
152
153        // Content guard: never let layout drop a solid token. On failure the
154        // engine falls back to the content-preserving conservative renderer.
155        String original = outcome.getParsedSql();
156        if (original != null
157            && !TokenEquivalence.equalsModuloFormatting(original, output, opts, vendor, true)) {
158            fallthroughCount++;
159            PPLogger.info("LexicalIslandRenderer: content guard failed; falling "
160                + "through to conservative renderer. range=" + range);
161            return null;
162        }
163        successCount++;
164        return output;
165    }
166}