001package gudusoft.gsqlparser.pp2.layout.rules;
002
003import gudusoft.gsqlparser.ETokenType;
004import gudusoft.gsqlparser.pp2.layout.LayoutContext;
005import gudusoft.gsqlparser.pp2.layout.LayoutPriorities;
006import gudusoft.gsqlparser.pp2.layout.LayoutRule;
007import gudusoft.gsqlparser.pp2.token.Pp2Token;
008import gudusoft.gsqlparser.pp2.token.Pp2TokenStream;
009
010/**
011 * The base spacing rule: requests the number of blank columns before each token
012 * so adjacent tokens are separated by a single space, with the usual tight
013 * exceptions around parentheses, commas, periods, and semicolons.
014 *
015 * <h2>Spacing model (per adjacent token pair)</h2>
016 *
017 * <ul>
018 *   <li>0 after an open paren {@code (} and before a close paren {@code )};</li>
019 *   <li>0 before a comma or semicolon;</li>
020 *   <li>0 around a period (qualified names {@code a.b});</li>
021 *   <li>0 before an open paren {@code (} (function calls and groups stay tight —
022 *       keyword-introduced groups are re-spaced by the higher-priority S26 rule);</li>
023 *   <li>1 otherwise (operators, keywords, identifiers, {@code *}).</li>
024 * </ul>
025 *
026 * <p>This is the lowest-priority layout rule ({@link LayoutPriorities#SPACING}),
027 * so later rules (clause linebreaks, indent, alignment) override its decisions
028 * where they apply. It only sets {@code blanksBefore}; linebreaks/indent are
029 * other rules' concern. Spacing around comment tokens is left to the original
030 * whitespace (untouched) to avoid disturbing comment placement.
031 *
032 * <p>Iterative single pass; read-only over tokens. Plan reference: §7.3/S24, §7.4/S24.
033 */
034public final class SpacingRules implements LayoutRule {
035
036    @Override
037    public int priority() { return LayoutPriorities.SPACING; }
038
039    @Override
040    public String name() { return "SpacingRules"; }
041
042    @Override
043    public void apply(LayoutContext context) {
044        Pp2TokenStream stream = context.getStream();
045        int n = stream.size();
046        for (int i = 1; i < n; i++) {
047            Pp2Token prev = stream.get(i - 1);
048            Pp2Token cur = stream.get(i);
049            ETokenType prevType = prev.getSourceToken().tokentype;
050            ETokenType curType = cur.getSourceToken().tokentype;
051            // Always keep at least one space adjacent to a comment so a comment
052            // can never glue to a token (e.g. SELECT/*c*/a -> SELECT /*c*/ a).
053            // Linebreaks around line comments are preserved separately (this rule
054            // does not touch linebreaksBefore).
055            if (isComment(prevType) || isComment(curType)) {
056                context.requestBlanksBefore(i, 1);
057                continue;
058            }
059            // A unary sign ( -1, +col, =-1, (-1) ) stays tight to its operand.
060            if (isSign(prev) && isUnaryPosition(stream, i - 1)) {
061                context.requestBlanksBefore(i, 0);
062                continue;
063            }
064            context.requestBlanksBefore(i, blanksBetween(prevType, curType));
065        }
066    }
067
068    /** Whether the token is a {@code +} or {@code -} sign. */
069    private static boolean isSign(Pp2Token t) {
070        String text = t.getText();
071        return "+".equals(text) || "-".equals(text);
072    }
073
074    /**
075     * Whether the sign token at {@code signIndex} is in a unary position: it is
076     * the first token, or the token before it starts an expression (an open
077     * paren, comma, keyword, or another operator) rather than ending an operand.
078     */
079    private static boolean isUnaryPosition(Pp2TokenStream stream, int signIndex) {
080        if (signIndex == 0) return true;
081        Pp2Token before = stream.get(signIndex - 1);
082        ETokenType type = before.getSourceToken().tokentype;
083        if (type == ETokenType.ttleftparenthesis
084            || type == ETokenType.ttcomma
085            || type == ETokenType.ttkeyword) {
086            return true;
087        }
088        return isOperator(before);
089    }
090
091    /** Heuristic operator detection by token type or symbol text. */
092    private static boolean isOperator(Pp2Token t) {
093        if (t.getSourceToken().tokentype == ETokenType.ttsinglecharoperator) return true;
094        String s = t.getText();
095        if (s == null || s.isEmpty()) return false;
096        return "=".equals(s) || "<".equals(s) || ">".equals(s) || "<=".equals(s)
097            || ">=".equals(s) || "<>".equals(s) || "!=".equals(s) || "+".equals(s)
098            || "-".equals(s) || "*".equals(s) || "/".equals(s) || "%".equals(s)
099            || "||".equals(s);
100    }
101
102    private static int blanksBetween(ETokenType prev, ETokenType cur) {
103        if (prev == ETokenType.ttleftparenthesis) return 0;   // no space after "("
104        if (cur == ETokenType.ttrightparenthesis) return 0;   // no space before ")"
105        if (cur == ETokenType.ttcomma) return 0;              // no space before ","
106        if (cur == ETokenType.ttsemicolon) return 0;          // no space before ";"
107        if (cur == ETokenType.ttperiod || prev == ETokenType.ttperiod) return 0; // a.b
108        if (cur == ETokenType.ttleftparenthesis) return 0;    // f( / group( stays tight
109        return 1;
110    }
111
112    private static boolean isComment(ETokenType type) {
113        return type == ETokenType.ttsimplecomment
114            || type == ETokenType.ttbracketedcomment
115            || type == ETokenType.ttCPPComment;
116    }
117}