/* CellRenderEvent.java

	Purpose:
		
	Description:
		
	History:
		August 23, 5:53:16 PM     2010, Created by Ashish Dasnurkar
		September 17			  2014, henrichen

Copyright (C) 2010 Potix Corporation. All Rights Reserved.
 */
package org.zkoss.zss.model.impl.pdf;


import java.io.IOException;
import java.io.Serializable;

import org.zkoss.zss.model.CellRegion;
import org.zkoss.zss.model.SBorder.BorderType;
import org.zkoss.zss.model.SCell;
import org.zkoss.zss.model.SCell.CellType;
import org.zkoss.zss.model.SCellStyle;
import org.zkoss.zss.model.SCellStyle.Alignment;
import org.zkoss.zss.model.SCellStyle.VerticalAlignment;
import org.zkoss.zss.model.SColor;
import org.zkoss.zss.model.SFill;
import org.zkoss.zss.model.SFill.FillPattern;
import org.zkoss.zss.model.SFont;
import org.zkoss.zss.model.SFont.Underline;
import org.zkoss.zss.model.SSheet;
import org.zkoss.zss.model.impl.FillImpl;
import org.zkoss.zss.range.impl.imexp.BookHelper;

import com.lowagie.text.BadElementException;
import com.lowagie.text.DocumentException;
import com.lowagie.text.ExceptionConverter;
import com.lowagie.text.Image;
import com.lowagie.text.Phrase;
import com.lowagie.text.Rectangle;
import com.lowagie.text.html.WebColors;
import com.lowagie.text.pdf.ColumnText;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfPCell;
import com.lowagie.text.pdf.PdfPCellEvent;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.text.pdf.PdfTemplate;

/**
 * Internal Use Only. Utility class to render PDF cell and cell borders on 
 * page end event.
 *
 * 1. Render cell content with renderInfo and clipInfo
 * 2. Render border and grid-line with each cell rectangle(cellRect) along the boundary of renderRegion
 *    
 * @author ashish henrichen
 */
/*package*/ class CellRenderEvent implements PdfPCellEvent, Serializable {
	private static final long serialVersionUID = -7280463782329527875L;
	
	private SCellStyle style;
	private SCell zssCell;
	private SSheet sheet;
	
	//ZSS-549
	private Phrase phrase;

	//ZSS-772
	private CellRegion clipRegion; //clipped region to be rendered
	private CellRegion outlineRegion; //page bound
	private CellRegion renderRegion; //real rendered region for this clip region
	private RenderInfo renderInfo; //geometric information of renderRegion relative to clipRegion
	private PixelInfo pixelInfo; //geometric information of clip region in pixels
	
	//ZSS-773
	private RenderInfo clipInfo; //geometric information of the clipping rectangle
	
	public SCell getZSSCell() {
		return zssCell;
	}

	public void setZSSCell(SCell zssCell) {
		this.zssCell = zssCell;
	}

	private boolean printGridLines;
	private boolean printOutline;
	private enum BORDER {TOP, RIGHT, BOTTOM, LEFT, BACKGROUND;
		Object getType (SCellStyle cellStyle) {
			switch (this) {
			case TOP:
				return cellStyle.getBorderTop();
			case RIGHT:
				return cellStyle.getBorderRight();
			case BOTTOM:
				return cellStyle.getBorderBottom();
			case LEFT:
				return cellStyle.getBorderLeft();
			case BACKGROUND:
				return cellStyle.getFillPattern();
			}
			throw new RuntimeException();
		}
		SColor getColor(SCellStyle cellStyle) {
			switch (this) {
			case TOP:
				return cellStyle.getBorderTopColor();
			case RIGHT:
				return cellStyle.getBorderRightColor();
			case BOTTOM:
				return cellStyle.getBorderBottomColor();
			case LEFT:
				return cellStyle.getBorderLeftColor();
			case BACKGROUND:
				return cellStyle.getBackColor();
			}
			throw new RuntimeException();
		}
	};
	
	/**
	 * Constructor of the BorderDrawEvent.
	 * @param wb
	 */
	private CellRenderEvent(SSheet sheet, CellRegion clipRegion, RenderInfo clipInfo, CellRegion renderRegion, RenderInfo renderInfo, CellRegion outlineRegion, Phrase phrase) {
		this.sheet = sheet;
		this.clipRegion = clipRegion;
		this.outlineRegion = outlineRegion;
		this.phrase = phrase;
		this.renderInfo = renderInfo;
		this.clipInfo = clipInfo;
		this.renderRegion = renderRegion;
	}

	/**
	 * returns a new cell BorderDrawEvent cell event object which is set 
	 * to draw pdf cell borders using provided CellStyle
	 * @param style The cell style
 	 * @param printGridLines whether print the grid line
	 * @param wb associated spreadsheet book
	 * @return a new cell BorderDrawEvent cell event object which is set 
	 * to draw pdf cell borders using provided CellStyle
	 */
	public static CellRenderEvent getCellRenderEvent(CellRegion clipRegion, RenderInfo clipInfo, CellRegion renderRegion, RenderInfo renderInfo, SCell zssCell, SCellStyle style, boolean printOutline,
			boolean printGridLines, CellRegion outlineRegion, PixelInfo pixelInfo, Phrase phrase, SSheet sheet) {
		CellRenderEvent event = new CellRenderEvent(sheet, clipRegion, clipInfo, renderRegion, renderInfo, outlineRegion, phrase);
		event.setZSSCell(zssCell);
		event.setStyle(style);
		event.setPrintGridLines(printGridLines);
		event.setPrintOutline(printOutline);
		event.setPixelInfo(pixelInfo);
		return event;
	}
	
	/*
	 * draws cell borders after cell contents are rendered to pdf format.
	 */
	public void cellLayout(PdfPCell cell, Rectangle rect, PdfContentByte[] canvases) {
		Rectangle clipRect = null;
		// handle rowSpan/colSpan cell which has real clipping information
		if (clipInfo != null) {
			final float top = rect.getTop();
			final float bottom = (float) (top - clipInfo.getHeight());
			final float left = rect.getLeft();
			final float right = (float)(left + clipInfo.getWidth());
			clipRect = new Rectangle(left, bottom, right, top);
		} else {
			clipRect = rect;
		}
		
		//ZSS-974
		final SFill fill = style == null ? null : style.getFill();
		if (fill != null && fill.getFillPattern() != FillPattern.NONE && fill.getFillPattern() != FillPattern.SOLID) {
			PdfContentByte canvas = canvases[PdfPTable.LINECANVAS];
			drawFillPattern(clipRect, fill, canvas);			
		}
		
		//ZSS-549
		if (phrase != null) {
			PdfContentByte txtCanvas =  canvases[PdfPTable.TEXTCANVAS];
			processPhrase(clipRect, rect, txtCanvas);
		}
		
		if (outlineRegion != null) {
			PdfContentByte lineCanvas = canvases[PdfPTable.LINECANVAS];
			lineCanvas.setLineJoin(PdfContentByte.LINE_JOIN_MITER);
			
			processBottomBorder(rect, lineCanvas, zssCell);
			processRightBorder(rect, lineCanvas, zssCell);
			processTopBorder(rect, lineCanvas, zssCell);
			processLeftBorder(rect, lineCanvas, zssCell);
		}
	}
	
	private BorderInfo getBorderInfo(BORDER border, SCell cell) {
		if (cell != null) {
			SCellStyle cellStyle = cell.getCellStyle();
			if (cellStyle != null) {
				Object type = border.getType(cellStyle);
				
				if (border == BORDER.BACKGROUND && FillPattern.NONE != type) {
					return new BorderInfo(type, border.getColor(cellStyle));
				} else	if (BorderType.NONE != type && FillPattern.NONE != type) {
					return new BorderInfo(type, border.getColor(cellStyle));
				}
			}
		}
		return null;
	}

	//ZSS-776
	// check if need to handle the double border corners
	// 0: whether top/left a double border; 1: whether bottom/right a double border
	private boolean[] getDoubleBorderCornerInfo(BORDER border, SCell cell, int rowIdx, int colIdx) {
		boolean start;
		boolean end;
		switch(border) {
		case BOTTOM:
		case TOP:
			start = getLeftBorderType(cell, rowIdx, colIdx) == BorderType.DOUBLE;
			end = getRightBorderType(cell, rowIdx, colIdx) == BorderType.DOUBLE;
			break;
		case LEFT:
		case RIGHT:
			start = getTopBorderType(cell, rowIdx, colIdx) == BorderType.DOUBLE;
			end = getBottomBorderType(cell, rowIdx, colIdx) == BorderType.DOUBLE;
			break;
		default:
			return null;
		}
		return new boolean[] {start, end};
	}

	//ZSS-776
	private BorderType getLeftBorderType(SCell cell, int rowIdx, int colIdx) {
		// check left sibling's right border
		if (colIdx > 0) {
			SCell leftCell = sheet.getCell(rowIdx, colIdx - 1);
			BorderType type = getBorderType(BORDER.RIGHT, leftCell);
			if (type != null && type != BorderType.NONE) return type;
		}
		
		// check this cell
		return getBorderType(BORDER.LEFT, cell);
	}
	
	//ZSS-776
	private BorderType getTopBorderType(SCell cell, int rowIdx, int colIdx) {
		// check top sibling's bottom border
		if (rowIdx > 0) {
			SCell leftCell = sheet.getCell(rowIdx - 1, colIdx);
			BorderType type = getBorderType(BORDER.BOTTOM, leftCell);
			if (type != null && type != BorderType.NONE) return type;
		}
		
		// check this cell
		return getBorderType(BORDER.TOP, cell);
	}
	
	//ZSS-776
	private BorderType getRightBorderType(SCell cell, int rowIdx, int colIdx) {
		// check this cell
		BorderType type = getBorderType(BORDER.RIGHT, cell);
		if (type != null && type != BorderType.NONE) return type;
		
		// check right sibling's left border
		SCell rightCell = sheet.getCell(rowIdx, colIdx + 1);
		return getBorderType(BORDER.LEFT, rightCell);
	}
	
	//ZSS-776
	private BorderType getBottomBorderType(SCell cell, int rowIdx, int colIdx) {
		// check this cell
		BorderType type = getBorderType(BORDER.BOTTOM, cell);
		if (type != null && type != BorderType.NONE) return type;
		
		// check bottom sibling's top border
		SCell bottomCell = sheet.getCell(rowIdx + 1, colIdx);
		return getBorderType(BORDER.TOP, bottomCell);
	}
	
	//ZSS-776
	private BorderType getBorderType(BORDER border, SCell cell) {
		if (cell != null) {
			SCellStyle cellStyle = cell.getCellStyle();
			if (cellStyle != null) {
				return (BorderType) border.getType(cellStyle);
			}
		}
		return null;
	}
	
	private CellType getRealType(SCell zssCell) {
		if (zssCell == null)
			return CellType.BLANK;
		CellType type = zssCell.getType();
		if (type == CellType.FORMULA) {
			type = zssCell.getFormulaResultType();
		}
		return type;
	}

	//ZSS-774
	//20140924,henrichen: by experiment the ratio is 1.325
	// ratio between text line-space to font size of the maximum font
	private static final float LINE_SPACING_RATIO = 1.325f; // by experiment
	
	//ZSS-549
	//rect is the geometric info of the clipRegion(aka pdfCell)
	private void processPhrase(Rectangle clipRect, Rectangle cellRect, PdfContentByte txtCanvas) {
		//ZSS-772
		if (renderInfo == null) {
			renderInfo = new RenderInfo(0d, 0d, clipRect.getWidth(), clipRect.getHeight(), false, false);
		}
		try {
			boolean wrapText = style == null ? false : this.style.isWrapText() && getRealType(zssCell) == CellType.STRING;
			Alignment align = PdfExporter.getRealAlignment(zssCell);
			//find the real height of the phrase
			double width = renderInfo.getWidth();
			double wrapWidth = width - PdfExporter.PAD_LEFT - PdfExporter.PAD_RIGHT;
			double height = renderInfo.getHeight();
			ColumnText ct  = new ColumnText(txtCanvas);
			ct.setLeading(0f, LINE_SPACING_RATIO); //ZSS-774
			ct.setSimpleColumn(0f, 0f, wrapText ? (float) wrapWidth: 2000f, -2000f);
			ct.setAlignment(StyleConversionUtil.getCorrespondingPdfPCellAlignment(align));
			ct.setText(phrase);
			ct.go(true); // true: simulate rendering
			
			// dimension for rendering
			double renderDecender = -ct.getDescender(); // space under the baseline
			double renderHeight = 0 - ct.getYLine();
			double realWidth = ColumnText.getWidth(phrase);
			double renderWidth = wrapText ? 
					wrapWidth : Math.max(realWidth, width);
			
			//Rectangle is (left, bottom, right, top);   
			//vertical alignment is controlled by offset
			VerticalAlignment valign = PdfExporter.getRealVerticalAlignment(zssCell); 
			double offset; // y offset for vertical alignment(between real and render) 
			switch(valign) {
			default:
			case JUSTIFY:
			case TOP:
				offset = height - renderHeight; // the extra PdfExporter
				break;
			case CENTER:
				offset = (height - renderHeight) / 2;
				break;
			case BOTTOM:
				offset = 0;
				break;
			}
			double offsetX = renderInfo.getOffsetX();
			switch(align) {
			default:
			case FILL:
			case JUSTIFY:
			case LEFT:
			case CENTER:
			case CENTER_SELECTION:
				offsetX += PdfExporter.PAD_LEFT; 
				break;
			case RIGHT:
				offsetX -= PdfExporter.PAD_LEFT; 
				break;
			}
			double offsetY = renderInfo.getOffsetY() + offset + PdfExporter.PAD_BOTTOM;
			//clip the text
			PdfTemplate tmpl = txtCanvas.createTemplate(clipRect.getWidth(), clipRect.getHeight()); //clipping rectangle
			ct = new ColumnText(tmpl);
			ct.setLeading(0f, LINE_SPACING_RATIO); //ZSS-774
			ct.setSimpleColumn((float) offsetX, (float) offsetY, 
					(float) (renderWidth +  offsetX), 
					(float) (renderHeight + renderDecender + offsetY));
			ct.setAlignment(StyleConversionUtil.getCorrespondingPdfPCellAlignment(align));
			ct.setText(phrase);
			ct.go();
			txtCanvas.addTemplate(tmpl, clipRect.getLeft(), clipRect.getBottom());
			drawAccountingUnderline(clipRect, cellRect, (float) offsetX, (float) offsetY, (float) renderWidth, (float) realWidth, txtCanvas);
		} catch (DocumentException e) {
			throw new ExceptionConverter(e);
		}
	}
	
	//ZSS-789
	private void drawAccountingUnderline(Rectangle clipRect, Rectangle cellRect, 
			float offsetX, float offsetY, float renderWidth, float realWidth, PdfContentByte txtCanvas) {
		if (style == null) {
			return; // no style cell; e.g. row heading and column heading
		}
		SFont sFont = style.getFont();
		Underline under = sFont.getUnderline();
		if (under == Underline.DOUBLE_ACCOUNTING || under == Underline.SINGLE_ACCOUNTING) {
			final boolean extended = renderInfo.isExtended(); 
			// extended long text does not overflow the accounting underline to sibling cell 
			if (extended) {
				clipRect = cellRect;
			}
			
			float renderL = (float) (clipRect.getLeft() + renderInfo.getOffsetX());
			float renderB = (float) (clipRect.getBottom() + renderInfo.getOffsetY());
			float renderR = (float) (renderL + renderInfo.getWidth());
			BorderStyle lineStyle = new BorderStyle(PdfExporter.UNDERLINE_WIDTH, PdfContentByte.LINE_CAP_BUTT, new float[]{}, 0f);
			String lineColor = sFont.getColor().getHtmlColor();
			float l = clipRect.getLeft() + (renderL == clipRect.getLeft() || extended ? PdfExporter.PAD_LEFT : 0f);
			float r = clipRect.getRight() - (renderR == clipRect.getRight() || extended ? PdfExporter.PAD_RIGHT : 0f);
			
			final boolean number = renderInfo.isNumber();
			if (number) {
				float offsetH = 0f;
				float realL = 0f;
				// accounting underline of number's is the same width of the number 
				switch (style.getAlignment()) {
				default:
				case CENTER:
				case CENTER_SELECTION:
					// center
					offsetH = (renderWidth - realWidth) / 2;
					realL = renderL + offsetH + PdfExporter.PAD_LEFT;
					break;
				case LEFT:
				case FILL:
				case JUSTIFY:
					// left
					offsetH = 0f;
					realL = renderL + PdfExporter.PAD_LEFT;
					break;
				case GENERAL:
				case RIGHT:
					// right
					offsetH = renderWidth - realWidth;
					realL = renderL + offsetH - PdfExporter.PAD_LEFT;;
					break;
				}
				
				l = Math.max(realL, l);
				float realR = realL + realWidth;
				r = Math.min(realR, r);
			}
			
			float b = renderB + offsetY - PdfExporter.ACCOUNT_UNDERLINE_OFFSET + PdfExporter.DOUBLE_LINE_SPACE;
			float t = b + clipRect.getHeight();
			if (clipRect.getBottom() <= b && b <= clipRect.getTop()) {
				Rectangle rect0 = new Rectangle(l, b, r, t);
				drawBorder(rect0, txtCanvas, lineStyle, lineColor, BORDER.BOTTOM, null);
			}
			
			if (under == Underline.DOUBLE_ACCOUNTING) {
				b = b - PdfExporter.DOUBLE_LINE_SPACE;
				if (clipRect.getBottom() <= b && b <= clipRect.getTop()) {
					Rectangle rect1 = new Rectangle(l, b, r, t);
					drawBorder(rect1, txtCanvas, lineStyle, lineColor, BORDER.BOTTOM, null);
				}
			}
		}
	}
	
	
	private void processBottomBorder(Rectangle rect, PdfContentByte lineCanvas, SCell sCell) {
		processBottomBorder0(rect, lineCanvas, sCell);
		drawOutline(rect, lineCanvas, BORDER.BOTTOM);
	}
	private void processBottomBorder0(Rectangle rect, PdfContentByte lineCanvas, SCell sCell) {
		final int rowIdx = clipRegion.getRow();
		
		//20140925, henrichen: avoid draw bottom side border of a merged/extended area
		if (renderRegion == null || rowIdx == renderRegion.getLastRow()) {
			final int colIdx = clipRegion.getColumn();
			if (sCell != null && (sCell.getColumnIndex() != colIdx || sCell.getRowIndex() != rowIdx)) {
				sCell = sheet.getCell(rowIdx, colIdx);
			}
			
			//1. check current cell's Bottom border
			BorderInfo info = getBorderInfo(BORDER.BOTTOM, sCell);
			if (info != null) {
				if (info.type != BorderType.DOUBLE) {
					drawBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.BOTTOM, info.type);
				} else { //ZSS-776: a double border
					SCell nextCell = sheet.getCell(rowIdx + 1, colIdx);
					drawDoubleBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.BOTTOM, 
							sCell, rowIdx, colIdx, nextCell, rowIdx+1, colIdx);
				}
				return;
			}
			
			//2. check bottom cell's "Top" border
			SCell nextCell = sheet.getCell(rowIdx + 1, colIdx);
			info = getBorderInfo(BORDER.TOP, nextCell);
			if (info != null) {
				if (info.type != BorderType.DOUBLE) {
					drawBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.BOTTOM, info.type);
				} else { //ZSS-776: a double border
					drawDoubleBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.BOTTOM, 
							sCell, rowIdx, colIdx, nextCell, rowIdx + 1, colIdx);
				}
				return;
			}

			//3. check current cell's background color
			info = getBorderInfo(BORDER.BACKGROUND, sCell);
			if (info != null) {
				//ZSS-780: fill background; don't draw grid line
				return;
			}
			
			//3A. check bottom cell's background color
			info = getBorderInfo(BORDER.BACKGROUND, nextCell);
			if (info != null) {
				//ZSS-780: fill background; don't draw grid line
				return;
			}
			
			// ZSS-399: print grid lines according to print options
			drawGridlines(rect, lineCanvas, BORDER.BOTTOM);
		}
	}

	private void processRightBorder(Rectangle rect, PdfContentByte lineCanvas, SCell sCell) {
		processRightBorder0(rect, lineCanvas, sCell);
		drawOutline(rect, lineCanvas, BORDER.RIGHT);
	}
	private void processRightBorder0(Rectangle rect, PdfContentByte lineCanvas, SCell sCell) {
		final int colIdx = clipRegion.getColumn();
		//20140925, henrichen: avoid draw right hand side border of a merged/extended area
		if (renderRegion == null || colIdx == renderRegion.getLastColumn()) {
			final int rowIdx = clipRegion.getRow();
			if (sCell != null && (sCell.getColumnIndex() != colIdx || sCell.getRowIndex() != rowIdx)) {
				sCell = sheet.getCell(rowIdx, colIdx);
			}

			//1. check current cell's Right border
			BorderInfo info = getBorderInfo(BORDER.RIGHT, sCell);
			if (info != null) {
				if (info.type != BorderType.DOUBLE) {
					drawBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.RIGHT, info.type);
				} else { //ZSS-776: a double border
					SCell rightCell = sheet.getCell(rowIdx, colIdx + 1);
					drawDoubleBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.RIGHT, 
							sCell, rowIdx, colIdx, rightCell, rowIdx, colIdx + 1);
				}
				return;
			}
			
			//2. check right cell's "Left" border
			SCell rightCell = sheet.getCell(rowIdx, colIdx + 1);
			info = getBorderInfo(BORDER.LEFT, rightCell);
			if (info != null) {
				if (info.type != BorderType.DOUBLE) {
					drawBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.RIGHT, info.type);
				} else { //ZSS-776: a double border
					drawDoubleBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.RIGHT, 
							sCell, rowIdx, colIdx, rightCell, rowIdx, colIdx + 1);
				}
				return;
			}

			//3. check current cell's background color
			info = getBorderInfo(BORDER.BACKGROUND, sCell);
			if (info != null) {
				//ZSS-780: fill background; don't draw grid line
				return;
			}
			
			//3A. check right cell's background color
			info = getBorderInfo(BORDER.BACKGROUND, rightCell);
			if (info != null) {
				//ZSS-780: fill background; don't draw grid line
				return;
			}
			
			// ZSS-399: print grid lines according to print options
			drawGridlines(rect, lineCanvas, BORDER.RIGHT);
		}
	}
	
	private void processTopBorder(Rectangle rect, PdfContentByte lineCanvas, SCell sCell) {
		if (clipRegion.getRow() == outlineRegion.getRow()) {
			processTopBorder0(rect, lineCanvas, sCell);
			drawOutline(rect, lineCanvas, BORDER.TOP);
		}
	}
	private void processTopBorder0(Rectangle rect, PdfContentByte lineCanvas, SCell sCell) {
		final int rowIdx = clipRegion.getRow();
		//20140925, henrichen: avoid draw top hand side border of a merged/extended area
		if (renderRegion == null || rowIdx == renderRegion.getRow()) {
			final int colIdx = clipRegion.getColumn();
			BorderInfo info = null;
			SCell topCell = null;
			//1. check top cell's "Bottom" border
			if (rowIdx > 0) {
				topCell = sheet.getCell(rowIdx - 1, colIdx);
				info = getBorderInfo(BORDER.BOTTOM, topCell);
				if (info != null && info.type != BorderType.DOUBLE) {
					drawBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.TOP, info.type);
					return;
				}
			}

			if (sCell != null && (sCell.getColumnIndex() != colIdx || sCell.getRowIndex() != rowIdx)) {
				sCell = sheet.getCell(rowIdx, colIdx);
			}
			
			//ZSS-776
			//1A. check if a double border
			if (info != null && info.type == BorderType.DOUBLE) {
				drawDoubleBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.TOP,
						sCell, rowIdx, colIdx, topCell, rowIdx - 1, colIdx);
				return;
			}

			//2. check current cell's "Top" border
			info = getBorderInfo(BORDER.TOP, sCell);
			if (info != null) {
				if (info.type != BorderType.DOUBLE) {
					drawBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.TOP, info.type);
				} else { //ZSS-776: a double border
					drawDoubleBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.TOP,
							sCell, rowIdx, colIdx, topCell, rowIdx - 1, colIdx);
				}
				return;
			}
			
			//3. check top cell's background color
			info = getBorderInfo(BORDER.BACKGROUND, topCell);
			if (info != null) {
				//ZSS-780: fill background; don't draw grid line
				return;
			}
			
			//3A. check current cell's background color
			info = getBorderInfo(BORDER.BACKGROUND, sCell);
			if (info != null) {
				//ZSS-780: fill background; don't draw grid line
				return;
			}
			
			// ZSS-399: print grid lines according to print options
			drawGridlines(rect, lineCanvas, BORDER.TOP);
		}	
	}
	
	private void processLeftBorder(Rectangle rect, PdfContentByte lineCanvas, SCell sCell) {
		if (clipRegion.getColumn() == outlineRegion.getColumn()) {
			processLeftBorder0(rect, lineCanvas, sCell);
			drawOutline(rect, lineCanvas, BORDER.LEFT);
		}
	}
	
	private void processLeftBorder0(Rectangle rect, PdfContentByte lineCanvas, SCell sCell) {
		final int colIdx = clipRegion.getColumn();
		//20140925, henrichen: avoid draw left side border of a merged/extended area
		if (renderRegion == null || colIdx == renderRegion.getColumn()) {
			final int rowIdx = clipRegion.getRow();
			BorderInfo info = null;
			SCell leftCell = null;
			//1. check left cell's "Right" border
			if (colIdx > 0) {
				leftCell = sheet.getCell(rowIdx, colIdx - 1);
				info = getBorderInfo(BORDER.RIGHT, leftCell);
				if (info != null && info.type != BorderType.DOUBLE) {
					drawBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.LEFT, info.type);
					return;
				}
			}
			
			if (sCell != null && (sCell.getColumnIndex() != colIdx || sCell.getRowIndex() != rowIdx)) {
				sCell = sheet.getCell(rowIdx, colIdx);
			}
			
			//ZSS-776
			//1A. check if a double border
			if (info != null && info.type == BorderType.DOUBLE) {
				drawDoubleBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.LEFT,
						sCell, rowIdx, colIdx, leftCell, rowIdx, colIdx - 1);
				return;
			}
			
			//2. check current cell's "Left" border
			info = getBorderInfo(BORDER.LEFT, sCell);
			if (info != null) {
				if (info.type != BorderType.DOUBLE) {
					drawBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.LEFT, info.type);
				} else { //ZSS-776: a double border
					drawDoubleBorder(rect, lineCanvas, info.style, info.htmlColor, BORDER.LEFT,
							sCell, rowIdx, colIdx, leftCell, rowIdx, colIdx - 1);
				}
				return;
			}
			
			//3. check left cell's background color
			info = getBorderInfo(BORDER.BACKGROUND, leftCell);
			if (info != null) {
				//ZSS-780: fill background; don't draw grid line
				return;
			}
			
			//3A. check current cell's background color
			info = getBorderInfo(BORDER.BACKGROUND, sCell);
			if (info != null) {
				//ZSS-780: fill background; don't draw grid line
				return;
			}
			
			// ZSS-399: print grid lines according to print options
			drawGridlines(rect, lineCanvas, BORDER.LEFT);
		}
	}
	
	/**
	 * draw outline of PdfPCell
	 */
	private void drawOutline(Rectangle rect, PdfContentByte lineCanvas, BORDER border) {
		boolean printOutline = (isPrintGridLines() || isPrintOutline());
		if (printOutline) {
			switch(border) {
			case TOP:
				printOutline = clipRegion.getRow() == outlineRegion.getRow();
				break;
			case LEFT:
				printOutline = clipRegion.getColumn() == outlineRegion.getColumn();
				break;
			case BOTTOM:
				printOutline = clipRegion.getRow() == outlineRegion.getLastRow();
				break;
			case RIGHT:
				printOutline = clipRegion.getColumn() == outlineRegion.getLastColumn();
				break;
			default:
				printOutline = false;
			}
		}
		if (printOutline) {
			lineCanvas.saveState();
			lineCanvas.setLineWidth(PdfExporter.OUTLINE_WIDTH);
			lineCanvas.setLineCap(PdfContentByte.LINE_CAP_BUTT);
			lineCanvas.setLineDash(new float[]{}, 0f);
			lineCanvas.setColorStroke(java.awt.Color.BLACK);
			setBorderEndPoints(rect, lineCanvas, border);
			lineCanvas.stroke();
			lineCanvas.restoreState();
		}
	}
	
	private void drawGridlines(Rectangle rect, PdfContentByte lineCanvas, BORDER border) {
		if (isPrintGridLines()) { // ZSS-399: adjust to grid line style
			lineCanvas.saveState();
			lineCanvas.setLineWidth(PdfExporter.GRIDLINE_WIDTH);
			lineCanvas.setLineCap(PdfContentByte.LINE_CAP_BUTT);
			lineCanvas.setLineDash(new float[]{}, 0f);
			lineCanvas.setColorStroke(java.awt.Color.GRAY);
			setBorderEndPoints(rect, lineCanvas, border);
			lineCanvas.stroke();
			lineCanvas.restoreState();
		}
	}

	/**
	 * draws border of PdfPCell
	 * @param rect
	 * @param lineCanvas
	 * @param borderStyle
	 * @param bottomBorderColor
	 * @param border
	 */
	private void drawBorder(Rectangle rect, PdfContentByte lineCanvas, 
			BorderStyle borderStyle, String sBorderColor, BORDER border, Object type) {
		// draw the border line
		drawBorderLine(rect, lineCanvas, borderStyle, sBorderColor, border);
	}
	
	//ZSS-776
	private final static float DOUBLE_LINE_HALF_SPACE = PdfExporter.DOUBLE_LINE_SPACE / 2; 
	private void drawDoubleBorder(Rectangle rect, PdfContentByte lineCanvas, 
			BorderStyle borderStyle, String sBorderColor, BORDER border, 
			SCell sCell, int rowIdx, int colIdx, 
			SCell sibling, int sibRowIdx, int sibColIdx) {
		final float t = rect.getTop();
		final float l = rect.getLeft();
		final float r = rect.getRight();
		final float b = rect.getBottom();
		
		//handle the corner offset
		final boolean[] inner = getDoubleBorderCornerInfo(border, sCell, rowIdx, colIdx);
		final boolean[] outer = getDoubleBorderCornerInfo(border, sibling, sibRowIdx, sibColIdx);
		float innerOffsetB = inner[0] ? DOUBLE_LINE_HALF_SPACE : outer[0] ? -DOUBLE_LINE_HALF_SPACE : 0f;
		float innerOffsetE = inner[1] ? DOUBLE_LINE_HALF_SPACE : outer[1] ? -DOUBLE_LINE_HALF_SPACE : 0f;
		float outerOffsetB = outer[0] ? DOUBLE_LINE_HALF_SPACE : inner[0] ? -DOUBLE_LINE_HALF_SPACE : 0f;
		float outerOffsetE = outer[1] ? DOUBLE_LINE_HALF_SPACE : inner[1] ? -DOUBLE_LINE_HALF_SPACE : 0f;
		
		Rectangle innerRect;
		Rectangle outerRect;
		switch(border) {
		default:
		case TOP:
			// avoid border stick out outline border
			if (colIdx == outlineRegion.getLastColumn()) {
				if (outerOffsetE < 0f) outerOffsetE = 0f;
				else if (innerOffsetE < 0f) innerOffsetE = 0f;
			}
			if (colIdx == outlineRegion.getColumn()) {
				if (outerOffsetB < 0f) outerOffsetB = 0f;
				else if (innerOffsetB < 0f) innerOffsetB = 0f;
			}
			innerRect = new Rectangle(l + innerOffsetB, b, r - innerOffsetE, t - DOUBLE_LINE_HALF_SPACE);
			outerRect = new Rectangle(l + outerOffsetB, b, r - outerOffsetE, t + DOUBLE_LINE_HALF_SPACE);
			if (colIdx != outlineRegion.getRow()) {
				// draw the outer border line
				drawBorderLine(outerRect, lineCanvas, borderStyle, sBorderColor, border);
			}
			break;
		case BOTTOM:
			// avoid border stick out outline border
			if (colIdx == outlineRegion.getLastColumn()) {
				if (outerOffsetE < 0f) outerOffsetE = 0f;
				else if (innerOffsetE < 0f) innerOffsetE = 0f;
			}
			if (colIdx == outlineRegion.getColumn()) {
				if (outerOffsetB < 0f) outerOffsetB = 0f;
				else if (innerOffsetB < 0f) innerOffsetB = 0f;
			}
			outerRect = new Rectangle(l + outerOffsetB, b - DOUBLE_LINE_HALF_SPACE, r - outerOffsetE, t);
			innerRect = new Rectangle(l + innerOffsetB, b + DOUBLE_LINE_HALF_SPACE, r - innerOffsetE, t);
			if (rowIdx != outlineRegion.getLastRow()) {
				// draw the outer border line
				drawBorderLine(outerRect, lineCanvas, borderStyle, sBorderColor, border);
			}
			break;
		case LEFT:
			// avoid border stick out outline border
			if (rowIdx == outlineRegion.getLastRow()) {
				if (outerOffsetE < 0f) outerOffsetE = 0f;
				else if (innerOffsetE < 0f) innerOffsetE = 0f;
			}
			if (rowIdx == outlineRegion.getRow()) {
				if (outerOffsetB < 0f) outerOffsetB = 0f;
				else if (innerOffsetB < 0f) innerOffsetB = 0f;
			}
			outerRect = new Rectangle(l - DOUBLE_LINE_HALF_SPACE, b + outerOffsetE, r, t - outerOffsetB);
			innerRect = new Rectangle(l + DOUBLE_LINE_HALF_SPACE, b + innerOffsetE, r, t - innerOffsetB);
			if (colIdx != outlineRegion.getColumn()) {
				// draw the outer border line
				drawBorderLine(outerRect, lineCanvas, borderStyle, sBorderColor, border);
			}
			break;
		case RIGHT:
			// avoid border stick out outline border
			if (rowIdx == outlineRegion.getLastRow()) {
				if (outerOffsetE < 0f) outerOffsetE = 0f;
				else if (innerOffsetE < 0f) innerOffsetE = 0f;
			}
			if (rowIdx == outlineRegion.getRow()) {
				if (outerOffsetB < 0f) outerOffsetB = 0f;
				else if (innerOffsetB < 0f) innerOffsetB = 0f;
			}
			innerRect = new Rectangle(l, b + innerOffsetE, r - DOUBLE_LINE_HALF_SPACE, t - innerOffsetB);
			outerRect = new Rectangle(l, b + outerOffsetE, r + DOUBLE_LINE_HALF_SPACE, t - outerOffsetB);
			if (colIdx != outlineRegion.getLastColumn()) {
				// draw the outer border line
				drawBorderLine(outerRect, lineCanvas, borderStyle, sBorderColor, border);
			}
			break;
		}
		// draw the inner border line
		drawBorderLine(innerRect, lineCanvas, borderStyle, sBorderColor, border);		
	}

	private void drawBorderLine(Rectangle rect, PdfContentByte lineCanvas, 
			BorderStyle borderStyle, String sBorderColor, BORDER border) {
		lineCanvas.saveState();
		lineCanvas.setLineWidth(borderStyle.getLineWidth());
		lineCanvas.setLineCap(borderStyle.getLineCapStyle());
		lineCanvas.setLineDash(borderStyle.getUnits(), borderStyle.getPhase());
		//ZSS-446, auto-color of bolder should be black
		java.awt.Color borderColor = WebColors.getRGBColor((sBorderColor == null 
				|| BookHelper.AUTO_COLOR.equals(sBorderColor)) ? "BLACK" : sBorderColor);
		lineCanvas.setColorStroke(java.awt.Color.black);
		if (borderColor != null && borderColor != java.awt.Color.white) {
			lineCanvas.setColorStroke(borderColor);
		}
		
		setBorderEndPoints(rect, lineCanvas, border);
		lineCanvas.stroke();
		lineCanvas.restoreState();
	}

	/**
	 * @param rect
	 * @param lineCanvas
	 * @param border
	 */
	private void setBorderEndPoints(Rectangle rect, PdfContentByte lineCanvas,
			BORDER border) {
		switch(border) {
		default:
		case BOTTOM:
			lineCanvas.moveTo(rect.getLeft(), rect.getBottom());
			lineCanvas.lineTo(rect.getRight(), rect.getBottom());
			break;
		case RIGHT:
			lineCanvas.moveTo(rect.getRight(), rect.getBottom());
			lineCanvas.lineTo(rect.getRight(), rect.getTop());
			break;
		case TOP:
			lineCanvas.moveTo(rect.getLeft(), rect.getTop());
			lineCanvas.lineTo(rect.getRight(), rect.getTop());
			break;
		case LEFT:
			lineCanvas.moveTo(rect.getLeft(), rect.getBottom());
			lineCanvas.lineTo(rect.getLeft(), rect.getTop());
			break;
		}
	}
	
	/**
	 * sets POI style information
	 * @param style
	 */
	private void setStyle(SCellStyle style) {
		this.style = style;
	}
	
	/**
	 * sets print grid lines
	 * @param printGridLines
	 */
	private void setPrintGridLines(boolean printGridLines) {
		this.printGridLines = printGridLines;
	}
	
	/**
	 * returns print grid lines flag status
	 * @return
	 */
	private boolean isPrintGridLines() {
		return printGridLines;
	}

	/**
	 * sets whether print outline
	 * @param printOutline
	 */
	private void setPrintOutline(boolean printOutline) {
		this.printOutline = printOutline;
	}
	
	/**
	 * returns print headings flag status
	 * @return
	 */
	private boolean isPrintOutline() {
		return printOutline;
	}

	private class BorderInfo {
		final Object type; // in BORDER.BACKGROUND, type is FillPattern; otherwise, a BorderStyle 
		final BorderStyle style;
		final String htmlColor;
		
		BorderInfo (Object borderType, SColor borderColor) {
			this.type = borderType;
			if(borderType instanceof BorderType) {
				style = StyleConversionUtil.getBorderStyle((BorderType)borderType);
			} else {
				style = null;
			}
			htmlColor = borderColor.getHtmlColor();
		}
	}
	
	//ZSS-974
	private void setPixelInfo(PixelInfo pixelInfo) {
		this.pixelInfo = pixelInfo;
	}
	
	//ZSS-974
	private void drawFillPattern(Rectangle rect, SFill fill, PdfContentByte canvas) {
		final int offsetX = pixelInfo.getOffsetX();
		final int offsetY = pixelInfo.getOffsetY();
		final int widthPx = pixelInfo.getWidth();
		final int heightPx = pixelInfo.getHeight();
		byte[] rawData = FillImpl.getFillPatternBytes(fill, offsetX, offsetY, widthPx, heightPx);
		Image image;
		try {
			image = Image.getInstance(rawData);

			float width = rect.getWidth();
			float height = rect.getHeight();
			float x = rect.getLeft();
			float y = rect.getBottom();
			image.scaleAbsolute((float)width, (float)height);
			image.setAbsolutePosition((float)x, (float)y); // left-bottom corner
			canvas.addImage(image);
		} catch (BadElementException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (DocumentException e) {
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
