#!/usr/bin/ruby

# Author: Daniel "Trizen" Șuteu
# License: GPLv3
# Date: 15th October 2013
# https://trizenx.blogspot.com
# https://trizenx.blogspot.com/2013/11/smart-word-wrap.html

# Smart word wrap algorithm
# See: https://en.wikipedia.org/wiki/Word_wrap#Minimum_raggedness

class SmartWordWrap(width=80) {

    # This is the ugliest method! It, recursively,
    # prepares the words for the combine() function.
    method prepare_words(array) {

        var root = []
        var len = 0

        for (var(i, limit) = (0, array.end); i <= limit; ++i) {
            len += (var word_len = array[i].len)

            if (len > width) {
                if (word_len > width) {
                    len -= word_len;
                    array.insert(i-1, array[i].split(width)...)
                    array.delete_at(i)
                    limit = array.end
                    --i; next
                }
                break
            }

            root << [
                array.first(i+1).join(' '),
                self.prepare_words(array.slice(i+1))
            ]
            break if (++len >= width)
        }

        root || nil
    }

    # This function combines the
    # the parents with the children.
    method combine(root, path) {

        var row = []
        var key = path.shift
        path.each { |value|
            root << key
            if (defined(value)) {
                value.each { |item|
                    row += self.combine(root, item)
                }
            }
            else {
                row = [root + []]
            }
            root.pop
        }

        row;
    }

    # This function finds the best
    # combination available and returns it.
    method find_best(arrays) {

        var best = Hash(score => Inf);

        arrays.each { |array|
            var score = 0;
            array.each { |line|
                score += sqr(width - line.len);
            }

            if (score < best{:score}) {
                best{:score} = score;
                best{:value} = array;
            }
        }

        best{:value};
    }

    # This is the main function of the algorithm
    # which calls all the other functions and
    # returns the best possible wrapped string.
    method smart_wrap(text, width) {

        local self{:width} = width;
        var words = (text.is_a(Array) ? text : text.words);

        var lines = [];
        self.prepare_words(words).each { |path|
            lines += self.combine([], path);
        }

        var best = self.find_best(lines);
        best == nil && return nil;
        return best.join("\n");
    }
}

#
## Usage examples
#

var obj = SmartWordWrap();

var text = 'aaa bb cc ddddd';
var t1 = obj.smart_wrap(text, 6);
say t1;

assert_eq(t1, <<'EOT'.chomp)
aaa
bb cc
ddddd
EOT

say '-'*80;

text = 'As shown in the above phases (or steps), the algorithm does many useless transformations';
var t2 = obj.smart_wrap(text, 20);
say t2;

assert_eq(t2, <<'EOT'.chomp)
As shown in the
above phases
(or steps), the
algorithm does
many useless
transformations
EOT

say '-'*80;

text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
var t3 = obj.smart_wrap(text, 20);
say t3;

assert_eq(t3, <<'EOT'.chomp)
Lorem ipsum
dolor sit amet,
consectetur
adipiscing elit.
EOT
