001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to you under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package gudusoft.gsqlparser.sqlenv.calcite;
018
019import gudusoft.gsqlparser.EDbVendor;
020import gudusoft.gsqlparser.ext.calcite.sqlnamematcher.SqlNameMatcher;
021import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
022import gudusoft.gsqlparser.sqlenv.IdentifierService;
023import gudusoft.gsqlparser.sqlenv.TSQLEnv;
024
025import java.util.*;
026
027/**
028 * Centralized name matching service using Apache Calcite's SqlNameMatcher framework.
029 *
030 * <p>This service provides vendor-aware and type-aware name comparison utilities,
031 * replacing scattered string comparisons throughout GSP with a unified approach.</p>
032 *
033 * <p>Key responsibilities:</p>
034 * <ul>
035 *   <li>Name equality checking (case-sensitive or insensitive per vendor/type)</li>
036 *   <li>Case-aware collection operations (indexOf, distinct, Set creation)</li>
037 *   <li>Bridge between GSP's normalization rules and Calcite's matching framework</li>
038 * </ul>
039 *
040 * <p>Design: This service delegates to {@link NamePolicyFactory} for matcher instances
041 * and preserves {@link TSQLEnv#normalizeIdentifier} as the authority for identifier
042 * quoting and default-case normalization.</p>
043 *
044 * @since 3.2.0.3 (Phase 1)
045 */
046public class NameService {
047
048    private final EDbVendor vendor;
049    private final NamePolicyFactory factory;
050
051    /**
052     * Creates a new NameService for the specified database vendor.
053     *
054     * @param vendor the database vendor
055     */
056    public NameService(EDbVendor vendor) {
057        if (vendor == null) {
058            throw new IllegalArgumentException("vendor cannot be null");
059        }
060        this.vendor = vendor;
061        this.factory = new NamePolicyFactory(vendor);
062    }
063
064    /**
065     * Returns the database vendor associated with this service.
066     *
067     * @return the database vendor
068     */
069    public EDbVendor getVendor() {
070        return vendor;
071    }
072
073    /**
074     * Checks if two names are equal according to vendor-specific and type-specific rules.
075     *
076     * <p>This method replaces {@code TSQLEnv.compareIdentifier()} with a more robust
077     * implementation backed by Calcite's SqlNameMatcher framework.</p>
078     *
079     * <p>Names are normalized using GSP's existing rules before matching.</p>
080     *
081     * @param type the SQL object type (table, column, function, etc.)
082     * @param name1 the first name to compare
083     * @param name2 the second name to compare
084     * @return true if the names match according to vendor/type rules
085     */
086    public boolean equals(ESQLDataObjectType type, String name1, String name2) {
087        if (name1 == null || name2 == null) {
088            return name1 == name2;
089        }
090
091        // Normalize identifiers using GSP's existing quoting/case rules
092        String normalized1 = IdentifierService.normalizeStatic(vendor, type, name1);
093        String normalized2 = IdentifierService.normalizeStatic(vendor, type, name2);
094
095        // Use Calcite matcher for case-aware comparison
096        SqlNameMatcher matcher = factory.getMatcherForType(type);
097        return matcher.matches(normalized1, normalized2);
098    }
099
100    /**
101     * Finds the index of a name in a collection, using vendor/type-aware matching.
102     *
103     * <p>This is a case-aware version of {@code List.indexOf()}.</p>
104     *
105     * @param type the SQL object type
106     * @param names the collection of names to search
107     * @param target the name to find
108     * @return the index of the first matching name, or -1 if not found
109     */
110    public int indexOf(ESQLDataObjectType type, Iterable<String> names, String target) {
111        if (names == null || target == null) {
112            return -1;
113        }
114
115        int index = 0;
116        for (String name : names) {
117            if (equals(type, name, target)) {
118                return index;
119            }
120            index++;
121        }
122        return -1;
123    }
124
125    /**
126     * Creates a distinct copy of a collection, removing duplicates using vendor/type-aware matching.
127     *
128     * <p>Preserves insertion order. For example, with case-insensitive matching:</p>
129     * <pre>
130     * ["foo", "FOO", "bar", "Bar"] → ["foo", "bar"]
131     * </pre>
132     *
133     * @param type the SQL object type
134     * @param names the collection of names (may contain duplicates)
135     * @return a new list with duplicates removed, preserving order
136     */
137    public List<String> distinctCopy(ESQLDataObjectType type, Iterable<String> names) {
138        if (names == null) {
139            return new ArrayList<>();
140        }
141
142        Set<String> seen = createSet(type);
143        List<String> result = new ArrayList<>();
144
145        for (String name : names) {
146            if (name != null) {
147                String normalized = IdentifierService.normalizeStatic(vendor, type, name);
148                if (seen.add(normalized)) {
149                    result.add(name); // Keep original name, not normalized
150                }
151            }
152        }
153
154        return result;
155    }
156
157    /**
158     * Creates a Set that uses vendor/type-aware name matching for equality.
159     *
160     * <p>The returned Set will treat names as equal if they match according to
161     * the vendor's case sensitivity rules for the specified object type.</p>
162     *
163     * <p>Implementation note: Uses a wrapper around LinkedHashSet that normalizes
164     * keys before insertion/lookup.</p>
165     *
166     * @param type the SQL object type
167     * @return a new Set with case-aware equality
168     */
169    public Set<String> createSet(ESQLDataObjectType type) {
170        return new NameSet(type);
171    }
172
173    /**
174     * Checks if a specific object type is case-sensitive for this vendor.
175     *
176     * <p>Convenience method that delegates to {@link NamePolicyFactory}.</p>
177     *
178     * @param type the object type
179     * @return true if case-sensitive, false otherwise
180     */
181    public boolean isCaseSensitive(ESQLDataObjectType type) {
182        return factory.isCaseSensitive(type);
183    }
184
185    /**
186     * Internal Set implementation that uses vendor/type-aware name matching.
187     *
188     * <p>This wrapper normalizes all keys before delegating to a standard LinkedHashSet,
189     * ensuring that name matching follows vendor-specific case sensitivity rules.</p>
190     */
191    private class NameSet extends AbstractSet<String> {
192        private final ESQLDataObjectType type;
193        private final Map<String, String> backingMap; // normalized -> original
194
195        NameSet(ESQLDataObjectType type) {
196            this.type = type;
197            this.backingMap = new LinkedHashMap<>();
198        }
199
200        @Override
201        public boolean add(String name) {
202            if (name == null) {
203                throw new NullPointerException("NameSet does not permit null elements");
204            }
205            String normalized = IdentifierService.normalizeStatic(vendor, type, name);
206            return backingMap.putIfAbsent(normalized, name) == null;
207        }
208
209        @Override
210        public boolean contains(Object o) {
211            if (!(o instanceof String)) {
212                return false;
213            }
214            String normalized = IdentifierService.normalizeStatic(vendor, type, (String) o);
215            return backingMap.containsKey(normalized);
216        }
217
218        @Override
219        public boolean remove(Object o) {
220            if (!(o instanceof String)) {
221                return false;
222            }
223            String normalized = IdentifierService.normalizeStatic(vendor, type, (String) o);
224            return backingMap.remove(normalized) != null;
225        }
226
227        @Override
228        public Iterator<String> iterator() {
229            return backingMap.values().iterator();
230        }
231
232        @Override
233        public int size() {
234            return backingMap.size();
235        }
236
237        @Override
238        public void clear() {
239            backingMap.clear();
240        }
241    }
242}