/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.sys.service.impl;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.Message;
import org.kuali.kfs.sys.batch.service.WrappingBatchService;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.report.BusinessObjectReportHelper;
import org.kuali.kfs.sys.service.ReportWriterService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.krad.bo.BusinessObject;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Text output implementation of <code>ReportWriterService</code> interface. If you are a developer attempting to add a new business
 * object for error report writing, take a look at the Spring definitions for BusinessObjectReportHelper.<br>
 * This class CANNOT be used by 2 processes simultaneously. It is for very specific batch processes that should not run at the same
 * time, and initialize and destroy must be called and the beginning and end of each process that uses it.
 *
 * @see org.kuali.kfs.sys.report.BusinessObjectReportHelper
 */
public class ReportWriterTextServiceImpl implements ReportWriterService, WrappingBatchService {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ReportWriterTextServiceImpl.class);

    // Changing the initial line number would only affect that a page break occurs early. It does not actually print in the
    // middle of the page. Hence changing this has little use.
    protected static final int INITIAL_LINE_NUMBER = 0;

    protected String filePath;
    protected String fileNamePrefix;
    protected String fileNameSuffix;
    protected String title;
    protected int pageWidth;
    protected int pageLength;
    protected int initialPageNumber;
    protected String errorSubTitle;
    protected String statisticsLabel;
    protected String statisticsLeftPadding;
    private String parametersLabel;
    private String parametersLeftPadding;
    protected String pageLabel;
    protected String newLineCharacter;
    protected DateTimeService dateTimeService;
    protected boolean aggregationModeOn;

    /**
     * A map of BO classes to {@link BusinessObjectReportHelper} bean names, to configure which BO's will be rendered by which
     * BusinessObjectReportHelper. This property should be configured via the spring bean definition
     */
    protected Map<Class<? extends BusinessObject>, String> classToBusinessObjectReportHelperBeanNames;

    // Local caching field to speed up the selection of formatting BusinessObjectReportHelper to use per configuration in Spring
    protected Map<Class<? extends BusinessObject>, BusinessObjectReportHelper> businessObjectReportHelpers;

    protected PrintStream printStream;
    protected int page;
    protected int line = INITIAL_LINE_NUMBER;
    protected String errorFormat;

    // Ensures that the statistics header isn't written multiple times. Does not check that a user doesn't write other stuff into
    // the statistics
    // section. A developer is responsible for ensuring that themselves
    protected boolean modeStatistics = false;

    // Ensures that the parameters header isn't written multiple times. Does not check that a user doesn't write other stuff into
    // the parameters
    // section. A developer is responsible for ensuring that themselves
    protected boolean modeParameters = false;

    // So that writeError knows when to writeErrorHeader
    protected boolean newPage = true;

    // For printing new headers when the BO is changed
    protected Class<? extends BusinessObject> businessObjectClass;

    public ReportWriterTextServiceImpl() {
        // TODO Auto-generated constructor stub
    }

    /**
     * @see org.kuali.kfs.sys.batch.service.WrappingBatchService#initialize()
     */
    public void initialize() {
        try {
            printStream = new PrintStream(generateFullFilePath());
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }

        page = initialPageNumber;
        initializeBusinessObjectReportHelpers();
        // Initial header
        this.writeHeader(title);
    }

    protected void initializeBusinessObjectReportHelpers() {
        businessObjectReportHelpers = new HashMap<Class<? extends BusinessObject>, BusinessObjectReportHelper>();
        if (classToBusinessObjectReportHelperBeanNames != null) {
            for (Class<? extends BusinessObject> clazz : classToBusinessObjectReportHelperBeanNames.keySet()) {
                String businessObjectReportHelperBeanName = classToBusinessObjectReportHelperBeanNames.get(clazz);
                BusinessObjectReportHelper reportHelper = (BusinessObjectReportHelper) SpringContext.getService(businessObjectReportHelperBeanName);
                if (ObjectUtils.isNull(reportHelper)) {
                    LOG.error("Cannot find BusinessObjectReportHelper implementation for class: " + clazz.getName() + " bean name: " + businessObjectReportHelperBeanName);
                    throw new RuntimeException("Cannot find BusinessObjectReportHelper implementation for class: " + clazz.getName() + " bean name: " + businessObjectReportHelperBeanName);
                }
                businessObjectReportHelpers.put(clazz, reportHelper);
            }
        }
    }

    protected String generateFullFilePath() {
        if (aggregationModeOn) {
            return filePath + File.separator + this.fileNamePrefix + fileNameSuffix;
        } else {
            return filePath + File.separator + this.fileNamePrefix + dateTimeService.toDateTimeStringForFilename(dateTimeService.getCurrentDate()) + fileNameSuffix;
        }
    }

    /**
     * @see org.kuali.kfs.sys.batch.service.WrappingBatchService#destroy()
     */
    public void destroy() {
        if (printStream != null) {
            printStream.close();
            printStream = null;
        }

        // reset variables that track state
        page = initialPageNumber;
        line = INITIAL_LINE_NUMBER;
        modeStatistics = false;
        modeParameters = false;
        newPage = true;
        businessObjectClass = null;
    }

    /**
     * @see org.kuali.kfs.sys.service.ReportWriterService#writeSubTitle(java.lang.String)
     */
    public void writeSubTitle(String message) {
        if (message.length() > pageWidth) {
            LOG.warn("sub title to be written exceeds pageWidth. Printing anyway.");
            this.writeFormattedMessageLine(message);
        } else {
            int padding = (pageWidth - message.length()) / 2;
            this.writeFormattedMessageLine("%" + (padding + message.length()) + "s", message);
        }
    }

    /**
     * @see org.kuali.kfs.sys.service.ReportWriterService#writeError(java.lang.Class, org.kuali.kfs.sys.Message)
     */
    public void writeError(BusinessObject businessObject, Message message) {
        this.writeError(businessObject, message, true);
    }

    /**
     * @param printBusinessObjectValues indicates whether the bo values should be printed before the message
     * @see org.kuali.kfs.sys.service.ReportWriterService#writeError(java.lang.Class, org.kuali.kfs.sys.Message)
     */
    public void writeError(BusinessObject businessObject, Message message, boolean printBusinessObjectValues) {
        // Check if we need to write a new table header. We do this if it hasn't been written before or if the businessObject
        // changed
        if (newPage || businessObjectClass == null || !businessObjectClass.getName().equals(businessObject.getClass().getName())) {
            if (businessObjectClass == null) {
                // If we didn't write the header before, write it with a subTitle
                this.writeSubTitle(errorSubTitle);
            } else if (!businessObjectClass.getName().equals(businessObject.getClass().getName())) {
                // If it changed push a newline in for neater formatting
                this.writeNewLines(1);
            }

            this.writeErrorHeader(businessObject);
            newPage = false;
            businessObjectClass = businessObject.getClass();
        }

        // Get business object formatter that will be used
        BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);

        // Print the values of the businessObject per formatting determined by writeErrorHeader
        List<Object> formatterArgs = new ArrayList<Object>();
        if (printBusinessObjectValues) {
            formatterArgs.addAll(businessObjectReportHelper.getValues(businessObject));
        } else {
            formatterArgs.addAll(businessObjectReportHelper.getBlankValues(businessObject));
        }

        // write rest of message on new line(s) if it was cut off
        int maxMessageLength = Integer.parseInt(StringUtils.substringBefore(StringUtils.substringAfterLast(errorFormat, "%-"), "s"));
        String messageToPrint = message.getMessage();

        boolean firstMessageLine = true;
        while (messageToPrint.length() > 0 && StringUtils.isNotBlank(messageToPrint)) {
            if (!firstMessageLine) {
                formatterArgs = new ArrayList<Object>();
                formatterArgs.addAll(businessObjectReportHelper.getBlankValues(businessObject));
            } else {
                firstMessageLine = false;
            }

            messageToPrint = StringUtils.trim(messageToPrint);
            String messageLine = messageToPrint;
            if (messageLine.length() > maxMessageLength) {
                messageLine = StringUtils.substring(messageLine, 0, maxMessageLength);
                if (StringUtils.contains(messageLine, " ")) {
                    messageLine = StringUtils.substringBeforeLast(messageLine, " ");
                }
            }

            formatterArgs.add(new Message(messageLine, message.getType()));
            this.writeFormattedMessageLine(errorFormat, formatterArgs.toArray());

            messageToPrint = StringUtils.removeStart(messageToPrint, messageLine);
        }
    }

    /**
     * @see org.kuali.kfs.sys.service.ReportWriterService#writeError(java.lang.Class, java.util.List)
     */
    public void writeError(BusinessObject businessObject, List<Message> messages) {
        int i = 0;
        for (Iterator<Message> messagesIter = messages.iterator(); messagesIter.hasNext(); ) {
            Message message = messagesIter.next();

            if (i == 0) {
                // First one has its values written
                this.writeError(businessObject, message, true);
            } else {
                // Any consecutive one only has message written
                this.writeError(businessObject, message, false);
            }

            i++;
        }
    }

    /**
     * @see org.kuali.kfs.sys.service.ReportWriterService#writeNewLines(int)
     */
    public void writeNewLines(int lines) {
        for (int i = 0; i < lines; i++) {
            this.writeFormattedMessageLine("");
        }
    }

    /**
     * @see org.kuali.kfs.sys.service.ReportWriterService#writeStatisticLine(java.lang.String, java.lang.Object[])
     */
    public void writeStatisticLine(String message, Object... args) {
        // Statistics header is only written if it hasn't been written before
        if (!modeStatistics) {
            this.modeStatistics = true;

            // If nothing has been written to the report we don't want to page break
            if (!(page == initialPageNumber && line == INITIAL_LINE_NUMBER + 2)) {
                this.pageBreak();
            }

            this.writeFormattedMessageLine("*********************************************************************************************************************************");
            this.writeFormattedMessageLine("*********************************************************************************************************************************");
            this.writeFormattedMessageLine("*******************" + statisticsLabel + "*******************");
            this.writeFormattedMessageLine("*********************************************************************************************************************************");
            this.writeFormattedMessageLine("*********************************************************************************************************************************");
        }

        this.writeFormattedMessageLine(statisticsLeftPadding + message, args);
    }

    /**
     * @see org.kuali.kfs.sys.service.ReportWriterService#writeParameterLine(java.lang.String, java.lang.Object[])
     */
    public void writeParameterLine(String message, Object... args) {
        // Statistics header is only written if it hasn't been written before
        if (!modeParameters) {
            this.modeParameters = true;

            // If nothing has been written to the report we don't want to page break
            if (!(page == initialPageNumber && line == INITIAL_LINE_NUMBER + 2)) {
                this.pageBreak();
            }

            this.writeFormattedMessageLine("*********************************************************************************************************************************");
            this.writeFormattedMessageLine("*********************************************************************************************************************************");
            this.writeFormattedMessageLine("*******************" + getParametersLabel() + "*******************");
            this.writeFormattedMessageLine("*********************************************************************************************************************************");
            this.writeFormattedMessageLine("*********************************************************************************************************************************");
        }

        this.writeFormattedMessageLine(getParametersLeftPadding() + message, args);
    }

    /**
     * @see org.kuali.kfs.sys.service.ReportWriterService#writeFormattedMessageLine(java.lang.String, java.lang.Object[])
     */
    public void writeFormattedMessageLine(String format, Object... args) {
        if (format.indexOf("% s") > -1) {
            LOG.warn("Cannot properly format: " + format);
        } else {
            Object[] escapedArgs = escapeArguments(args);
            if (LOG.isDebugEnabled()) {
                LOG.debug("writeFormattedMessageLine, format: " + format);
            }

            String message = null;

            if (escapedArgs.length > 0) {
                message = String.format(format + newLineCharacter, escapedArgs);
            } else {
                message = format + newLineCharacter;
            }

            // Log we are writing out of bounds. Would be nice to show message here but not so sure if it's wise to dump that data into
            // logs
            if (message.length() > pageWidth) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("message is out of bounds writing anyway");
                }
            }

            printStream.print(message);
            printStream.flush();

            line++;
            if (line >= pageLength) {
                this.pageBreak();
            }
        }
    }

    /**
     * Determines if all formatting on the given String is escaped - ie, that it has no formatting
     *
     * @param format the format to test
     * @return true if the String is without formatting, false otherwise
     */
    protected boolean allFormattingEscaped(String format) {
        int currPoint = 0;
        int currIndex = format.indexOf('%', currPoint);
        while (currIndex > -1) {
            char nextChar = format.charAt(currIndex + 1);
            if (nextChar != '%') {
                return false;
            }
            currPoint = currIndex + 2;
        }
        return true;
    }

    /**
     * @see org.kuali.kfs.sys.service.ReportWriterService#pageBreak()
     */
    public void pageBreak() {
        // Intentionally not using writeFormattedMessageLine here since it would loop trying to page break ;)
        // 12 represents the ASCII Form Feed character
        printStream.printf("%c" + newLineCharacter, 12);
        page++;
        line = INITIAL_LINE_NUMBER;
        newPage = true;

        this.writeHeader(title);
    }

    /**
     * Helper method to write a header for placement at top of new page
     *
     * @param title that should be printed on this header
     */
    protected void writeHeader(String title) {
        String headerText = String.format("%1$tY-%1$tm-%1$td %1$tH:%1$tM", dateTimeService.getCurrentDate());
        int reportTitlePadding = pageWidth / 2 - headerText.length() - title.length() / 2;
        headerText = String.format("%s%" + (reportTitlePadding + title.length()) + "s%" + reportTitlePadding + "s", headerText, title, "");

        if (aggregationModeOn) {
            this.writeFormattedMessageLine("%s%s%s", headerText, pageLabel, KFSConstants.REPORT_WRITER_SERVICE_PAGE_NUMBER_PLACEHOLDER);
        } else {
            this.writeFormattedMessageLine("%s%s%,9d", headerText, pageLabel, page);
        }
        this.writeNewLines(1);
    }

    /**
     * Helper method to write the error header
     *
     * @param businessObject to print header for
     */
    protected void writeErrorHeader(BusinessObject businessObject) {
        BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
        List<String> errorHeader = businessObjectReportHelper.getTableHeader(pageWidth);

        // If we are at end of page and don't have space for table header, go ahead and page break
        if (errorHeader.size() + line >= pageLength) {
            this.pageBreak();
        }

        // Print the header one by one. Note the last element is the formatter. So capture that seperately
        for (Iterator<String> headers = errorHeader.iterator(); headers.hasNext(); ) {
            String header = headers.next();

            if (headers.hasNext()) {
                this.writeFormattedMessageLine("%s", header);
            } else {
                errorFormat = header;
            }
        }
    }

    /**
     * @see org.kuali.kfs.sys.service.ReportWriterService#writeTableHeader(org.kuali.rice.krad.bo.BusinessObject)
     */
    public void writeTableHeader(BusinessObject businessObject) {
        BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);

        Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
        String tableHeaderFormat = tableDefinition.get(KFSConstants.ReportConstants.TABLE_HEADER_LINE_KEY);

        String[] headerLines = this.getMultipleFormattedMessageLines(tableHeaderFormat, new Object());
        this.writeMultipleFormattedMessageLines(headerLines);
    }

    /**
     * Writes out the table header, based on a business object class
     *
     * @param businessObjectClass the class to write the header out for
     */
    public void writeTableHeader(Class<? extends BusinessObject> businessObjectClass) {
        BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObjectClass);

        Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
        String tableHeaderFormat = tableDefinition.get(KFSConstants.ReportConstants.TABLE_HEADER_LINE_KEY);

        String[] headerLines = this.getMultipleFormattedMessageLines(tableHeaderFormat, new Object());
        this.writeMultipleFormattedMessageLines(headerLines);
    }

    /**
     * @see org.kuali.kfs.sys.service.ReportWriterService#writeTableRow(org.kuali.rice.krad.bo.BusinessObject)
     */
    public void writeTableRowSeparationLine(BusinessObject businessObject) {
        BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
        Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();

        String separationLine = tableDefinition.get(KFSConstants.ReportConstants.SEPARATOR_LINE_KEY);
        this.writeFormattedMessageLine(separationLine);
    }

    /**
     * @see org.kuali.kfs.sys.service.ReportWriterService#writeTableRow(org.kuali.rice.krad.bo.BusinessObject)
     */
    public void writeTableRow(BusinessObject businessObject) {
        BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
        Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();

        String tableCellFormat = tableDefinition.get(KFSConstants.ReportConstants.TABLE_CELL_FORMAT_KEY);
        List<String> tableCellValues = businessObjectReportHelper.getTableCellValuesPaddingWithEmptyCell(businessObject, false);

        String[] rowMessageLines = this.getMultipleFormattedMessageLines(tableCellFormat, tableCellValues.toArray());
        this.writeMultipleFormattedMessageLines(rowMessageLines);
    }

    /**
     * @see org.kuali.kfs.sys.service.ReportWriterService#writeTableRowWithColspan(org.kuali.rice.krad.bo.BusinessObject)
     */
    public void writeTableRowWithColspan(BusinessObject businessObject) {
        BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
        Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();

        String tableCellFormat = businessObjectReportHelper.getTableCellFormat(true, true, StringUtils.EMPTY);
        List<String> tableCellValues = businessObjectReportHelper.getTableCellValuesPaddingWithEmptyCell(businessObject, true);

        String[] rowMessageLines = this.getMultipleFormattedMessageLines(tableCellFormat, tableCellValues.toArray());
        this.writeMultipleFormattedMessageLines(rowMessageLines);
    }

    /**
     * @see org.kuali.kfs.sys.service.ReportWriterService#writeTable(java.util.List, boolean, boolean)
     */
    public void writeTable(List<? extends BusinessObject> businessObjects, boolean isHeaderRepeatedInNewPage, boolean isRowBreakAcrossPageAllowed) {
        if (ObjectUtils.isNull(businessObjects) || businessObjects.isEmpty()) {
            return;
        }

        BusinessObject firstBusinessObject = businessObjects.get(0);
        this.writeTableHeader(firstBusinessObject);

        BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObjects.get(0));
        Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
        String tableHeaderFormat = tableDefinition.get(KFSConstants.ReportConstants.TABLE_HEADER_LINE_KEY);
        String[] headerLines = this.getMultipleFormattedMessageLines(tableHeaderFormat, new Object());

        String tableCellFormat = tableDefinition.get(KFSConstants.ReportConstants.TABLE_CELL_FORMAT_KEY);

        for (BusinessObject businessObject : businessObjects) {

            List<String> tableCellValues = businessObjectReportHelper.getTableCellValuesPaddingWithEmptyCell(businessObject, false);
            String[] messageLines = this.getMultipleFormattedMessageLines(tableCellFormat, tableCellValues.toArray());

            boolean hasEnoughLinesInPage = messageLines.length <= (this.pageLength - this.line);
            if (!hasEnoughLinesInPage && !isRowBreakAcrossPageAllowed) {
                this.pageBreak();

                if (isHeaderRepeatedInNewPage) {
                    this.writeTableHeader(firstBusinessObject);
                }
            }

            this.writeMultipleFormattedMessageLines(messageLines, headerLines, isRowBreakAcrossPageAllowed);
        }

    }

    /**
     * get the business report helper for the given business object
     *
     * @param businessObject the given business object
     * @return the business report helper for the given business object
     */
    public BusinessObjectReportHelper getBusinessObjectReportHelper(BusinessObject businessObject) {
        if (LOG.isDebugEnabled()) {
            if (businessObject == null) {
                LOG.debug("reporting " + filePath + " but can't because null business object sent in");
            } else if (businessObjectReportHelpers == null) {
                LOG.debug("Logging " + businessObject + " in report " + filePath + " but businessObjectReportHelpers are null");
            }
        }
        BusinessObjectReportHelper businessObjectReportHelper = this.businessObjectReportHelpers.get(businessObject.getClass());
        if (ObjectUtils.isNull(businessObjectReportHelper)) {
            throw new RuntimeException(businessObject.getClass().toString() + " is not handled");
        }

        return businessObjectReportHelper;
    }

    /**
     * get the business report helper for the given business object
     *
     * @param businessObject the given business object
     * @return the business report helper for the given business object
     */
    public BusinessObjectReportHelper getBusinessObjectReportHelper(Class<? extends BusinessObject> businessObjectClass) {
        BusinessObjectReportHelper businessObjectReportHelper = this.businessObjectReportHelpers.get(businessObjectClass);
        if (ObjectUtils.isNull(businessObjectReportHelper)) {
            throw new RuntimeException(businessObjectClass.getName() + " is not handled");
        }

        return businessObjectReportHelper;
    }

    /**
     * write the given information as multiple lines if it contains more than one line breaks
     *
     * @param format               the given text format definition
     * @param messageLines         the given information being written out
     * @param headerLinesInNewPage the given header lines being written in the begin of a new page
     */
    protected void writeMultipleFormattedMessageLines(String[] messageLines, String[] headerLinesInNewPage, boolean isRowBreakAcrossPageAllowed) {
        int currentPageNumber = this.page;

        for (String line : messageLines) {
            boolean hasEnoughLinesInPage = messageLines.length <= (this.pageLength - this.line);
            if (!hasEnoughLinesInPage && !isRowBreakAcrossPageAllowed) {
                this.pageBreak();
            }

            if (currentPageNumber < this.page && ObjectUtils.isNotNull(headerLinesInNewPage)) {
                currentPageNumber = this.page;

                for (String headerLine : headerLinesInNewPage) {
                    this.writeFormattedMessageLine(headerLine);
                }
            }

            this.writeFormattedMessageLine(line);
        }
    }

    /**
     * write the given information as multiple lines if it contains more than one line breaks
     *
     * @param format the given text format definition
     * @param args   the given information being written out
     */
    public void writeMultipleFormattedMessageLines(String[] messageLines) {
        this.writeMultipleFormattedMessageLines(messageLines, null, false);
    }

    public void writeMultipleFormattedMessageLines(String format, Object... args) {
        Object[] escapedArgs = escapeArguments(args);
        String[] messageLines = getMultipleFormattedMessageLines(format, escapedArgs);
        writeMultipleFormattedMessageLines(messageLines);
    }

    /**
     * This method...
     *
     * @param format
     * @param args
     * @return
     */
    public String[] getMultipleFormattedMessageLines(String format, Object... args) {
        Object[] escapedArgs = escapeArguments(args);
        String message = String.format(format, escapedArgs);
        return StringUtils.split(message, newLineCharacter);
    }

    /**
     * Iterates through array and escapes special formatting characters
     *
     * @param args Object array to process
     * @return Object array with String members escaped
     */
    protected Object[] escapeArguments(Object... args) {
        Object[] escapedArgs = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            Object arg = args[i];
            if (arg == null) {
                args[i] = "";
            } else if (arg != null && arg instanceof String) {
                String escapedArg = escapeFormatCharacters((String) arg);
                escapedArgs[i] = escapedArg;
            } else {
                escapedArgs[i] = arg;
            }
        }

        return escapedArgs;
    }

    /**
     * Escapes characters in a string that have special meaning for formatting
     *
     * @param replacementString string to escape
     * @return string with format characters escaped
     * @see KFSConstants.ReportConstants.FORMAT_ESCAPE_CHARACTERS
     */
    protected String escapeFormatCharacters(String replacementString) {
        String escapedString = replacementString;
        for (int i = 0; i < KFSConstants.ReportConstants.FORMAT_ESCAPE_CHARACTERS.length; i++) {
            String characterToEscape = KFSConstants.ReportConstants.FORMAT_ESCAPE_CHARACTERS[i];
            escapedString = StringUtils.replace(escapedString, characterToEscape, characterToEscape + characterToEscape);
        }

        return escapedString;
    }

    /**
     * Sets the filePath
     *
     * @param filePath The filePath to set.
     */
    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }

    /**
     * Sets the fileNamePrefix
     *
     * @param fileNamePrefix The fileNamePrefix to set.
     */
    public void setFileNamePrefix(String fileNamePrefix) {
        this.fileNamePrefix = fileNamePrefix;
    }

    /**
     * Sets the fileNameSuffix
     *
     * @param fileNameSuffix The fileNameSuffix to set.
     */
    public void setFileNameSuffix(String fileNameSuffix) {
        this.fileNameSuffix = fileNameSuffix;
    }

    /**
     * Sets the title
     *
     * @param title The title to set.
     */
    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * Sets the pageWidth
     *
     * @param pageWidth The pageWidth to set.
     */
    public void setPageWidth(int pageWidth) {
        this.pageWidth = pageWidth;
    }

    /**
     * Sets the pageLength
     *
     * @param pageLength The pageLength to set.
     */
    public void setPageLength(int pageLength) {
        this.pageLength = pageLength;
    }

    /**
     * Sets the initialPageNumber
     *
     * @param initialPageNumber The initialPageNumber to set.
     */
    public void setInitialPageNumber(int initialPageNumber) {
        this.initialPageNumber = initialPageNumber;
    }

    /**
     * Sets the errorSubTitle
     *
     * @param errorSubTitle The errorSubTitle to set.
     */
    public void setErrorSubTitle(String errorSubTitle) {
        this.errorSubTitle = errorSubTitle;
    }

    /**
     * Sets the statisticsLabel
     *
     * @param statisticsLabel The statisticsLabel to set.
     */
    public void setStatisticsLabel(String statisticsLabel) {
        this.statisticsLabel = statisticsLabel;
    }

    /**
     * Sets the statisticsLeftPadding
     *
     * @param statisticsLeftPadding The statisticsLeftPadding to set.
     */
    public void setStatisticsLeftPadding(String statisticsLeftPadding) {
        this.statisticsLeftPadding = statisticsLeftPadding;
    }

    /**
     * Sets the pageLabel
     *
     * @param pageLabel The pageLabel to set.
     */
    public void setPageLabel(String pageLabel) {
        this.pageLabel = pageLabel;
    }

    /**
     * Sets the newLineCharacter
     *
     * @param newLineCharacter The newLineCharacter to set.
     */
    public void setNewLineCharacter(String newLineCharacter) {
        this.newLineCharacter = newLineCharacter;
    }

    /**
     * Sets the DateTimeService
     *
     * @param dateTimeService The DateTimeService to set.
     */
    public void setDateTimeService(DateTimeService dateTimeService) {
        this.dateTimeService = dateTimeService;
    }

    /**
     * Sets a map of BO classes to {@link BusinessObjectReportHelper} bean names, to configure which BO's will be rendered by which
     * BusinessObjectReportHelper. This property should be configured via the spring bean definition
     *
     * @param classToBusinessObjectReportHelperBeanNames The classToBusinessObjectReportHelperBeanNames to set.
     */
    public void setClassToBusinessObjectReportHelperBeanNames(Map<Class<? extends BusinessObject>, String> classToBusinessObjectReportHelperBeanNames) {
        this.classToBusinessObjectReportHelperBeanNames = classToBusinessObjectReportHelperBeanNames;
    }

    /**
     * Gets the parametersLabel attribute.
     *
     * @return Returns the parametersLabel.
     */
    public String getParametersLabel() {
        return parametersLabel;
    }

    /**
     * Sets the parametersLabel attribute value.
     *
     * @param parametersLabel The parametersLabel to set.
     */
    public void setParametersLabel(String parametersLabel) {
        this.parametersLabel = parametersLabel;
    }

    /**
     * Gets the parametersLeftPadding attribute.
     *
     * @return Returns the parametersLeftPadding.
     */
    public String getParametersLeftPadding() {
        return parametersLeftPadding;
    }

    /**
     * Sets the parametersLeftPadding attribute value.
     *
     * @param parametersLeftPadding The parametersLeftPadding to set.
     */
    public void setParametersLeftPadding(String parametersLeftPadding) {
        this.parametersLeftPadding = parametersLeftPadding;
    }

    /**
     * Gets the aggregationModeOn attribute.
     *
     * @return Returns the aggregationModeOn.
     */
    public boolean isAggregationModeOn() {
        return aggregationModeOn;
    }

    /**
     * Sets the aggregationModeOn attribute value.
     *
     * @param aggregationModeOn The aggregationModeOn to set.
     */
    public void setAggregationModeOn(boolean aggregationModeOn) {
        this.aggregationModeOn = aggregationModeOn;
    }


}
