001package gudusoft.gsqlparser.resolver2.model;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.List;
006
007/**
008 * Represents a field path for deep/record field access in structured types.
009 *
010 * <p>In BigQuery, Snowflake, and other databases with STRUCT/RECORD types,
011 * column references can include field paths like {@code customer.address.city}.
012 * This class captures the field path portion (segments beyond the base column).</p>
013 *
014 * <p>Design principles:</p>
015 * <ul>
016 *   <li>Immutable - once created, cannot be modified</li>
017 *   <li>Only stores segments beyond the base column (e.g., for {@code customer.address.city},
018 *       if base column is {@code customer}, fieldPath stores {@code ["address", "city"]})</li>
019 *   <li>Empty fieldPath means no field access (regular column reference)</li>
020 * </ul>
021 *
022 * <p>Example:</p>
023 * <pre>
024 * -- SQL: SELECT customer.address.city FROM orders
025 * -- base column: customer
026 * -- fieldPath: ["address", "city"]
027 * </pre>
028 *
029 * @see ColumnSource#getFieldPath()
030 */
031public class FieldPath {
032
033    /** Empty field path singleton for regular column references */
034    public static final FieldPath EMPTY = new FieldPath(Collections.emptyList());
035
036    /** The field path segments (beyond the base column) */
037    private final List<String> segments;
038
039    /**
040     * Create a FieldPath from a list of segments.
041     *
042     * @param segments The field path segments (will be copied)
043     */
044    public FieldPath(List<String> segments) {
045        if (segments == null || segments.isEmpty()) {
046            this.segments = Collections.emptyList();
047        } else {
048            this.segments = Collections.unmodifiableList(new ArrayList<>(segments));
049        }
050    }
051
052    /**
053     * Create a FieldPath from a single segment.
054     *
055     * @param segment The single field segment
056     * @return A new FieldPath with one segment
057     */
058    public static FieldPath of(String segment) {
059        if (segment == null || segment.isEmpty()) {
060            return EMPTY;
061        }
062        return new FieldPath(Collections.singletonList(segment));
063    }
064
065    /**
066     * Create a FieldPath from multiple segments.
067     *
068     * @param segments The field segments
069     * @return A new FieldPath
070     */
071    public static FieldPath of(String... segments) {
072        if (segments == null || segments.length == 0) {
073            return EMPTY;
074        }
075        List<String> list = new ArrayList<>(segments.length);
076        for (String s : segments) {
077            if (s != null && !s.isEmpty()) {
078                list.add(s);
079            }
080        }
081        return list.isEmpty() ? EMPTY : new FieldPath(list);
082    }
083
084    /**
085     * Create a FieldPath from a list of segments.
086     *
087     * @param segments The field segments
088     * @return A new FieldPath, or EMPTY if null/empty
089     */
090    public static FieldPath of(List<String> segments) {
091        if (segments == null || segments.isEmpty()) {
092            return EMPTY;
093        }
094        return new FieldPath(segments);
095    }
096
097    /**
098     * Get the field path segments.
099     *
100     * @return Unmodifiable list of segments
101     */
102    public List<String> getSegments() {
103        return segments;
104    }
105
106    /**
107     * Get the number of segments in this field path.
108     *
109     * @return Number of segments
110     */
111    public int size() {
112        return segments.size();
113    }
114
115    /**
116     * Check if this field path is empty (no field access).
117     *
118     * @return true if empty
119     */
120    public boolean isEmpty() {
121        return segments.isEmpty();
122    }
123
124    /**
125     * Get the first segment (immediate field).
126     *
127     * @return First segment, or null if empty
128     */
129    public String getFirst() {
130        return segments.isEmpty() ? null : segments.get(0);
131    }
132
133    /**
134     * Get the last segment (deepest field).
135     *
136     * @return Last segment, or null if empty
137     */
138    public String getLast() {
139        return segments.isEmpty() ? null : segments.get(segments.size() - 1);
140    }
141
142    /**
143     * Get a segment by index.
144     *
145     * @param index The index
146     * @return The segment at that index
147     * @throws IndexOutOfBoundsException if index is invalid
148     */
149    public String get(int index) {
150        return segments.get(index);
151    }
152
153    /**
154     * Create a new FieldPath with an additional segment appended.
155     *
156     * @param segment The segment to append
157     * @return New FieldPath with the segment appended
158     */
159    public FieldPath append(String segment) {
160        if (segment == null || segment.isEmpty()) {
161            return this;
162        }
163        List<String> newSegments = new ArrayList<>(segments.size() + 1);
164        newSegments.addAll(segments);
165        newSegments.add(segment);
166        return new FieldPath(newSegments);
167    }
168
169    /**
170     * Create a new FieldPath with additional segments appended.
171     *
172     * @param other The FieldPath to append
173     * @return New FieldPath with all segments
174     */
175    public FieldPath append(FieldPath other) {
176        if (other == null || other.isEmpty()) {
177            return this;
178        }
179        if (this.isEmpty()) {
180            return other;
181        }
182        List<String> newSegments = new ArrayList<>(segments.size() + other.segments.size());
183        newSegments.addAll(segments);
184        newSegments.addAll(other.segments);
185        return new FieldPath(newSegments);
186    }
187
188    /**
189     * Create a sub-path starting from the given index.
190     *
191     * @param fromIndex Start index (inclusive)
192     * @return New FieldPath with segments from fromIndex
193     */
194    public FieldPath subPath(int fromIndex) {
195        if (fromIndex >= segments.size()) {
196            return EMPTY;
197        }
198        if (fromIndex <= 0) {
199            return this;
200        }
201        return new FieldPath(segments.subList(fromIndex, segments.size()));
202    }
203
204    /**
205     * Convert to dot-separated string representation.
206     *
207     * @return String like "address.city", or empty string if empty
208     */
209    @Override
210    public String toString() {
211        if (segments.isEmpty()) {
212            return "";
213        }
214        StringBuilder sb = new StringBuilder();
215        for (int i = 0; i < segments.size(); i++) {
216            if (i > 0) {
217                sb.append('.');
218            }
219            sb.append(segments.get(i));
220        }
221        return sb.toString();
222    }
223
224    /**
225     * Convert to full reference string with base column.
226     *
227     * @param baseColumn The base column name
228     * @return String like "customer.address.city"
229     */
230    public String toFullReference(String baseColumn) {
231        if (segments.isEmpty()) {
232            return baseColumn != null ? baseColumn : "";
233        }
234        StringBuilder sb = new StringBuilder();
235        if (baseColumn != null && !baseColumn.isEmpty()) {
236            sb.append(baseColumn);
237            sb.append('.');
238        }
239        sb.append(toString());
240        return sb.toString();
241    }
242
243    @Override
244    public boolean equals(Object o) {
245        if (this == o) return true;
246        if (o == null || getClass() != o.getClass()) return false;
247        FieldPath fieldPath = (FieldPath) o;
248        return segments.equals(fieldPath.segments);
249    }
250
251    @Override
252    public int hashCode() {
253        return segments.hashCode();
254    }
255}