=head1 NAME

CFX5::Output::Utils - misc helpers for output

=head1 SYNOPSIS

  use CFX5::Output::Utils qw(wraptext);

=head1 DESCRIPTION

This module contains various utility functions, used by the various
error message modules.

=cut

package CFX5::Output::Utils;

use base 'Exporter';
@EXPORT_OK=qw(unwraptext wrapunwrappedtext wraptext);

use strict;

=head2 TEXT WRAPPING

All routine in this section use a single unified concept of text flow,
which (quite accidentally!) is similar to POD, except without the
markup (!).  It is designed around the standard format used for error
messages in ANSYS CFX scripts.

Any line beginning with whitespace is considered "verbatim", and will
be left as a paragraph on its own (although, for some output formats,
it may be wrapped).  Trailing whitespace is ignored.  Otherwise, lines
of text starting at the left margin are taken as belonging to a single
paragraph, with paragraphs separated by blank lines.

=head3 unwraptext

I<exportable>

The C<unwraptext> routine takes as input any block(s) of text
formatted as above, and returns a list of paragraphs with newlines
removed.  For example, the following text (indented for POD purposes,
you can work out where the left margin is meant to be!):

  This is a paragraph
  along with this line.
  
  This is another.
  
     This line is verbatim
     And so this is a new paragraph.

  This line
     Does not get combined with this line.
  Or this one.

will result in a list as follows:

  (
    "This is a paragraph along with this line.",
    "This is another.",
    "  This line is verbatim",
    "  And so this is a new paragraph.",
    "This line",
    "  Does not get combined with this line.",
    "Or this one."
  )

This is the routine on which most of the remaining formatting routines
are based, but it can also be used directly.

=cut

sub unwraptext {
  my @in=@_;
  
  # we want to ignore whitespace before newlines, but not afterwards
  # ...
  my @lines=split(/[ \t]*\n/,join(' ',@in));
  
  # now join up the "free text" paragraphs
  my @pars;
  my $c;
  for my $l (@lines) {
    if ($l =~ m!^\s!) {
      # whitespace, so it's verbatim.  To signal this, we undef $c and
      # just add it to the end.
      push @pars, $l;
      undef $c;
      next;
    }

    # back in real text, so make sure $c points to the next free space
    unless (defined $c) {
      $c=scalar(@pars);
    }

    # nope, this line doesn't begin with whitespace.  Is it blank?
    if ($l eq '') {
      # yep, new paragraph please (unless we've already got one)
      $c++ if defined $pars[$c];
      next;
    }

    # $l is just zis line, you know ...  (we have to add space, as
    # trailing space has been stripped, and leading space is
    # forbidden, unless it's empty in which case we don't need any).
    $pars[$c] .= ' ' if defined $pars[$c];
    $pars[$c] .= $l;
  }

  map { untabify($_) } @pars;
}

=head3 untabify

I<exportable>

  $len=length(untabify($str));

Replaces any tabs in the string with spaces, assuming tab stops every
8 characters.

=cut

sub untabify {
  my $str=shift;
  my @sp=split(/\t/,$str);
  my $rv=shift @sp;
  for (@sp) {
    $rv .= ' ' x (8 - (length($rv) % 8)) . $_;
  }
  $rv;
}

=head3 wrapunwrappedtext

I<exportable>

  my @lines=wrapunwraptext(78, @text);

This is a simple text-wrapping call.  As input, it takes a list as
returned by C<unwraptext>, and returns a list of lines for the output
stream.  You probably want C<wraptext> instead.

Paragraph spacing is restored by inserting a blank line between any
pair of paragraphs which are not both verbatim.

The first argument gives the maximum line length, in characters (a
fixed-width font is assumed!)  Any normal paragraphs will be
word-wrapped to go up to this length.  Verbatim lines will be cut at
this point, and a hyphen inserted, before they continue in column zero
on the next line.

If the maximum length is negative, normal paragraphs will be wrapped
as above, but verbatim lines will be left as-is.  If the length is
given as zero, no wrapping will be done, but the correct paragraph
spacing will be applied.

=cut

sub wrapunwrappedtext {
  my $length=shift;
  my @text=@_;
  my @rv;

  if ($length == 0) {
    return @text;
  }

  my $wrapverb=1;
  if ($length < 0) {
    $wrapverb=0;
    $length=-$length;
  }

  for (my $c=0; $c < @text; $c++) {
    my $l=$text[$c];
    if ($length) {
      if ($l =~ m!^\s!) {
	if ($wrapverb) {
	  push @rv, wraptext_verb($length, $l)
	} else {
	  push @rv, $l;
	}
      } else {
	push @rv, wraptext_word($length, $l);
      }
    } else {
      # $length == 0, just do paragraph spacing
      push @rv, $l;
    }
    if (defined $text[$c+1]) {
      # got a following paragraph, so unless both this one and the
      # next one are verbatim, we need a blank line
      unless ($l =~ m!^\s! and $text[$c+1] =~ m!^\s!) {
	push @rv, '';
      }
    }
  }

  @rv;
}

# this routine simply chops after $length and inserts a hyphen.  It
# does not require leading whitespace.
sub wraptext_verb {
  my $length=shift;
  my $line=shift;

  my @rv;
  while (length($line) > $length) {
    push @rv, substr($line, 0, $length-1) . '-';
    $line=substr($line, $length-1);
  }

  push @rv, $line;
  @rv;
}

# a basic word-wrap.
sub wraptext_word {
  my $length=shift;
  my $line=shift;

  # algorithm: we take off a word at a time from the line, preserving
  # whitespace, and see if we can add the word (along with its
  # whitespace) to the current line.  If not, we store the current
  # line, discard the whitespace, and start the next.  We do keep the
  # initial leading whitespace, though.  If this is longer than we
  # want, we'll be discarding it when we have to wrap the first word.
  $line =~ s!^(\s*)!!;
  my @rv=($1);
  my ($ws,$word);
  while (($ws,$word)=($line =~ m!^(\s*)(\S+)!)) {
    $line=substr($line,length($ws.$word));
    if (length($rv[$#rv]) + length($ws.$word) <= $length) {
      # the word, with its leading whitespace, fits on this line
      $rv[$#rv] .= $ws . $word;
      next;
    } else {
      # need to wrap, so discard the whitespace.  We also check that
      # the previous line is not empty, and if it is, discard it.
      # This catches the "too much whitespace at start" problem, as
      # well as the "this word is just too long" problem.
      if ($rv[$#rv] =~ m!^\s*$!) {
	pop @rv;
      }
      if (length($word) <= $length) {
	# the word is sensible!
	push @rv, $word;
      } else {
	# the word is too long for the stated length, so wrap it as
	# verbatim text.
	push @rv, wraptext_verb($length, $word);
      }
    }
  }
  @rv;
}

=head3 wraptext

I<exportable>

  warn join("\n",wraptext(75, $errmsg));
  warn scalar(wraptext(75, $errmsg));

This is the normal entry-point word-wrap function.  It takes as input
text as specified in the introduction to this section, and returns a
list of lines wrapped to the specified length.  If called in a scalar
context, it joins the list together with "\n" and returns that, along
with a trailing "\n".

For example, when run with the text from the C<unwraptext> example
above and a width of 30, this results in:

  This is a paragraph along with
  this line.

  This is another.

    This line is verbatim
    And so this is a new paragr-
  aph.

  This line

    Does not get combined with -
  this line.

  Or this one.

=cut

sub wraptext {
  my $length=shift;
  my @lines=wrapunwrappedtext($length,unwraptext(@_));
  if (wantarray) {
    return @lines;
  } else {
    return join("\n",@lines)."\n";
  }
}

1;
