/*-
 *  Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
 * 
 *  This file is part of CityDoctor2.
 *
 *  CityDoctor2 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 3 of the License, or
 *  (at your option) any later version.
 *
 *  CityDoctor2 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 CityDoctor2.  If not, see <https://www.gnu.org/licenses/>.
 */
package de.hft.stuttgart.citydoctor2.reporting.pdf;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;

import javax.xml.XMLConstants;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;

import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.events.EventFormatter;
import org.apache.fop.events.model.EventSeverity;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.xmlgraphics.util.MimeConstants;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Namespace;
import org.jdom2.output.DOMOutputter;

import de.hft.stuttgart.citydoctor2.checkresult.utility.CheckReportWriteException;

/**
 * 
 * @author Matthias Betz
 *
 */
public class PdfReport {

	private static final Logger logger = LogManager.getLogger(PdfReport.class);

	private static final String FRONT_PAGE = "front-page";
	private static final String REGION_BODY = "region-body";
	private static final String MASTER_NAME = "master-name";
	private static final String SIMPLE_PAGE_MASTER = "simple-page-master";
	private static final String MARGIN_BOTTOM = "margin-bottom";
	private static final String FONT_SIZE = "font-size";
	private static final String CENTER = "center";
	private static final String XSL_REGION_BODY = "xsl-region-body";
	private static final String TEXT_ALIGN = "text-align";
	private static final String BLOCK = "block";
	private static final String FLOW_NAME = "flow-name";
	private static final String MASTER_REFERENCE = "master-reference";
	private static final String PAGE_SEQUENCE = "page-sequence";

	private static final FopFactory FOP_FACTORY;
	private static final String HEADLINE_FONT_SIZE = "24pt";
	private static final String SUB_HEADLINE_FONT_SIZE = "18pt";
	static final Namespace FO_NS = Namespace.getNamespace("fo", "http://www.w3.org/1999/XSL/Format");
	static final Namespace SVG_NS = Namespace.getNamespace("svg", "http://www.w3.org/2000/svg");

	private static final String COPY_RIGHT = "Copyright (c) 2021 HFT Stuttgart. All rights reserved."
			+ " HFT Stuttgart and its licensors retain all intellectual property"
			+ " and proprietary rights in and to this software and related documentation."
			+ " Any use, reproduction, disclosure, or distribution of this software and"
			+ " related documentation without an express license agreement from HFT Stuttgart is strictly prohibited.";

	private Document doc;
	private Element fileElement;
	private Element tocFlow;
	private Element pageFlow;
	private Element headerBlock;

	private List<Section> sections;

	private Element externalPicture;

	static {
		URI uri = new File(".").toURI();
		FOP_FACTORY = FopFactory.newInstance(uri);
	}

	public PdfReport() {
		doc = new Document();
		sections = new ArrayList<>();
		Element root = new Element("root", FO_NS);
		doc.setRootElement(root);
		root.addNamespaceDeclaration(FO_NS);

		createLayout(root);
		writeFrontPage(root);
		writeTableOfContent(root);

		createHeaderAndFooter(root);

	}

	public void writeSourceFileName(String sourceFile) {
		sourceFile = split(sourceFile, 35);
		fileElement.addContent("For File: " + sourceFile);
		headerBlock.addContent(sourceFile);
	}

	/**
	 * Split string in multiple lines
	 * 
	 * @param s    the string to be split
	 * @param size the number of characters per line
	 * @return the split string concatenated with \n
	 */
	private String split(String s, int size) {
		if (s.length() <= 35) {
			return s;
		}
		StringJoiner joiner = new StringJoiner("\n");
		for (int start = 0; start < s.length(); start += size) {
			joiner.add(s.substring(start, Math.min(s.length(), start + size)));
		}
		return joiner.toString();
	}

	public Section createSection(String headline) {
		return createSection(headline, false);
	}

	public Section createSection(String headline, boolean newPage) {
		Section s = new Section(headline, 0, newPage);
		sections.add(s);
		pageFlow.addContent(s.getContentBlock());
		return s;
	}

	public void save(OutputStream outFile) throws CheckReportWriteException {
		// finish writing the table of content, 3 layers
		for (Section s : sections) {
			tocFlow.addContent(s.getTocBlock());
			for (Section ss : s.getSubSections()) {
				tocFlow.addContent(ss.getTocBlock());
				for (Section sss : ss.getSubSections()) {
					tocFlow.addContent(sss.getTocBlock());
				}
			}
		}
		File tempFile = null;
		try {
			// write logo file into temp dir
			tempFile = File.createTempFile("logo", ".png");
			Files.copy(getClass().getResourceAsStream("/Logo.png"), tempFile.toPath(),
					StandardCopyOption.REPLACE_EXISTING);
			externalPicture.setAttribute("src", tempFile.toURI().toASCIIString());
		} catch (IOException e) {
			logger.warn("Could not write logo into temporary file. Report won't contain the CityDoctor logo", e);
			if (tempFile != null) {
				tempFile.delete();
			}
		}

		try {
			FOUserAgent userAgent = FOP_FACTORY.newFOUserAgent();
			userAgent.setDocumentHandlerOverride(null);
			userAgent.getEventBroadcaster().addEventListener(event -> {
				EventSeverity severity = event.getSeverity();
				String msg = EventFormatter.format(event);
				if (severity == EventSeverity.ERROR) {
					logger.error(msg);
				} else if (severity == EventSeverity.FATAL) {
					logger.fatal(msg);
				} else if (severity == EventSeverity.INFO) {
					logger.info(msg);
				} else if (severity == EventSeverity.WARN) {
					logger.warn(msg);
				}
			});
			Fop fop = FOP_FACTORY.newFop(MimeConstants.MIME_PDF, userAgent, outFile);
			TransformerFactory factory = TransformerFactory.newInstance();
			// deactivate external dtds because of security issues
			factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);

			// identity transformer
			Transformer transformer = factory.newTransformer();

			DOMOutputter domOutputter = new DOMOutputter();
			Source src = new DOMSource(domOutputter.output(doc));
			Result res = new SAXResult(fop.getDefaultHandler());
			transformer.transform(src, res);
			outFile.flush();
		} catch (FOPException | JDOMException | TransformerException | IOException e) {
			throw new CheckReportWriteException(e);
		} finally {
			if (tempFile != null) {
				tempFile.delete();
			}
		}
	}

	private void createHeaderAndFooter(Element root) {
		Element pageSequence = new Element(PAGE_SEQUENCE, FO_NS);
		root.addContent(pageSequence);
		pageSequence.setAttribute("initial-page-number", "1");
		pageSequence.setAttribute(MASTER_REFERENCE, "content");

		Element headerContent = new Element("static-content", FO_NS);
		pageSequence.addContent(headerContent);
		headerContent.setAttribute(FLOW_NAME, "header");
		headerBlock = new Element(BLOCK, FO_NS);
		applyFont(headerBlock);
		headerBlock.setAttribute(FONT_SIZE, "8pt");
		headerBlock.setAttribute("text-align-last", "justify");
		Element inlineHeader = new Element("inline", FO_NS);
		inlineHeader.addContent("Check Report");
		headerBlock.addContent(inlineHeader);
		headerBlock.addContent(new Element("leader", FO_NS));
		headerContent.addContent(headerBlock);

		Element footerContent = new Element("static-content", FO_NS);
		pageSequence.addContent(footerContent);
		footerContent.setAttribute(FLOW_NAME, "footer");
		Element pageNumberBlock = new Element(BLOCK, FO_NS);
		footerContent.addContent(pageNumberBlock);
		pageNumberBlock.setAttribute(TEXT_ALIGN, "end");
		pageNumberBlock.addContent(new Element("page-number", FO_NS));

		pageFlow = new Element("flow", FO_NS);
		pageFlow.setAttribute(FLOW_NAME, XSL_REGION_BODY);
		pageSequence.addContent(pageFlow);
	}

	private void writeTableOfContent(Element root) {
		Element pageSequence = new Element(PAGE_SEQUENCE, FO_NS);
		root.addContent(pageSequence);
		pageSequence.setAttribute(MASTER_REFERENCE, "toc");

		tocFlow = new Element("flow", FO_NS);
		pageSequence.addContent(tocFlow);
		tocFlow.setAttribute(FLOW_NAME, XSL_REGION_BODY);

		Element title = new Element(BLOCK, FO_NS);
		tocFlow.addContent(title);
		applyHeadlineAttributes(title);
		title.addContent("Table of Contents");
	}

	private void writeFrontPage(Element root) {
		Element pageSequence = new Element(PAGE_SEQUENCE, FO_NS);
		root.addContent(pageSequence);
		pageSequence.setAttribute(MASTER_REFERENCE, FRONT_PAGE);
		Element frontPageFlow = new Element("flow", FO_NS);
		pageSequence.addContent(frontPageFlow);
		frontPageFlow.setAttribute(FLOW_NAME, XSL_REGION_BODY);
		Element title = new Element(BLOCK, FO_NS);
		frontPageFlow.addContent(title);
		title.setAttribute(TEXT_ALIGN, CENTER);
		applyFont(title);
		title.setAttribute(FONT_SIZE, "34pt");
		title.setAttribute("font-weight", "bold");
		title.addContent("Check Report");

		Element blockPicture = new Element(BLOCK, FO_NS);
		frontPageFlow.addContent(blockPicture);
		blockPicture.setAttribute(TEXT_ALIGN, CENTER);
		externalPicture = new Element("external-graphic", FO_NS);
		blockPicture.addContent(externalPicture);

		fileElement = new Element(BLOCK, FO_NS);
		frontPageFlow.addContent(fileElement);
		fileElement.setAttribute(TEXT_ALIGN, CENTER);
		applyFont(fileElement);
		fileElement.setAttribute(FONT_SIZE, "24pt");

		String date = ZonedDateTime.now().format(DateTimeFormatter.RFC_1123_DATE_TIME);
		Element createdOn = new Element(BLOCK, FO_NS);
		frontPageFlow.addContent(createdOn);
		createdOn.setAttribute(TEXT_ALIGN, CENTER);
		createdOn.setAttribute("space-before", "10mm");
		applyFont(createdOn);
		createdOn.addContent("Created on " + date);

		Element copyRightText = createTextElement(COPY_RIGHT);
		frontPageFlow.addContent(copyRightText);
		copyRightText.setAttribute("space-before", "100mm");
		copyRightText.setAttribute(FONT_SIZE, "8pt");
	}

	static void applyHeadlineAttributes(Element headline) {
		applyFont(headline);
		headline.setAttribute(FONT_SIZE, HEADLINE_FONT_SIZE);
		headline.setAttribute("font-weight", "bold");
		headline.setAttribute("margin-top", "10mm");
		headline.setAttribute(MARGIN_BOTTOM, "10mm");
	}

	static void applySubHeadlineAttributes(Element subHeadline) {
		applyFont(subHeadline);
		subHeadline.setAttribute(FONT_SIZE, SUB_HEADLINE_FONT_SIZE);
		subHeadline.setAttribute(MARGIN_BOTTOM, "10mm");
	}

	static void applySubSubHeadlineAttributes(Element h) {
		applyFont(h);
		h.setAttribute(FONT_SIZE, "14pt");
		h.setAttribute(MARGIN_BOTTOM, "5mm");
	}

	static void applyFont(Element e) {
		e.setAttribute("font-family", "Helvetica");
	}

	static void applySolidBorder(Element e) {
		e.setAttribute("border-style", "solid");
		e.setAttribute("border-width", "1pt");
	}

	private void createLayout(Element root) {
		Element layoutMaster = new Element("layout-master-set", FO_NS);
		root.addContent(layoutMaster);

		Element firstPageMaster = new Element(SIMPLE_PAGE_MASTER, FO_NS);
		layoutMaster.addContent(firstPageMaster);
		firstPageMaster.setAttribute(MASTER_NAME, FRONT_PAGE);
		applyPageSize(firstPageMaster);

		Element firstPageRegionBody = new Element(REGION_BODY, FO_NS);
		firstPageMaster.addContent(firstPageRegionBody);

		Element tableOfContentLayout = new Element(SIMPLE_PAGE_MASTER, FO_NS);
		layoutMaster.addContent(tableOfContentLayout);
		tableOfContentLayout.setAttribute(MASTER_NAME, "toc");
		applyPageSize(tableOfContentLayout);

		Element tocRegionBody = new Element(REGION_BODY, FO_NS);
		tableOfContentLayout.addContent(tocRegionBody);

		Element contentLayout = new Element(SIMPLE_PAGE_MASTER, FO_NS);
		layoutMaster.addContent(contentLayout);
		contentLayout.setAttribute(MASTER_NAME, "content");
		applyPageSize(contentLayout);

		Element contentRegionBody = new Element(REGION_BODY, FO_NS);
		contentRegionBody.setAttribute("margin", "5mm");
		contentLayout.addContent(contentRegionBody);

		Element contentRegionBefore = new Element("region-before", FO_NS);
		contentLayout.addContent(contentRegionBefore);
		contentRegionBefore.setAttribute("region-name", "header");
		contentRegionBefore.setAttribute("extent", "10mm");

		Element contentRegionAfter = new Element("region-after", FO_NS);
		contentLayout.addContent(contentRegionAfter);
		contentRegionAfter.setAttribute("region-name", "footer");
		contentRegionAfter.setAttribute("extent", "10mm");
		contentRegionAfter.setAttribute("display-align", "after");
	}

	static Element createTextElement(String text) {
		Element textElement = new Element(BLOCK, FO_NS);
		textElement.setAttribute("space-after", "6pt");
		applyFont(textElement);
		textElement.addContent(text);
		return textElement;
	}

	private void applyPageSize(Element pageMaster) {
		pageMaster.setAttribute("margin-left", "15mm");
		pageMaster.setAttribute("margin-right", "15mm");
		pageMaster.setAttribute("margin-top", "10mm");
		pageMaster.setAttribute(MARGIN_BOTTOM, "10mm");
		pageMaster.setAttribute("page-height", "297mm");
		pageMaster.setAttribute("page-width", "210mm");
	}

}
