/**
 * ===========================================
 * LibLayout : a free Java layouting library
 * ===========================================
 *
 * Project Info:  http://reporting.pentaho.org/liblayout/
 *
 * (C) Copyright 2006-2007, by Pentaho Corporation and Contributors.
 *
 * This library is free software; you can redistribute it and/or modify it under the terms
 * of the GNU Lesser General Public License as published by the Free Software Foundation;
 * either version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
 * in the United States and other countries.]
 *
 * ------------
 * $Id: PaginationStep.java 6653 2008-12-02 14:53:40Z tmorgner $
 * ------------
 * (C) Copyright 2006-2007, by Pentaho Corporation.
 */
package org.jfree.layouting.renderer.process;

import org.jfree.layouting.input.style.keys.box.DisplayRole;
import org.jfree.layouting.input.style.values.CSSConstant;
import org.jfree.layouting.renderer.model.PageAreaRenderBox;
import org.jfree.layouting.renderer.model.ParagraphPoolBox;
import org.jfree.layouting.renderer.model.ParagraphRenderBox;
import org.jfree.layouting.renderer.model.RenderBox;
import org.jfree.layouting.renderer.model.RenderNode;
import org.jfree.layouting.renderer.model.page.LogicalPageBox;
import org.jfree.layouting.renderer.model.table.TableCellRenderBox;
import org.jfree.layouting.renderer.model.table.TableColumnGroupNode;
import org.jfree.layouting.renderer.model.table.TableRenderBox;
import org.jfree.layouting.renderer.model.table.TableSectionRenderBox;

/**
 * Computes the pagination. This step checks, whether content crosses an inner
 * or outer page boundary. In that case, the content is shifted downwards to the
 * next page and then marked as sticky (so it wont move any further later; this
 * prevents infinite loops).
 * <p/>
 * This kind of shifting does not apply to inline-elements - they get shifted
 * when their linebox gets shifted.
 *
 * @author Thomas Morgner
 */
public class PaginationStep extends IterateVisualProcessStep
{
  /**
   * The start of the current logical page. Any content that starts before that
   * is considered sicky and therefore not shifted.
   */
  private long pageStartOffset;
  private boolean pageOverflow;

  /**
   * The current shift-distance. This can increase, but never decrease.
   */
  private long shift;
  private long[] physicalBreaks;
  private long headerHeight;
  private long footerHeight;
  private long stickyMarker;
  private BoxShifter boxShifter;

  public PaginationStep()
  {
    boxShifter = new BoxShifter();
  }

  public boolean performPagebreak(final LogicalPageBox pageBox)
  {
    pageStartOffset = 0;
    pageOverflow = false;
    shift = 0;
    physicalBreaks = null;
    headerHeight = 0;
    footerHeight = 0;
    stickyMarker = 0;

    pageStartOffset = pageBox.getPageOffset();
    pageOverflow = false;
    stickyMarker = pageBox.getChangeTracker();
    shift = 0;

    startRootProcessing(pageBox);
    return pageOverflow;
  }

  public long getNextOffset()
  {
    return physicalBreaks[physicalBreaks.length - 1];
  }

  private boolean isCrossingBreak(final RenderNode node)
  {
    int y1Index = -1;
    int y2Index = -1;
    final long nodeY1 = node.getY() + shift;
    final long nodeY2 = nodeY1 + node.getHeight();

    for (int i = 0; i < physicalBreaks.length; i++)
    {
      final long physicalBreak = physicalBreaks[i];
      if (nodeY1 >= physicalBreak)
      {
        y1Index = i;
      }
      if (nodeY2 >= physicalBreak)
      {
        y2Index = i;
      }
    }
    return (y1Index != y2Index);
  }

  private void startRootProcessing(final LogicalPageBox pageBox)
  {
    // Note: For now, we limit both the header and footer to a single physical
    // page. This safes me a lot of trouble for now.

    // we have to iterate using a more complex schema here.
    // Step one: layout the header-section. Record that height.
    final PageAreaRenderBox headerArea = pageBox.getHeaderArea();
    headerHeight = headerArea.getHeight();
    headerArea.setHeight(headerHeight);

    // Step two: The footer. For the footer, we have to traverse the whole
    // thing backwards. Nonetheless, we've got the height.
    final PageAreaRenderBox footerArea = pageBox.getFooterArea();
    footerHeight = footerArea.getHeight();
    footerArea.setHeight(footerHeight);

    // Step three: Perform the breaks. Make sure that at least one
    // line of the normal-flow content can be processed.
    // Reduce the footer and if that's not sufficient the header as well.
    final long[] originalBreaks = pageBox.getPhysicalBreaks(RenderNode.VERTICAL_AXIS);
    physicalBreaks = new long[originalBreaks.length + 1];
    physicalBreaks[0] = pageStartOffset;
    for (int i = 0; i < originalBreaks.length; i++)
    {
      physicalBreaks[i + 1] = pageStartOffset +
          (originalBreaks[i] - headerHeight);
    }

    // This is a bit hacky, isnt it?
    physicalBreaks[physicalBreaks.length - 1] -= footerHeight;

    // assertation
    final long totalPageHeight = physicalBreaks[physicalBreaks.length - 1] - physicalBreaks[0];
    if (totalPageHeight + headerHeight + footerHeight != pageBox.getPageHeight())
    {
      throw new IllegalStateException("Assertation failed: Page height");
    }

    // now consume the usable height and stop if all space is used or the
    // end of the document has been reached. Process at least one line of
    // content.

    // now process all the other content (excluding the header and footer area)
    if (startBlockLevelBox(pageBox))
    {
      processBoxChilds(pageBox);
    }
    finishBlockLevelBox(pageBox);
//    if (pageOverflow)
//    {
//      Log.debug ("Pagination: " + totalPageHeight);
//    }
  }



  protected boolean startInlineLevelBox(final RenderBox box)
  {
    // Skip the contents ..
    return false;
  }

  protected void processParagraphChilds(final ParagraphRenderBox box)
  {
    // Process the direct childs of the paragraph
    // Each direct child represents a line ..
    // Todo: Include orphan and widow stuff ..

    // First: Check the number of lines. (Should have been precomputed)
    // Second: Check whether and where the orphans- and widows-rules apply
    // Third: Shift the lines.
    RenderNode node = box.getVisibleFirst();
    while (node != null)
    {
      // all childs of the linebox container must be inline boxes. They
      // represent the lines in the paragraph. Any other element here is
      // a error that must be reported
      if (node instanceof ParagraphPoolBox == false)
      {
        throw new IllegalStateException("Encountered " + node.getClass());
      }
      final ParagraphPoolBox inlineRenderBox = (ParagraphPoolBox) node;
      if (startLine(inlineRenderBox))
      {
        processBoxChilds(inlineRenderBox);
      }
      finishLine(inlineRenderBox);

      node = node.getVisibleNext();
    }
  }

  private boolean isNodeProcessable(final RenderNode node)
  {
    if (node.getStickyMarker() == stickyMarker)
    {
      // already finished. No need to touch that thing ..
      return false;
    }
    final long y2 = node.getY() + node.getHeight();
    if (y2 < pageStartOffset)
    {
      // not in range.
      return false;
    }
    if (node.isIgnorableForRendering())
    {
      return false;
    }
    return true;
  }

  protected void processBlockLevelNode(final RenderNode node)
  {
    if (isNodeProcessable(node) == false)
    {
      return;
    }

    // Apply the current shift.
    node.setY(node.getY() + shift);
    // question: Does the node cross a boundary or is is a simple child.
    // this also rejects childs that left the visible area ..
    if (isCrossingBreak(node) == false)
    {
      return;
    }

    // oh, we have to move the node downwards.
    final long nextBreakShiftDistance = getNextBreak(node.getY()) - node.getY();
    node.setY(node.getY() + nextBreakShiftDistance);
    boxShifter.extendHeight(node, nextBreakShiftDistance);
    shift += nextBreakShiftDistance;

    // Make sure we do not move that node later on ..
    node.setStickyMarker(stickyMarker);
  }

  protected void processTable(final TableRenderBox table)
  {
    final long originalShift = shift;
    final long height = table.getHeight();

    final long y = table.getY();
    final long y2 = shift + y + height;
    if (y2 < pageStartOffset)
    {
      // not in range.
      return;
    }
    if (table.isIgnorableForRendering())
    {
      return;
    }


    final long nextBreak = getNextBreak(y + shift);
    final long usableHeight = nextBreak - (y + shift);
    if (usableHeight > 0)
    {
      final long reservedHeight = Math.min(height,
          table.getOrphansSize() + table.getWidowsSize());

      if (table.isAvoidPagebreakInside() ||
          reservedHeight > usableHeight ||
          table.getOrphansSize() > usableHeight)
      {
        final long newShift = nextBreak - y;

        // orphans and widows rules prevent a break.

        if (newShift > 0)
        {
          table.setY(table.getY() + newShift);
          // the table itself shifts, but the parent extends ..
          boxShifter.extendHeight(table.getParent(), newShift - shift);
          shift = newShift;
          table.setStickyMarker(stickyMarker);
        }
        else if (newShift < 0)
        {
          table.setY(table.getY() + shift);
          pageOverflow = true;
        }
        else
        {
          // No shift needed, no processing of the childs needed.
        }
      }
      else
      {
        // ordinary shifting is used ..
        table.setY(table.getY() + shift);
        table.setStickyMarker(stickyMarker);
      }
    }
    else
    {
      // ordinary shifting is used ..
      table.setY(table.getY() + shift);
      table.setStickyMarker(stickyMarker);
    }

    processTableSection(table, DisplayRole.TABLE_HEADER_GROUP);
    processTableSection(table, DisplayRole.TABLE_ROW_GROUP);
    processTableSection(table, DisplayRole.TABLE_FOOTER_GROUP);

    // Processing all he sections can have an effect on the height of the table
    // Shifting content down increases the height of the table.
    final long finalHeight = table.getHeight();
    final long delta = finalHeight - height;
    if (delta < 0)
    {
      throw new IllegalStateException("A table can/must not shrink!");
    }
  }

  private void processTableSection(final TableRenderBox box,
                                   final CSSConstant role)
  {
    RenderNode rowGroupNode = box.getFirstChild();
    while (rowGroupNode != null)
    {
      if (rowGroupNode instanceof TableSectionRenderBox == false)
      {
        rowGroupNode = rowGroupNode.getNext();
        continue;
      }

      final TableSectionRenderBox sectionBox =
          (TableSectionRenderBox) rowGroupNode;
      if (role.equals
          (sectionBox.getDisplayRole()) == false)
      {
        // not a header ..
        rowGroupNode = rowGroupNode.getNext();
        continue;
      }

      startProcessing(rowGroupNode);
      rowGroupNode = rowGroupNode.getNext();
    }
  }

  protected void processBlockLevelChild(final RenderNode node)
  {
    if (node instanceof TableRenderBox)
    {
      final TableRenderBox table = (TableRenderBox) node;
      processTable(table);
    }
    else
    {
      super.processBlockLevelChild(node);
    }
  }

  protected boolean startBlockLevelBox(final RenderBox box)
  {
    if (isNodeProcessable(box) == false)
    {
      // Not shifted, as this box will not be affected. It is not part of the
      // content window.
      return false;
    }

    final long y = box.getY();
    if (box instanceof TableCellRenderBox)
    {
      // table cells get a special treatment when the row is computed ..
      box.setY(y + shift);
      box.setStickyMarker(stickyMarker);
      return true;
    }

    if (isOverflow(box))
    {
      pageOverflow = true;
      boxShifter.shiftBox(box, shift);
      return false;
    }

    // Does the full box fit into the current page?
    // Apply the current shift to the box only (non-recursive)
    if (isCrossingBreak(box) == false)
    {
      // It fits into the page. So why bother ..
      boxShifter.shiftBox(box, shift);
      return false;
    }

    if (box instanceof TableColumnGroupNode)
    {
      throw new IllegalArgumentException("This is not expected here");
    }

    // At this point, we know that the node is in fact in range - that
    // means it intersects the current content-window.

    // Break rules: We check for collapsing top-edges. (This is like the margin
    // processing. If we encounter content at this edge, we can check whether
    // that content will fit on the page. If it does, ok, if not - move all
    // boxed from here up to (and including) the content to the bottom.

    final long nextBreak = getNextBreak(y + shift);
    final long usableHeight = nextBreak - (y + shift);
    final long height = box.getHeight();
    final long reservedHeight = Math.min
        (height, box.getOrphansSize() + box.getWidowsSize());

    if (box.isAvoidPagebreakInside() ||
        usableHeight < 0 || reservedHeight > height ||
        box.getOrphansSize() > usableHeight)
    {
      final long newShift = nextBreak - y;

      // orphans and widows rules prevent a break.

      if (newShift > 0)
      {
        boxShifter.shiftBox(box, newShift);
        boxShifter.extendHeight(box.getParent(), newShift - shift);
        shift = newShift;
        box.setStickyMarker(stickyMarker);
        return false;
      }
      else if (newShift < 0)
      {
        boxShifter.shiftBox(box, shift);
        pageOverflow = true;
        return false;
      }
      else
      {
        return false;
      }
    }

    // At this point, we know that the box is way to large to fit the remaining
    // space. But we know, that content will be processed on that page (by
    // looking at the orphan-size) and that the break occurrs somewhere inside
    // of the children of this box. So shall it be!

    box.setY(box.getY() + shift);
    box.setStickyMarker(stickyMarker);
    return true;
  }

  private boolean isOverflow(final RenderNode box)
  {
    final long lastBreak = physicalBreaks[physicalBreaks.length - 1];
    if (box.getY() + shift > lastBreak)
    {
      return true;
    }
    return false;
  }

  private long getNextBreak(final long y)
  {
    for (int i = 0; i < physicalBreaks.length; i++)
    {
      final long physicalBreak = physicalBreaks[i];
      if (y < physicalBreak)
      {
        return physicalBreak;
      }
    }
    // Fall back to the last one, for heavens sake..
    return physicalBreaks[physicalBreaks.length - 1];
  }

  protected boolean startLine(final ParagraphPoolBox box)
  {
    if (isNodeProcessable(box) == false)
    {
      return false;
    }

    if (isOverflow(box))
    {
      boxShifter.shiftBox(box, shift);
      pageOverflow = true;
      return false;
    }

    final long nextBreak = getNextBreak(box.getY() + shift);
    final long usableHeight = nextBreak - (box.getY() + shift);
    if (box.getHeight() < usableHeight)
    {
      boxShifter.shiftBox(box, shift);
      box.setStickyMarker(stickyMarker);
      return false;
    }

    // It gets more complicated. Shift it to the next page. The first node shall
    // be marked as done.
    final long newShift = nextBreak - box.getY();
    if (newShift > 0)
    {
      // we already left the current page.
      boxShifter.shiftBox(box, newShift);
      boxShifter.extendHeight(box, newShift - shift);
      shift = newShift;
      box.setStickyMarker(stickyMarker);
    }
    else if (newShift < 0)
    {
      pageOverflow = true;
      boxShifter.shiftBox(box, shift);
    }
    else
    {
      boxShifter.shiftBox(box, shift);
    }
    return false;
  }

  protected void finishLine(final ParagraphPoolBox inlineRenderBox)
  {
  }
}
