001package gudusoft.gsqlparser.pp2.layout.rules;
002
003import gudusoft.gsqlparser.ETokenType;
004import gudusoft.gsqlparser.pp.para.GFmtOpt;
005import gudusoft.gsqlparser.pp.para.styleenums.TCaseOption;
006import gudusoft.gsqlparser.pp2.layout.LayoutContext;
007import gudusoft.gsqlparser.pp2.layout.LayoutPriorities;
008import gudusoft.gsqlparser.pp2.layout.LayoutRule;
009import gudusoft.gsqlparser.pp2.token.Pp2Token;
010import gudusoft.gsqlparser.pp2.token.Pp2TokenStream;
011
012import java.util.Locale;
013
014/**
015 * Applies keyword / identifier / function-name case modification, reusing the
016 * existing {@link GFmtOpt#caseKeywords}, {@link GFmtOpt#caseIdentifier}, and
017 * {@link GFmtOpt#caseFuncname} options.
018 *
019 * <p>This is a <i>text</i> rule, not a whitespace rule: it requests a per-token
020 * render-time text override via {@code LayoutContext.requestText}. The wrapped
021 * {@link gudusoft.gsqlparser.TSourceToken} is never mutated.
022 *
023 * <ul>
024 *   <li>{@code ttkeyword} → {@code caseKeywords} (default upper-case).</li>
025 *   <li>{@code ttidentifier} immediately followed by {@code (} → a function name
026 *       → {@code caseFuncname}; otherwise an identifier → {@code caseIdentifier}.</li>
027 * </ul>
028 *
029 * <p>Quoted identifiers, string/numeric literals, comments, and punctuation are
030 * never re-cased — only bare keywords and identifiers. Priority
031 * {@link LayoutPriorities#CASE_MODIFICATION} (highest; text is orthogonal to
032 * whitespace). Iterative; read-only over tokens.
033 *
034 * <p>Plan reference: §7.3/S29, §7.4/S29.
035 */
036public final class CaseModificationRules implements LayoutRule {
037
038    @Override
039    public int priority() { return LayoutPriorities.CASE_MODIFICATION; }
040
041    @Override
042    public String name() { return "CaseModificationRules"; }
043
044    @Override
045    public void apply(LayoutContext context) {
046        GFmtOpt opt = context.getOptions().toGFmtOpt();
047        Pp2TokenStream stream = context.getStream();
048        int n = stream.size();
049        for (int i = 0; i < n; i++) {
050            Pp2Token t = stream.get(i);
051            ETokenType type = t.getSourceToken().tokentype;
052            String text = t.getText();
053            if (text == null || text.isEmpty()) continue;
054
055            TCaseOption option;
056            if (type == ETokenType.ttkeyword) {
057                option = opt.caseKeywords;
058            } else if (type == ETokenType.ttidentifier) {
059                // A delimited identifier ("X" / [X] / `X`) is case-sensitive and
060                // must never be re-cased, even if tokenised as ttidentifier.
061                if (isDelimitedIdentifier(text)) continue;
062                option = isFunctionName(stream, i) ? opt.caseFuncname : opt.caseIdentifier;
063            } else {
064                continue; // literals, quoted ids, punctuation, comments: never re-cased
065            }
066            if (option == null || option == TCaseOption.CoNoChange) continue;
067            String recased = applyCase(text, option);
068            if (!recased.equals(text)) {
069                context.requestText(i, recased);
070            }
071        }
072    }
073
074    /** True if the identifier text begins with a quote/bracket/backtick delimiter. */
075    private static boolean isDelimitedIdentifier(String text) {
076        char c = text.charAt(0);
077        return c == '"' || c == '[' || c == '`';
078    }
079
080    /** An identifier is a function name when its next solid token is an open paren. */
081    private static boolean isFunctionName(Pp2TokenStream stream, int i) {
082        for (int j = i + 1; j < stream.size(); j++) {
083            ETokenType type = stream.get(j).getSourceToken().tokentype;
084            if (type == ETokenType.ttsimplecomment
085                || type == ETokenType.ttbracketedcomment
086                || type == ETokenType.ttCPPComment) {
087                continue;
088            }
089            return type == ETokenType.ttleftparenthesis;
090        }
091        return false;
092    }
093
094    private static String applyCase(String text, TCaseOption option) {
095        switch (option) {
096            case CoUppercase: return text.toUpperCase(Locale.ROOT);
097            case CoLowercase: return text.toLowerCase(Locale.ROOT);
098            case CoInitCap:   return initCap(text);
099            case CoNoChange:
100            default:          return text;
101        }
102    }
103
104    /** Upper-case the first letter, lower-case the rest (per word is overkill for a single token). */
105    private static String initCap(String text) {
106        if (text.isEmpty()) return text;
107        return Character.toUpperCase(text.charAt(0))
108            + text.substring(1).toLowerCase(Locale.ROOT);
109    }
110}