001/*
002 * ============================================================================
003 * Copyright © 2015 Square, Inc.
004 * Copyright for the modifications © 2018-2024 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 org.apiguardian.api.API.Status.INTERNAL;
023import static org.tquadrat.foundation.lang.Objects.nonNull;
024import static org.tquadrat.foundation.lang.Objects.require;
025import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
026import static org.tquadrat.foundation.util.StringUtils.isNotEmpty;
027
028import java.io.Closeable;
029import java.io.IOException;
030
031import org.apiguardian.api.API;
032import org.tquadrat.foundation.annotation.ClassVersion;
033import org.tquadrat.foundation.exception.UnsupportedEnumError;
034
035/**
036 *  Implements soft line wrapping on an
037 *  {@link Appendable}.
038 *  To use, append characters using
039 *  {@link #append(CharSequence)}
040 *  or soft-wrapping spaces using
041 *  {@link #wrappingSpace(int)}.
042 *
043 *  @author Square,Inc.
044 *  @modified Thomas Thrien - thomas.thrien@tquadrat.org
045 *  @version $Id: LineWrapper.java 1105 2024-02-28 12:58:46Z tquadrat $
046 *  @since 0.0.5
047 *
048 *  @UMLGraph.link
049 */
050@ClassVersion( sourceVersion = "$Id: LineWrapper.java 1105 2024-02-28 12:58:46Z tquadrat $" )
051@API( status = INTERNAL, since = "0.0.5" )
052public final class LineWrapper implements Closeable
053{
054        /*---------------*\
055    ====** Inner Classes **====================================================
056        \*---------------*/
057    /**
058     *  The flush types.
059     *
060     *  @see LineWrapper#flush(FlushType)
061     *
062     *  @author Square,Inc.
063     *  @modified Thomas Thrien - thomas.thrien@tquadrat.org
064     *  @version $Id: LineWrapper.java 1105 2024-02-28 12:58:46Z tquadrat $
065     *  @since 0.0.5
066     *
067     *  @UMLGraph.link
068     */
069    private enum FlushType
070    {
071            /*------------------*\
072        ====** Enum Declaration **=============================================
073            \*------------------*/
074        /**
075         *  Add nothing.
076         */
077        EMPTY,
078
079        /**
080         *  Add a single blank space.
081         */
082        SPACE,
083
084        /**
085         *  Add a new line.
086         */
087        WRAP
088    }
089    //  enum FlushType
090
091        /*------------*\
092    ====** Attributes **=======================================================
093        \*------------*/
094    /**
095     *  Characters written since the last wrapping space that haven't yet been
096     *  flushed.
097     */
098    @SuppressWarnings( "StringBufferField" )
099    private final StringBuilder m_Buffer = new StringBuilder();
100
101    /**
102     *  The flag that indicates whether this line wrapper was already closed.
103     */
104    private boolean m_Closed = false;
105
106    /**
107     *  The number of characters since the most recent newline. Includes both
108     *  {@link #m_Out}
109     *  and
110     *  {@link #m_Buffer}.
111     */
112    private int m_Column = 0;
113
114    /**
115     *  The maximum line length.
116     */
117    private final int m_ColumnLimit;
118
119    /**
120     *  The indentation String.
121     */
122    private final String m_Indent;
123
124    /**
125     * -1 if we have no buffering; otherwise the number of
126     * {@link #m_Indent}s
127     * to write after wrapping.
128     */
129    private int m_IndentLevel = -1;
130
131    /**
132     * {@code null} if we have no buffering; otherwise the type to pass to the
133     * next call to
134     * {@link #flush}.
135     */
136    private FlushType m_NextFlush;
137
138    /**
139     *  The output target.
140     */
141    private final Appendable m_Out;
142
143        /*--------------*\
144    ====** Constructors **=====================================================
145        \*--------------*/
146    /**
147     *  Creates a new {@code LineWrapper} instance.
148     *
149     *  @param  out The output target.
150     *  @param  indent  The indentation string.
151     *  @param  columnLimit The maximum line length.
152     */
153    public LineWrapper( final Appendable out, final String indent, final int columnLimit )
154    {
155        m_Out = requireNonNullArgument( out, "out" );
156        m_Indent = requireNonNullArgument( indent, "indent" );
157        m_ColumnLimit = require( columnLimit, $ -> "columnLimit is 0 or negative: %d".formatted( columnLimit ), v -> v > 0 );
158    }   //  LineWrapper()
159
160        /*---------*\
161    ====** Methods **==========================================================
162        \*---------*/
163    /**
164     *  Emits the given String. This may be buffered to permit line wraps to be
165     *  inserted.
166     *
167     *  @param  input The string to emit.
168     *  @throws IOException A problem occurred when writing to the
169     *      output target.
170     */
171    public final void append( final CharSequence input ) throws IOException
172    {
173        if( m_Closed ) throw new IllegalStateException( "closed" );
174
175        if( isNotEmpty( input ) )
176        {
177            final var data = input.toString();
178            final var len = data.length();
179            var buffered = false;
180            if( nonNull( m_NextFlush ) )
181            {
182                final var nextNewline = data.indexOf( '\n' );
183
184                /*
185                 * If data doesn't cause the current line to cross the limit,
186                 * buffer it and return. We'll decide later whether we have to
187                 * wrap it or not.
188                 */
189                if( (nextNewline == -1) && (m_Column + len <= m_ColumnLimit) )
190                {
191                    m_Buffer.append( data );
192                    m_Column += len;
193                    buffered = true;
194                }
195                else
196                {
197                    /*
198                     * Wrap if appending s would overflow the current line.
199                     */
200                    final var wrap = nextNewline == -1 || m_Column + nextNewline > m_ColumnLimit;
201                    flush( wrap ? FlushType.WRAP : m_NextFlush );
202                }
203            }
204
205            if( !buffered )
206            {
207                m_Out.append( data );
208                final var lastNewline = data.lastIndexOf( '\n' );
209                //noinspection ConditionalExpressionWithNegatedCondition
210                m_Column = lastNewline != -1 ? len - lastNewline - 1 : m_Column + len;
211            }
212        }
213    }   //  append()
214
215    /**
216     *  This implementation flushes any outstanding text and forbid future
217     *  writes to this line wrapper.
218     */
219    @Override
220    public final void close() throws IOException
221    {
222        if( !m_Closed )
223        {
224            if( nonNull( m_NextFlush ) ) flush( m_NextFlush );
225            m_Closed = true;
226        }
227    }   //  close()
228
229    /**
230     *  Writes the space followed by any buffered text that follows it.
231     *
232     *  @param  flushType   The flush type.
233     *  @throws IOException A problem occurred when writing to the output
234     *      target.
235     */
236    private final void flush( final FlushType flushType ) throws IOException
237    {
238        switch( flushType )
239        {
240            case WRAP:
241            {
242                m_Out.append( '\n' );
243                for( var i = 0; i < m_IndentLevel; ++i )
244                {
245                    m_Out.append( m_Indent );
246                }
247                m_Column = m_IndentLevel * m_Indent.length();
248                m_Column += m_Buffer.length();
249                break;
250            }
251
252            case SPACE:
253            {
254                m_Out.append( ' ' );
255                break;
256            }
257
258            case EMPTY:
259                break;
260
261            default:
262                throw new UnsupportedEnumError( flushType );
263        }
264
265        m_Out.append( m_Buffer );
266
267        /*
268         * Originally, this was:
269         *
270         *    m_Buffer.delete( 0, m_Buffer.length() );
271         */
272        m_Buffer.setLength( 0 );
273        /*
274         * This implementation will keep the internal size of the
275         * StringBuilder, so some heap space is wasted until the LineWrapper
276         * will be garbage collected.
277         */
278        m_IndentLevel = -1;
279        m_NextFlush = null;
280    }   //  flush()
281
282    /**
283     *  Emits either a space or a newline character.
284     *
285     *  @param  indentLevel The indentation level.
286     *  @throws IOException A problem occurred when writing to the output
287     *      target.
288     */
289    public final void wrappingSpace( final int indentLevel ) throws IOException
290    {
291        if( m_Closed ) throw new IllegalStateException( "closed" );
292
293        if( nonNull( m_NextFlush ) ) flush( m_NextFlush );
294
295        /*
296         * Increment the column even though the space is deferred to next call
297         * to flush().
298         */
299        ++m_Column;
300
301        m_NextFlush = FlushType.SPACE;
302        m_IndentLevel = indentLevel;
303    }   //  wrappingSpace()
304
305    /**
306     *  Emits a newline character if the line will exceed its limit, otherwise
307     *  do nothing.
308     *
309     *  @param  indentLevel The indentation level.
310     *  @throws IOException A problem occurred when writing to the output
311     *      target.
312     */
313    public final void zeroWidthSpace( final int indentLevel ) throws IOException
314    {
315        if( m_Closed ) throw new IllegalStateException( "closed" );
316
317        if( m_Column > 0 )
318        {
319            if( nonNull( m_NextFlush ) ) flush( m_NextFlush );
320            m_NextFlush = FlushType.EMPTY;
321            m_IndentLevel = indentLevel;
322        }
323    }   //  zeroWidthSpace()
324}
325//  class LineWrapper
326
327/*
328 *  End of File
329 */