001package gudusoft.gsqlparser.resolver2.format;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.nodes.TObjectName;
005import gudusoft.gsqlparser.nodes.TTable;
006
007/**
008 * Normalizes identifier names for display output.
009 *
010 * <p>This class provides display-focused normalization that is separate from
011 * the matching/equality normalization used during name resolution. The key
012 * principle is:</p>
013 *
014 * <ul>
015 *   <li><b>Matching normalization</b> (in {@code IdentifierService}): applies case folding
016 *       and other transformations to create a canonical key for comparison</li>
017 *   <li><b>Display normalization</b> (this class): strips delimiters but preserves
018 *       the original case as written by the user</li>
019 * </ul>
020 *
021 * <h3>Usage Example:</h3>
022 * <pre>
023 * DisplayNameNormalizer normalizer = new DisplayNameNormalizer(EDbVendor.dbvbigquery);
024 * normalizer.setMode(DisplayNameMode.DISPLAY);
025 *
026 * // Strip backticks, preserve case
027 * String display = normalizer.normalizeTableName("`kalyan-DB`.`test-schema`.`t1`");
028 * // Result: "kalyan-DB.test-schema.t1"
029 * </pre>
030 *
031 * @see DisplayNameMode
032 * @see DisplayNamePolicy
033 */
034public class DisplayNameNormalizer {
035
036    private final EDbVendor vendor;
037    private DisplayNameMode mode = DisplayNameMode.DISPLAY;
038    private boolean stripDelimiters = true;
039
040    /**
041     * Create a normalizer for the specified database vendor.
042     *
043     * @param vendor the database vendor (determines delimiter style)
044     */
045    public DisplayNameNormalizer(EDbVendor vendor) {
046        this.vendor = vendor != null ? vendor : EDbVendor.dbvansi;
047    }
048
049    /**
050     * Create a normalizer with default vendor (ANSI).
051     */
052    public DisplayNameNormalizer() {
053        this(EDbVendor.dbvansi);
054    }
055
056    /**
057     * Get the current display mode.
058     */
059    public DisplayNameMode getMode() {
060        return mode;
061    }
062
063    /**
064     * Set the display mode.
065     *
066     * @param mode the display mode
067     * @return this normalizer for chaining
068     */
069    public DisplayNameNormalizer setMode(DisplayNameMode mode) {
070        this.mode = mode != null ? mode : DisplayNameMode.DISPLAY;
071        return this;
072    }
073
074    /**
075     * Check if delimiters should be stripped.
076     */
077    public boolean isStripDelimiters() {
078        return stripDelimiters;
079    }
080
081    /**
082     * Set whether to strip delimiters.
083     *
084     * @param stripDelimiters true to strip delimiters
085     * @return this normalizer for chaining
086     */
087    public DisplayNameNormalizer setStripDelimiters(boolean stripDelimiters) {
088        this.stripDelimiters = stripDelimiters;
089        return this;
090    }
091
092    /**
093     * Normalize a table name for display.
094     *
095     * @param table the table object
096     * @return the normalized display name
097     */
098    public String normalizeTableName(TTable table) {
099        if (table == null) {
100            return null;
101        }
102
103        // Try to get the full name from the table
104        String fullName = table.getFullName();
105        if (fullName != null && !fullName.isEmpty()) {
106            return normalizeQualifiedName(fullName);
107        }
108
109        // Fallback to table name
110        String name = table.getName();
111        return name != null ? normalizeIdentifier(name) : null;
112    }
113
114    /**
115     * Normalize an object name (table or column) for display.
116     *
117     * @param objectName the object name node
118     * @return the normalized display name
119     */
120    public String normalizeObjectName(TObjectName objectName) {
121        if (objectName == null) {
122            return null;
123        }
124
125        // Use toString() which gives the full representation
126        String name = objectName.toString();
127        return normalizeQualifiedName(name);
128    }
129
130    /**
131     * Normalize a column name for display.
132     *
133     * @param columnName the column name (may include table prefix)
134     * @return the normalized display name
135     */
136    public String normalizeColumnName(String columnName) {
137        if (columnName == null || columnName.isEmpty()) {
138            return columnName;
139        }
140        return normalizeIdentifier(columnName);
141    }
142
143    /**
144     * Normalize a qualified name (e.g., schema.table.column) for display.
145     *
146     * <p>This method handles multi-part names by normalizing each segment
147     * separately while preserving the dot separators.</p>
148     *
149     * @param qualifiedName the qualified name
150     * @return the normalized display name
151     */
152    public String normalizeQualifiedName(String qualifiedName) {
153        if (qualifiedName == null || qualifiedName.isEmpty()) {
154            return qualifiedName;
155        }
156
157        if (mode == DisplayNameMode.SQL_RENDER) {
158            // In SQL_RENDER mode, preserve the original form
159            return qualifiedName;
160        }
161
162        // Split by dots, but be careful with quoted identifiers that contain dots
163        // For now, use a simple approach: normalize the whole string if it's a single identifier,
164        // or split and normalize each part
165
166        // Check if the entire name is a single quoted identifier (contains dot inside quotes)
167        if (isSingleQuotedIdentifier(qualifiedName)) {
168            return normalizeIdentifier(qualifiedName);
169        }
170
171        // Split by dots that are not inside quotes
172        String[] parts = splitQualifiedName(qualifiedName);
173        StringBuilder result = new StringBuilder();
174
175        for (int i = 0; i < parts.length; i++) {
176            if (i > 0) {
177                result.append(".");
178            }
179            result.append(normalizeIdentifier(parts[i]));
180        }
181
182        return result.toString();
183    }
184
185    /**
186     * Normalize a single identifier (without dots) for display.
187     *
188     * @param identifier the identifier
189     * @return the normalized identifier
190     */
191    public String normalizeIdentifier(String identifier) {
192        if (identifier == null || identifier.isEmpty()) {
193            return identifier;
194        }
195
196        switch (mode) {
197            case SQL_RENDER:
198                // Preserve original form including delimiters
199                return identifier;
200
201            case CANONICAL:
202                // Apply vendor-specific folding (delegate to IdentifierService if needed)
203                // For now, just strip delimiters - full canonical would need IdentifierService
204                return stripDelimiters ? stripAllDelimiters(identifier) : identifier;
205
206            case DISPLAY:
207            default:
208                // Strip delimiters, preserve case
209                return stripDelimiters ? stripAllDelimiters(identifier) : identifier;
210        }
211    }
212
213    /**
214     * Strip all SQL delimiters from an identifier.
215     *
216     * <p>Handles multiple delimiter styles:</p>
217     * <ul>
218     *   <li>Double quotes: {@code "name"} → {@code name}</li>
219     *   <li>Backticks: {@code `name`} → {@code name}</li>
220     *   <li>Square brackets: {@code [name]} → {@code name}</li>
221     *   <li>Single quotes (for some vendors): {@code 'name'} → {@code name}</li>
222     * </ul>
223     *
224     * @param identifier the identifier (may be quoted)
225     * @return the identifier without delimiters
226     */
227    public String stripAllDelimiters(String identifier) {
228        if (identifier == null || identifier.isEmpty()) {
229            return identifier;
230        }
231
232        String result = identifier;
233
234        // Strip double quotes
235        if (result.startsWith("\"") && result.endsWith("\"") && result.length() > 2) {
236            result = result.substring(1, result.length() - 1);
237            // Handle escaped double quotes inside
238            result = result.replace("\"\"", "\"");
239        }
240        // Strip backticks (MySQL, BigQuery, Databricks, Hive, Spark)
241        else if (result.startsWith("`") && result.endsWith("`") && result.length() > 2) {
242            result = result.substring(1, result.length() - 1);
243            // Handle escaped backticks inside
244            result = result.replace("``", "`");
245        }
246        // SQL Server square brackets - strip them for deduplication
247        // Previously kept for special names like [1], [2], [3] from PIVOT, but this causes
248        // duplicates when same column is referenced with and without brackets (e.g., [col3] vs col3)
249        else if (result.startsWith("[") && result.endsWith("]") && result.length() > 2) {
250            result = result.substring(1, result.length() - 1);
251            // Handle escaped brackets inside
252            result = result.replace("]]", "]");
253        }
254        // Strip single quotes (for string literals used as identifiers in some contexts)
255        else if (result.startsWith("'") && result.endsWith("'") && result.length() > 2) {
256            result = result.substring(1, result.length() - 1);
257            // Handle escaped single quotes inside
258            result = result.replace("''", "'");
259        }
260
261        return result;
262    }
263
264    /**
265     * Check if the name is a single quoted identifier that may contain dots.
266     * A single quoted identifier has matching delimiters at start/end AND
267     * no unquoted dots inside (which would indicate a multi-part name).
268     *
269     * @param name the name to check
270     * @return true if it's a single quoted identifier
271     */
272    private boolean isSingleQuotedIdentifier(String name) {
273        if (name == null || name.length() < 3) {
274            return false;
275        }
276
277        char first = name.charAt(0);
278        char last = name.charAt(name.length() - 1);
279        char expectedClose;
280
281        // Check for matching delimiters
282        if (first == '"' && last == '"') {
283            expectedClose = '"';
284        } else if (first == '`' && last == '`') {
285            expectedClose = '`';
286        } else if (first == '[' && last == ']') {
287            expectedClose = ']';
288        } else {
289            return false;
290        }
291
292        // Check if there are any unquoted dots inside
293        // If so, this is a multi-part identifier, not a single quoted one
294        boolean inQuote = true;  // We start inside the first quote
295        for (int i = 1; i < name.length() - 1; i++) {
296            char c = name.charAt(i);
297            if (c == expectedClose) {
298                inQuote = !inQuote;
299            } else if (c == '.' && !inQuote) {
300                // Found an unquoted dot - this is multi-part
301                return false;
302            }
303        }
304
305        return true;
306    }
307
308    /**
309     * Split a qualified name by dots, respecting quoted identifiers.
310     *
311     * @param qualifiedName the qualified name
312     * @return array of parts
313     */
314    private String[] splitQualifiedName(String qualifiedName) {
315        if (qualifiedName == null || qualifiedName.isEmpty()) {
316            return new String[0];
317        }
318
319        java.util.List<String> parts = new java.util.ArrayList<>();
320        StringBuilder current = new StringBuilder();
321        boolean inDoubleQuote = false;
322        boolean inBacktick = false;
323        boolean inBracket = false;
324
325        for (int i = 0; i < qualifiedName.length(); i++) {
326            char c = qualifiedName.charAt(i);
327
328            // Track quote state
329            if (c == '"' && !inBacktick && !inBracket) {
330                inDoubleQuote = !inDoubleQuote;
331                current.append(c);
332            } else if (c == '`' && !inDoubleQuote && !inBracket) {
333                inBacktick = !inBacktick;
334                current.append(c);
335            } else if (c == '[' && !inDoubleQuote && !inBacktick && !inBracket) {
336                inBracket = true;
337                current.append(c);
338            } else if (c == ']' && inBracket) {
339                inBracket = false;
340                current.append(c);
341            } else if (c == '.' && !inDoubleQuote && !inBacktick && !inBracket) {
342                // Split point
343                parts.add(current.toString());
344                current = new StringBuilder();
345            } else {
346                current.append(c);
347            }
348        }
349
350        // Add the last part
351        if (current.length() > 0) {
352            parts.add(current.toString());
353        }
354
355        return parts.toArray(new String[0]);
356    }
357
358    /**
359     * Get a display name for a table, handling various TTable configurations.
360     *
361     * @param table the table
362     * @return the display name
363     */
364    public String getTableDisplayName(TTable table) {
365        if (table == null) {
366            return null;
367        }
368
369        // Build the full qualified name from parts if available
370        StringBuilder sb = new StringBuilder();
371
372        // Server/catalog
373        String server = table.getPrefixServer();
374        if (server != null && !server.isEmpty()) {
375            sb.append(normalizeIdentifier(server));
376            sb.append(".");
377        }
378
379        // Database
380        String database = table.getPrefixDatabase();
381        if (database != null && !database.isEmpty()) {
382            sb.append(normalizeIdentifier(database));
383            sb.append(".");
384        }
385
386        // Schema
387        String schema = table.getPrefixSchema();
388        if (schema != null && !schema.isEmpty()) {
389            sb.append(normalizeIdentifier(schema));
390            sb.append(".");
391        }
392
393        // Table name
394        String tableName = table.getName();
395        if (tableName != null && !tableName.isEmpty()) {
396            sb.append(normalizeIdentifier(tableName));
397        }
398
399        String result = sb.toString();
400
401        // If we got nothing useful, fall back to getFullName
402        if (result.isEmpty() || result.equals(".")) {
403            String fullName = table.getFullName();
404            if (fullName != null) {
405                return normalizeQualifiedName(fullName);
406            }
407        }
408
409        return result.isEmpty() ? null : result;
410    }
411
412    @Override
413    public String toString() {
414        return String.format("DisplayNameNormalizer{vendor=%s, mode=%s, stripDelimiters=%s}",
415                vendor, mode, stripDelimiters);
416    }
417}