001/*
002 * ============================================================================
003 * Copyright © 2015 Square, Inc.
004 * Copyright for the modifications © 2018-2026 by Thomas Thrien.
005 * ============================================================================
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 * http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019
020package org.tquadrat.foundation.javacomposer.internal;
021
022import static java.lang.Math.min;
023import static java.lang.String.format;
024import static java.util.stream.Collectors.toList;
025import static org.apiguardian.api.API.Status.INTERNAL;
026import static org.apiguardian.api.API.Status.STABLE;
027import static org.tquadrat.foundation.javacomposer.internal.Util.NULL_REFERENCE;
028import static org.tquadrat.foundation.javacomposer.internal.Util.createDebugOutput;
029import static org.tquadrat.foundation.lang.Objects.checkState;
030import static org.tquadrat.foundation.lang.Objects.isNull;
031import static org.tquadrat.foundation.lang.Objects.nonNull;
032import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
033import static org.tquadrat.foundation.lang.Objects.requireValidArgument;
034import static org.tquadrat.foundation.lang.Objects.requireValidNonNullArgument;
035import static org.tquadrat.foundation.util.StringUtils.isNotEmpty;
036import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank;
037
038import javax.lang.model.element.Element;
039import javax.lang.model.type.TypeMirror;
040import java.io.UncheckedIOException;
041import java.lang.reflect.Type;
042import java.util.ArrayList;
043import java.util.Arrays;
044import java.util.Collection;
045import java.util.List;
046import java.util.Map;
047import java.util.Set;
048import java.util.TreeSet;
049import java.util.regex.Matcher;
050import java.util.regex.Pattern;
051import java.util.regex.PatternSyntaxException;
052import java.util.stream.Collector;
053import java.util.stream.IntStream;
054import java.util.stream.Stream;
055
056import org.apiguardian.api.API;
057import org.tquadrat.foundation.annotation.ClassVersion;
058import org.tquadrat.foundation.exception.UnlikelyExceptionError;
059import org.tquadrat.foundation.exception.ValidationException;
060import org.tquadrat.foundation.javacomposer.ClassName;
061import org.tquadrat.foundation.javacomposer.CodeBlock;
062import org.tquadrat.foundation.javacomposer.FieldSpec;
063import org.tquadrat.foundation.javacomposer.JavaComposer;
064import org.tquadrat.foundation.javacomposer.MethodSpec;
065import org.tquadrat.foundation.javacomposer.ParameterSpec;
066import org.tquadrat.foundation.javacomposer.TypeSpec;
067import org.tquadrat.foundation.lang.Lazy;
068import org.tquadrat.foundation.lang.Objects;
069
070/**
071 *  The implementation of
072 *  {@link CodeBlock}
073 *  for a fragment of a {@code *.java} file.
074 *
075 *  @author Square,Inc.
076 *  @modified Thomas Thrien - thomas.thrien@tquadrat.org
077 *  @version $Id: CodeBlockImpl.java 1258 2026-06-04 18:33:06Z tquadrat $
078 *  @since 0.0.5
079 *
080 *  @UMLGraph.link
081 */
082@ClassVersion( sourceVersion = "$Id: CodeBlockImpl.java 1258 2026-06-04 18:33:06Z tquadrat $" )
083@API( status = INTERNAL, since = "0.0.5" )
084public final class CodeBlockImpl implements CodeBlock
085{
086        /*---------------*\
087    ====** Inner Classes **====================================================
088        \*---------------*/
089    /**
090     *  The implementation of
091     *  {@link org.tquadrat.foundation.javacomposer.CodeBlock.Builder}
092     *  as the builder for a new
093     *  {@link CodeBlockImpl}
094     *  instance.
095     *
096     *  @author Square,Inc.
097     *  @modified Thomas Thrien - thomas.thrien@tquadrat.org
098     *  @version $Id: CodeBlockImpl.java 1258 2026-06-04 18:33:06Z tquadrat $
099     *  @since 0.0.5
100     *
101     *  @UMLGraph.link
102     */
103    @ClassVersion( sourceVersion = "$Id: CodeBlockImpl.java 1258 2026-06-04 18:33:06Z tquadrat $" )
104    @API( status = INTERNAL, since = "0.0.5" )
105    public static final class BuilderImpl implements CodeBlock.Builder
106    {
107            /*------------*\
108        ====** Attributes **===================================================
109            \*------------*/
110        /**
111         *  The arguments.
112         */
113        private final Collection<Object> m_Args = new ArrayList<>();
114
115        /**
116         *  The reference to the factory.
117         */
118        @SuppressWarnings( "UseOfConcreteClass" )
119        private final JavaComposer m_Composer;
120
121        /**
122         *  The format Strings.
123         */
124        private final List<String> m_FormatParts = new ArrayList<>();
125
126        /**
127         *  The static imports.
128         */
129        private final Collection<String> m_StaticImports = new TreeSet<>();
130
131            /*--------------*\
132        ====** Constructors **=================================================
133            \*--------------*/
134        /**
135         *  <p>{@summary {@constructor}}</p>
136         *
137         *  @param  composer    The reference to the factory that created this
138         *      builder instance.
139         */
140        public BuilderImpl( @SuppressWarnings( "UseOfConcreteClass" ) final JavaComposer composer )
141        {
142            m_Composer = requireNonNullArgument( composer, "composer" );
143        }   //  BuilderImpl()
144
145        /**
146         *  <p>{@summary {@constructor}}</p>
147         *
148         *  @param  composer    The reference to the factory that created this
149         *      builder instance.
150         *  @param  formatParts The format parts.
151         *  @param  args    The arguments.
152         */
153        public BuilderImpl( @SuppressWarnings( "UseOfConcreteClass" ) final JavaComposer composer, final List<String> formatParts, final List<Object> args )
154        {
155            this( composer );
156            m_FormatParts.addAll( requireNonNullArgument( formatParts, "formatParts" ) );
157            m_Args.addAll( requireNonNullArgument( args, "args" ) );
158        }   //  BuilderImpl()
159
160            /*---------*\
161        ====** Methods **======================================================
162            \*---------*/
163        /**
164         *  {@inheritDoc}
165         */
166        @Override
167        public final BuilderImpl add( final CodeBlock codeBlock )
168        {
169            addDebug();
170            final var retValue = addWithoutDebugInfo( codeBlock );
171
172            //---* Done *------------------------------------------------------
173            return retValue;
174        }   //  add()
175
176        /**
177         *  {@inheritDoc}
178         */
179        @API( status = INTERNAL, since = "0.2.0" )
180        @Override
181        public final BuilderImpl add( final String format, final Object... args )
182        {
183            addDebug();
184
185            final var retValue = addWithoutDebugInfo( format, args );
186
187            //---* Done *----------------------------------------------------------
188            return retValue;
189        }   //  add()
190
191        /**
192         *  Adds the placeholder's argument.
193         *
194         *  @param  format  The format.
195         *  @param  placeholder The placeholder character.
196         *  @param  arg     The argument.
197         */
198        private final void addArgument( final String format, final char placeholder, final Object arg )
199        {
200            final var argument = switch( placeholder )
201            {
202                case 'N' -> argToName( arg );
203                case 'L' -> argToLiteral( arg );
204                case 'S' -> argToString( arg );
205                case 'T' -> argToType( arg );
206                default -> throw new IllegalArgumentException( format( "invalid format string: '%s'", format ) );
207            };
208            m_Args.add( argument );
209        }   //  addArgument()
210
211        /**
212         *  Adds debug output.
213         */
214        private final void addDebug()
215        {
216            createDebugOutput( m_Composer.addDebugOutput() )
217                .ifPresent( v -> m_FormatParts.add( v.asComment() ) );
218        }   //  addDebug()
219
220        /**
221         *  {@inheritDoc}
222         */
223        @API( status = INTERNAL, since = "0.2.0" )
224        @Override
225        public final BuilderImpl addNamed( final String format, final Map<String,?> args )
226        {
227            addDebug();
228
229            for( final var argument : requireNonNullArgument( args, "args" ).keySet() )
230            {
231                checkState( LOWERCASE.matcher( argument ).matches(), () -> new ValidationException( "argument '%s' must start with a lowercase character".formatted( argument ) ) );
232            }
233            if( isNotEmpty( requireNonNullArgument( format, "format" ) ) )
234            {
235                var currentPos = 0;
236                ParseLoop: while( currentPos < format.length() )
237                {
238                    final var nextPos = format.indexOf( "$", currentPos );
239                    if( nextPos == -1 )
240                    {
241                        m_FormatParts.add( format.substring( currentPos ) );
242                        break ParseLoop;
243                    }
244
245                    if( currentPos != nextPos )
246                    {
247                        m_FormatParts.add( format.substring( currentPos, nextPos ) );
248                        currentPos = nextPos;
249                    }
250
251                    Matcher matcher = null;
252                    final var colon = format.indexOf( ':', currentPos );
253                    if( colon != -1 )
254                    {
255                        final var endIndex = min( colon + 2, format.length() );
256                        matcher = NAMED_ARGUMENT.matcher( format.substring( currentPos, endIndex ) );
257                    }
258                    if( nonNull( matcher ) && matcher.lookingAt() )
259                    {
260                        final var argumentName = matcher.group( "argumentName" );
261                        checkState( args.containsKey( argumentName ), () -> new ValidationException( "Missing named argument for $%s".formatted( argumentName ) ) );
262                        final var formatChar = matcher.group( "typeChar" ).charAt( 0 );
263                        addArgument( format, formatChar, args.get( argumentName ) );
264                        m_FormatParts.add( "$" + formatChar );
265                        currentPos += matcher.regionEnd();
266                    }
267                    else
268                    {
269                        checkState( currentPos < format.length() - 1, () -> new ValidationException( "dangling $ at end" ) );
270                        if( !isNoArgPlaceholder( format.charAt( currentPos + 1 ) ) )
271                        {
272                            throw new ValidationException( "unknown format $%s at %s in '%s'".formatted( format.charAt( currentPos + 1 ), currentPos + 1, format ) );
273                        }
274                        m_FormatParts.add( format.substring( currentPos, currentPos + 2 ) );
275                        currentPos += 2;
276                    }
277                }   //  ParseLoop:
278            }
279
280            //---* Done *------------------------------------------------------
281            return this;
282        }   //  addNamed()
283
284        /**
285         *  {@inheritDoc}
286         */
287        @API( status = STABLE, since = "0.2.0" )
288        @Override
289        public final BuilderImpl addStatement( final String format, final Object... args )
290        {
291            final var retValue = add( "$[" )
292                .addWithoutDebugInfo( format, args )
293                .addWithoutDebugInfo( ";\n$]" );
294
295            //---* Done *----------------------------------------------------------
296            return retValue;
297        }   //  addStatement()
298
299        /**
300         *  {@inheritDoc}
301         */
302        @API( status = STABLE, since = "0.2.0" )
303        @Override
304        public final BuilderImpl addStaticImport( final Class<?> clazz, final String... names )
305        {
306            return addStaticImport( ClassNameImpl.from( clazz ), names );
307        }   //  addStaticImport()
308
309        /**
310         *  {@inheritDoc}
311         */
312        @API( status = STABLE, since = "0.2.0" )
313        @Override
314        public final BuilderImpl addStaticImport( final ClassName className, final String... names )
315        {
316            final var canonicalName = requireNonNullArgument( className, "className" ).canonicalName();
317            //noinspection StandardVariableNames
318            for( final var name : requireValidNonNullArgument( names, "names", v -> v.length > 0, (n,_) -> "%s array is empty".formatted( n ) ) )
319            {
320                m_StaticImports.add(
321                    format(
322                        "%s.%s",
323                        canonicalName,
324                        requireValidArgument(
325                            name,
326                            "name",
327                            Objects::nonNull,
328                            (_,_) -> "null entry in names array: %s".formatted( Arrays.toString( names ) )
329                        )
330                    )
331                );
332            }
333
334            //---* Done *------------------------------------------------------
335            return this;
336        }   //  addStaticImport()
337
338        /**
339         *  {@inheritDoc}
340         */
341        @API( status = STABLE, since = "0.2.0" )
342        @Override
343        public final BuilderImpl addStaticImport( final Enum<?> constant )
344        {
345            return addStaticImport( ClassNameImpl.from( requireNonNullArgument( constant, "constant" ).getDeclaringClass() ), constant.name() );
346        }   //  addStaticImport()
347
348        /**
349         *  Adds a
350         *  {@link CodeBlock}
351         *  instance without prepending any debug output.
352         *
353         *  @param  codeBlock   The code block.
354         *  @return This {@code Builder} instance.
355         */
356        @SuppressWarnings( {"PublicMethodNotExposedInInterface"} )
357        public final BuilderImpl addWithoutDebugInfo( final CodeBlock codeBlock )
358        {
359            final var builder = (BuilderImpl) requireNonNullArgument( codeBlock, "codeBlock" )
360                .toBuilder();
361            m_FormatParts.addAll( builder.formatParts() );
362            m_Args.addAll( builder.args() );
363            m_StaticImports.addAll( ((CodeBlockImpl) codeBlock).getStaticImports() );
364
365            //---* Done *------------------------------------------------------
366            return this;
367        }   //  addWithoutDebugInfo()
368
369        /**
370         *  <p>{@summary Adds code with positional or relative arguments,
371         *  without prepending any debug output.}</p>
372         *  <p>Relative arguments map 1:1 with the placeholders in the format
373         *  string.</p>
374         *  <p>Positional arguments use an index after the placeholder to
375         *  identify which argument index to use. For example, for a literal to
376         *  reference the 3<sup>rd</sup> argument, use {@code "$3L"} (1 based
377         *  index).</p>
378         *  <p>Mixing relative and positional arguments in a call to add is
379         *  illegal and will result in an error.</p>
380         *
381         *  @param  format  The format; may be empty.
382         *  @param  args    The arguments.
383         *  @return This {@code Builder} instance.
384         */
385        @SuppressWarnings( {"PublicMethodNotExposedInInterface", "OverlyComplexMethod", "CharacterComparison"} )
386        @API( status = INTERNAL, since = "0.2.0" )
387        public final BuilderImpl addWithoutDebugInfo( final String format, final Object... args )
388        {
389            var hasRelative = false;
390            var hasIndexed = false;
391            @SuppressWarnings( "LocalVariableNamingConvention" )
392            var relativeParameterCount = 0;
393
394            final var length = requireNonNullArgument( format, "format" ).length();
395            @SuppressWarnings( "LocalVariableNamingConvention" )
396            final var indexedParameterCount = new int [requireNonNullArgument( args, "args" ).length];
397
398            ParseLoop:
399            //noinspection ForLoopWithMissingComponent
400            for( var pos = 0; pos < length; /* Update is inside the loop body */ )
401            {
402                if( format.charAt( pos ) != '$' )
403                {
404                    var nextPos = format.indexOf( '$', pos + 1 );
405                    if( nextPos == -1 ) nextPos = format.length();
406                    m_FormatParts.add( format.substring( pos, nextPos ) );
407                    pos = nextPos;
408                    continue ParseLoop;
409                }
410
411                //---* The update for the for-loop … *-------------------------
412                ++pos ; // '$'.
413
414                /*
415                 * Consume zero or more digits, leaving 'c' as the first
416                 * non-digit char after the '$'.
417                 */
418                final var indexStart = pos;
419                char c;
420                do
421                {
422                    checkState( pos < format.length(), () -> new ValidationException( "dangling format characters in '%s'".formatted( format ) ) );
423                    c = format.charAt( pos++ );
424                }
425                while( c >= '0' && c <= '9' );
426                final var indexEnd = pos - 1;
427
428                //---* If 'c' doesn't take an argument, we're done *-----------
429                if( isNoArgPlaceholder( c ) )
430                {
431                    checkState( indexStart == indexEnd, () -> new ValidationException( "$$, $>, $<, $[, $], $W, and $Z may not have an index" ) );
432                    m_FormatParts.add( "$" + c );
433                    continue ParseLoop;
434                }
435
436                /*
437                 * Find either the indexed argument, or the relative argument
438                 * (0-based).
439                 */
440                final int index;
441                if( indexStart < indexEnd )
442                {
443                    index = Integer.parseInt( format.substring( indexStart, indexEnd ) ) - 1;
444                    hasIndexed = true;
445                    if( args.length > 0 )
446                    {
447                        //---* modulo is needed, checked below anyway *--------
448                        ++indexedParameterCount [index % args.length];
449                    }
450                }
451                else
452                {
453                    index = relativeParameterCount++;
454                    hasRelative = true;
455                }
456
457                checkState( index >= 0 && index < args.length, () -> new ValidationException( "index %d for '%s' not in range (received %s arguments)".formatted( index + 1, format.substring( indexStart - 1, indexEnd + 1 ), args.length ) ) );
458                checkState( !hasIndexed || !hasRelative, () -> new ValidationException(  "cannot mix indexed and positional parameters" ) );
459
460                addArgument( format, c, args [index] );
461
462                m_FormatParts.add( "$" + c );
463            }   //  ParseLoop:
464
465            if( hasRelative && (relativeParameterCount < args.length) )
466            {
467                throw new ValidationException( "unused arguments: expected %s, received %s".formatted( relativeParameterCount, args.length ) );
468            }
469            if( hasIndexed )
470            {
471                final Collection<String> unused = IntStream.range( 0, args.length )
472                    .filter( i -> indexedParameterCount[i] == 0 )
473                    .mapToObj( i -> "$" + (i + 1) )
474                    .collect( toList() );
475                if( !unused.isEmpty() )
476                {
477                    throw new ValidationException( "unused argument%s: %s".formatted( unused.size() == 1 ? "" : "s", String.join( ", ", unused ) ) );
478                }
479            }
480
481            //---* Done *------------------------------------------------------
482            return this;
483        }   //  addWithoutDebugInfo()
484
485        /**
486         *  Returns the arguments.
487         *
488         *  @return The arguments.
489         */
490        @SuppressWarnings( "PublicMethodNotExposedInInterface" )
491        public final List<Object> args() { return List.copyOf( m_Args ); }
492
493        /**
494         *  Returns the given object literally.
495         *
496         *  @param  o   The object.
497         *  @return The literal.
498         */
499        private static final Object argToLiteral( final Object o )
500        {
501            final var retValue = nonNull( o ) ? o : NULL_REFERENCE;
502
503            //---* Done *------------------------------------------------------
504            return retValue;
505        }   //  argToLiteral()
506
507        /**
508         *  Translates the given object to a name.
509         *
510         *  @param  o   The object.
511         *  @return The name.
512         */
513        private static final Object argToName( final Object o )
514        {
515            final var retValue = switch( o )
516                {
517                    case final CharSequence charSequence -> charSequence.toString();
518                    case final ParameterSpec parameterSpec -> parameterSpec.name();
519                    case final FieldSpec fieldSpec -> fieldSpec.name();
520                    case final MethodSpec methodSpec -> methodSpec.name();
521                    case final TypeSpec typeSpec ->
522                        /*
523                         * Does not work for anonymous types, so no check for the name
524                         * is required.
525                         */
526                        //noinspection OptionalGetWithoutIsPresent
527                        typeSpec.name().get();
528                    case null, default -> throw new IllegalArgumentException( "expected name but was " + o );
529                };
530
531            //---* Done *------------------------------------------------------
532            return retValue;
533        }   //  argToName()
534
535        /**
536         *  Translates the given object to a String.
537         *
538         *  @param  o   The object.
539         *  @return The resulting String, or
540         *      {@link Util#NULL_REFERENCE}
541         *      if the object is
542         *      {@null}.
543         */
544        private static final Object argToString( final Object o )
545        {
546            final var retValue = isNull( o ) ? NULL_REFERENCE : Objects.toString( o );
547
548            //---* Done *------------------------------------------------------
549            return retValue;
550        }   //  argToString()
551
552        /**
553         *  Translates the given object to a type.
554         *
555         *  @param  o   The object.
556         *  @return The resulting type.
557         */
558        private static final TypeNameImpl argToType( final Object o )
559        {
560            final var retValue = switch( o )
561                {
562                    case final TypeNameImpl typeName -> typeName;
563                    case final TypeMirror typeMirror -> TypeNameImpl.from( typeMirror );
564                    case final Element element -> TypeNameImpl.from( element.asType() );
565                    case final Type type -> TypeNameImpl.from( type );
566                    case null, default -> throw new IllegalArgumentException( "expected type but was " + o );
567                };
568
569            //---* Done *------------------------------------------------------
570            return retValue;
571        }   //  argToType()
572
573        /**
574         *  {@inheritDoc}
575         */
576        @API( status = INTERNAL, since = "0.0.5" )
577        @Override
578        public final BuilderImpl beginControlFlow( final String controlFlow, final Object... args )
579        {
580            addDebug();
581            if( isNotEmptyOrBlank( requireNonNullArgument( controlFlow, "controlFlow" ) ) )
582            {
583                addWithoutDebugInfo( controlFlow, args );
584                if( !controlFlow.endsWith( "\n" ) ) addWithoutDebugInfo( " " );
585            }
586            addWithoutDebugInfo( "{\n" );
587            indent();
588
589            //---* Done *------------------------------------------------------
590            return this;
591        }   //  beginControlFlow()
592
593        /**
594         *  {@inheritDoc}
595         */
596        @Override
597        public final CodeBlockImpl build() { return new CodeBlockImpl( this ); }
598
599        /**
600         *  {@inheritDoc}
601         */
602        @Override
603        public final BuilderImpl endControlFlow()
604        {
605            addDebug();
606            unindent();
607            addWithoutDebugInfo( "}\n" );
608
609            //---* Done *------------------------------------------------------
610            return this;
611        }   //  endControlFlow()
612
613        /**
614         *  {@inheritDoc}
615         */
616        @API( status = INTERNAL, since = "0.0.5" )
617        @Override
618        public final BuilderImpl endControlFlow( final String controlFlow, final Object... args )
619        {
620            addDebug();
621            unindent();
622            addWithoutDebugInfo( "} " + requireNonNullArgument( controlFlow, "controlFlow" ) + ";\n", args );
623
624            //---* Done *------------------------------------------------------
625            return this;
626        }   //  endControlFlow()
627
628        /**
629         *  Returns the format parts.
630         *
631         *  @return The format parts.
632         */
633        @SuppressWarnings( "PublicMethodNotExposedInInterface" )
634        public final List<String> formatParts() { return List.copyOf( m_FormatParts ); }
635
636        /**
637         *  {@inheritDoc}
638         */
639        @Override
640        public final BuilderImpl indent()
641        {
642            m_FormatParts.add( "$>" );
643
644            //---* Done *------------------------------------------------------
645            return this;
646        }   //  indent()
647
648        /**
649         *  {@inheritDoc}
650         */
651        @Override
652        public final boolean isEmpty() { return m_FormatParts.isEmpty(); }
653
654        /**
655         *  Checks whether the given placeholder character would expect an
656         *  argument.
657         *
658         *  @param  placeholder The placeholder character.
659         *  @return {@true} if there is no argument expected,
660         *      {@false} otherwise.
661         */
662        private static final boolean isNoArgPlaceholder( final char placeholder )
663        {
664            final var retValue = IntStream.of( '$', '>', '<', '[', ']', 'W', 'Z' )
665                .anyMatch( p -> p == placeholder );
666
667            //---* Done *------------------------------------------------------
668            return retValue;
669        }   //  isNoArgPlaceholder()
670
671        /**
672         *  {@inheritDoc}
673         */
674        @API( status = INTERNAL, since = "0.0.5" )
675        @Override
676        public final BuilderImpl nextControlFlow( final String controlFlow, final Object... args )
677        {
678            unindent();
679            addWithoutDebugInfo( "}" );
680            if( !requireNonNullArgument( controlFlow, "controlFlow" ).startsWith( "\n" ) ) addWithoutDebugInfo( " " );
681            add( controlFlow, args );
682            if( !controlFlow.endsWith( "\n" ) ) addWithoutDebugInfo(" " );
683            addWithoutDebugInfo( "{\n" );
684            indent();
685
686            //---* Done *------------------------------------------------------
687            return this;
688        }   //  nextControlFlow()
689
690        /**
691         *  {@inheritDoc}
692         */
693        @Override
694        public final BuilderImpl unindent()
695        {
696            m_FormatParts.add( "$<" );
697
698            //---* Done *------------------------------------------------------
699            return this;
700        }   //  unindent()
701    }
702    //  class BuilderImpl
703
704    /**
705     *  A helper class that supports to join code blocks.
706     *
707     *  @author Thomas Thrien - thomas.thrien@tquadrat.org
708     *  @version $Id: CodeBlockImpl.java 1258 2026-06-04 18:33:06Z tquadrat $
709     *  @since 0.0.5
710     *
711     *  @UMLGraph.link
712     */
713    @ClassVersion( sourceVersion = "$Id: CodeBlockImpl.java 1258 2026-06-04 18:33:06Z tquadrat $" )
714    @API( status = INTERNAL, since = "0.0.5" )
715    private static final class CodeBlockJoiner
716    {
717            /*------------*\
718        ====** Attributes **===================================================
719            \*------------*/
720        /**
721         *  The builder that is used to deliver the final code block.
722         */
723        @SuppressWarnings( "UseOfConcreteClass" )
724        private final BuilderImpl m_Builder;
725
726        /**
727         *  The separator for the joined code blocks.
728         */
729        private final String m_Delimiter;
730
731        /**
732         *  Flag that indicates whether to add the delimiter on adding a new
733         *  code block.
734         */
735        private boolean m_First = true;
736
737            /*--------------*\
738        ====** Constructors **=================================================
739            \*--------------*/
740        /**
741         *  <p>{@summary {@constructor}}</p>
742         *
743         *  @param  delimiter   The separator for the joined code blocks.
744         *  @param  builder The builder that is used to deliver the final code
745         *      block.
746         */
747        public CodeBlockJoiner( final String delimiter, @SuppressWarnings( "UseOfConcreteClass" ) final BuilderImpl builder )
748        {
749            m_Delimiter = requireNonNullArgument( delimiter, "delimiter" );
750            m_Builder = requireNonNullArgument( builder, "builder" );
751        }   //  CodeBlockJoiner()
752
753            /*---------*\
754        ====** Methods **======================================================
755            \*---------*/
756        /**
757         *  Adds another code block.
758         *
759         *  @param  codeBlock   The new code block.
760         *  @return This {@code CodeBlockJoiner} instance.
761         */
762        @SuppressWarnings( {"TypeMayBeWeakened", "UnusedReturnValue"} )
763        public final CodeBlockJoiner add( @SuppressWarnings( "UseOfConcreteClass" ) final CodeBlockImpl codeBlock )
764        {
765            if( !m_First ) m_Builder.addWithoutDebugInfo( m_Delimiter );
766            m_First = false;
767
768            m_Builder.add( codeBlock );
769
770            //---* Done *------------------------------------------------------
771            return this;
772        }   //  add()
773
774        /**
775         *  Returns the new code block with the joined ones.
776         *
777         *  @return The new code block.
778         */
779        public final CodeBlockImpl join() { return m_Builder.build(); }
780
781        /**
782         *  Merges this code block joiner with the given other one.
783         *
784         *  @param  other   The other code block joiner.
785         *  @return This {@code CodeBlockJoiner} instance.
786         */
787        public final CodeBlockJoiner merge( @SuppressWarnings( "UseOfConcreteClass" ) final CodeBlockJoiner other )
788        {
789            final var otherBlock = requireNonNullArgument( other, "other" ).m_Builder.build();
790            if( !otherBlock.isEmpty() ) add( otherBlock );
791
792            //---* Done *------------------------------------------------------
793            return this;
794        }   //  merge()
795    }
796    //  class CodeBlockJoiner
797
798        /*------------*\
799    ====** Attributes **=======================================================
800        \*------------*/
801    /**
802     *  The arguments.
803     */
804    private final List<Object> m_Args;
805
806    /**
807     *  Lazily initialised return value of
808     *  {@link #toString()}
809     *  for this code block.
810     */
811    private final Lazy<String> m_CachedString;
812
813    /**
814     *  The reference to the factory.
815     */
816    @SuppressWarnings( "UseOfConcreteClass" )
817    private final JavaComposer m_Composer;
818
819    /**
820     *  A heterogeneous list containing string literals and value placeholders.
821     */
822    private final List<String> m_FormatParts;
823
824    /**
825     *  The static imports.
826     */
827    private final Set<String> m_StaticImports;
828
829        /*------------------------*\
830    ====** Static Initialisations **===========================================
831        \*------------------------*/
832    /**
833     *  The regular expression that is used to determine whether a parameter
834     *  name starts with a lowercase character.
835     */
836    public static final Pattern LOWERCASE;
837
838    /**
839     *  The regular expression that is used to obtain the argument name from
840     *  a format string.
841     */
842    public static final Pattern NAMED_ARGUMENT;
843
844    static
845    {
846        try
847        {
848            LOWERCASE = Pattern.compile( "[a-z]+[\\w_]*" );
849            NAMED_ARGUMENT = Pattern.compile( "\\$(?<argumentName>[\\w_]+):(?<typeChar>\\w).*" );
850        }
851        catch( final PatternSyntaxException e )
852        {
853            throw new ExceptionInInitializerError( e );
854        }
855    }
856
857        /*--------------*\
858    ====** Constructors **=====================================================
859        \*--------------*/
860    /**
861     *  <p>{@summary {@constructor}}</p>
862     *
863     *  @param  builder The builder for this instance.
864     */
865    @SuppressWarnings( {"AccessingNonPublicFieldOfAnotherObject"} )
866    public CodeBlockImpl( @SuppressWarnings( "UseOfConcreteClass" ) final BuilderImpl builder )
867    {
868        m_Composer = builder.m_Composer;
869        m_FormatParts = builder.formatParts();
870        m_Args = builder.args();
871        m_StaticImports = Set.copyOf( builder.m_StaticImports );
872
873        m_CachedString = Lazy.use( this::initialiseCachedString );
874    }   //  CodeBlockImpl()
875
876        /*---------*\
877    ====** Methods **==========================================================
878        \*---------*/
879    /**
880     *  Returns the arguments.
881     *
882     *  @return The arguments.
883     */
884    /*
885     * Originally, this was the reference to the internal collection!
886     */
887    @SuppressWarnings( "PublicMethodNotExposedInInterface" )
888    public final List<Object> args() { return List.copyOf( m_Args ); }
889
890    /**
891     *  {@inheritDoc}
892     */
893    @Override
894    public final boolean equals( final Object o )
895    {
896        var retValue = this == o;
897        if( !retValue && (o instanceof final CodeBlockImpl other) )
898        {
899            retValue = m_Composer.equals( other.m_Composer ) && toString().equals( o.toString() );
900        }
901
902        //---* Done *----------------------------------------------------------
903        return retValue;
904    }   //  equals()
905
906    /**
907     *  Returns the format parts from this code block.
908     *
909     *  @return The format parts.
910     */
911    @SuppressWarnings( "PublicMethodNotExposedInInterface" )
912    public final List<String> formatParts() { return List.copyOf( m_FormatParts ); }
913
914    /**
915     *  Returns the
916     *  {@link JavaComposer}
917     *  factory.
918     *
919     *  @return The reference to the factory.
920     */
921    @SuppressWarnings( {"PublicMethodNotExposedInInterface"} )
922    public final JavaComposer getFactory() { return m_Composer; }
923
924    /**
925     *  Returns the static imports for this code block.
926     *
927     *  @return The static imports.
928     */
929    @SuppressWarnings( "PublicMethodNotExposedInInterface" )
930    @API( status = INTERNAL, since = "0.2.0" )
931    public final Set<String> getStaticImports() { return m_StaticImports; }
932
933    /**
934     *  {@inheritDoc}
935     */
936    @Override
937    public final int hashCode() { return toString().hashCode(); }
938
939    /**
940     *  <p>{@summary The initialiser for
941     *  {@link #m_CachedString}.}</p>
942     *
943     *  @return The return value for
944     *      {@link #toString()}.
945     */
946    private final String initialiseCachedString()
947    {
948        final var resultBuilder = new StringBuilder();
949        final var codeWriter = new CodeWriter( m_Composer, resultBuilder );
950        try
951        {
952            codeWriter.emit( this );
953        }
954        catch( final UncheckedIOException e )
955        {
956            throw new UnlikelyExceptionError( e.getCause() );
957        }
958        final var retValue = resultBuilder.toString();
959
960        //---* Done *----------------------------------------------------------
961        return retValue;
962    }   //  initialiseCachedString()
963
964    /**
965     *  {@inheritDoc}
966     */
967    @Override
968    public final boolean isEmpty() { return m_FormatParts.isEmpty(); }
969
970    /**
971     * {@inheritDoc}
972     */
973    @Override
974    public final CodeBlock join( final String separator, final CodeBlock... codeBlocks )
975    {
976        final var retValue = makeCodeBlockStream( this, requireNonNullArgument( codeBlocks, "codeBlocks" ) )
977            .collect( joining( separator ) );
978
979        //---* Done *----------------------------------------------------------
980        return retValue;
981    }   //  join()
982
983    /**
984     * <p>{@summary Joins this code block with the given code blocks into a
985     * single new {@code CodeBlock} instance, each separated by the given
986     * separator.} The given prefix will be prepended to the new
987     * {@code CodeBloc}, and the given suffix will be appended to it.</p>
988     * <p>For example, joining &quot;{@code String s}&quot;,
989     * &quot;{@code Object o}&quot; and &quot;{@code int i}&quot; using
990     * &quot;{@code , }&quot; as the separator would produce
991     * &quot;{@code String s, Object o, int i}&quot;.</p>
992     *
993     * @param separator The separator.
994     * @param prefix The prefix.
995     * @param suffix The suffix.
996     * @param codeBlocks The code blocks to join.
997     * @return The new code block.
998     */
999    @Override
1000    public final CodeBlock join( final String separator, final String prefix, final String suffix, final CodeBlock... codeBlocks )
1001    {
1002        final var retValue = makeCodeBlockStream( this, requireNonNullArgument( codeBlocks, "codeBlocks" ) )
1003            .collect( joining( separator, prefix, suffix ) );
1004
1005        //---* Done *----------------------------------------------------------
1006        return retValue;
1007    }   //  join()
1008
1009    /**
1010     *  <p>{@summary A
1011     *  {@link Collector}
1012     *  implementation that joins {@code CodeBlock} instances together into one
1013     *  new code block, separated by the given separator.}</p>
1014     *  <p>For example, joining &quot;{@code String s}&quot;,
1015     *  &quot;{@code Object o}&quot; and &quot;{@code int i}&quot; using
1016     *  &quot;{@code , }&quot; as the separator would produce
1017     *  &quot;{@code String s, Object o, int i}&quot;.</p>
1018     *
1019     *  @param  separator   The separator.
1020     *  @return The new collector.
1021     */
1022    @SuppressWarnings( "PublicMethodNotExposedInInterface" )
1023    public final Collector<CodeBlockImpl,?,CodeBlockImpl> joining( final String separator )
1024    {
1025        final Collector<CodeBlockImpl,?,CodeBlockImpl> retValue = Collector.of
1026        (
1027            () -> new CodeBlockJoiner( separator, new BuilderImpl( m_Composer ) ), CodeBlockJoiner::add, CodeBlockJoiner::merge, CodeBlockJoiner::join
1028        );
1029
1030        //---* Done *----------------------------------------------------------
1031        return retValue;
1032    }   //  joining()
1033
1034    /**
1035     *  <p>{@summary A
1036     *  {@link Collector}
1037     *  implementation that joins {@code CodeBlock} instances together into one
1038     *  new code block, separated by the given separator.} The given prefix
1039     *  will be prepended to the new {@code CodeBloc}, and the given suffix
1040     *  will be appended to it.</p>
1041     *  <p>For example, joining &quot;{@code String s}&quot;,
1042     *  &quot;{@code Object o}&quot; and &quot;{@code int i}&quot; using
1043     *  &quot;{@code , }&quot; as the separator, and
1044     *  &quot;{@code int func( }&quot; as the prefix and &quot; {@code )}&quot;
1045     *  as the suffix respectively would produce
1046     *  &quot;{@code int func( String s, Object o, int i )}&quot;.</p>
1047     *
1048     *  @param  separator   The separator.
1049     *  @param  prefix  The prefix.
1050     *  @param  suffix  The suffix.
1051     *  @return The new collector.
1052     */
1053    @SuppressWarnings( "PublicMethodNotExposedInInterface" )
1054    public final Collector<CodeBlockImpl,?,CodeBlockImpl> joining( final String separator, final String prefix, final String suffix )
1055    {
1056        final var builder = new BuilderImpl( m_Composer );
1057        builder.add( "$N", prefix );
1058        final Collector<CodeBlockImpl,?,CodeBlockImpl> retValue = Collector.of
1059        (
1060            () -> new CodeBlockJoiner( separator, builder ), CodeBlockJoiner::add, CodeBlockJoiner::merge, joiner ->
1061            {
1062                builder.add( m_Composer.codeBlockOf( "$N", suffix ) );
1063                return joiner.join();
1064            }
1065        );
1066
1067        //---* Done *----------------------------------------------------------
1068        return retValue;
1069    }   //  joining()
1070
1071    /**
1072     *  Composes a stream from the given {@code CodeBlock} instances.
1073     *
1074     *  @param  head    The first code block.
1075     *  @param  tail    The other code blocks.
1076     *  @return The
1077     *      {@link Stream}
1078     *      instance with the {@code CodeBlock} instances.
1079     */
1080    private static final Stream<CodeBlockImpl> makeCodeBlockStream( final CodeBlock head, final CodeBlock... tail )
1081    {
1082        final var builder = Stream.<CodeBlockImpl>builder();
1083        builder.add( (CodeBlockImpl) requireNonNullArgument( head, "head" ) );
1084        for( final var block : requireNonNullArgument( tail, "tail" ) )
1085        {
1086            builder.add( (CodeBlockImpl) block );
1087        }
1088        final var retValue = builder.build();
1089
1090        //---* Done *----------------------------------------------------------
1091        return retValue;
1092    }   //  makeCodeBlockStream()
1093
1094    /**
1095     *  Creates a new builder that is initialised with the components of this
1096     *  code block.
1097     *
1098     *  @return The new builder.
1099     */
1100    @SuppressWarnings( {"AccessingNonPublicFieldOfAnotherObject"} )
1101    @Override
1102    public final BuilderImpl toBuilder()
1103    {
1104        final var retValue = new BuilderImpl( m_Composer, m_FormatParts, m_Args );
1105        retValue.m_StaticImports.addAll( m_StaticImports );
1106
1107        //---* Done *----------------------------------------------------------
1108        return retValue;
1109    }   //  toBuilder()
1110
1111    /**
1112     *  {@inheritDoc}
1113     */
1114    @Override
1115    public final String toString() { return m_CachedString.get(); }
1116}
1117//  class CodeBlockImpl
1118
1119/*
1120 *  End of File
1121 */