/*
 * JBoss, Home of Professional Open Source
 * Copyright ${year}, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This 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 software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.richfaces.renderkit;

import java.io.IOException;
import java.util.Iterator;

import javax.faces.application.ResourceDependencies;
import javax.faces.application.ResourceDependency;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

import org.richfaces.component.Row;
import org.richfaces.component.UIDataTableBase;
import org.richfaces.component.util.HtmlUtil;

/**
 * @author Anton Belevich
 *
 */
@ResourceDependencies({ @ResourceDependency(name = "richfaces-event.js") })
public abstract class AbstractTableRenderer extends AbstractTableBaseRenderer implements MetaComponentRenderer {
    public static final String HIDDEN_CONTAINER_ID = ":sc";

    public abstract EncodeStrategy getHiddenContainerStrategy(UIDataTableBase dataTableBase);

    public boolean isColumnAttributeSet(UIDataTableBase table, String attributeName) {
        Iterator<UIComponent> columns = table.columns();
        boolean result = false;
        while (columns.hasNext() && !result) {
            UIComponent component = columns.next();
            result = (component.isRendered() && (null != component.getValueExpression(attributeName)));
        }
        return result;
    }

    @Override
    public RowHolderBase createRowHolder(FacesContext context, UIComponent component, Object[] options) {
        RowHolder rowHolder = null;
        if (component instanceof UIDataTableBase) {
            rowHolder = new RowHolder(context, (UIDataTableBase) component);
            rowHolder.setUpdatePartial((Boolean) options[0]);
            rowHolder.setEncodeParentTBody((Boolean) options[1]);
        }

        return rowHolder;
    }

    protected class SimpleHeaderEncodeStrategy implements EncodeStrategy {
        public void begin(ResponseWriter writer, FacesContext context, UIComponent column, Object[] params) throws IOException {
        }

        public void end(ResponseWriter writer, FacesContext context, UIComponent column, Object[] params) throws IOException {
        }
    }

    protected void doDecode(FacesContext context, UIComponent component) {
        decodeSortingFiltering(context, component);
    }

    protected void putRowStylesIntoContext(FacesContext facesContext, RowHolderBase rowHolder) {
        UIDataTableBase dataTableBase = (UIDataTableBase) rowHolder.getRow();

        String rowClass = getRowSkinClass();
        String cellClass = getCellSkinClass();
        String firstClass = getFirstRowSkinClass();

        rowClass = mergeStyleClasses(ROW_CLASS_KEY, rowClass, dataTableBase);
        cellClass = mergeStyleClasses(CELL_CLASS_KEY, cellClass, dataTableBase);
        firstClass = mergeStyleClasses(FIRST_ROW_CLASS_KEY, firstClass, dataTableBase);

        saveRowStyles(facesContext, dataTableBase.getClientId(facesContext), firstClass, rowClass, cellClass);
    }

    protected boolean isEncodeHeaders(UIDataTableBase table) {
        return table.isColumnFacetPresent(UIDataTableBase.HEADER) || isColumnAttributeSet(table, "sortBy")
            || isColumnAttributeSet(table, "comparator") || isColumnAttributeSet(table, "filterBy");
    }

    public void encodeTableStructure(ResponseWriter writer, FacesContext context, UIDataTableBase dataTable) throws IOException {
        // DataTableRenderer override this method
    }

    public void encodeBeforeRows(ResponseWriter writer, FacesContext facesContext, UIDataTableBase dataTableBase,
        boolean encodeParentTBody, boolean partialUpdate) throws IOException {
    }

    public void encodeAfterRows(ResponseWriter writer, FacesContext facesContext, UIDataTableBase dataTableBase,
        boolean encodeParentTBody, boolean partialUpdate) throws IOException {
    }

    public abstract boolean encodeParentTBody(UIDataTableBase dataTableBase);

    public void encodeTableFacets(ResponseWriter writer, FacesContext context, UIDataTableBase dataTable) throws IOException {

        Object key = dataTable.getRowKey();
        dataTable.captureOrigValue(context);
        dataTable.setRowKey(context, null);

        encodeTableStructure(writer, context, dataTable);

        encodeHeaderFacet(writer, context, dataTable, false);
        encodeFooterFacet(writer, context, dataTable, false);
        dataTable.setRowKey(context, key);
        dataTable.restoreOrigValue(context);
    }

    public void encodeTableRows(ResponseWriter writer, FacesContext facesContext, UIDataTableBase dataTableBase,
        boolean encodePartialUpdate) throws IOException {

        int rowCount = dataTableBase.getRowCount();

        Object key = dataTableBase.getRowKey();
        dataTableBase.captureOrigValue(facesContext);
        dataTableBase.setRowKey(facesContext, null);

        boolean encodeParentTBody = encodeParentTBody(dataTableBase);
        encodeBeforeRows(writer, facesContext, dataTableBase, encodeParentTBody, encodePartialUpdate);

        if (rowCount > 0) {
            processRows(writer, facesContext, dataTableBase, new Object[] { encodePartialUpdate, encodeParentTBody });
        } else {
            encodeNoDataFacetOrLabel(writer, facesContext, dataTableBase);
        }

        encodeAfterRows(writer, facesContext, dataTableBase, encodeParentTBody, encodePartialUpdate);

        if (encodePartialUpdate) {
            String id = dataTableBase.getClientId(facesContext) + HIDDEN_CONTAINER_ID;
            partialStart(facesContext, id);
        }

        encodeHiddens(writer, facesContext, dataTableBase, new Object[] { encodeParentTBody });

        if (encodePartialUpdate) {
            partialEnd(facesContext);
        }

        dataTableBase.setRowKey(facesContext, key);
        dataTableBase.restoreOrigValue(facesContext);
    }

    public void encodeNoDataFacetOrLabel(ResponseWriter writer, FacesContext facesContext, UIDataTableBase dataTableBase)
        throws IOException {

        int columns = getColumnsCount(dataTableBase.columns());
        UIComponent noDataFacet = dataTableBase.getNoData();
        String noDataLabel = dataTableBase.getNoDataLabel();

        writer.startElement(HtmlConstants.TR_ELEMENT, dataTableBase);
        String styleClass = (String) dataTableBase.getAttributes().get("noDataStyleClass");
        styleClass = concatClasses(getNoDataClass(), styleClass);
        writer.writeAttribute(HtmlConstants.CLASS_ATTRIBUTE, styleClass, null);

        writer.startElement(HtmlConstants.TD_ELEM, dataTableBase);
        writer.writeAttribute(HtmlConstants.COLSPAN_ATTRIBUTE, columns, null);

        String cellStyleClass = (String) dataTableBase.getAttributes().get("noDataCellStyleClass");
        cellStyleClass = concatClasses(getNoDataCellClass(), cellStyleClass);
        writer.writeAttribute(HtmlConstants.CLASS_ATTRIBUTE, cellStyleClass, null);

        if (noDataFacet != null && noDataFacet.isRendered()) {
            noDataFacet.encodeAll(facesContext);
        } else if (noDataLabel != null && noDataLabel.length() > 0) {
            writer.writeText(noDataLabel, "noDataLabel");
        } else {
            writer.writeAttribute(HtmlConstants.STYLE_ATTRIBUTE, "display: none;", null);
        }
        writer.endElement(HtmlConstants.TD_ELEM);
        writer.endElement(HtmlConstants.TR_ELEMENT);
    }

    protected void doEncodeChildren(ResponseWriter writer, FacesContext context, UIComponent component) throws IOException {
        if (component instanceof UIDataTableBase) {
            encodeTableRows(writer, context, (UIDataTableBase) component, false);
        }
    }

    public void encodeTableStart(ResponseWriter writer, FacesContext context, UIDataTableBase component) throws IOException {
        writer.startElement(HtmlConstants.TABLE_ELEMENT, component);
        writer.writeAttribute(HtmlConstants.ID_ATTRIBUTE, component.getClientId(context), null);
        String styleClass = getTableSkinClass();
        encodeStyleClass(writer, context, component, HtmlConstants.STYLE_CLASS_ATTR, styleClass);
    }

    protected void encodeHiddens(ResponseWriter writer, FacesContext facesContext, UIDataTableBase dataTableBase,
        Object[] params) throws IOException {
        EncodeStrategy encodeStrategy = getHiddenContainerStrategy(dataTableBase);
        if (encodeStrategy != null) {
            encodeStrategy.begin(writer, facesContext, dataTableBase, params);

            encodeClientScript(writer, facesContext, dataTableBase);
            encodeHiddenInput(writer, facesContext, dataTableBase);

            encodeStrategy.end(writer, facesContext, dataTableBase, params);
        }
    }

    public void encodeTableEnd(ResponseWriter writer) throws IOException {
        writer.endElement(HtmlConstants.TABLE_ELEMENT);
    }

    public abstract void encodeClientScript(ResponseWriter writer, FacesContext context, UIDataTableBase component)
        throws IOException;

    public abstract void encodeHiddenInput(ResponseWriter writer, FacesContext context, UIDataTableBase component)
        throws IOException;

    public void encodeTableBodyStart(ResponseWriter writer, FacesContext facesContext, UIDataTableBase dataTableBase)
        throws IOException {

        writer.startElement(HtmlConstants.TBODY_ELEMENT, dataTableBase);
        String clientId = (dataTableBase.getRowKey() == null) ? dataTableBase.getContainerClientId(facesContext)
            : dataTableBase.getRelativeClientId(facesContext);

        writer.writeAttribute(HtmlConstants.ID_ATTRIBUTE, clientId + ":tb", null);
        writer.writeAttribute(HtmlConstants.CLASS_ATTRIBUTE, getTableBodySkinClass(), null);
    }

    public void encodeTableBodyEnd(ResponseWriter writer) throws IOException {
        writer.endElement(HtmlConstants.TBODY_ELEMENT);
    }

    public void encodeFooterFacet(ResponseWriter writer, FacesContext facesContext, UIDataTableBase dataTable,
        boolean encodePartialUpdate) throws IOException {

        UIComponent footer = dataTable.getFooter();
        boolean columnFacetPresent = dataTable.isColumnFacetPresent("footer");

        if ((footer != null && footer.isRendered()) || columnFacetPresent) {
            boolean partialUpdateEncoded = false;

            String clientId = dataTable.getClientId(facesContext);
            boolean encodeTfoot = containsThead();
            if (encodeTfoot) {
                String footerClientId = clientId + ":tf";

                if (encodePartialUpdate) {
                    partialUpdateEncoded = true;
                    partialStart(facesContext, footerClientId);
                }

                writer.startElement(HtmlConstants.TFOOT_ELEMENT, dataTable);
                writer.writeAttribute(HtmlConstants.ID_ATTRIBUTE, footerClientId, null);
                writer.writeAttribute(HtmlConstants.CLASS_ATTRIBUTE, "rf-dt-tft", null);
            }

            int columns = getColumnsCount(dataTable.columns());

            boolean encodePartialUpdateForChildren = (encodePartialUpdate && !partialUpdateEncoded);

            if (columnFacetPresent) {

                String rowClass = getColumnFooterSkinClass();
                String cellClass = getColumnFooterCellSkinClass();
                String firstClass = getColumnFooterFirstSkinClass();

                rowClass = mergeStyleClasses("columnFooterClass", rowClass, dataTable);
                cellClass = mergeStyleClasses("columnFooterCellClass", cellClass, dataTable);
                firstClass = mergeStyleClasses("firstColumnFooterClass", firstClass, dataTable);

                saveRowStyles(facesContext, clientId, firstClass, rowClass, cellClass);

                String targetId = clientId + ":cf";

                if (encodePartialUpdateForChildren) {
                    partialStart(facesContext, targetId);
                }

                writer.startElement(HtmlConstants.TR_ELEMENT, dataTable);
                writer.writeAttribute(HtmlConstants.ID_ATTRIBUTE, targetId, null);

                encodeStyleClass(writer, facesContext, dataTable, null, rowClass);
                encodeColumnFacet(facesContext, writer, dataTable, UIDataTableBase.FOOTER, columns, cellClass);
                writer.endElement(HtmlConstants.TR_ELEMENT);

                if (encodePartialUpdateForChildren) {
                    partialEnd(facesContext);
                }
            }

            if (footer != null && footer.isRendered()) {

                String rowClass = getFooterSkinClass();
                String cellClass = getFooterCellSkinClass();
                String firstClass = getFooterFirstSkinClass();

                rowClass = mergeStyleClasses("footerClass", rowClass, dataTable);
                cellClass = mergeStyleClasses("footerCellClass", cellClass, dataTable);
                firstClass = mergeStyleClasses("footerFirstClass", firstClass, dataTable);
                // TODO nick - rename method "encodeTableHeaderFacet"
                saveRowStyles(facesContext, clientId, firstClass, rowClass, cellClass);
                encodeTableFacet(facesContext, writer, clientId, columns, footer, UIDataTableBase.FOOTER, rowClass, cellClass,
                    encodePartialUpdateForChildren);
            }

            if (encodeTfoot) {
                writer.endElement(HtmlConstants.TFOOT_ELEMENT);

                if (partialUpdateEncoded) {
                    partialEnd(facesContext);
                }
            }
        }
    }

    protected String mergeStyleClasses(String classAttribibute, String skinClass, UIComponent component) {
        String resultClass = skinClass;

        String styleClass = null;
        if (classAttribibute != null && component != null) {
            styleClass = (String) component.getAttributes().get(classAttribibute);
        }

        return HtmlUtil.concatClasses(resultClass, styleClass);
    }

    public void encodeHeaderFacet(ResponseWriter writer, FacesContext facesContext, UIDataTableBase dataTable,
        boolean encodePartialUpdate) throws IOException {

        UIComponent header = dataTable.getHeader();
        boolean isEncodeHeaders = isEncodeHeaders(dataTable);

        boolean encodeThead = containsThead();

        if ((header != null && header.isRendered()) || isEncodeHeaders) {

            String id = dataTable.getClientId(facesContext);

            boolean partialUpdateEncoded = false;

            String clientId = dataTable.getClientId(facesContext);
            if (encodeThead) {
                String headerClientId = clientId + ":th";

                if (encodePartialUpdate) {
                    partialUpdateEncoded = true;
                    partialStart(facesContext, headerClientId);
                }

                writer.startElement(HtmlConstants.THEAD_ELEMENT, dataTable);
                setCellElement(facesContext, id, HtmlConstants.TH_ELEM);

                writer.writeAttribute(HtmlConstants.ID_ATTRIBUTE, headerClientId, null);
                writer.writeAttribute(HtmlConstants.CLASS_ATTRIBUTE, "rf-dt-thd", null);
            }

            int columns = getColumnsCount(dataTable.columns());

            boolean encodePartialUpdateForChildren = (encodePartialUpdate && !partialUpdateEncoded);

            if (header != null && header.isRendered()) {

                String rowClass = getHeaderSkinClass();
                String cellClass = getHeaderCellSkinClass();
                String firstClass = getHeaderFirstSkinClass();

                rowClass = mergeStyleClasses("headerClass", rowClass, dataTable);
                cellClass = mergeStyleClasses("headerCellClass", cellClass, dataTable);
                firstClass = mergeStyleClasses("headerFirstClass", firstClass, dataTable);
                saveRowStyles(facesContext, clientId, firstClass, rowClass, cellClass);

                encodeTableFacet(facesContext, writer, clientId, columns, header, UIDataTableBase.HEADER, rowClass, cellClass,
                    encodePartialUpdateForChildren);
            }

            if (isEncodeHeaders) {

                String rowClass = getColumnHeaderSkinClass();
                String cellClass = getColumnHeaderCellSkinClass();
                String firstClass = getColumnHeaderFirstSkinClass();

                rowClass = mergeStyleClasses("columnHeaderClass", rowClass, dataTable);
                cellClass = mergeStyleClasses("columnHeaderCellClass", cellClass, dataTable);
                firstClass = mergeStyleClasses("columnHeaderFirstClass", firstClass, dataTable);
                saveRowStyles(facesContext, clientId, firstClass, rowClass, cellClass);

                String targetId = clientId + ":ch";

                if (encodePartialUpdateForChildren) {
                    partialStart(facesContext, targetId);
                }

                writer.startElement(HtmlConstants.TR_ELEMENT, dataTable);
                writer.writeAttribute(HtmlConstants.ID_ATTRIBUTE, targetId, null);

                encodeStyleClass(writer, facesContext, dataTable, null, rowClass);

                encodeColumnFacet(facesContext, writer, dataTable, UIDataTableBase.HEADER, columns, cellClass);
                writer.endElement(HtmlConstants.TR_ELEMENT);

                if (encodePartialUpdateForChildren) {
                    partialEnd(facesContext);
                }
            }

            if (encodeThead) {
                setCellElement(facesContext, id, null);
                writer.endElement(HtmlConstants.THEAD_ELEMENT);

                if (partialUpdateEncoded) {
                    partialEnd(facesContext);
                }
            }
        }
    }

    protected void encodeColumnFacet(FacesContext context, ResponseWriter writer, UIDataTableBase dataTableBase,
        String facetName, int colCount, String cellClass) throws IOException {
        int tColCount = 0;
        String id = dataTableBase.getClientId(context);
        String element = getCellElement(context, id);

        Iterator<UIComponent> headers = dataTableBase.columns();

        while (headers.hasNext()) {
            UIComponent column = headers.next();

            if (!column.isRendered() || (column instanceof Row)) {
                continue;
            }

            Integer colspan = (Integer) column.getAttributes().get(HtmlConstants.COLSPAN_ATTRIBUTE);
            if (colspan != null && colspan.intValue() > 0) {
                tColCount += colspan.intValue();
            } else {
                tColCount++;
            }

            if (tColCount > colCount) {
                break;
            }

            writer.startElement(element, column);

            encodeStyleClass(writer, context, column, facetName + "Class", cellClass);

            if (HtmlConstants.TH_ELEM.equals(element)) { // HTML5 allows scope attr only on th elements
                writer.writeAttribute(HtmlConstants.SCOPE_ATTRIBUTE, HtmlConstants.COL_ELEMENT, null);
            }
            getUtils().encodeAttribute(context, column, HtmlConstants.COLSPAN_ATTRIBUTE);

            EncodeStrategy strategy = getHeaderEncodeStrategy(column, facetName);
            if (strategy != null) {
                strategy.begin(writer, context, column, new String[] { facetName });

                UIComponent facet = column.getFacet(facetName);
                if (facet != null && facet.isRendered()) {
                    facet.encodeAll(context);
                }
                strategy.end(writer, context, column, new String[] { facetName });
            }
            writer.endElement(element);
        }
    }

    protected void encodeTableFacet(FacesContext facesContext, ResponseWriter writer, String id, int columns,
        UIComponent footer, String facetName, String rowClass, String cellClass, boolean encodePartialUpdate)
        throws IOException {

        boolean isColumnGroup = (footer instanceof Row);
        String element = getCellElement(facesContext, id);

        boolean partialUpdateEncoded = false;

        if (!isColumnGroup) {
            String targetId = id + ":" + facetName.charAt(0);

            if (encodePartialUpdate) {
                partialUpdateEncoded = true;
                partialStart(facesContext, targetId);
            }

            writer.startElement(HtmlConstants.TR_ELEMENT, footer);
            writer.writeAttribute(HtmlConstants.ID_ATTRIBUTE, targetId, null);

            encodeStyleClass(writer, facesContext, footer, null, rowClass);

            writer.startElement(element, footer);

            encodeStyleClass(writer, facesContext, footer, null, cellClass);

            if (columns > 0) {
                writer.writeAttribute(HtmlConstants.COLSPAN_ATTRIBUTE, String.valueOf(columns), null);
            }

            if (HtmlConstants.TH_ELEM.equals(element)) { // HTML5 allows scope attr only on th elements
                writer.writeAttribute(HtmlConstants.SCOPE_ATTRIBUTE, HtmlConstants.COLGROUP_ELEMENT, null);
            }
        }

        if (encodePartialUpdate && !partialUpdateEncoded) {
            partialStart(facesContext, footer.getClientId(facesContext));
        }

        footer.encodeAll(facesContext);

        if (encodePartialUpdate && !partialUpdateEncoded) {
            partialEnd(facesContext);
        }

        if (isColumnGroup) {
            writer.endElement(HtmlConstants.TR_ELEMENT);
        } else {
            writer.endElement(element);
            writer.endElement(HtmlConstants.TR_ELEMENT);

            if (partialUpdateEncoded) {
                partialEnd(facesContext);
            }
        }
    }

    public abstract EncodeStrategy getHeaderEncodeStrategy(UIComponent column, String tableFacetName);

    public abstract boolean containsThead();

    public abstract String getTableSkinClass();

    public abstract String getTableBodySkinClass();

    public abstract String getFirstRowSkinClass();

    public abstract String getRowSkinClass();

    public abstract String getHeaderCellSkinClass();

    public abstract String getHeaderSkinClass();

    public abstract String getHeaderFirstSkinClass();

    public abstract String getColumnHeaderCellSkinClass();

    public abstract String getColumnHeaderSkinClass();

    public abstract String getColumnHeaderFirstSkinClass();

    public abstract String getFooterCellSkinClass();

    public abstract String getFooterSkinClass();

    public abstract String getFooterFirstSkinClass();

    public abstract String getColumnFooterCellSkinClass();

    public abstract String getColumnFooterSkinClass();

    public abstract String getColumnFooterFirstSkinClass();

    public abstract String getCellSkinClass();

    public abstract String getNoDataClass();

    public abstract String getNoDataCellClass();

    public void encodeMetaComponent(FacesContext context, UIComponent component, String metaComponentId) throws IOException {

        UIDataTableBase table = (UIDataTableBase) component;

        if (UIDataTableBase.HEADER.equals(metaComponentId)) {
            encodeHeaderFacet(context.getResponseWriter(), context, table, true);
        } else if (UIDataTableBase.FOOTER.equals(metaComponentId)) {
            encodeFooterFacet(context.getResponseWriter(), context, table, true);
        } else if (UIDataTableBase.BODY.equals(metaComponentId)) {
            encodeTableRows(context.getResponseWriter(), context, table, true);
        } else {
            throw new IllegalArgumentException("Unsupported metaComponentIdentifier: " + metaComponentId);
        }
    }

    public void decodeMetaComponent(FacesContext context, UIComponent component, String metaComponentId) {
        throw new UnsupportedOperationException();
    }

    protected void partialStart(FacesContext facesContext, String id) throws IOException {
        facesContext.getPartialViewContext().getPartialResponseWriter().startUpdate(id);
    }

    protected void partialEnd(FacesContext facesContext) throws IOException {
        facesContext.getPartialViewContext().getPartialResponseWriter().endUpdate();
    }
}
