/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2007-2008 Sun Microsystems, Inc.
 */
package org.opends.server.util.table;



import static org.opends.server.util.ServerConstants.*;

import java.io.BufferedWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;



/**
 * An interface for creating a text based table. Tables have
 * configurable column widths, padding, and column separators.
 */
public final class TextTablePrinter extends TablePrinter {

  /**
   * Table serializer implementation.
   */
  private final class Serializer extends TableSerializer {

    // The current column being output.
    private int column = 0;

    // The real column widths taking into account size constraints but
    // not including padding or separators.
    private final List<Integer> columnWidths = new ArrayList<Integer>();

    // The cells in the current row.
    private final List<String> currentRow = new ArrayList<String>();

    // Width of the table in columns.
    private int totalColumns = 0;

    // The padding to use for indenting the table.
    private final String indentPadding;



    // Private constructor.
    private Serializer() {
      // Compute the indentation padding.
      StringBuilder builder = new StringBuilder();
      for (int i = 0; i < indentWidth; i++) {
        builder.append(' ');
      }
      this.indentPadding = builder.toString();
    }



    /**
     * {@inheritDoc}
     */
    @Override
    public void addCell(String s) {
      currentRow.add(s);
      column++;
    }



    /**
     * {@inheritDoc}
     */
    @Override
    public void addColumn(int width) {
      columnWidths.add(width);
      totalColumns++;
    }



    /**
     * {@inheritDoc}
     */
    @Override
    public void addHeading(String s) {
      if (displayHeadings) {
        addCell(s);
      }
    }



    /**
     * {@inheritDoc}
     */
    @Override
    public void endHeader() {
      if (displayHeadings) {
        endRow();

        // Print the header separator.
        StringBuilder builder = new StringBuilder(indentPadding);
        for (int i = 0; i < totalColumns; i++) {
          int width = columnWidths.get(i);
          if (totalColumns > 1) {
            if (i == 0 || i == (totalColumns - 1)) {
              // Only one lot of padding for first and last columns.
              width += padding;
            } else {
              width += padding * 2;
            }
          }

          for (int j = 0; j < width; j++) {
            if (headingSeparatorStartColumn > 0) {
              if (i < headingSeparatorStartColumn) {
                builder.append(' ');
              } else if (i == headingSeparatorStartColumn && j < padding) {
                builder.append(' ');
              } else {
                builder.append(headingSeparator);
              }
            } else {
              builder.append(headingSeparator);
            }
          }

          if ((i >= headingSeparatorStartColumn) && i < (totalColumns - 1)) {
            builder.append(columnSeparator);
          }
        }
        writer.println(builder.toString());
      }
    }



    /**
     * {@inheritDoc}
     */
    @Override
    public void endRow() {
      boolean isRemainingText;
      do {
        StringBuilder builder = new StringBuilder(indentPadding);
        isRemainingText = false;
        for (int i = 0; i < currentRow.size(); i++) {
          int width = columnWidths.get(i);
          String contents = currentRow.get(i);

          // Determine what parts of contents can be displayed on this
          // line.
          String head;
          String tail = null;

          if (contents == null) {
            // This cell has been displayed fully.
            head = "";
          } else if (contents.length() > width) {
            // We're going to have to split the cell on next word
            // boundary.
            int endIndex = contents.lastIndexOf(' ', width);
            if (endIndex == -1) {
              endIndex = width;
              head = contents.substring(0, endIndex);
              tail = contents.substring(endIndex);

            } else {
              head = contents.substring(0, endIndex);
              tail = contents.substring(endIndex + 1);
            }
          } else {
            // The contents fits ok.
            head = contents;
          }

          // Add this cell's contents to the current line.
          if (i > 0) {
            // Add right padding for previous cell.
            for (int j = 0; j < padding; j++) {
              builder.append(' ');
            }

            // Add separator.
            builder.append(columnSeparator);

            // Add left padding for this cell.
            for (int j = 0; j < padding; j++) {
              builder.append(' ');
            }
          }

          // Add cell contents.
          builder.append(head);

          // Now pad with extra space to make up the width.
          // Only if it's not the last cell (see issue #3210)
          if (i != currentRow.size() - 1)
          {
            for (int j = head.length(); j < width; j++)
            {
              builder.append(' ');
            }
          }

          // Update the row contents.
          currentRow.set(i, tail);
          if (tail != null) {
            isRemainingText = true;
          }
        }

        // Output the line.
        writer.println(builder.toString());

      } while (isRemainingText);
    }



    /**
     * {@inheritDoc}
     */
    @Override
    public void endTable() {
      writer.flush();
    }



    /**
     * {@inheritDoc}
     */
    @Override
    public void startHeader() {
      determineColumnWidths();

      column = 0;
      currentRow.clear();
    }



    /**
     * {@inheritDoc}
     */
    @Override
    public void startRow() {
      column = 0;
      currentRow.clear();
    }



    // We need to calculate the effective width of each column.
    private void determineColumnWidths() {
      // First calculate the minimum width so that we know how much
      // expandable columns can expand.
      int minWidth = indentWidth;
      int expandableColumnSize = 0;

      for (int i = 0; i < totalColumns; i++) {
        int actualSize = columnWidths.get(i);

        if (fixedColumns.containsKey(i)) {
          int requestedSize = fixedColumns.get(i);

          if (requestedSize == 0) {
            expandableColumnSize += actualSize;
          } else {
            columnWidths.set(i, requestedSize);
            minWidth += requestedSize;
          }
        } else {
          minWidth += actualSize;
        }

        // Must also include padding and separators.
        if (i > 0) {
          minWidth += padding * 2 + columnSeparator.length();
        }
      }

      if (minWidth > totalWidth) {
        // The table is too big: leave expandable columns at their
        // requested width, as there's not much else that can be done.
      } else {
        int available = totalWidth - minWidth;

        if (expandableColumnSize > available) {
          // Only modify column sizes if necessary.
          for (int i = 0; i < totalColumns; i++) {
            int actualSize = columnWidths.get(i);

            if (fixedColumns.containsKey(i)) {
              int requestedSize = fixedColumns.get(i);
              if (requestedSize == 0) {
                // Calculate size based on requested actual size as a
                // proportion of the total.
                requestedSize =
                  ((actualSize * available) / expandableColumnSize);
                columnWidths.set(i, requestedSize);
              }
            }
          }
        }
      }
    }
  }

  /**
   * The default string which should be used to separate one column
   * from the next (not including padding).
   */
  private static final String DEFAULT_COLUMN_SEPARATOR = "";

  /**
   * The default character which should be used to separate the table
   * heading row from the rows beneath.
   */
  private static final char DEFAULT_HEADING_SEPARATOR = '-';

  /**
   * The default padding which will be used to separate a cell's
   * contents from its adjacent column separators.
   */
  private static final int DEFAULT_PADDING = 1;

  // The string which should be used to separate one column
  // from the next (not including padding).
  private String columnSeparator = DEFAULT_COLUMN_SEPARATOR;

  // Indicates whether or not the headings should be output.
  private boolean displayHeadings = true;

  // Table indicating whether or not a column is fixed width.
  private final Map<Integer, Integer> fixedColumns =
    new HashMap<Integer, Integer>();

  // The number of characters the table should be indented.
  private int indentWidth = 0;

  // The character which should be used to separate the table
  // heading row from the rows beneath.
  private char headingSeparator = DEFAULT_HEADING_SEPARATOR;

  // The column where the heading separator should begin.
  private int headingSeparatorStartColumn = 0;

  // The padding which will be used to separate a cell's
  // contents from its adjacent column separators.
  private int padding = DEFAULT_PADDING;

  // Total permitted width for the table which expandable columns
  // can use up.
  private int totalWidth = MAX_LINE_WIDTH;

  // The output destination.
  private PrintWriter writer = null;



  /**
   * Creates a new text table printer for the specified output stream.
   * The text table printer will have the following initial settings:
   * <ul>
   * <li>headings will be displayed
   * <li>no separators between columns
   * <li>columns are padded by one character
   * </ul>
   *
   * @param stream
   *          The stream to output tables to.
   */
  public TextTablePrinter(OutputStream stream) {
    this(new BufferedWriter(new OutputStreamWriter(stream)));
  }



  /**
   * Creates a new text table printer for the specified writer. The
   * text table printer will have the following initial settings:
   * <ul>
   * <li>headings will be displayed
   * <li>no separators between columns
   * <li>columns are padded by one character
   * </ul>
   *
   * @param writer
   *          The writer to output tables to.
   */
  public TextTablePrinter(Writer writer) {
    this.writer = new PrintWriter(writer);
  }



  /**
   * Sets the column separator which should be used to separate one
   * column from the next (not including padding).
   *
   * @param columnSeparator
   *          The column separator.
   */
  public void setColumnSeparator(String columnSeparator) {
    this.columnSeparator = columnSeparator;
  }



  /**
   * Set the maximum width for a column. If a cell is too big to fit
   * in its column then it will be wrapped.
   *
   * @param column
   *          The column to make fixed width (0 is the first column).
   * @param width
   *          The width of the column (this should not include column
   *          separators or padding), or <code>0</code> to indicate
   *          that this column should be expandable.
   * @throws IllegalArgumentException
   *           If column is less than 0.
   */
  public void setColumnWidth(int column, int width)
      throws IllegalArgumentException {
    if (column < 0) {
      throw new IllegalArgumentException("Negative column " + column);
    }

    if (width < 0) {
      throw new IllegalArgumentException("Negative width " + width);
    }

    fixedColumns.put(column, width);
  }



  /**
   * Specify whether the column headings should be displayed or not.
   *
   * @param displayHeadings
   *          <code>true</code> if column headings should be
   *          displayed.
   */
  public void setDisplayHeadings(boolean displayHeadings) {
    this.displayHeadings = displayHeadings;
  }



  /**
   * Sets the heading separator which should be used to separate the
   * table heading row from the rows beneath.
   *
   * @param headingSeparator
   *          The heading separator.
   */
  public void setHeadingSeparator(char headingSeparator) {
    this.headingSeparator = headingSeparator;
  }



  /**
   * Sets the heading separator start column. The heading separator
   * will only be display in the specified column and all subsequent
   * columns. Usually this should be left at zero (the default) but
   * sometimes it useful to indent the heading separate in order to
   * provide additional emphasis (for example in menus).
   *
   * @param startColumn
   *          The heading separator start column.
   */
  public void setHeadingSeparatorStartColumn(int startColumn) {
    if (startColumn < 0) {
      throw new IllegalArgumentException("Negative start column "
          + startColumn);
    }
    this.headingSeparatorStartColumn = startColumn;
  }



  /**
   * Sets the amount of characters that the table should be indented.
   * By default the table is not indented.
   *
   * @param indentWidth
   *          The number of characters the table should be indented.
   * @throws IllegalArgumentException
   *           If indentWidth is less than 0.
   */
  public void setIndentWidth(int indentWidth) throws IllegalArgumentException {
    if (indentWidth < 0) {
      throw new IllegalArgumentException("Negative indentation width "
          + indentWidth);
    }

    this.indentWidth = indentWidth;
  }



  /**
   * Sets the padding which will be used to separate a cell's contents
   * from its adjacent column separators.
   *
   * @param padding
   *          The padding.
   */
  public void setPadding(int padding) {
    this.padding = padding;
  }



  /**
   * Sets the total permitted width for the table which expandable
   * columns can use up.
   *
   * @param totalWidth
   *          The total width.
   */
  public void setTotalWidth(int totalWidth) {
    this.totalWidth = totalWidth;
  }



  /**
   * {@inheritDoc}
   */
  @Override
  protected TableSerializer getSerializer() {
    return new Serializer();
  }
}
