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