001package squidpony.panel;
002
003import squidpony.annotation.Beta;
004
005import java.util.*;
006
007/**
008 * A {@link String} divided in chunks of different colors. Use the
009 * {@link Iterable} interface to get the pieces.
010 * 
011 * @author smelC
012 * 
013 * @param <T>
014 *            The type of colors;
015 */
016@Beta
017public interface IColoredString<T> extends Iterable<IColoredString.Bucket<T>> {
018
019        /**
020         * Mutates {@code this} by appending {@code c} to it.
021         * 
022         * @param c
023         *            The text to append.
024         * @param color
025         *            {@code text}'s color. Or {@code null} to let the panel decide.
026         */
027        void append(char c, /* @Nullable */T color);
028
029        /**
030         * Mutates {@code this} by appending {@code text} to it. Does nothing if
031         * {@code text} is {@code null}.
032         * 
033         * @param text
034         *            The text to append.
035         * @param color
036         *            {@code text}'s color. Or {@code null} to let the panel decide.
037         */
038        void append(/* @Nullable */String text, /* @Nullable */T color);
039
040        /**
041         * Mutates {@code this} by appending {@code i} to it.
042         * 
043         * @param i
044         *            The int to append.
045         * @param color
046         *            {@code text}'s color. Or {@code null} to let the panel decide.
047         */
048        void appendInt(int i, /* @Nullable */T color);
049
050        /**
051         * Mutates {@code this} by appending {@code f} to it.
052         * 
053         * @param f
054         *            The float to append.
055         * @param color
056         *            {@code text}'s color. Or {@code null} to let the panel decide.
057         */
058        void appendFloat(float f, /* @Nullable */T color);
059
060        /**
061         * Mutates {@code this} by appending {@code other} to it.
062         * 
063         * @param other
064         */
065        void append(IColoredString<T> other);
066
067        /**
068         * Deletes all content after index {@code len} (if any).
069         * 
070         * @param len
071         */
072        void setLength(int len);
073
074        /**
075         * @return {@code true} if {@link #present()} is {@code ""}.
076         */
077        boolean isEmpty();
078
079        /**
080         * @param width
081         *            A positive integer
082         * @return {@code this} split in pieces that would fit in a display with
083         *         {@code width} columns (if all words in {@code this} are smaller
084         *         or equal in length to {@code width}, otherwise wrapping will fail
085         *         for these words).
086         */
087        List<IColoredString<T>> wrap(int width);
088
089        /**
090         * This method does NOT guarantee that the result's length is {@code width}.
091         * It is impossible to do correct justifying if {@code this}'s length is
092         * greater than {@code width} or if {@code this} has no space character.
093         * 
094         * @param width
095         * @return A variant of {@code this} where spaces have been introduced
096         *         in-between words, so that {@code this}'s length is as close as
097         *         possible to {@code width}. Or {@code this} itself if unaffected.
098         */
099        IColoredString<T> justify(int width);
100
101        /**
102         * Empties {@code this}.
103         */
104        void clear();
105
106        /**
107         * This method is typically more efficient than {@link #colorAt(int)}.
108         * 
109         * @return The color of the last bucket, if any.
110         */
111        /* @Nullable */ T lastColor();
112
113        /**
114         * @param index
115         * @return The color at {@code index}, if any.
116         * @throws NoSuchElementException
117         *             If {@code index} equals or is greater to {@link #length()}.
118         */
119        /* @Nullable */ T colorAt(int index);
120
121        /**
122         * @return The length of text.
123         */
124        int length();
125
126        /**
127         * @return The text that {@code this} represents.
128         */
129        String present();
130
131        /**
132         * Given some way of converting from a T value to an in-line markup tag, returns a string representation of
133         * this IColoredString with in-line markup representing colors.
134         * @param markup an IMarkup implementation
135         * @return a String with markup inserted inside.
136         */
137        String presentWithMarkup(IMarkup<T> markup);
138
139        /**
140         * A basic implementation of {@link IColoredString}.
141         * 
142         * @author smelC
143         * 
144         * @param <T>
145         *            The type of colors
146         */
147        class Impl<T> implements IColoredString<T> {
148
149                protected final LinkedList<Bucket<T>> fragments;
150
151                /**
152                 * An empty instance.
153                 */
154                public Impl() {
155                        fragments = new LinkedList<>();
156                }
157
158                /**
159                 * An instance initially containing {@code text} (with {@code color}).
160                 * 
161                 * @param text
162                 *            The text that {@code this} should contain.
163                 * @param color
164                 *            The color of {@code text}.
165                 */
166                public Impl(String text, /* @Nullable */T color) {
167                        this();
168
169                        append(text, color);
170                }
171
172                /**
173                 * A static constructor, to avoid having to write {@code <T>} in the
174                 * caller.
175                 * 
176                 * @return {@code new Impl(s, t)}.
177                 */
178                public static <T> IColoredString.Impl<T> create() {
179                        return new IColoredString.Impl<>("", null);
180                }
181
182                /**
183                 * A convenience method, equivalent to {@code create(s, null)}.
184                 * 
185                 * @param s
186                 * @return {@code create(s, null)}
187                 */
188                public static <T> IColoredString.Impl<T> create(String s) {
189                        return create(s, null);
190                }
191
192                /**
193                 * A static constructor, to avoid having to write {@code <T>} in the
194                 * caller.
195                 * 
196                 * @return {@code new Impl(s, t)}.
197                 */
198                public static <T> IColoredString.Impl<T> create(String s, /* @Nullable */ T t) {
199                        return new IColoredString.Impl<>(s, t);
200                }
201
202                @Override
203                public void append(char c, T color) {
204                        append(String.valueOf(c), color);
205                }
206
207                @Override
208                public void append(String text, T color) {
209                        if (text == null || text.isEmpty())
210                                return;
211
212                        if (fragments.isEmpty())
213                                fragments.add(new Bucket<>(text, color));
214                        else {
215                                final Bucket<T> last = fragments.getLast();
216                                if (equals(last.color, color)) {
217                                        /* Append to the last bucket, to avoid extending the list */
218                                        final Bucket<T> novel = last.append(text);
219                                        fragments.removeLast();
220                                        fragments.addLast(novel);
221                                } else
222                                        fragments.add(new Bucket<>(text, color));
223                        }
224                }
225
226                @Override
227                public void appendInt(int i, T color) {
228                        append(String.valueOf(i), color);
229                }
230
231                @Override
232                public void appendFloat(float f, T color) {
233                        final int i = Math.round(f);
234                        append(i == f ? String.valueOf(i) : String.valueOf(f), color);
235                }
236
237                @Override
238                /* KISS implementation */
239                public void append(IColoredString<T> other) {
240                        for (IColoredString.Bucket<T> ofragment : other)
241                                append(ofragment.getText(), ofragment.getColor());
242                }
243
244                public void append(Bucket<T> bucket) {
245                        this.fragments.add(new Bucket<>(bucket.getText(), bucket.getColor()));
246                }
247
248                @Override
249                public void setLength(int len) {
250                        int l = 0;
251                        final ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator();
252                        while (it.hasNext()) {
253                                final IColoredString.Bucket<T> next = it.next();
254                                final String ftext = next.text;
255                                final int flen = ftext.length();
256                                final int nextl = l + flen;
257                                if (nextl < len) {
258                                        /* Nothing to do */
259                                        l += flen;
260                                        continue;
261                                } else if (nextl == len) {
262                                        /* Delete all next fragments */
263                                        while (it.hasNext()) {
264                                                it.next();
265                                                it.remove();
266                                        }
267                                        /* We'll exit the outer loop right away */
268                                } else {
269                                        assert len < nextl;
270                                        /* Trim this fragment */
271                                        final IColoredString.Bucket<T> trimmed = next.setLength(len - l);
272                                        /* Replace this fragment */
273                                        it.remove();
274                                        it.add(trimmed);
275                                        /* Delete all next fragments */
276                                        while (it.hasNext()) {
277                                                it.next();
278                                                it.remove();
279                                        }
280                                        /* We'll exit the outer loop right away */
281                                }
282                        }
283                }
284
285                @Override
286                public List<IColoredString<T>> wrap(int width) {
287                        if (width <= 0) {
288                                /* Really, you should not rely on this behavior */
289                                System.err.println("Cannot wrap string in empty display");
290                                final List<IColoredString<T>> result = new LinkedList<>();
291                                result.add(this);
292                                return result;
293                        }
294
295                        final List<IColoredString<T>> result = new ArrayList<>();
296                        if (isEmpty()) {
297                                /*
298                                 * Catch this case early on, as empty lines are eaten below (see
299                                 * code after the while). Checking emptyness is cheap anyway.
300                                 */
301                                result.add(this);
302                                return result;
303                        }
304
305                        IColoredString<T> current = create();
306                        int curlen = 0;
307                        final Iterator<Bucket<T>> it = iterator();
308                        while (it.hasNext()) {
309                                final Bucket<T> next = it.next();
310                                final String bucket = next.getText();
311                                final String[] split = bucket.split(" ");
312                                final T color = next.color;
313                                for (int i = 0; i < split.length; i++) {
314                                        if (i == split.length - 1 && bucket.endsWith(" "))
315                                                /*
316                                                 * Do not loose trailing space that got eaten by
317                                                 * 'bucket.split'.
318                                                 */
319                                                split[i] = split[i] + " ";
320                                        final String chunk = split[i];
321                                        final int chunklen = chunk.length();
322                                        final boolean addLeadingSpace = 0 < curlen && 0 < i;
323                                        if (curlen + chunklen + (addLeadingSpace ? 1 : 0) <= width) {
324                                                if (addLeadingSpace) {
325                                                        /*
326                                                         * Do not forget space on which chunk got split. If
327                                                         * the space is offscreen, it's harmless, hence not
328                                                         * checking it.
329                                                         */
330                                                        current.append(' ', null);
331                                                        curlen++;
332                                                }
333
334                                                /* Can add it */
335                                                current.append(chunk, color);
336                                                /* Extend size */
337                                                curlen += chunklen;
338                                        } else {
339                                                /* Need to wrap */
340                                                /* Flush content so far */
341                                                if (!current.isEmpty())
342                                                        result.add(current);
343                                                /*
344                                                 * else: line was prepared, but did not contain anything
345                                                 */
346                                                if (chunklen <= width) {
347                                                        curlen = chunklen;
348                                                        current = create();
349                                                        current.append(chunk, color);
350                                                        /* Reinit size */
351                                                        curlen = chunklen;
352                                                } else {
353                                                        /*
354                                                         * This word is too long. Adding it and preparing a
355                                                         * new line immediately.
356                                                         */
357                                                        /* Add */
358                                                        result.add(new Impl<>(chunk, color));
359                                                        /* Prepare for next rolls */
360                                                        current = create();
361                                                        /* Reinit size */
362                                                        curlen = 0;
363                                                }
364                                        }
365                                }
366                        }
367
368                        if (!current.isEmpty()) {
369                                /* Flush rest */
370                                result.add(current);
371                        }
372
373                        return result;
374                }
375
376                @Override
377                /*
378                 * smelC: not the cutest result (we should add spaces both from the left
379                 * and the right, instead of just from the left), but better than
380                 * nothing.
381                 */
382                public IColoredString<T> justify(int width) {
383                        int length = length();
384
385                        if (width <= length)
386                                /*
387                                 * If width==length, we're good. If width<length, we cannot
388                                 * adjust
389                                 */
390                                return this;
391
392                        int totalDiff = width - length;
393                        assert 0 < totalDiff;
394
395                        if (width <= totalDiff * 3)
396                                /* Too much of a difference, it would look very weird. */
397                                return this;
398
399                        final IColoredString.Impl<T> result = create();
400
401                        ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator();
402                        final int nbb = fragments.size();
403                        final int[] bucketToNbSpaces = new int[nbb];
404                        /* The number of buckets that can contribute to justifying */
405                        int totalNbSpaces = 0;
406                        /* The index of the last bucket that has spaces */
407                        int lastHopeIndex = -1;
408                        {
409                                int i = 0;
410                                while (it.hasNext()) {
411                                        final Bucket<T> next = it.next();
412                                        final int nbs = nbSpaces(next.getText());
413                                        totalNbSpaces += nbs;
414                                        bucketToNbSpaces[i] = nbs;
415                                        i++;
416                                }
417
418                                if (totalNbSpaces == 0)
419                                        /* Cannot do anything */
420                                        return this;
421
422                                for (int j = bucketToNbSpaces.length - 1; 0 <= j; j--) {
423                                        if (0 < bucketToNbSpaces[j]) {
424                                                lastHopeIndex = j;
425                                                break;
426                                        }
427                                }
428                                /* Holds because we ruled out 'totalNbSpaces == 0' before */
429                                assert 0 <= lastHopeIndex;
430                        }
431
432                        int toAddPerSpace = totalNbSpaces == 0 ? 0 : (totalDiff / totalNbSpaces);
433                        int totalRest = totalDiff - (toAddPerSpace * totalNbSpaces);
434                        assert 0 <= totalRest;
435
436                        int bidx = -1;
437
438                        it = fragments.listIterator();
439
440                        while (it.hasNext() && 0 < totalDiff) {
441                                bidx++;
442                                final Bucket<T> next = it.next();
443                                final String bucket = next.getText();
444                                final int blength = bucket.length();
445                                final int localNbSpaces = bucketToNbSpaces[bidx];
446                                if (localNbSpaces == 0) {
447                                        /* Cannot change it */
448                                        result.append(next);
449                                        continue;
450                                }
451                                int localDiff = localNbSpaces * toAddPerSpace;
452                                assert localDiff <= totalDiff;
453                                int nb = localDiff / localNbSpaces;
454                                int localRest = localDiff - (nb * localNbSpaces);
455                                if (localRest == 0 && 0 < totalRest) {
456                                        /*
457                                         * Take one for the group. This avoids flushing all spaces
458                                         * needed in the 'last hope' cases below.
459                                         */
460                                        localRest = 1;
461                                }
462                                assert 0 <= localRest;
463                                assert localRest <= totalRest;
464                                String novel = "";
465                                int eatenSpaces = 1;
466                                for (int i = 0; i < blength; i++) {
467                                        final char c = bucket.charAt(i);
468                                        novel += c;
469                                        if (c == ' ' && (0 < localDiff || 0 < totalDiff || 0 < localRest || 0 < totalRest)) {
470                                                /* Can (and should) add an extra space */
471                                                for (int j = 0; j < nb && 0 < localDiff; j++) {
472                                                        novel += " ";
473                                                        localDiff--;
474                                                        totalDiff--;
475                                                }
476                                                if (0 < localRest || 0 < totalRest) {
477                                                        if (eatenSpaces == localNbSpaces) {
478                                                                /* I'm the last hope for this bucket */
479                                                                for (int j = 0; j < localRest; j++) {
480                                                                        novel += " ";
481                                                                        localRest--;
482                                                                        totalRest--;
483                                                                }
484                                                                if (bidx == lastHopeIndex) {
485                                                                        /* I'm the last hope globally */
486                                                                        while (0 < totalRest) {
487                                                                                novel += " ";
488                                                                                totalRest--;
489                                                                        }
490                                                                }
491                                                        } else {
492                                                                if (0 < localRest && 0 < totalRest) {
493                                                                        /* Not the last hope: take one only */
494                                                                        novel += " ";
495                                                                        localRest--;
496                                                                        totalRest--;
497                                                                }
498                                                        }
499                                                }
500                                                eatenSpaces++;
501                                        }
502                                }
503                                /* I did my job */
504                                assert localRest == 0;
505                                /* If I was the hope, I did my job */
506                                assert bidx != lastHopeIndex || totalRest == 0;
507                                result.append(novel, next.getColor());
508                        }
509
510                        while (it.hasNext())
511                                result.append(it.next());
512
513                        return result;
514                }
515
516                @Override
517                public void clear() {
518                        fragments.clear();
519                }
520
521                @Override
522                public int length() {
523                        int result = 0;
524                        for (Bucket<T> fragment : fragments)
525                                result += fragment.getText().length();
526                        return result;
527                }
528
529                @Override
530                /* This implementation is resilient to empty buckets */
531                public boolean isEmpty() {
532                        for (Bucket<?> bucket : fragments) {
533                                if (bucket.text == null || bucket.text.isEmpty())
534                                        continue;
535                                else
536                                        return false;
537                        }
538                        return true;
539                }
540
541                @Override
542                public T lastColor() {
543                        return fragments.isEmpty() ? null : fragments.getLast().color;
544                }
545
546                @Override
547                public T colorAt(int index) {
548                        final ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator();
549                        int now = 0;
550                        while (it.hasNext()) {
551                                final IColoredString.Bucket<T> next = it.next();
552                                final String ftext = next.text;
553                                final int flen = ftext.length();
554                                final int nextl = now + flen;
555                                if (index <= nextl)
556                                        return next.color;
557                                now += flen;
558                        }
559                        throw new NoSuchElementException("Character at index " + index + " in " + this);
560                }
561
562                @Override
563                public String present() {
564                        final StringBuilder result = new StringBuilder();
565                        for (Bucket<T> fragment : fragments)
566                                result.append(fragment.text);
567                        return result.toString();
568                }
569
570                /**
571                 * Given some way of converting from a T value to an in-line markup tag, returns a string representation of
572                 * this IColoredString with in-line markup representing colors.
573                 * @param markup an IMarkup implementation
574         * @return a String with markup inserted inside.
575         */
576                @Override
577                public String presentWithMarkup(IMarkup<T> markup) {
578                        final StringBuilder result = new StringBuilder();
579                        for (Bucket<T> fragment : fragments) {
580                                if(fragment.color != null)
581                                        result.append(markup.getMarkup(fragment.color));
582                                else
583                                        result.append(markup.closeMarkup());
584                                result.append(fragment.text);
585                        }
586            result.append(markup.closeMarkup());
587                        return result.toString();
588                }
589
590                @Override
591                public Iterator<Bucket<T>> iterator() {
592                        return fragments.iterator();
593                }
594
595                @Override
596                public String toString() {
597                        return present();
598                }
599
600                protected static boolean equals(Object o1, Object o2) {
601                        if (o1 == null)
602                                return o2 == null;
603                        else
604                                return o1.equals(o2);
605                }
606
607                private int nbSpaces(String s) {
608                        final int bd = s.length();
609                        int result = 0;
610                        for (int i = 0; i < bd; i++) {
611                                final char c = s.charAt(i);
612                                if (c == ' ')
613                                        result++;
614                        }
615                        return result;
616                }
617        }
618
619        /**
620         * A piece of a {@link IColoredString}: a text and its color.
621         * 
622         * @author smelC
623         * 
624         * @param <T>
625         *            The type of colors;
626         */
627        class Bucket<T> {
628
629                protected final String text;
630                protected final/* @Nullable */T color;
631
632                public Bucket(String text, /* @Nullable */T color) {
633                        this.text = text == null ? "" : text;
634                        this.color = color;
635                }
636
637                /**
638                 * @param text
639                 * @return An instance whose text is {@code this.text + text}. Color is
640                 *         unchanged.
641                 */
642                public Bucket<T> append(String text) {
643                        if (text == null || text.isEmpty())
644                                /* Let's save an allocation */
645                                return this;
646                        else
647                                return new Bucket<>(this.text + text, color);
648                }
649
650                public Bucket<T> setLength(int l) {
651                        final int here = text.length();
652                        if (here <= l)
653                                return this;
654                        else
655                                return new Bucket<>(text.substring(0, l), color);
656                }
657
658                /**
659                 * @return The text that this bucket contains.
660                 */
661                public String getText() {
662                        return text;
663                }
664
665                /**
666                 * @return The color of {@link #getText()}. Or {@code null} if none.
667                 */
668                public/* @Nullable */T getColor() {
669                        return color;
670                }
671
672                @Override
673                public String toString() {
674                        if (color == null)
675                                return text;
676                        else
677                                return text + "(" + color + ")";
678                }
679
680        }
681}