package com.acme.tif.transform;

import com.technia.tif.core.transform.TransformContext;
import com.technia.tif.core.transform.Transformer;
import com.technia.tif.core.transform.TransformerException;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFFont;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * An example class that utilizes Apache POI library (3.17) to transform TIF payload to
 * an Excel file.
 *
 * To execute the class in TIF, the following POI JAR files needs to be placed under 
 * TIF_ROOT/modules/enovia/lib/custom: 
 * 
 * poi-3.17.jar
 * poi-ooxml-3.17.jar
 * poi-ooxml-schemas-3.17.jar
 * 
 * @author <a href="mailto:mikko.tuomela@technia.com">Mikko Tuomela</a>
 * @since 2019.1.0
 */
public class POIExcelTransformer implements Transformer {

    /**
     * Helper class to store header information.
     */
    private static class Header {

        private final String label;

        private final XPathExpression cellValueExpr;

        public Header(String id, String label) throws XPathExpressionException {
            cellValueExpr = compileExpression(String.format("cell[@colRef='%s']/value/text()", id));
            this.label = label;
        }

        public String getLabel() {
            return label;
        }

        public XPathExpression getCellValueExpression() {
            return cellValueExpr;
        }

    }

    private static final XPathFactory xpathFactory = XPathFactory.newInstance();

    /**
     * Main entry point of the tranformer.
     */
    @Override
    public void transform(TransformContext ctx) throws TransformerException {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(ctx.getInputStream());
            List<Header> headers = parseHeaders(document);
            NodeList rowNodes = queryListValue(document, compileExpression("/payload/objects/row"));
            writeWorkbook(headers, rowNodes, ctx.getOutputStream());
            ctx.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        } catch (IOException | XPathExpressionException | SAXException | ParserConfigurationException e) {
            throw new TransformerException(e);
        }
    }

    private void writeWorkbook(List<Header> headers, NodeList rowNodes, OutputStream out) throws IOException,
                                                                                          XPathExpressionException {
        XSSFWorkbook workbook = null;
        try {
            workbook = new XSSFWorkbook();
            XSSFSheet sheet = workbook.createSheet();
            createHeaderRow(headers, workbook, sheet);
            for (int i = 0; i < rowNodes.getLength(); ++i) {
                createRow(rowNodes.item(i), headers, sheet, i + 1);
            }
            workbook.write(out);
        } finally {
            if (workbook != null) {
                workbook.close();
            }
        }
    }

    private void createHeaderRow(List<Header> headers, XSSFWorkbook workbook, XSSFSheet sheet) {
        int count = 0;
        XSSFCellStyle style = workbook.createCellStyle();
        XSSFFont font = workbook.createFont();
        font.setBold(true);
        style.setFont(font);
        Row row = sheet.createRow(0);
        for (Header h : headers) {
            Cell cell = row.createCell(count++);
            cell.setCellValue(h.getLabel());
            cell.setCellStyle(style);
        }
    }

    private void createRow(Node rowNode,
                           List<Header> headers,
                           XSSFSheet sheet,
                           int rowNum) throws XPathExpressionException {
        int count = 0;
        Row row = sheet.createRow(rowNum);
        for (Header h : headers) {
            String value = queryTextValue(rowNode, h.getCellValueExpression());
            Cell cell = row.createCell(count++);
            cell.setCellValue(value);
        }
    }

    private List<Header> parseHeaders(Document document) throws XPathExpressionException {
        List<Header> headers = new ArrayList<>();
        NodeList headerNodes = queryListValue(document, compileExpression("/payload/headers/header"));
        XPathExpression headerIdExpr = compileExpression("@id");
        XPathExpression headerLabelExpr = compileExpression("label");
        for (int i = 0; i < headerNodes.getLength(); ++i) {
            Node node = headerNodes.item(i);
            String id = queryTextValue(node, headerIdExpr);
            String label = queryTextValue(node, headerLabelExpr);
            headers.add(new Header(id, label));
        }
        return headers;
    }

    private NodeList queryListValue(Node node, XPathExpression expression) throws XPathExpressionException {
        return (NodeList) expression.evaluate(node, XPathConstants.NODESET);
    }

    private String queryTextValue(Node node, XPathExpression expression) throws XPathExpressionException {
        return (String) expression.evaluate(node, XPathConstants.STRING);
    }

    private static XPathExpression compileExpression(String expression) throws XPathExpressionException {
        return xpathFactory.newXPath().compile(expression);
    }

}
