package com.capgemini.reports.printing;

import com.capgemini.framework.teamcenter.exceptions.KurtException;
import com.capgemini.reports.exceptions.ReportPrintException;
import com.capgemini.reports.jasper.compilers.JasperCompiler;
import com.capgemini.reports.jasper.fillers.JasperFiller;
import com.capgemini.reports.jasper.fillers.MonoFiller;
import com.capgemini.reports.jasper.fillers.MultiFiller;
import com.capgemini.reports.jasper.printers.JasperCSVPrinter;
import com.capgemini.reports.jasper.printers.JasperPDFPrinter;
import com.capgemini.reports.jasper.printers.JasperPrinter;
import com.capgemini.reports.jasper.printers.JasperXLSXPrinter;
import com.capgemini.reports.printing.Printer.PrintFormat;
import com.capgemini.reports.reports.output.ZipReport;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.InputStream;
import java.util.*;
import java.util.Map.Entry;

/**
 * Constructs a report printing parameters. Use the static factory methods to obtain an instance.
 */
public class PrintParameters {
    private static final Logger LOGGER = LoggerFactory.getLogger(PrintParameters.class);
    private static final String REPORT_TITLE_MARKER = "report_title";
    private static final String ERROR_MESSAGE = "Could not prepare sub-report template from InputStream";
    // Main report content
    private final JasperReport template;
    private final Map<String, Object> parameters = new HashMap<>();
    private final List<Map<String, ?>> reportData = new ArrayList<>();

    // Sub reports content
    private final Map<String, PrintParameters> subReports = new HashMap<>();

    // Printing data
    private final PrintFormat printTypes;
    private final String outputFolder;
    private final String outputFileName;

    /**
     * Creates some {@link PrintParameters} to print the MAIN Report template.<br>
     * Specifies the file type (.xlsx, .pdf, ...), the file name, the Folder, and the template.
     *
     * @param printTypes the file type
     * @param fileName   the file name
     * @param folder     the Folder
     * @param template   the template (an {@link InputStream} to the .jrxml file)
     * @throws JRException if the compilation goes wrong
     */
    protected PrintParameters(final PrintFormat printTypes, final String fileName, final String folder, final InputStream template)
            throws JRException {
        this.printTypes = printTypes;
        this.outputFileName = fileName;
        this.outputFolder = folder;
        this.template = JasperCompiler.compile(template);
    }

    /**
     * Creates some {@link PrintParameters} to print the MAIN Report template.<br>
     * Specifies the file type (.xlsx, .pdf, ...), the file name, the Folder, and the template.
     *
     * @param printTypes the file type
     * @param fileName   the file name
     * @param folder     the Folder
     * @param template   the template (a {@link String} whith the whole content)
     * @throws JRException if the compilation goes wrong
     */
    protected PrintParameters(final PrintFormat printTypes, final String fileName, final String folder, final String template) throws JRException {
        this.printTypes = printTypes;
        this.outputFileName = fileName;
        this.outputFolder = folder;
        this.template = JasperCompiler.compile(template);
    }

    /**
     * Creates some {@link PrintParameters} for a subReport, using its template's {@link InputStream}.
     *
     * @param template the template
     * @throws JRException if the compilation goes wrong
     */
    protected PrintParameters(final InputStream template) throws JRException {
        this.printTypes = null;
        this.outputFileName = "";
        this.outputFolder = "";
        this.template = JasperCompiler.compile(template);
    }


    /**
     * Creates some {@link PrintParameters} for a subReport, using its template's full content (as a {@link String}).
     *
     * @param template the template
     * @throws JRException if the compilation goes wrong
     */
    protected PrintParameters(final String template) throws JRException {
        this.printTypes = null;
        this.outputFileName = "";
        this.outputFolder = "";
        this.template = JasperCompiler.compile(template);
    }

    public PrintParameters(PrintFormat printFormat, String fileName, String folder) {
        this.printTypes = printFormat;
        this.outputFileName = fileName;
        this.outputFolder = folder;
        this.template = null;
    }

    /**
     * Creates some {@link PrintParameters} to be used as a <b>sub-report</b>.
     *
     * @param template the template (an {@link InputStream} to the .jrxml file)
     * @return the {@link PrintParameters} for the sub-report
     * @throws ReportPrintException if a Jasper exception was thrown
     */
    public static PrintParameters createSubReport(final InputStream template) throws ReportPrintException {
        try {
            return new PrintParameters(template);
        } catch (JRException e) {
            throw new ReportPrintException(ERROR_MESSAGE, e);
        }
    }

    /**
     * Creates some {@link PrintParameters} for the <b>main</b> report.<br>
     * Specifies the file type (.xlsx, .pdf, ...), the file name, the Folder, and the template.
     *
     * @param printTypes the file type
     * @param fileName   the file name
     * @param folder     the Folder
     * @param template   the template (an {@link InputStream} to the .jrxml file)
     * @return the {@link PrintParameters} for the main report
     * @throws ReportPrintException if the template could not be understood by the printer.
     */
    public static PrintParameters createMainReport(final PrintFormat printTypes, final String fileName, final String folder,
                                                   final InputStream template) throws ReportPrintException {
        try {
            return new PrintParameters(printTypes, fileName, folder, template);
        } catch (JRException e) {
            throw new ReportPrintException(ERROR_MESSAGE, e);
        }
    }

    /**
     * Creates some {@link PrintParameters} for the <b>main</b> report.<br>
     * Specifies the file type (.xlsx, .pdf, ...), the file name, the Folder, and the template.
     *
     * @param printTypes the file type
     * @param fileName   the file name
     * @param folder     the Folder
     * @param template   the template (a {@link String} containing the actual jrxml content)
     * @return the {@link PrintParameters} for the main report
     * @throws ReportPrintException if a Jasper exception was thrown
     */
    public static PrintParameters createMainReport(final PrintFormat printTypes, final String fileName, final String folder, final String template)
            throws ReportPrintException {
        try {
            return new PrintParameters(printTypes, fileName, folder, template);
        } catch (JRException e) {
            throw new ReportPrintException(ERROR_MESSAGE, e);
        }
    }

    /**
     * Creates some {@link PrintParameters} to be used as a <b>sub-report</b>.<br>
     *
     * @param template the template (a {@link String} containing the actual jrxml content)
     * @return the {@link PrintParameters} for the main report
     * @throws ReportPrintException if a Jasper exception was thrown
     */
    public static PrintParameters createSubReport(final String template) throws ReportPrintException {
        try {
            return new PrintParameters(template);
        } catch (JRException e) {
            throw new ReportPrintException("Could not prepare sub-report template from String", e);
        }
    }

    /**
     * Returns <code>true</code> if the report has sub reports, <code>false</code> otherwise.
     *
     * @return a boolean
     */
    boolean hasSubReports() {
        return !subReports.isEmpty();
    }

    /**
     * Adds some parameters to this report.
     *
     * @param key                 the String key
     * @param additionalParameter the parameter content
     */
    public void addReportParameter(final String key, final Object additionalParameter) {
        parameters.put(key, additionalParameter);
    }

    /**
     * Adds some parameters to this report.
     *
     * @param key                 the enum key
     * @param additionalParameter the parameter content
     * @param <K>                 generic object
     */
    public <K extends Enum<K>> void addReportParameter(final K key, final Object additionalParameter) {
        parameters.put(key.toString(), additionalParameter);
    }

    /**
     * Adds some parameters to this report.
     *
     * @param <K>                  Generic object
     * @param additionalParameters the parameters, as a {@link Map} whose keys are {@link Enum}s
     */
    public <K extends Enum<K>> void addReportParameters(final Map<K, ?> additionalParameters) {
        for (Entry<K, ?> entry : additionalParameters.entrySet()) {
            parameters.put(entry.getKey().toString(), entry.getValue());
        }
    }

    /**
     * Sets the title of the report.
     *
     * @param title the title of this report
     */
    public void setReportTitle(final String title) {
        parameters.put(REPORT_TITLE_MARKER, title);
    }

    /**
     * Adds data to this report.
     *
     * @param data a {@link Collection} of several {@link Map}s of data. The Maps use {@link String}s as keys.
     */
    public void addReportData(final Collection<Map<String, ?>> data) {
        reportData.addAll(data);
    }

    /**
     * Adds data to this report.
     *
     * @param <K>  Generic object
     * @param data a {@link Collection} of several {@link EnumMap}s of data. The Maps use {@link Enum}s as keys
     */
    public <K extends Enum<K>> void addReportDataFromEnum(final Collection<Map<K, ?>> data) {
        for (Map<K, ?> map : data) {
            Map<String, Object> newMap = new HashMap<>();
            for (Entry<K, ?> entry : map.entrySet()) {
                newMap.put(entry.getKey().toString(), entry.getValue());
            }
            reportData.add(newMap);
        }
    }

    /**
     * Adds a simple sub-report to this report.
     *
     * @param key        a {@link String} key identifying the name of this sub-report
     * @param aTemplate  the sub-report's template, as an {@link InputStream}
     * @param data       the data forming this sub-report
     * @param someParams some parameters for this sub-report
     * @param <K>        An enum whose values will be used as keys to link to the report data, one map of those keys for each line of the report.
     * @param <V>        An enum whose values will be used as keys to link to report parameters. Parameters are the same throughout the report.
     * @throws ReportPrintException if a Jasper exception was thrown
     */
    public <K extends Enum<K>, V extends Enum<V>> void addSubReport(final String key, final InputStream aTemplate,
                                                                    final Collection<Map<K, ?>> data, final Map<V, ?> someParams) throws ReportPrintException {
        try {
            PrintParameters subReport = new PrintParameters(aTemplate);
            subReport.addReportDataFromEnum(data);
            subReport.addReportParameters(someParams);
            subReports.put(key, subReport);
        } catch (JRException e) {
            throw new ReportPrintException(ERROR_MESSAGE, e);
        }
    }

    /**
     * Adds a potentially more complex sub-report to this report. <br>
     * The sub-report may itself have some sub-reports
     *
     * @param key       a {@link String} key identifying the name of this sub-report
     * @param subReport a {@link PrintParameters} whith all the sub-report data configured
     */
    public void addSubReport(final String key, final PrintParameters subReport) {
        subReports.put(key, subReport);
    }

    /**
     * Returns the expected extension of the printed file: ".xlsx", ".csv" etc.
     *
     * @return the extension (including the dot)
     */
    public String getOuputFileExtension() {
        String extension;
        switch (this.printTypes) {
            case CSV:
                extension = ".csv";
                break;
            case PDF:
                extension = ".pdf";
                break;
            case XLSX:
                extension = ".xlsx";
                break;
            case ZIP:
                extension = ".zip";
                break;
            default:
                extension = ".txt";
                break;
        }
        return extension;
    }

    /**
     * Creates a {@link JasperFiller} from this report, configured with the template, the data, and the parameters.
     *
     * @return the {@link JasperFiller}
     * @throws JRException if report creation failed
     */
    public JasperFiller createReportFiller() throws JRException {
        if (this.hasSubReports()) {
            return createReportMultiFiller();
        } else {
            return createReportMonoFiller();
        }
    }

    /**
     * Creates a simple mono-report. Must be called only if this report does not have sub-reports.
     *
     * @return a {@link MultiFiller} set up
     */
    private MonoFiller createReportMonoFiller() {
        return new MonoFiller(template, reportData, parameters);
    }

    /**
     * Recursive for sub-(sub)-reports.
     *
     * @return a {@link MultiFiller} set up
     */
    private MultiFiller createReportMultiFiller() {
        MultiFiller mainFiller = new MultiFiller(template, reportData, parameters);
        for (Entry<String, PrintParameters> entry : subReports.entrySet()) {
            PrintParameters subReport = entry.getValue();
            JasperReport compiledTemplate = subReport.getTemplate();
            if (subReport.hasSubReports()) {
                MultiFiller subReportFiller = subReport.createReportMultiFiller();
                mainFiller.addSubReport(entry.getKey(), subReportFiller);
            } else {
                mainFiller.addSubReport(entry.getKey(), compiledTemplate, subReport.getReportData(), subReport.getParameters());
            }
        }
        return mainFiller;
    }

    /**
     * Effectue l'impression proprement dite ave le plugin JASPER.
     */
    File jasperPrint() throws ReportPrintException {
        // Fill in the data
        JasperPrint print;
        try {
            print = createReportFiller().generate();
        } catch (JRException e) {
            throw new ReportPrintException("An exception occurred while filling the reports", e);
        }

        // Print
        try {
            return jasperPrinter().print(print);
        } catch (JRException e) {
            throw new ReportPrintException("An exception occurred while printing the reports", e);

        }
    }

    private JasperPrinter jasperPrinter() {
        // Create the printer
        switch (printTypes) {
            case PDF:
                return new JasperPDFPrinter(outputFolder, outputFileName);
            case XLSX:
                return new JasperXLSXPrinter(outputFolder, outputFileName);
            case CSV:
            default:
                return new JasperCSVPrinter(outputFolder, outputFileName);
        }
    }

    File createZip() throws KurtException {
        List<File> filesToZip = new ArrayList<>();
        for (Entry<String, PrintParameters> entry : subReports.entrySet()) {
            PrintParameters printParameters = entry.getValue();
            if (printParameters.printTypes == PrintFormat.ZIP) {
                filesToZip.add(printParameters.createZip());
            }
            else {
                filesToZip.add(printParameters.jasperPrint());
            }
        }
        return new ZipReport(outputFolder + outputFileName + ".zip", filesToZip).print().getFile();
    }

    /**
     * Prints to console.
     */
    File consolePrint() {
        // Print
        LOGGER.info("Printing to console");
        LOGGER.info("outputFolder   ={}", outputFolder);
        LOGGER.info("outputFileName ={}", outputFileName);
        LOGGER.info("Parameters     ={}", parameters);
        LOGGER.info("dataMaps size  ={}", reportData.size());
        for (Map<String, ?> line : reportData) {
            LOGGER.info(" + {}", line.toString());
        }
        return null; // No file to be had! this is console
    }

    public JasperReport getTemplate() {
        return template;
    }

    public List<Map<String, ?>> getReportData() {
        return reportData;
    }

    public Map<String, Object> getParameters() {
        return parameters;
    }

    public Map<String, PrintParameters> getSubReports() {
        return subReports;
    }
}
