// fragment.cc
//
//
//   Copyright (C) 2004-2005, 2007-2009 Daniel Burrows
//
//   This program is free software; you can redistribute it and/or
//   modify it under the terms of the GNU General Public License as
//   published by the Free Software Foundation; either version 2 of
//   the License, or (at your option) any later version.
//
//   This program is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//   General Public License for more details.
//
//   You should have received a copy of the GNU General Public License
//   along with this program; see the file COPYING.  If not, write to
//   the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
//   Boston, MA 02111-1307, USA.

#include "fragment.h"
#include <cwidget/generic/util/transcode.h>

#include "config/colors.h"

#include <cstdarg>

#include <algorithm>

#include <cwctype>

using namespace std;

namespace cwidget
{
  fragment::~fragment()
  {
  }

  /** A fragment of text, possibly with attached attributes.  It's
   *  assumed that the text contains no literal newlines.
   */
  class _text_fragment:public fragment
  {
  public:
    _text_fragment(const wstring &_s):s(_s) {}

    fragment_contents layout(size_t firstw, size_t restw,
			     const style &st)
    {
      fragment_contents rval;
      rval.push_back(fragment_line(s, st));
      return rval;
    }

    size_t max_width(size_t first_indent,
		     size_t rest_indent) const
    {
      return first_indent+wcswidth(s.c_str(), s.size());
    }

    size_t trailing_width(size_t first_indent,
			  size_t rest_indent) const
    {
      return first_indent+wcswidth(s.c_str(), s.size());
    }

    bool final_newline() const
    {
      return false;
    }
  private:
    wstring s;
  };

  fragment *text_fragment(const wstring &s)
  {
    std::vector<fragment *> fragments;

    wstring tmp;

    for(size_t i = 0; i < s.size(); ++i)
      {
	if(s[i] == L'\t')
	  tmp.append(8, L' ');
	else if(s[i] == L'\n' || !iswprint(s[i]))
	  {
	    fragments.push_back(new _text_fragment(tmp));

	    if(s[i] == L'\n')
	      fragments.push_back(newline_fragment());
	    else
	      fragments.push_back(style_fragment(new _text_fragment(L"?"),
						 get_style("ERROR")));

	    tmp.clear();
	  }
	else
	  tmp += s[i];
      }

    if(fragments.size() == 0)
      return new _text_fragment(tmp);
    else
      {
	if(!tmp.empty())
	  fragments.push_back(new _text_fragment(tmp));

	return sequence_fragment(fragments);
      }
  }



  fragment *text_fragment(const wstring &s,
			  const style &st)
  {
    return style_fragment(text_fragment(s),
			  st);
  }

  fragment *text_fragment(const string &s,
			  const char *encoding)
  {
    return text_fragment(util::transcode(s, encoding));
  }

  fragment *text_fragment(const string &s,
			  const style &st,
			  const char *encoding)
  {
    return style_fragment(text_fragment(s, encoding),
			  st);
  }

  /** This fragment just expands to a newline. */
  class _newline_fragment:public fragment
  {
  public:
    _newline_fragment() {}

    fragment_contents layout(size_t firstw, size_t restw, const style &st)
    {
      fragment_contents rval;
      rval.set_final_nl(true);
      return rval;
    }

    size_t max_width(size_t first_indent, size_t rest_indent) const
    {
      return first_indent;
    }

    size_t trailing_width(size_t first_indent, size_t rest_indent) const
    {
      return rest_indent;
    }

    bool final_newline() const
    {
      return true;
    }
  };

  fragment *newline_fragment()
  {
    return new _newline_fragment;
  }

  class _style_fragment:public fragment
  {
  public:
    _style_fragment(fragment *_contents, const style &_st)
      :contents(_contents), st(_st)
    {
    }
    ~_style_fragment() {delete contents;}

    fragment_contents layout(size_t firstw, size_t restw,
			     const style &st2)
    {
      return contents->layout(firstw, restw, st2+st);
    }

    size_t max_width(size_t first_indent, size_t rest_indent) const
    {
      return contents->max_width(first_indent, rest_indent);
    }

    size_t trailing_width(size_t first_indent, size_t rest_indent) const
    {
      return contents->trailing_width(first_indent, rest_indent);
    }

    bool final_newline() const
    {
      return contents->final_newline();
    }
  private:
    fragment *contents;
    style st;
  };

  fragment *style_fragment(fragment *f,
			   const style &st)
  {
    return new _style_fragment(f, st);
  }

  /** A base class for fragment containers that supports caching
   *  information about the children.  (yum, premature optimization)
   */
  class fragment_container:public fragment
  {
    void update_width(size_t first_indent,
		      size_t rest_indent) const
    {
      if(width_stale ||
	 first_indent != stale_first_indent ||
	 rest_indent != stale_rest_indent)
	{
	  max_width_cache=calc_max_width(first_indent, rest_indent);
	  trailing_width_cache=calc_trailing_width(first_indent,
						   rest_indent);
	  width_stale=false;
	  stale_first_indent=first_indent;
	  stale_rest_indent=rest_indent;
	}
    }
  public:
    fragment_container()
      :width_stale(true), final_nl_stale(true)
    {
    }

    /** Actually calculate the maximum width. */
    virtual size_t calc_max_width(size_t first_indent,
				  size_t rest_indent) const=0;

    /** Actually calculate the trailing width. */
    virtual size_t calc_trailing_width(size_t first_indent,
				       size_t rest_indent) const=0;

    /** Actually calculate the final-nl status. */
    virtual bool calc_final_newline() const=0;

    size_t max_width(size_t first_indent,
		     size_t rest_indent) const
    {
      update_width(first_indent, rest_indent);
      return max_width_cache;
    }

    size_t trailing_width(size_t first_indent,
			  size_t rest_indent) const
    {
      update_width(first_indent, rest_indent);
      return trailing_width_cache;
    }

    bool final_newline() const
    {
      if(final_nl_stale)
	{
	  final_nl_cache=calc_final_newline();
	  final_nl_stale=false;
	}

      return final_nl_cache;
    }

  private:
    mutable size_t max_width_cache, trailing_width_cache;
    mutable size_t stale_first_indent, stale_rest_indent;
    mutable bool final_nl_cache:1;
    mutable bool width_stale:1, final_nl_stale:1;
  };

  /** A fragment generated by composing a sequence of other fragments. */
  class _sequence_fragment:public fragment_container
  {
  public:
    _sequence_fragment(const vector<fragment*> &_contents):contents(_contents)

    {
    }

    fragment_contents layout(size_t firstw, const size_t restw,
			     const style &st)
    {
      fragment_contents rval;

      rval.push_back(fragment_line(L""));

      for(vector<fragment*>::const_iterator i=contents.begin();
	  i!=contents.end(); ++i)
	{
	  fragment_contents lines=(*i)->layout(firstw, restw, st);

	  // Update firstw appropriately.
	  if(lines.get_final_nl())
	    firstw=restw;
	  else if(lines.size()>0)
	    {
	      int deduct_from;

	      if(lines.size()==1)
		deduct_from=firstw;
	      else
		deduct_from=restw;

	      if(deduct_from>=lines.back().width())
		firstw=deduct_from-lines.back().width();
	      else
		firstw=0;
	    }

	  // Make sure that implicit newlines are handled correctly.
	  if(lines.size()==0)
	    {
	      if(rval.get_final_nl() && lines.get_final_nl())
		rval.push_back(fragment_line(L""));

	      rval.set_final_nl(rval.get_final_nl() || lines.get_final_nl());
	    }

	  for(fragment_contents::const_iterator j=lines.begin();
	      j!=lines.end(); ++j)
	    {
	      if(!rval.get_final_nl())
		{
		  rval.back()+=*j;
		  rval.set_final_nl(true);
		}
	      else
		rval.push_back(*j);
	    }

	  rval.set_final_nl(lines.get_final_nl());
	}

      return rval;
    }

    ~_sequence_fragment()
    {
      for(vector<fragment*>::const_iterator i=contents.begin();
	  i!=contents.end(); ++i)
	delete *i;
    }

    size_t calc_max_width(size_t first_indent,
			  size_t rest_indent) const
    {
      size_t rval=0;
      size_t partial=first_indent;
      for(vector<fragment*>::const_iterator i=contents.begin();
	  i!=contents.end(); ++i)
	{
	  rval=max(rval, (*i)->max_width(partial, rest_indent));

	  if((*i)->final_newline())
	    rval=max(partial, rval);

	  partial=(*i)->trailing_width(partial, rest_indent);
	}

      rval=max(partial, rval);

      return rval;
    }

    size_t calc_trailing_width(size_t first_indent,
			       size_t rest_indent) const
    {
      size_t rval=first_indent;

      for(vector<fragment*>::const_iterator i=contents.begin();
	  i!=contents.end(); ++i)
	rval=(*i)->trailing_width(rval, rest_indent);

      return rval;
    }

    bool calc_final_newline() const
    {
      return !contents.empty() && contents.back()->final_newline();
    }

  private:
    const vector<fragment*> contents;
  };

  fragment *sequence_fragment(const vector<fragment*> &contents)
  {
    return new _sequence_fragment(contents);
  }

  fragment *sequence_fragment(fragment *f, ...)
  {
    vector<fragment*> fragments;

    if(f== nullptr)
      return new _sequence_fragment(fragments);

    fragments.push_back(f);

    va_list lst;

    va_start(lst, f);
    do
      {
	f=va_arg(lst, fragment*);
	if(f!= nullptr)
	  fragments.push_back(f);
      } while(f!= nullptr);

    va_end(lst);

    return new _sequence_fragment(fragments);
  }

  // Fall back on sequence_fragment [for now?]
  fragment *join_fragments(const vector<fragment *> &fragments,
			   const wstring &between)
  {
    vector<fragment*> rval;

    for(vector<fragment *>::const_iterator i=fragments.begin();
	i!=fragments.end();
	++i)
      {
	if(i!=fragments.begin())
	  rval.push_back(text_fragment(between));

	rval.push_back(*i);
      }

    return sequence_fragment(rval);
  }

  class _flowbox:public fragment_container
  {
  public:
    _flowbox(fragment *_contents):contents(_contents) {}

    fragment_contents layout(size_t firstw, const size_t restw,
			     const style &st)
    {
      if(restw==0)
	return fragment_contents();

      fragment_contents rval,lines=contents->layout(firstw, restw,
						    st);

      for(fragment_contents::const_iterator i=lines.begin();
	  i!=lines.end(); ++i)
	{
	  fragment_line s=*i;

	  // Make sure we at least advance the cursor for every line read.
	  //
	  // Is there a less gross way to express this?
	  bool output_something=false;

	  size_t first=0;

	  // Flow the current line:
	  while(first<s.size())
	    {
	      // Strip leading whitespace.
	      while(first<s.size() && iswspace(s[first].ch))
		++first;

	      if(first==s.size())
		break;

	      // If there are few enough characters to fit on a single
	      // line, do that.
	      fragment_line maybe_curr(s, first, s.size()-first);
	      if(maybe_curr.width()<=(signed) firstw)
		{
		  rval.push_back(maybe_curr);
		  firstw=restw;
		  first=s.size();
		  output_something=true;
		}
	      else
		{
		  int chunkw=0;
		  size_t chars=0;
		  while(chunkw<(signed) firstw && chars+first<s.size())
		    {
		      chunkw+=wcwidth(s[first+chars].ch);
		      ++chars;
		    }

		  // Save this for later (see below).  Note that if we
		  // actually overshot the width goal, we need to
		  // possibly strip the last character off (it's
		  // guaranteed to be only one since otherwise we'd have
		  // stopped sooner...)
		  const size_t high_water_mark=chars;
		  const int high_water_width=chunkw;

		  // We pushed the line as far as possible; back up
		  // until we are no longer in the middle of a word AND
		  // the string is short enough.  (we know it's not at
		  // the end of the whole string because of the earlier
		  // test)
		  while(chars>0 && (chunkw>(signed) firstw ||
				    !iswspace(s[first+chars].ch)))
		    {
		      --chars;
		      chunkw-=wcwidth(s[first+chars].ch);
		    }

		  if(chars==0)
		    {
		      // Oops, there's a word that's longer than the
		      // current line.  Push as much as fits onto the
		      // current line.

		      // First, try to exclude any characters that
		      // overlap the right margin.
		      chars=high_water_mark;
		      chunkw=high_water_width;
		      while(chars>0 && chunkw>(signed) firstw)
			{
			  --chars;
			  chunkw-=wcwidth(s[first+chars].ch);
			}

		      // If even that's impossible, go ahead and push a
		      // single character onto the end.  Note that this
		      // means we're probably in such a tiny space that
		      // the result will suck no matter what..
		      if(chars==0)
			chars=1;

		      rval.push_back(fragment_line(s, first, chars));
		      first+=chars;
		      firstw=restw;
		      output_something=true;
		    }
		  else
		    {
		      // Strip trailing whitespace, then `output' the
		      // line.
		      while(chars>0 &&
			    iswspace(s[first+chars-1].ch))
			--chars;

		      rval.push_back(fragment_line(s, first, chars));
		      firstw=restw;
		      first+=chars;
		      output_something=true;
		    }
		}
	    }

	  if(!output_something)
	    {
	      rval.push_back(fragment_line(L""));
	      firstw=restw;
	    }

	  // Ok, go to the next line now.
	}

      // flowboxes always have a final newline.
      rval.set_final_nl(true);

      return rval;
    }

    size_t calc_max_width(size_t first_indent,
			  size_t rest_indent) const
    {
      return contents->max_width(first_indent, rest_indent);
    }

    size_t calc_trailing_width(size_t first_indent,
			       size_t rest_indent) const
    {
      return rest_indent;
    }

    bool calc_final_newline() const
    {
      return true;
    }

    ~_flowbox() { delete contents; }

  private:
    fragment * const contents;
  };

  fragment *flowbox(fragment *contents) {return new _flowbox(contents);}

  class _fillbox:public fragment_container
  {
  public:
    _fillbox(fragment *_contents):contents(_contents) {}

    fragment_contents layout(size_t firstw, size_t restw,
			     const style &st)
    {
      // As far as I know, this is valid everywhere...but it would be
      // rather tricky to write this algorithm without making this
      // assumption.
      eassert(wcwidth(L' ')==1);

      if(restw==0)
	return fragment_contents();

      fragment_contents rval,lines=contents->layout(firstw, restw,
						    st);

      for(fragment_contents::const_iterator i=lines.begin();
	  i!=lines.end(); ++i)
	{
	  fragment_line s=*i;

	  size_t first=0;

	  // Build a list of words on the current line.
	  vector<fragment_line> words;

	  bool output_something=false;

	  while(first<s.size())
	    {
	      // Strip leading whitespace.
	      while(first<s.size() && iswspace(s[first].ch))
		++first;

	      size_t amt=0;
	      while(first+amt<s.size() && !iswspace(s[first+amt].ch))
		++amt;

	      if(amt>0)
		words.push_back(fragment_line(s, first, amt));

	      first+=amt;
	    }

	  // Now place them onto output lines.

	  size_t word_start=0;

	  while(word_start<words.size())
	    {
	      size_t curwidth=0;
	      size_t nwords=0;

	      // As long as adding the *next* word doesn't put us
	      // past the right edge, add it.
	      while(word_start+nwords < words.size() &&
		    curwidth+words[word_start+nwords].width()+nwords <= firstw)
		{
		  curwidth+=words[word_start+nwords].width();
		  ++nwords;
		}

	      if(nwords==0)
		{
		  // Split a single word: just chop the beginning off.
		  size_t chars=0;
		  fragment_line &word=words[word_start];
		  while(chars<word.size() && curwidth<firstw)
		    {
		      curwidth+=wcwidth(word[chars].ch);
		      ++chars;
		    }

		  while(chars>0 && curwidth>firstw)
		    {
		      --chars;
		      curwidth-=wcwidth(word[chars].ch);
		    }

		  if(chars==0)
		    chars=1;

		  rval.push_back(fragment_line(words[word_start], 0, chars));
		  words[word_start]=fragment_line(words[word_start], chars);
		  firstw=restw;
		  output_something=true;
		}
	      else
		{
		  size_t diff;

		  if(word_start+nwords<words.size())
		    diff=firstw-(curwidth+nwords-1);
		  else
		    // Cheat to disable filling on the last line of the
		    // paragraph.
		    diff=0;

		  // Now spit the words into an output string, filled
		  // left and right.
		  fragment_line final(L"");

		  // This is similar to the famous algorithm for drawing
		  // a line.  The idea is to add diff/(words-1) spaces
		  // (in addition to one space per word); since
		  // fractional spaces aren't allowed, I approximate by
		  // adding a number of spaces equal to the integral
		  // part, then keeping the remainder for the next word.
		  size_t extra_spaces=0;

		  for(size_t word=0; word<nwords; ++word)
		    {
		      if(word>0)
			// Insert spaces between words:
			{
			  extra_spaces+=diff;

			  size_t nspaces=1+extra_spaces/(nwords-1);
			  extra_spaces%=nwords-1;

			  final+=fragment_line(nspaces, L' ', st.get_attrs());
			}

		      final+=words[word+word_start];
		    }

		  output_something=true;
		  rval.push_back(final);
		  firstw=restw;

		  word_start+=nwords;
		}
	    }

	  if(!output_something)
	    {
	      rval.push_back(fragment_line(L""));
	      firstw=restw;
	    }
	}

      // fillboxes always have a final newline.
      rval.set_final_nl(true);

      return rval;
    }

    size_t calc_max_width(size_t first_indent,
			  size_t rest_indent) const
    {
      return contents->max_width(first_indent, rest_indent);
    }

    size_t calc_trailing_width(size_t first_indent,
			       size_t rest_indent) const
    {
      return rest_indent;
    }

    bool calc_final_newline() const
    {
      return true;
    }

    ~_fillbox() { delete contents; }

  private:
    fragment * const contents;
  };

  fragment *fillbox(fragment *contents) {return new _fillbox(contents);}

  class _hardwrapbox:public fragment_container
  {
  public:
    _hardwrapbox(fragment *_contents):contents(_contents) {}

    ~_hardwrapbox() {delete contents;}

    fragment_contents layout(size_t firstw, const size_t restw,
			     const style &st)
    {
      if(restw==0)
	return fragment_contents();

      fragment_contents rval, lines=contents->layout(firstw, restw,
						     st);

      for(fragment_contents::const_iterator i=lines.begin();
	  i!=lines.end(); ++i)
	{
	  if(i->empty())
	    {
	      rval.push_back(fragment_line(L""));
	      firstw=restw;
	    }
	  else
	    {
	      fragment_line s=*i;
	      fragment_line::size_type start=0;

	      while(start<s.size())
		{
		  size_t chars=0;
		  int width=0;

		  while(width<(signed) firstw && start+chars<s.size())
		    {
		      width+=wcwidth(s[start+chars].ch);
		      ++chars;
		    }

		  // If we spilled over, it's the last character that's
		  // responsible.
		  if(width>(signed) firstw && chars>1)
		    --chars;

		  rval.push_back(fragment_line(s, start, chars));
		  start+=chars;
		  firstw=restw;
		}
	    }
	}

      rval.set_final_nl(true);

      return rval;
    }

    size_t calc_max_width(size_t first_indent, size_t rest_indent) const
    {
      return contents->max_width(first_indent, rest_indent);
    }

    size_t calc_trailing_width(size_t first_indent, size_t rest_indent) const
    {
      return rest_indent;
    }

    bool calc_final_newline() const
    {
      return true;
    }

  private:
    fragment * const contents;
  };

  fragment *hardwrapbox(fragment *contents)
  {
    return new _hardwrapbox(contents);
  }

  class _clipbox:public fragment_container
  {
  public:
    _clipbox(fragment *_contents):contents(_contents) {}

    fragment_contents layout(size_t firstw, const size_t restw,
			     const style &st)
    {
      fragment_contents rval, lines=contents->layout(firstw, restw,
						     st);

      for(fragment_contents::const_iterator i=lines.begin(); i!=lines.end(); ++i)
	{
	  size_t chars=0;
	  int width=0;

	  while(width<(signed) firstw && chars<i->size())
	    {
	      width+=wcwidth((*i)[chars].ch);
	      ++chars;
	    }

	  if(width>(signed) firstw && chars>1)
	    --chars;

	  rval.push_back(fragment_line(*i, 0, chars));
	  firstw=restw;
	}

      // Clipboxes are always followed by a final newline.
      rval.set_final_nl(true);

      return rval;
    }

    size_t calc_max_width(size_t first_indent,
			  size_t rest_indent) const
    {
      return contents->max_width(first_indent, rest_indent);
    }

    size_t calc_trailing_width(size_t first_indent,
			       size_t rest_indent) const
    {
      return rest_indent;
    }

    bool calc_final_newline() const
    {
      return true;
    }

    ~_clipbox() { delete contents; }

  private:
    fragment *contents;
  };

  fragment *clipbox(fragment *contents) {return new _clipbox(contents);}

  class _indentbox:public fragment_container
  {
  public:
    _indentbox(size_t _firstindent, size_t _restindent, fragment *_contents)
      :contents(_contents), firstindent(_firstindent),
       restindent(_restindent) {}

    fragment_contents layout(size_t firstw, size_t restw,
			     const style &st)
    {
      if(restw<=restindent)
	return fragment_contents();

      fragment_line firstprepend(firstindent, L' ', st.get_attrs());
      fragment_line restprepend(restindent, L' ', st.get_attrs());

      firstprepend.apply_style(st);
      restprepend.apply_style(st);

      size_t child_firstw=firstw>=firstindent?firstw-firstindent:0;
      size_t child_restw=restw>=restindent?restw-restindent:0;

      fragment_contents rval, lines=contents->layout(child_firstw,
						     child_restw,
						     st);

      for(fragment_contents::const_iterator i=lines.begin(); i!=lines.end(); ++i)
	{
	  fragment_line l=((i==lines.begin())?firstprepend:restprepend)+*i;

	  rval.push_back(l);
	}

      // Indentboxes are always followed by a final newline.
      rval.set_final_nl(true);

      return rval;
    }

    size_t calc_max_width(size_t my_first_indent,
			  size_t my_rest_indent) const
    {
      return contents->max_width(my_first_indent+firstindent,
				 my_rest_indent+restindent);
    }

    size_t calc_trailing_width(size_t my_first_indent,
			       size_t my_rest_indent) const
    {
      return my_rest_indent+restindent;
    }

    bool calc_final_newline() const
    {
      return true;
    }

    ~_indentbox() { delete contents; }
  private:
    fragment *contents;

    size_t firstindent, restindent;
  };

  fragment *indentbox(size_t firstindent,
		      size_t restindent,
		      fragment *contents)
  {
    return new _indentbox(firstindent, restindent, contents);
  }

  fragment *dropbox(fragment *header, fragment *contents)
  {
    const int indent = header->max_width(0, 0);

    return indentbox(0, indent, sequence_fragment(header, contents, NULL));
  }

  class _fragment_columns : public fragment_container
  {
    vector<fragment_column_entry> columns;

    void update_widths(vector<size_t> &widths,
		       size_t w) const
    {
      size_t total = 0, denominator = 0;
      bool contains_resizable = false;

      for(size_t i = 0; i < columns.size(); ++i)
	{
	  const fragment_column_entry &col(columns[i]);
	  if(col.proportional)
	    {
	      widths[i]    = 0;
	      denominator += columns[i].width;
	    }
	  else
	    {
	      widths[i] = columns[i].width;

	      if(col.expandable)
		{
		  for(std::vector<fragment *>::const_iterator line_it =
			col.lines.begin();
		      line_it != col.lines.end();
		      ++line_it)
		    {
		      fragment *f = *line_it;
		      if(f != NULL)
			{
			  contains_resizable = true;
			  widths[i] = std::max(widths[i],
					       f->max_width(0, 0));
			}
		    }
		}

	      total += widths[i];
	    }
	}

      if(total < w && denominator > 0)
	{
	  size_t remainder = w - total;

	  for(size_t i = 0; i < columns.size(); ++i)
	    if(columns[i].proportional)
	      {
		widths[i]   += (remainder * columns[i].width) / denominator;
		denominator -= columns[i].width;
		remainder   -= widths[i];
	      }
	}
      else if(total > w && contains_resizable)
	{
	  // How much larger columns are than their minimum amounts.
	  int avail_shrinkage = 0;
	  size_t worst_case_shrinkage = 0;
	  for(size_t i = 0; i < columns.size(); ++i)
	    if(columns[i].expandable)
	      {
		avail_shrinkage      += widths[i] - columns[i].width;
		worst_case_shrinkage += widths[i] - 1;
	      }

	  if(total - avail_shrinkage <= w)
	    {
	      int needed_shrinkage = total - w;
	      for(size_t i = 0; i < columns.size(); ++i)
		{
		  // Shrink proprtionally to how much this was expanded.
		  const int expansion = widths[i] - columns[i].width;

		  int shrinkamt;
		  if(avail_shrinkage > 0)
		    shrinkamt = (expansion * needed_shrinkage) / avail_shrinkage;
		  else
		    shrinkamt = 0; // Give up if we ran out of space
 		                   // for some reason.

		  avail_shrinkage  -= expansion;
		  needed_shrinkage -= shrinkamt;
		  widths[i]        -= shrinkamt;
		}
	    }
	  else
	    {
	      // Erk.  Shrink the expandable columns as much as
	      // necessary, to a minimum of 1 char per column. (whatever
	      // we do here will suck anyway, so..)
	      size_t needed_shrinkage = std::min(worst_case_shrinkage,
						 total - w);
	      for(size_t i = 0; i < columns.size(); ++i)
		{
		  const int expansion = widths[i] - 1;
		  int shrinkamt;

		  // We can end up with no shrinkage at all possible
		  // if the minimum widths are too large to fit.  In
		  // that case, just make everything 1 character wide
		  // until we've shrunk enough.  Otherwise, shrink
		  // proportionally to how much this was expanded.

		  if(avail_shrinkage > 0)
		    shrinkamt = (expansion * needed_shrinkage) / avail_shrinkage;
		  else
		    shrinkamt = std::min<std::size_t>(expansion, needed_shrinkage);

		  avail_shrinkage  -= expansion;
		  needed_shrinkage -= shrinkamt;
		  widths[i]        -= shrinkamt;
		}
	    }
	}

      // Clip.
      for(size_t i = 0; i < columns.size(); ++i)
	{
	  widths[i] = min<int>(w, widths[i]);
	  w        -= widths[i];
	}
    }
  public:
    _fragment_columns(const vector<fragment_column_entry> &_columns)
      :columns(_columns)
    {
    }

    ~_fragment_columns()
    {
      for(vector<fragment_column_entry>::const_iterator
	    i = columns.begin(); i != columns.end(); ++i)
	{
	  for(std::vector<fragment *>::const_iterator line_it =
		i->lines.begin(); line_it != i->lines.end(); ++line_it)
	    delete *line_it;
	}
    }

  private:
    fragment *get_column_line(size_t colnum, size_t linenum) const
    {
      const std::vector<fragment *>& line(columns[colnum].lines);
      if(linenum < line.size())
	return line[linenum];
      else
	return NULL;
    }

    // Build the kth line as a contents structure.
    //
    // Note: the interface is optimized for clarity; for efficiency we
    // would output directly to the final location instead of building a
    // temporary copy.
    fragment_contents make_line(size_t linenum,
				const vector<size_t> &widths,
				const style &st) const
    {
      vector<fragment_contents> child_layouts(columns.size());

      for(size_t i = 0; i < columns.size(); ++i)
	{
	  fragment *f = get_column_line(i, linenum);
	  if(f != NULL)
	    child_layouts[i]    = f->layout(widths[i], widths[i], st);
	}

      // Figure out how to align the lines: find the height of this
      // table row and adjust positions accordingly.
      size_t height = 0;
      for(size_t i = 0; i < columns.size(); ++i)
	if(child_layouts[i].size() > height)
	  height = child_layouts[i].size();

      vector<size_t> starting_lines(columns.size());

      for(size_t i = 0; i < columns.size(); ++i)
	{
	  switch(columns[i].vert_align)
	    {
	    case fragment_column_entry::top:
	      starting_lines[i] = 0;
	      break;

	    case fragment_column_entry::center:
	      starting_lines[i] = (height - child_layouts[i].size()) / 2;
	      break;

	    case fragment_column_entry::bottom:
	      starting_lines[i] = height - child_layouts[i].size();
	      break;
	    }
	}

      fragment_contents rval;
      for(size_t y = 0; y < height; ++y)
	{
	  fragment_line tmp(L"");

	  for(size_t i = 0; i < columns.size(); ++i)
	    if(get_column_line(i, linenum) != NULL &&
	       y >= starting_lines[i] &&
	       y < starting_lines[i] + child_layouts[i].size())
	      {
		fragment_line &s = child_layouts[i][y - starting_lines[i]];
		if((unsigned)s.width() > widths[i])
		  s.erase(widths[i], s.size());
		tmp += s;

		if(widths[i] > (unsigned)s.width())
		  tmp += fragment_line(widths[i] - s.width(), L' ',
				       st.get_attrs());
	      }
	    else
	      tmp += fragment_line(widths[i], L' ', st.get_attrs());

	  rval.push_back(tmp);
	}

      rval.set_final_nl(true);

      return rval;
    }

  public:
    fragment_contents layout(size_t firstw, size_t restw, const style &st)
    {
      eassert(firstw == restw);

      vector<size_t> widths(columns.size());
      update_widths(widths, restw);

      size_t num_lines = 0;
      for(std::vector<fragment_column_entry>::const_iterator
	    it = columns.begin(); it != columns.end(); ++it)
	num_lines = std::max(num_lines, it->lines.size());

      fragment_contents rval;
      for(size_t i = 0; i < num_lines; ++i)
	{
	  fragment_contents curr = make_line(i, widths, st);

	  if(curr.size() == 0 && curr.get_final_nl())
	    rval.push_back(fragment_line(L""));
	  else
	    {
	      for(fragment_contents::const_iterator curr_it = curr.begin();
		  curr_it != curr.end(); ++curr_it)
		rval.push_back(*curr_it);
	    }
	}

      rval.set_final_nl(true);

      return rval;
    }

    size_t calc_max_width(size_t first_indent, size_t rest_indent) const
    {
      eassert(first_indent == rest_indent);

      size_t rval = 0;

      for(vector<fragment_column_entry>::const_iterator
	    i = columns.begin(); i != columns.end(); ++i)
	{
	  size_t thisw = 0;
	  for(std::vector<fragment *>::const_iterator
		line_it = i->lines.begin();
	      line_it != i->lines.end();
	      ++line_it)
	    {
	      // Start by making the line large enough to hold its
	      // content.
	      size_t line_width = 0;
	      if((*line_it) != NULL)
		line_width = std::max(thisw,
				      (*line_it)->max_width(first_indent,
							    rest_indent));

	      // If this line doesn't have content or it can be
	      // expanded, make its width at least the declared
	      // column width.
	      if(!i->proportional &&
		 ((*line_it) == NULL ||
		  (i->expandable && line_width < i->width)))
		line_width = std::max(line_width, i->width);

	      // Expand the column to cover this line.
	      thisw = std::max(thisw, line_width);
	    }

	  rval += thisw;

	  if(first_indent < thisw)
	    first_indent = 0;
	  else
	    first_indent -= thisw;

	  if(rest_indent < thisw)
	    rest_indent = 0;
	  else
	    rest_indent -= thisw;
	}

      return rval;
    }

    size_t calc_trailing_width(size_t first_indent, size_t rest_indent) const
    {
      eassert(first_indent == rest_indent);

      return rest_indent;
    }

    bool calc_final_newline() const
    {
      return true;
    }
  };

  fragment *fragment_columns(const vector<fragment_column_entry> &columns)
  {
    return new _fragment_columns(columns);
  }

  struct argument
  {
    argument():format(0) {}

    char format;
    /** If \b true, the 'l' modifier was attached to this argument. */
    bool islong:1;
    union
    {
      fragment *F;
      const char *s;
      const wchar_t *ls;
      int attr;
    };
  };

  // hack
  string char_to_str(char code)
  {
    string s;

    if(isprint(code))
      s+=code;
    else
      {
	char buf[64];

	snprintf(buf, 64, "\\%d", code);
	s+=buf;
      }

    return s;
  }

  fragment *fragf(const char *format, ...)
  {
    int argcount=0;
    int posargcount=0;

    const char *start=format;
    // find all the arguments.
    const char *nextpercent=strchr(start, '%');

    // loop 1: count the arguments.
    while(nextpercent!= nullptr)
      {
	if(*(nextpercent+1)=='l')
	  ++nextpercent;

	switch(*(nextpercent+1))
	  {
	  case '0':
	  case '1':
	  case '2':
	  case '3':
	  case '4':
	  case '5':
	  case '6':
	  case '7':
	  case '8':
	  case '9':
	    {
	      char *endptr;

	      int pos=strtol(nextpercent+1, &endptr, 10);

	      if(*endptr!='$' || (*(endptr+1)!='F' && *(endptr+1)!='s'))
		{
		  string s="Internal error: bad character in positional argument: '"+char_to_str(*endptr)+"'";

		  return text_fragment(s, get_style("Error"));
		}

	      posargcount=max(posargcount, pos);

	      start=endptr+2;
	    }
	    break;

	  case 'B':
	  case 'b':
	  case 'R':
	  case 'r':
	  case 'D':
	  case 'd':
	  case 'n':
	  case 'N':
	  case '%':
	    start=nextpercent+2;
	    break;
	  case 's':
	  case 'F':
	  case 'A':
	  case 'C':
	  case 'S':
	    ++argcount;
	    start=nextpercent+2;
	    break;
	  default:
	    return text_fragment("Internal error: bad format string code '"+char_to_str(*(nextpercent+1))+"'",
				 get_style("Error"));
	  }

	nextpercent=strchr(start, '%');
      }

    int nargs=max(argcount, posargcount);

    argument arguments[nargs];

    argcount=0;

    // loop 2: read the list of arguments and parse their type.
    start=format;
    nextpercent=strchr(start, '%');
    while(nextpercent!= nullptr)
      {
	bool islong=false;
	if(*(nextpercent+1)=='l')
	  {
	    islong=true;
	    ++nextpercent;
	  }
      
	switch(*(nextpercent+1))
	  {
	  case '0':
	  case '1':
	  case '2':
	  case '3':
	  case '4':
	  case '5':
	  case '6':
	  case '7':
	  case '8':
	  case '9':
	    {
	      char *endptr;

	      int pos=strtol(nextpercent+1, &endptr, 10)-1;

	      // if we saw it before it had better be the same type.
	      if(arguments[pos].format!=0 &&
		 (arguments[pos].format!=*(endptr+1) ||
		  arguments[pos].islong!=islong))
		return text_fragment("Bad argument string to fragf: inconsistent positional parameter types!",
				     get_style("Error"));

	      arguments[pos].format=*(endptr+1);

	      start=endptr+2;
	    }
	    break;

	  case 'B':
	  case 'b':
	  case 'R':
	  case 'r':
	  case 'D':
	  case 'd':
	  case 'n':
	  case 'N':
	  case '%':
	    start=nextpercent+2;
	    break;
	  case 's':
	  case 'F':
	  case 'S':
	    if(arguments[argcount].format!=0 && arguments[argcount].format!=*(nextpercent+1))
	      return text_fragment("Bad argument string to fragf: inconsistent parameter types!",
				   get_style("Error"));

	    arguments[argcount].format=*(nextpercent+1);

	    ++argcount;
	    start=nextpercent+2;
	    break;
	  default:
	    return text_fragment("Internal error: bad format string code '"+char_to_str(*(nextpercent+1))+"'",
				 get_style("Error"));
	  }

	nextpercent=strchr(start, '%');
      }

    style st;

    va_list arglst;
    va_start(arglst, format);

    for(int i=0; i<nargs; ++i)
      {
	switch(arguments[i].format)
	  {
	  case 0:
	    break; // I suppose unused arguments are ok
	  case 'F':
	    arguments[i].F=va_arg(arglst, fragment *);
	    break;
	  case 's':
	    arguments[i].s=va_arg(arglst, const char *);
	    break;
	  case 'S':
	    arguments[i].s=va_arg(arglst, const char *);
	    break;
	  default:
	    return text_fragment("Internal error: bad argument format '"+char_to_str(arguments[i].format)+"'",
				 get_style("Error"));
	  }
      }

    va_end(arglst);

    // Now parse this and generate a result.
    vector<fragment *> rval;

    start=format;
    nextpercent=strchr(start, '%');

    // Current argument for non-positional arguments.
    argcount=0;

    // Optimization: don't create lots of unnecessary text fragments
    // when one will do.
    string curstr("");

    // Loop 3: execute the program

    while(nextpercent!= nullptr)
      {
	bool islong=false;

	// First, make a fragment for everything until the percent:
	curstr+=string(start, nextpercent-start);

	if(*(nextpercent+1)=='l')
	  {
	    islong=true;
	    ++nextpercent;
	  }

	// This is almost always what we want; in the cases when it's not,
	// I override it explicitly.
	start=nextpercent+2;

	// Now, find what's at the percent.
	switch(*(nextpercent+1))
	  {
	  case '%':
	    curstr+="%";
	    break;
	  case 'B':
	  case 'b':
	  case 'R':
	  case 'r':
	  case 'D':
	  case 'd':
	    if(!curstr.empty())
	      {
		rval.push_back(text_fragment(curstr, st));
		curstr="";
	      }
	    switch(*(nextpercent+1))
	      {
	      case 'B':
	      case 'b':
		st.attrs_flip(A_BOLD);
		break;
	      case 'R':
	      case 'r':
		st.attrs_flip(A_REVERSE);
		break;
	      case 'D':
	      case 'd':
		st.attrs_flip(A_DIM);
		break;
	      }
	    break;
	  case 'n':
	    if(!curstr.empty())
	      {
		rval.push_back(text_fragment(curstr, st));
		curstr="";
	      }

	    rval.push_back(newline_fragment());
	    break;
	  case 'N':
	    if(!curstr.empty())
	      {
		rval.push_back(text_fragment(curstr, st));
		curstr="";
	      }
	    st=style();
	    break;
	  case 'F':
	    // should have been verified above.
	    eassert(arguments[argcount].format=='F');

	    if(!curstr.empty())
	      {
		rval.push_back(text_fragment(curstr, st));
		curstr="";
	      }
	    rval.push_back(arguments[argcount].F);
	    ++argcount;
	    break;
	  case 's':
	    // should have been verified above.
	    eassert(arguments[argcount].format=='s');

	    if(islong)
	      curstr += util::transcode(arguments[argcount].ls);
	    else
	      curstr+=arguments[argcount].s;
	    ++argcount;
	    break;
	  case 'S':
	    eassert(arguments[argcount].format=='S');

	    if(!curstr.empty())
	      {
		rval.push_back(text_fragment(curstr, st));
		curstr="";
	      }

	    st+=get_style(arguments[argcount].s);
	    ++argcount;
	    break;
	  case '0':
	  case '1':
	  case '2':
	  case '3':
	  case '4':
	  case '5':
	  case '6':
	  case '7':
	  case '8':
	  case '9':
	    {
	      char *endptr;

	      int pos=strtol(nextpercent+1, &endptr, 10)-1;

	      eassert(arguments[pos].format==*(endptr+1));

	      switch(*(endptr+1))
		{
		case 'F':
		  if(!curstr.empty())
		    {
		      rval.push_back(text_fragment(curstr, st));
		      curstr="";
		    }

		  rval.push_back(arguments[pos].F);
		  break;
		case 's':
		  curstr+=arguments[pos].s;
		  break;
		case 'S':
		  if(!curstr.empty())
		    {
		      rval.push_back(text_fragment(curstr, st));
		      curstr="";
		    }

		  st+=get_style(arguments[pos].s);
		  break;
		}

	      start=endptr+2;
	      break;
	    }
	  }

	nextpercent=strchr(start, '%');
      }

    // Get any trailing bit o' string:
    if(*start!=0)
      curstr+=start;

    if(!curstr.empty())
      rval.push_back(text_fragment(curstr, st));

    return sequence_fragment(rval);
  }
}

