1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
package Torello.Java;

import Torello.Java.Verbosity;
import Torello.Java.IOExceptionHandler;
import Torello.Java.FileNode;
import Torello.Java.FileRW;
import Torello.Java.StrPrint;
import Torello.Java.StringParse;
import Torello.Java.Q;
import Torello.Java.RegExException;

import Torello.Java.Additional.AppendableSafe;
import Torello.Java.Additional.BiAppendable;

import static Torello.Java.C.BGREEN;
import static Torello.Java.C.BRED;
import static Torello.Java.C.RESET;

import java.util.regex.Pattern;
import java.util.regex.MatchResult;
import java.util.List;
import java.util.function.Function;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.Collectors;
import java.io.IOException;

class SingleLineRegExMatch
{
    // Re-Use the Pointer, I guess
    private static final String I4 = Helper.I4;

    // Only Method
    static List<FileNode> match(
            final Iterable<FileNode>            files,
            final Pattern                       regEx,
            final Function<MatchResult, String> replaceFunction,
            final boolean                       askFirst,
            final IOExceptionHandler            ioeh,
            final Appendable                    outputSaver,
            final boolean                       useUNIXColors,
            final Verbosity                     verbosity
        )
        throws IOException
    {
        Helper.CHECK(askFirst, verbosity);

        // This Stream-Builder contains the list of FileNode's that are modified.
        Stream.Builder<FileNode> ret = Stream.builder();

        Appendable appendable = (outputSaver == null)

            // If no 'outputSaver' was provided, then just send text to Standard-Out
            ? System.out

            // This just allows for printing to **BOTH** System.out **AND** the 'outputSaver'
            : new BiAppendable
                (System.out, new AppendableSafe(outputSaver, AppendableSafe.USE_APPENDABLE_ERROR));

        // A very short "Consumer" that really just prints the file-name, nothing more.
        Consumer<String> fileNamePrinter = Helper.getFileNamePrinter
            (appendable, useUNIXColors, verbosity.level);


        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
        // MAIN-LOOP: Iterate all the FileNode's
        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

        for (FileNode file : files)
        {
            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
            // Load the File, and Find any/all Matches that occur in the File using the Reg-Ex
            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

            String fileName     = file.getFullPathName();
            String fileAsStr    = null;

            // Print the file-name, if the verbosity level mandates that it be printed
            fileNamePrinter.accept(fileName);

            try
                { fileAsStr = FileRW.loadFileToString(fileName); }

            catch (IOException e)
            {
                if (ioeh != null)   ioeh.accept(file, e);
                else                throw e;

                // if ioeh ignores the exception rather than halting the program, then just continue
                // the loop on to the next match

                continue;
            }

            // Retrieve the starting String-index of each and every match in the file
            MatchResult[] matchResults = StringParse.getAllMatches(fileAsStr, regEx, true);

            // If there aren't any matches, then skip to the next file.
            if (matchResults.length == 0) continue;


            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
            // For-Loop: Print the Matches to System.out
            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

            // These are used (only in one place) at the very end of this loop, for printing only
            // They are "re-initialized" below.  The '-1' initialization is just to shut up the
            // Java-Compiler

            int prevPos = -1, prevLineNumber = -1;

            // This is a "Printing Stream" which saves PrintingRecSingleLine instances.  This
            // allows the spacing to properly computed before actually writing text-information to
            // System.out

            Stream.Builder<PrintingRecSingleLine> PRSLB = Stream.builder();

            // This is to ensure that the replaceFunction.apply(...) is invoked only once per
            // match.  It seems that calling it twice could, occasionally cause non-deterministic
            // results, based on what the user has coded in that function.

            Stream.Builder<String> replaceStrsBuilder = Stream.builder();

            // This is used for an minor optimization
            boolean firstLoopIteration = true;

            for (MatchResult matchResult : matchResults)
            {
                final int matchStartPos = matchResult.start();

                // The line of text within the Text-File containing the positon of the match
                final String line = StrPrint.line(fileAsStr, matchStartPos);

                // It is POSSIBLE for there to be more than one match on a single line
                // It is NOT POSSIBLE for there to be zero matches....

                // Retrieve the starting String-index of each and every match in the file
                MatchResult[] matchResultsLine = StringParse.getAllMatches(line, regEx, false);

                // The purpose of this Record is to save output-data into a record, so that the
                // spacing may be computed **BEFORE** printing to System.out.  This allows for 
                // making sure that all String-Matches are **EVENLY-SPACED** when printed to 
                // System.out

                final PrintingRecSingleLine saverRecord = new PrintingRecSingleLine();


                // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
                // Generate the Original Line... but with UNIX (Escape-Sequence) HiLiting-Colors
                // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

                StringBuilder   sb      = new StringBuilder();
                int             prev    = 0;

                // If the user has requested No UNIX-Colors, then this whole loop can just be
                // skipped, and the original line that was extracted may simply be printed to the
                // terminal, telling the user about the match, with no color codes.  See the
                // Else-Statement that is after this If-Branch

                if (useUNIXColors)
                {
                    // Iterate all matches on just one line of the input-text. 
                    for (MatchResult matchResultLine : matchResultsLine)
                    {
                        final String    matchStr    = matchResultLine.group();
                        final int       posLine     = matchResultLine.start();
        
                        // This is the best place to do the "Multi-Line" Check
                        if (matchStr.indexOf('\n') != -1) throw new RegExException(
                            "The Regular-Expression parameter 'regEx' has returned a Multi-Line " +
                            "String Match-Result (a Match-String that contains a newline '\n' " +
                            "character):\n" +
                            matchStr
                        );

                        sb
                            .append(line.substring(prev, posLine))  // Stuff that didn't match
                            .append(BRED)
                            .append(matchStr)                       // Text that did match
                            .append(RESET);

                        prev = matchResultLine.end();
                    }

                    // Last-Ending Unmatched-Stuff, Don't forget to append it!
                    sb.append(line.substring(prev));

                    // This is the exact same "Original Line" of text, but with UNIX Color-Codes
                    saverRecord.saveOriginalLine(sb.toString());

                    // Reset the StringBuilder & 'prev' for the next loop
                    sb.setLength(0);
                    prev = 0;
                }

                // **ELSE-IF** no UNIX-Colors are used to print the message to the user, then the
                // "Original-Line" ... well ... is still the same Original-Line.  So save that.

                else saverRecord.saveOriginalLine(line);


                // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
                // Generate the Updated / New Line - with Escape-Sequence Colors
                // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

                // This needs to be computed here.
                int newLineLength = 0;

                // Iterate all matches on just one line of the input-text. 
                for (MatchResult matchResultLine : matchResultsLine)
                {
                    final String    replaceStr  = replaceFunction.apply(matchResultLine);
                    final int       posLine     = matchResultLine.start();

                    // This is the best place to do the "Multi-Line" Check
                    if (replaceStr.indexOf('\n') != -1) throw new RegExException(
                        "The Function<MatchResult, String> parameter 'replaceFunction' has " +
                        "returned a Multi-Line Replacement-String (a replacement which contains " +
                        "a newline '\n' character):\n" +
                        replaceStr
                    );

                    // Save this for the end.  This ensures that 'replaceFunction.apply(...)'
                    // is only invoked once per match.

                    replaceStrsBuilder.accept(replaceStr);

                    sb
                        .append(line.substring(prev, posLine))  // Stuff that didn't match
                        .append(useUNIXColors ? BGREEN : "")
                        .append(replaceStr)                     // Text that did match (updated)
                        .append(useUNIXColors ? RESET : "");

                    newLineLength +=
                        (posLine - prev) +      // Part of Line Added that was a NON-MATCH
                        replaceStr.length();    // Part of Appended-Line that was MATCH-REPLACEMENT

                    // Make sure to update 'prev' only *AFTER* updating the new line length
                    prev = matchResultLine.end();
                }

                // Last-Ending Unmatched-Stuff, Don't forget to append it! (and include it's length)
                String lastEndingUnmatchedChunk = line.substring(prev);

                sb.append(lastEndingUnmatchedChunk);
                newLineLength += lastEndingUnmatchedChunk.length();

                // This is the exact same "Original Line" of text, but with UNIX Color-Codes
                saverRecord.saveNewLine(sb.toString());


                // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
                // Generate the "PrintingRecSingleLine" and Save it for Future Printing
                // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

                saverRecord.saveOriginalLineLength(line.length());
                saverRecord.saveNewLineLength(newLineLength);

                // Only on the first iteration should we compute the line number, after the first
                // loop iteration, use the "lineNumberSince" which uses the prev-loop computations
                // This is just for efficiency purposes only.

                final int lineNumber = firstLoopIteration
                    ? StrPrint.lineNumber(fileAsStr, matchStartPos)
                    : StrPrint.lineNumberSince(fileAsStr, matchStartPos, prevLineNumber, prevPos);

                saverRecord.saveLineNumber(lineNumber);

                firstLoopIteration = false;

                // Update these ... The previous line of code directly above is the only place
                // where these two are used.  They make it faster/easier to "find the line number"

                prevPos         = matchStartPos;
                prevLineNumber  = lineNumber;

                PRSLB.accept(saverRecord);
            }

            // All Printing-Records as an Array
            PrintingRecSingleLine[] recs = PRSLB.build().toArray(PrintingRecSingleLine[]::new);

            if (recs.length == 0) continue;

            // unless Verbosity.Silent was requested, print the matches.
            if (verbosity.level > 0)
                PrintingRecSingleLine.printAll(recs, useUNIXColors, appendable, verbosity);


            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
            // Query the User about his or her feelings
            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

            if (askFirst) if (! Q.YN("Re-Write the Updated File to Disk?")) continue;


            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
            // Build the Replacement
            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

            StringBuilder   newFileSB   = new StringBuilder();
            String[]        replaceStrs = replaceStrsBuilder.build().toArray(String[]::new);
            int             prev        = 0;

            // Iterate all matches on just one line of the input-text. 
            for (int i=0; i < matchResults.length; i++)
            {
                newFileSB
                    .append(fileAsStr.substring(prev, matchResults[i].start()))
                    .append(replaceStrs[i]);

                prev = matchResults[i].end();
            }

            // Last-Ending Unmatched-Stuff, Don't forget to append it!
            newFileSB.append(fileAsStr.substring(prev));


            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
            // Write the Replacement
            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

            try
                { FileRW.writeFile(newFileSB.toString(), fileName); }

            catch (IOException e)
            {
                if (ioeh != null)   ioeh.accept(file, e);
                else                throw e;

                // if ioeh ignores the exception rather than halting the program, then just continue
                // the loop on to the next match

                continue;
            }

            // In "Verbosity.Verbose" mode, tell the user how many changes were updated in the file
            if (verbosity.level == 3)
                appendable.append(I4 + "Updated " + recs.length + " Matches");

            // This is a file that was updated, so put it in the returned list of "updated files"
            ret.accept(file);
        }


        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
        // Finished, so return the list of modified FileNode's
        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

        return ret.build().collect(Collectors.toList());
    }
}