001package gudusoft.gsqlparser.lineage.v2.producer;
002
003import java.util.Arrays;
004
005/**
006 * The decoded text of one source unit and the two coordinate systems that meet in it.
007 *
008 * <ul>
009 *   <li><b>GSP's lexer</b> numbers lines and columns in UTF-16 code units, and ends a line at LF, CR LF or a
010 *   lone CR ({@code TCustomLexer.readln}). Re-parsed text (routine bodies, dynamic SQL) has its token lines and
011 *   columns shifted into the parent text while {@code TSourceToken.offset} stays relative to the inner text, so
012 *   only the (line, column) pair is trusted: {@link #lexerOffset} turns it into a UTF-16 offset of this text.</li>
013 *   <li><b>SQL-LR positions</b> ({@code Position}) count lines from 1 with LF as the line end, columns in code
014 *   points from 1, and offsets in UTF-8 bytes from 0, as the reference validator checks them
015 *   ({@code conformance/run.py}, M1 {@code SemanticStage.SourceText}). {@link #position} gives that triple for a
016 *   UTF-16 offset.</li>
017 * </ul>
018 *
019 * <p>Immutable.</p>
020 *
021 * @since 4.2.17
022 */
023public final class SourceText {
024
025    /** A SQL-LR position. */
026    public static final class Position {
027        private final long line1;
028        private final long column1;
029        private final long utf8ByteOffset0;
030
031        Position(long line1, long column1, long utf8ByteOffset0) {
032            this.line1 = line1;
033            this.column1 = column1;
034            this.utf8ByteOffset0 = utf8ByteOffset0;
035        }
036
037        public long getLine1() {
038            return line1;
039        }
040
041        public long getColumn1() {
042            return column1;
043        }
044
045        public long getUtf8ByteOffset0() {
046            return utf8ByteOffset0;
047        }
048
049        @Override
050        public boolean equals(Object other) {
051            if (!(other instanceof Position)) {
052                return false;
053            }
054            Position p = (Position) other;
055            return line1 == p.line1 && column1 == p.column1 && utf8ByteOffset0 == p.utf8ByteOffset0;
056        }
057
058        @Override
059        public int hashCode() {
060            return (int) (line1 * 31 + column1) * 31 + (int) utf8ByteOffset0;
061        }
062
063        @Override
064        public String toString() {
065            return line1 + "," + column1 + "," + utf8ByteOffset0;
066        }
067    }
068
069    private final String text;
070    /** UTF-16 offset of the start of every line as GSP's lexer counts lines. */
071    private final int[] lexerLineStarts;
072    /** UTF-16 offset of the start of every SQL-LR line (LF only). */
073    private final int[] lineStarts;
074    /** UTF-8 byte offset before every UTF-16 offset; length text.length() + 1. */
075    private final int[] utf8Before;
076    /** Code points before every UTF-16 offset; length text.length() + 1. */
077    private final int[] codePointsBefore;
078
079    public SourceText(String text) {
080        if (text == null) {
081            throw new IllegalArgumentException("no text");
082        }
083        this.text = text;
084        int n = text.length();
085        int[] lexer = new int[16];
086        int[] sqllr = new int[16];
087        int lexerCount = 1;
088        int sqllrCount = 1;
089        utf8Before = new int[n + 1];
090        codePointsBefore = new int[n + 1];
091        int bytes = 0;
092        int points = 0;
093        for (int i = 0; i < n; i++) {
094            utf8Before[i] = bytes;
095            codePointsBefore[i] = points;
096            char c = text.charAt(i);
097            if (Character.isHighSurrogate(c) && i + 1 < n && Character.isLowSurrogate(text.charAt(i + 1))) {
098                bytes += 4;
099                points++;
100                utf8Before[i + 1] = bytes; // inside the pair: never a valid boundary, kept monotone
101                codePointsBefore[i + 1] = points;
102                i++;
103                continue;
104            }
105            if (Character.isSurrogate(c)) {
106                throw new IllegalArgumentException("text has an unpaired surrogate at UTF-16 offset " + i);
107            }
108            bytes += c < 0x80 ? 1 : c < 0x800 ? 2 : 3;
109            points++;
110            if (c == '\n') {
111                if (lexerCount == lexer.length) {
112                    lexer = Arrays.copyOf(lexer, lexerCount * 2);
113                }
114                lexer[lexerCount++] = i + 1;
115                if (sqllrCount == sqllr.length) {
116                    sqllr = Arrays.copyOf(sqllr, sqllrCount * 2);
117                }
118                sqllr[sqllrCount++] = i + 1;
119            } else if (c == '\r' && (i + 1 >= n || text.charAt(i + 1) != '\n')) {
120                if (lexerCount == lexer.length) {
121                    lexer = Arrays.copyOf(lexer, lexerCount * 2);
122                }
123                lexer[lexerCount++] = i + 1;
124            }
125        }
126        utf8Before[n] = bytes;
127        codePointsBefore[n] = points;
128        lexerLineStarts = Arrays.copyOf(lexer, lexerCount);
129        lineStarts = Arrays.copyOf(sqllr, sqllrCount);
130    }
131
132    public String getText() {
133        return text;
134    }
135
136    /** The UTF-8 length of the text. */
137    public int utf8Length() {
138        return utf8Before[text.length()];
139    }
140
141    /**
142     * The UTF-16 offset of a lexer (line, column), or -1 when the pair lies outside the text. Column {@code c}
143     * of a line is the offset {@code lineStart + c - 1}; a column past the end of its line is outside.
144     */
145    public int lexerOffset(long line1, long column1) {
146        if (line1 < 1 || line1 > lexerLineStarts.length || column1 < 1) {
147            return -1;
148        }
149        int start = lexerLineStarts[(int) line1 - 1];
150        int end = line1 < lexerLineStarts.length ? lexerLineStarts[(int) line1] : text.length();
151        if (column1 - 1 > end - start) {
152            return -1;
153        }
154        return start + (int) (column1 - 1);
155    }
156
157    /**
158     * The SQL-LR position of a UTF-16 offset, or {@code null} when the offset is outside the text or inside a
159     * surrogate pair.
160     */
161    public Position position(int utf16Offset) {
162        if (utf16Offset < 0 || utf16Offset > text.length()) {
163            return null;
164        }
165        if (utf16Offset > 0 && utf16Offset < text.length() && Character.isLowSurrogate(text.charAt(utf16Offset))
166                && Character.isHighSurrogate(text.charAt(utf16Offset - 1))) {
167            return null;
168        }
169        int low = 0;
170        int high = lineStarts.length - 1;
171        while (low < high) {
172            int mid = (low + high + 1) >>> 1;
173            if (lineStarts[mid] <= utf16Offset) {
174                low = mid;
175            } else {
176                high = mid - 1;
177            }
178        }
179        return new Position(low + 1, codePointsBefore[utf16Offset] - codePointsBefore[lineStarts[low]] + 1L,
180                utf8Before[utf16Offset]);
181    }
182
183    /** Whether {@code fragment} occurs in the text at {@code utf16Offset}. */
184    public boolean matches(int utf16Offset, String fragment) {
185        return fragment != null && utf16Offset >= 0 && text.regionMatches(utf16Offset, fragment, 0, fragment.length());
186    }
187}