
// The wordwrapper wraps words by using a space character as seperator only.
// The details of word-wrapping are not specified in the Z-Machine
// specification, so I'll not designate other characters than the space
// as seperators.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "wordwrap.h"
#include "tracelog.h"
#include "fizmo.h"
#include "types.h"
#include "i18n.h"
#include "z_ucs.h"
#include "splint.h"


static z_ucs wordwrap_newline_string[] = { Z_UCS_NEWLINE, 0 };


// "force_empty_buffer" will flush everything. This should only be set if
// a flush is required due to an input action or some other event that
// requires to have really everything dumped to screen. During processing
// a force_empty_buffer may be problematic in case we're at the right
// border of the screen and can't yet know if the rightmost contents of the
// buffer contain a yet unfinished word.
// Example: "(76 chars)with". Force-flushing here is okay if a space is
// following, but not if an "out" comes next ("without").
static void wordwrap_flush_buffer(WORDWRAP *wrapper, bool force_empty_buffer)
{
  z_ucs *output_ptr;

  // The "end_of_string" stores the current end of line.
  z_ucs *end_of_string;

  // "end_of_sting_buffer" stores the char we're currently overwriting
  // with a null terminator.
  z_ucs end_of_string_buffer;

  int len = 0;
  z_ucs *pos;
  int metadata_output_index;
  z_ucs metadata_char_buffer;
  struct wordwrap_metadata *metadata_entry;
  bool add_newline, add_padding = false;
  bool wrapping_finished = false;
  int index_shift;

  if ((wrapper == NULL) || (wrapper->output_buffer == NULL))
    return;

  // Since the output buffer may be larger than the line, we might have
  // to output multiple lines. During the loop over the lines, the
  // "output_ptr" stores the beginning of the current line. We start at
  // the beginning of the output buffer.
  output_ptr = wrapper->output_buffer;

  // In order to make locating the end of the output easier, we terminate
  // the buffer with a null char.
  // output_buffer_index may be referenced even if it's NULL since we're
  // writing to it.
  /*@-nullderef@*/
  *wrapper->output_buffer_index = 0;
  /*@-nullderef@*/

  /*
  if ( (*output_ptr != 0) && (wrapper->has_received_any_output == false) )
  {
    TRACE_LOG("First output.\n");
    wordwrap_output_left_padding(wrapper);
    wrapper->has_received_any_output = true;
  }
  */

  TRACE_LOG("Flushing word-wrap-buffer %p with '", wrapper);
  // splint doesn't seem to support annotations for variadic parameters
  TRACE_LOG_Z_UCS(/*@-ownedtrans@*/ wrapper->output_buffer /*@+ownedtrans@*/);
  TRACE_LOG("'.\n");

  TRACE_LOG("Current line-index:%d.\n", wrapper->line_index);

  while ( (*output_ptr != 0) && (wrapping_finished == false) )
  {
    add_newline = false;

    if (
        (
         (wrapper->newline_was_inserted == true)
         &&
         (*output_ptr == Z_UCS_SPACE)
         )
        ||
        (
         (wrapper->last_line_was_completely_filled == true)
         &&
         (*output_ptr == Z_UCS_NEWLINE)
        )
       )
    {
      output_ptr++;
      end_of_string = output_ptr;
      end_of_string_buffer = *end_of_string;
      *end_of_string = 0;
      wrapper->last_line_was_completely_filled = false;
    }
    else
    {
      wrapper->last_line_was_completely_filled = false;
      // Check if there's a newline in the string.
      if ((pos = z_ucs_chr(output_ptr, Z_UCS_NEWLINE)) != NULL)
      {
        pos++;
        len = (size_t)(pos - output_ptr);

        if (wrapper->line_index + len - 1 == wrapper->line_length)
        {
          wrapper->last_line_was_completely_filled = true;
          pos--;
          len-=1;
        }
      }

      TRACE_LOG("len:%d\n", (int)len);
      // If there is a newline, and the whole string fits onto the line, we
      // can simply flush everything up to the newline character.
      if (
          (pos != NULL)
          &&
          (len < wrapper->line_length - wrapper->line_index)
         )
      {
        end_of_string = pos;
        end_of_string_buffer = *end_of_string;
        *end_of_string = 0;
        wrapper->line_index = 0;
        add_padding = true;
      }
      else
      {
        // If there's no newline to break at, we'll have to find a point to
        // wrap in the current line.

        len = z_ucs_len(output_ptr);
        TRACE_LOG("len:%d\n", (int)len);

        // In case we have at least one more(!) character than the line-length,
        // we can safely output up until the last space or output a full,
        // "hard-broken" line in case a word is longer than the line.
        if (len > wrapper->line_length - wrapper->line_index)
        {
          add_newline = true;

          // If we have more chars, we'll have to wrap at the last space in
          // the line.

          TRACE_LOG("Output longer than line (index at %d).\n",
              wrapper->line_index);
          // First, we'll verify if there's a space directly following the
          // end-of-line. That's legal, since we know we have at least one
          // more character than the line is long.
          end_of_string
            = output_ptr + wrapper->line_length - wrapper->line_index;

          if (
              (*end_of_string == Z_UCS_SPACE)
              ||
              (*end_of_string == Z_UCS_NEWLINE)
             )
          {
            end_of_string_buffer = *end_of_string;
            *end_of_string = 0;
            wrapper->line_index = (int)wrapper->line_length;
            if (bool_equal(wrapper->add_newline_after_full_line, false))
              add_padding = true;
          }
          else
          {
            end_of_string_buffer = *end_of_string;
            *end_of_string = 0;

            TRACE_LOG("Splitting flush for %p: '", end_of_string);
            // splint doesn't seem to support annotations for variadic
            // parameters
            TRACE_LOG_Z_UCS(/*@-ownedtrans@*/ output_ptr /*@+ownedtrans@*/);
            TRACE_LOG("'\n");

            pos = z_ucs_rchr(output_ptr, Z_UCS_SPACE);

            if ( (pos != NULL) && (pos != end_of_string) )
            {
              // We've successfully found a space in the given line, so we can
              // adjust the end-of-string-marker.
              *end_of_string = end_of_string_buffer;

              end_of_string = pos;
              end_of_string_buffer = *end_of_string;
              *end_of_string = 0;

              TRACE_LOG("Found space for split: '");
              // splint doesn't seem to support annotations for variadic
              // parameters
              TRACE_LOG_Z_UCS(/*@-ownedtrans@*/ output_ptr /*@+ownedtrans@*/);
              TRACE_LOG("'\n\n");

              wrapper->line_index += z_ucs_len(output_ptr);

              if (
                  (wrapper->line_index == wrapper->line_length)
                  &&
                  (bool_equal(wrapper->add_newline_after_full_line, true))
                 )
              {
                TRACE_LOG("addnewline\n");
                add_newline = true;
              }

              //wrapper->line_index += pos - output_ptr;
              wrapper->line_index = 0;
            }
            else
            {
              TRACE_LOG("No space for split.\n");
              // If there was no space in the line we terminate the string
              // inside the word hitting the right border.
              wrapper->line_index = (int)wrapper->line_length;
            }

            TRACE_LOG("li: %d.\n", wrapper->line_index);
          }
        }
        else
        {
          // If we have less chars than the line-length, we must only dump
          // everything if we're forced to.
          if (bool_equal(force_empty_buffer, true))
          {
            end_of_string = output_ptr + len;
            end_of_string_buffer = 0;
            wrapper->line_index += len;

            // FIXME: In case a full buffer is flushed and we've filled the
            // line, "last_line_was_completely_filled" should be set.
            //wrapper->last_line_was_completely_filled = false;
          }
          else
          {
            // If we're not forced to clean the buffer, we'll just flush
            // everything up to the last space.
            pos = z_ucs_rchr(output_ptr, Z_UCS_SPACE);

            if (pos == NULL)
            {
              pos = output_ptr;
            }

            end_of_string = pos;
            end_of_string_buffer = *end_of_string;
            *end_of_string = 0;
            wrapper->line_index += z_ucs_len(output_ptr);

            TRACE_LOG("add-newline-check: %d, %d.\n",
                wrapper->line_index, wrapper->line_length);

            if (
                (wrapper->line_index == wrapper->line_length)
                &&
                (bool_equal(wrapper->add_newline_after_full_line, true))
               )
            {
              TRACE_LOG("addnewline\n");
              add_newline = true;
            }

            wrapping_finished = true;
          }
        }
      }
    }

    wrapper->newline_was_inserted = false;

    // We have to check for metadata in the current line. In case we find
    // anything we've stored before the end of the line, we have to split
    // the output and write metadata accordingly.
    if (wrapper->metadata_index != 0)
    {
      for (
          metadata_output_index = 0;
          metadata_output_index < wrapper->metadata_index;
          metadata_output_index++)
      {
        TRACE_LOG("Processing metadata entry #%d.\n", metadata_output_index);

        metadata_entry = &(wrapper->metadata[metadata_output_index]);

        TRACE_LOG("Metadata-entry at %p.\n", metadata_entry);
        TRACE_LOG("end-of-string at %p.\n", end_of_string);

        // If the current metadata entry index is after our current
        // output end, we can quit the loop.
        if (metadata_entry->output_index > end_of_string)
        {
          break;
        }

        if (metadata_entry->output_index != output_ptr)
        {
          // In case there's some output before the metadata, we'll
          // split the output and flush the first part.
          metadata_char_buffer = *(metadata_entry->output_index);
          *(metadata_entry->output_index) = 0;

          wrapper->wrapped_text_output_destination(
              output_ptr,
              wrapper->destination_parameter);

          *(metadata_entry->output_index) = metadata_char_buffer;
          output_ptr = metadata_entry->output_index;
        }

        // Once we have written everything that's supposed to appear before
        // the metadata, we'll write the metadata itself.
        metadata_entry->metadata_output_function(
            metadata_entry->ptr_parameter, metadata_entry->int_parameter);
      }

      if (metadata_output_index == wrapper->metadata_index)
      {
        // In case all metadata was output, we can simply reset the pointer.
        wrapper->metadata_index = 0;
      }
      else
      {
        // In case only some was output, we shift the rest to the left.
        memmove(
            // wrapper->metadata must be != NULL since the metadata_output_index
            // is set accordingly.
            /*@-nullpass@*/ wrapper->metadata /*@+nullpass@*/,
            &(wrapper->metadata[metadata_output_index]),
            (wrapper->metadata_index - metadata_output_index)
            * sizeof(struct wordwrap_metadata));

        wrapper->metadata_index -= metadata_output_index;
      }

      // Only output the rest of the buffer if there's actually something
      // left to print.
      if (output_ptr != end_of_string)
      {
        wrapper->wrapped_text_output_destination(
            output_ptr,
            wrapper->destination_parameter);
      }
    }
    else
    {
      wrapper->wrapped_text_output_destination(
          output_ptr,
          wrapper->destination_parameter);
    }

    TRACE_LOG("li: %d / ll: %d\n", (int)wrapper->line_index,
        (int)wrapper->line_length);

    if (bool_equal(add_newline, true))
    {
      if (
          (wrapper->line_index != (int)wrapper->line_length)
          ||
          (bool_equal(wrapper->add_newline_after_full_line, true))
         )
      {
        TRACE_LOG("Adding newline.\n");

        wrapper->wrapped_text_output_destination(
            wordwrap_newline_string,
            wrapper->destination_parameter);

        wordwrap_output_left_padding(wrapper);
      }

      wrapper->line_index = 0;
      wrapper->newline_was_inserted = true;
    }

    if (add_padding == true)
    {
      wordwrap_output_left_padding(wrapper);
      add_padding = false;
    }

    TRACE_LOG("line-index: %d\n", wrapper->line_index);

    *end_of_string = end_of_string_buffer;
    output_ptr = end_of_string;
  }

  memmove(
      wrapper->output_buffer,
      output_ptr,
      (wrapper->output_buffer_index - output_ptr) * sizeof(z_ucs));

  wrapper->output_buffer_index
    = wrapper->output_buffer + (wrapper->output_buffer_index - output_ptr);

  index_shift = output_ptr - wrapper->output_buffer;

  for (
      metadata_output_index = 0;
      metadata_output_index < wrapper->metadata_index;
      metadata_output_index++)
  {
    metadata_entry = &(wrapper->metadata[metadata_output_index]);
    metadata_entry->output_index -= index_shift;
  }

  TRACE_LOG("line-index at end: %d\n", wrapper->line_index);
  TRACE_LOG("moi:%d, mi:%d.\n", metadata_output_index, wrapper->metadata_index);
}


// The output_buffer must contain at least (line_length + 2) characters in
// order to store one complete line plus the following cahracter (required
// to determine whether the last word fills the line completely or continues
// after it) and a null terminator.
static void resize_output_buffer(WORDWRAP *wrapper, size_t new_buffer_size)
{
  size_t bytes_to_allocate;
  int index_position;

  TRACE_LOG("resize to %zd / %d.\n", new_buffer_size, wrapper->line_length);

  if (new_buffer_size < (size_t)(wrapper->line_length + 2))
  {
    TRACE_LOG("Not enough space for one char and the null-terminator.\n");
    exit(EXIT_FAILURE);
  }

  if (wrapper->output_buffer_index == NULL)
  {
    index_position = 0;
  }
  else
  {
    index_position = wrapper->output_buffer_index - wrapper->output_buffer;
  }

  if (
      (wrapper->output_buffer_size > 0)
      &&
      (new_buffer_size < wrapper->output_buffer_size)
     )
  {
    // When wrapper->output_buffer_size > 0, wrapper->output_buffer_index!=NULL.
    wordwrap_flush_buffer(
        /*@-nullstate@*/ wrapper /*@-nullstate@*/,
        false);
  }

  bytes_to_allocate = new_buffer_size * sizeof(z_ucs);

  wrapper->output_buffer
        = (z_ucs*)fizmo_realloc(wrapper->output_buffer, bytes_to_allocate);

  wrapper->output_buffer_size = new_buffer_size;
  wrapper->output_buffer_index = wrapper->output_buffer + index_position;
}


WORDWRAP *wordwrap_new_wrapper(size_t line_length,
    struct wordwrap_target *target,
    //void (*wrapped_text_output_destination)(z_ucs *output,
    //  /*@null@*/ void *parameter),
    /*@null@*/ void *destination_parameter,
    bool add_newline_after_full_line, int left_side_padding)
{
  WORDWRAP *new_wrapper;
  size_t bytes_to_allocate = sizeof(WORDWRAP);
  int i;

  TRACE_LOG("Creating new wrapper with linesize %zd.\n", line_length);

  if ( (left_side_padding < 0) || ((size_t)left_side_padding >= line_length) )
    left_side_padding = 0;

  new_wrapper = (WORDWRAP*)fizmo_malloc(bytes_to_allocate);

  new_wrapper->add_newline_after_full_line = add_newline_after_full_line;
  new_wrapper->wrapped_text_output_destination
    = target->wrapped_text_output_destination;
  new_wrapper->padding_starts = target->padding_starts;
  new_wrapper->padding_ends = target->padding_ends;
  new_wrapper->destination_parameter = destination_parameter;
  new_wrapper->metadata = NULL;
  new_wrapper->metadata_size = 0;
  new_wrapper->metadata_index = 0;
  new_wrapper->line_index = left_side_padding;
  new_wrapper->newline_was_inserted = false;
  new_wrapper->last_line_was_completely_filled = false;
  new_wrapper->left_side_padding = left_side_padding;
  //new_wrapper->has_received_any_output = false;

  new_wrapper->output_buffer_size = 0;
  new_wrapper->output_buffer = NULL;
  new_wrapper->output_buffer_index = NULL;

  if (left_side_padding > 0)
  {
    new_wrapper->padding_buffer
      = fizmo_malloc((left_side_padding + 1) * sizeof(z_ucs));
    for (i=0; i<left_side_padding; i++)
      new_wrapper->padding_buffer[i] = Z_UCS_SPACE;
    new_wrapper->padding_buffer[i] = 0;
  }
  else
    new_wrapper->padding_buffer = NULL;

  new_wrapper->line_length = 0; // To make verification in resize work.

  wordwrap_adjust_line_length(
      // new_wrapper->line_length doesn't have to be defined
      /*@-compdef@*/ new_wrapper /*@+compdef@*/, 
      line_length);

  resize_output_buffer(
      // new_wrapper->line_length doesn't have to be defined
      /*@-compdef@*/ new_wrapper /*@+compdef@*/, 
      (size_t)(line_length * 4));

  // new_wrapper->line_length is set in wordwrap_adjust_line_length.
  // new_wrapper->output_buffer is defined in wordwrap_adjust_line_length.
  return /*@-compdef@*//*@-nullret@*/ new_wrapper /*@+nullret@*//*@+compdef@*/ ;
}


void wordwrap_destroy_wrapper(/*@only@*/ WORDWRAP *wrapper_to_destroy)
{
  if (wrapper_to_destroy->padding_buffer != NULL)
    free(wrapper_to_destroy->padding_buffer);
  if (wrapper_to_destroy->metadata != NULL)
    free(wrapper_to_destroy->metadata);
  free(wrapper_to_destroy->output_buffer);
  free(wrapper_to_destroy);
}


// This function will add the given input to the wrapper's line buffer. In
// case the line buffer is shorter than required for the input, as many
// words as possible are stored in the line, the line is flushed to the
// output function and the function tries again.
void wordwrap_wrap_z_ucs(WORDWRAP *wrapper, z_ucs *input)
{
  size_t len;
  size_t chars_to_copy;
  //z_ucs *newline_index;

  TRACE_LOG("Performing word-wrap on '");
  TRACE_LOG_Z_UCS(input);
  TRACE_LOG("'.\n");

  while (*input != 0)
  {
    len = z_ucs_len(input);

    /*
    if (wrapper->left_side_padding == 0)
      len = z_ucs_len(input);
    else
    {
      if ((newline_index = z_ucs_chr(input, Z_UCS_NEWLINE)) == NULL)
        len = z_ucs_len(input);
      else
      {
        // We have to pad after newlines and a newline was found in the
        // output. We'll first output everything up to the newline char.
        len = newline_index - input;

        if (len == 0)
        {
          // In case we've hit the newline char we'll insert the
          // appropriate number of padding spaces after the newline char.

          // First, store newline char.
          if ((size_t)(wrapper->output_buffer_index - wrapper->output_buffer+2)
              >= wrapper->output_buffer_size)
          {
            *wrapper->output_buffer_index = 0;
            wordwrap_flush_buffer(wrapper, false);
          }

          *wrapper->output_buffer_index = Z_UCS_NEWLINE;
          *wrapper->output_buffer_index++;
          input++;

          len = wrapper->left_side_padding;
          while (len > 0)
          {
            TRACE_LOG("pad-len:%d\n", (int)len);
            chars_to_copy
              = (wrapper->output_buffer_index - wrapper->output_buffer + len + 1
                  >= wrapper->output_buffer_size)
              ? wrapper->output_buffer_size - 1 - (
                  wrapper->output_buffer_index - wrapper->output_buffer)
              : len;

            TRACE_LOG("cccp:%d\n", (int)chars_to_copy);

            len -= chars_to_copy;

            while (chars_to_copy != 0)
            {
              *wrapper->output_buffer_index = Z_UCS_SPACE;
              wrapper->output_buffer_index++;
              chars_to_copy--;
            }

            if (len != 0)
            {
              *wrapper->output_buffer_index = 0;
              wordwrap_flush_buffer(wrapper, false);
            }
          }
        }
      }
    }
    */

    // Check if we have enough space for string plus terminating null:
    if (wrapper->output_buffer_index - wrapper->output_buffer + len
      > wrapper->output_buffer_size - 1)
    {
      // If not, squeeze as much as possible in the string and flush the
      // buffer.
      chars_to_copy
        = wrapper->output_buffer_size - 1
        - (wrapper->output_buffer_index - wrapper->output_buffer);

      z_ucs_ncpy(
          wrapper->output_buffer_index,
          input,
          chars_to_copy);

      wrapper->output_buffer_index += chars_to_copy;
      *wrapper->output_buffer_index = 0;

      wordwrap_flush_buffer(wrapper, false);

      input += chars_to_copy;
    }
    else
    {
      z_ucs_ncpy(
          wrapper->output_buffer_index,
          input,
          len);

      wrapper->output_buffer_index += len;

      input += len;
    }
  }
}


void wordwrap_flush_output(WORDWRAP *wrapper)
{
  wordwrap_flush_buffer(wrapper, true);
}


void wordwrap_insert_metadata(WORDWRAP *wrapper,
    void (*metadata_output)(void *ptr_parameter, uint32_t int_parameter),
    void *ptr_parameter, uint32_t int_parameter)
{
  size_t bytes_to_allocate;

  // Before adding new metadata, check if we need to allocate more space.
  if (wrapper->metadata_index == wrapper->metadata_size)
  {
    bytes_to_allocate
      = (size_t)(
          (wrapper->metadata_size + 32) * sizeof(struct wordwrap_metadata));

    TRACE_LOG("Allocating %d bytes for wordwrap-metadata.\n",
        (int)bytes_to_allocate);

    wrapper->metadata = (struct wordwrap_metadata*)fizmo_realloc(
            wrapper->metadata, bytes_to_allocate);

    wrapper->metadata_size += 32;

    TRACE_LOG("Wordwrap-metadata at %p.\n", wrapper->metadata);
  }

  TRACE_LOG("Current wordwrap-metadata-index is %d.\n",
      wrapper->metadata_index);

  TRACE_LOG("Current wordwrap-metadata-entry at %p.\n",
      &(wrapper->metadata[wrapper->metadata_index]));

  wrapper->metadata[wrapper->metadata_index].output_index
    = wrapper->output_buffer_index;
  wrapper->metadata[wrapper->metadata_index].metadata_output_function
    = metadata_output;
  wrapper->metadata[wrapper->metadata_index].ptr_parameter
    = ptr_parameter;
  wrapper->metadata[wrapper->metadata_index].int_parameter
    = int_parameter;

  TRACE_LOG("Added new metadata entry at %p with int-parameter %ld, ptr:%p.\n",
      wrapper->output_buffer_index, (long int)int_parameter, ptr_parameter);

  wrapper->metadata_index++;
}


void wordwrap_set_line_index(WORDWRAP *wrapper, int new_line_index)
{
  TRACE_LOG("Setting word-wrap-buffer %p to index %d.\n",
      wrapper, new_line_index);
  wrapper->line_index = new_line_index;
}


void wordwrap_adjust_line_length(WORDWRAP *wrapper, size_t new_line_length)
{
  if (new_line_length * 3 < wrapper->output_buffer_size)
  {
    resize_output_buffer(wrapper, new_line_length * 4);
  }

  wrapper->line_length = new_line_length;
}


void wordwrap_output_left_padding(WORDWRAP *wrapper)
{
  if (wrapper->left_side_padding > 0)
  {
    wrapper->padding_starts(wrapper->destination_parameter);

    wrapper->wrapped_text_output_destination(
        wrapper->padding_buffer,
        wrapper->destination_parameter);

    wrapper->padding_ends(wrapper->destination_parameter);
  }
}

