/*
 * 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.gl.batch.service.impl;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.gl.GeneralLedgerConstants;
import org.kuali.kfs.gl.batch.service.ReconciliationParserService;
import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.StringTokenizer;

/**
 * Format of the reconciliation file:
 * <p>
 * <pre>
 *  C tableid rowcount ;
 *  S field1 dollaramount ;
 *  S field2 dollaramount ;
 *  E checksum ;
 * </pre>
 * <p>
 * The character '#' and everything following it on that line is ignored. Whitespace characters are tab and space.<br>
 * <br>
 * A 'C' 'S' or 'E' must be the first character on a line unless the line is entirely whitespace or a comment. The case of these
 * three codes is not significant.<br>
 * <br>
 * Semi-colons are required before any possible comments on C S or E lines. Any amount of whitespace delimits the elements of C, S
 * and E lines. (If an S line contains field1+field2 for the field element, take care NOT to put any whitespace between the
 * 'field1', the '+' and the 'field2'.) <br>
 * <br>
 * Tableid is an arbitrary identifier for the record<br>
 * <br>
 * Rowcount must be a non-negative integer. Fieldn is the technical fieldname(s) in the target database. Case *is* significant,
 * since this must match the database name(s) exactly.<br>
 * <br>
 * Dollaramount may be negative; the check is significant to 4 decimal places.<br>
 * <br>
 * The checksum on line E is the number of C and S lines. A C line and a terminating E line are mandatory; S lines are optional.<br>
 * <br>
 * There may be more than one C-E block per metadata file.<br>
 * <br>
 * In general, this implementation of the parser attempts to be error tolerant. It primarily looks at the C-E block that is being
 * looked for, by ignoring all other C-E blocks. A C-E block is "looked for" when the table ID of the C line is passed in as a
 * parameter of {@link #parseReconciliationData(Reader, String)}. However, if the C lines of any blocks before the looked for block
 * are incorrect, then it is likely to cause undesired behavior.
 */
public class ReconciliationParserServiceImpl implements ReconciliationParserService {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(FileEnterpriseFeederHelperServiceImpl.class);

    private enum ParseState {
        INIT, TABLE_DEF, COLUMN_DEF, CHECKSUM_DEF;
    }

    ;


    /**
     * Parses a reconciliation file
     *
     * @param reader  a source of data from which to build a reconciliation
     * @param tableId defined within the reconciliation file; defines which block to parse
     * @return parsed reconciliation data
     * @throws IOException thrown if the file cannot be written for any reason
     * @see org.kuali.kfs.gl.batch.service.ReconciliationParserService#parseReconciliatioData(java.io.Reader)
     */
    public ReconciliationBlock parseReconciliationBlock(Reader reader, String tableId) throws IOException {
        BufferedReader bufReader;
        if (reader instanceof BufferedReader) {
            bufReader = (BufferedReader) reader;
        } else {
            bufReader = new BufferedReader(reader);
        }

        // this variable is not null when we find the C line corresponding to the param table ID
        ReconciliationBlock reconciliationBlock = null;

        int linesInBlock = 0;

        // find the first "C" line of the C-E block by matching the table Id
        String line = bufReader.readLine();
        while (line != null && reconciliationBlock == null) {
            line = stripCommentsAndTrim(line);
            if (StringUtils.isBlank(line)) {
                line = bufReader.readLine();
                continue;
            }

            StringTokenizer strTok = new StringTokenizer(line);
            if (!strTok.hasMoreTokens()) {
                LOG.error("Cannot find TABLE_DEF_STRING");
                throw new RuntimeException();
            }
            String command = strTok.nextToken();
            if (command.equalsIgnoreCase(GeneralLedgerConstants.Reconciliation.TABLE_DEF_STRING)) {
                if (!strTok.hasMoreTokens()) {
                    LOG.error("Cannot find TABLE_DEF_STRING");
                    throw new RuntimeException();
                }
                String parsedTableId = strTok.nextToken();
                if (parsedTableId.equalsIgnoreCase(tableId)) {
                    if (!strTok.hasMoreTokens()) {
                        LOG.error("Cannot find Parsed Table Id");
                        throw new RuntimeException();
                    }
                    String parsedRowCountStr = StringUtils.removeEnd(strTok.nextToken(), ";");
                    parsedRowCountStr = StringUtils.removeEnd(parsedRowCountStr, ".00");
                    int parsedRowCount = Integer.parseInt(parsedRowCountStr);

                    reconciliationBlock = new ReconciliationBlock();
                    reconciliationBlock.setTableId(parsedTableId);
                    reconciliationBlock.setRowCount(parsedRowCount);

                    linesInBlock++;

                    break;
                }
            }
            line = bufReader.readLine();
        }

        if (reconciliationBlock == null) {
            return null;
        }

        boolean endBlockLineEncountered = false;
        line = bufReader.readLine();
        while (line != null && !endBlockLineEncountered) {
            line = stripCommentsAndTrim(line);
            if (StringUtils.isBlank(line)) {
                continue;
            }

            StringTokenizer strTok = new StringTokenizer(line);
            if (!strTok.hasMoreTokens()) {
                LOG.error("Cannot find COLUMN_DEF_STRING");
                throw new RuntimeException();
            }

            String command = strTok.nextToken();
            if (command.equalsIgnoreCase(GeneralLedgerConstants.Reconciliation.COLUMN_DEF_STRING)) {
                if (!strTok.hasMoreTokens()) {
                    LOG.error("Cannot find COLUMN_DEF_STRING");
                    throw new RuntimeException();
                }
                String fieldName = strTok.nextToken();
                if (!strTok.hasMoreTokens()) {
                    LOG.error("Cannot find COLUMN_DEF_STRING");
                    throw new RuntimeException();
                }
                String columnAmountStr = strTok.nextToken();
                columnAmountStr = StringUtils.removeEnd(columnAmountStr, ";");

                KualiDecimal columnAmount = new KualiDecimal(columnAmountStr);
                ColumnReconciliation columnReconciliation = new ColumnReconciliation();
                columnReconciliation.setFieldName(fieldName);
                columnReconciliation.setDollarAmount(columnAmount);
                reconciliationBlock.addColumn(columnReconciliation);
                linesInBlock++;
            } else if (command.equalsIgnoreCase(GeneralLedgerConstants.Reconciliation.CHECKSUM_DEF_STRING)) {
                if (!strTok.hasMoreTokens()) {
                    LOG.error("Cannot find CHECKSUM_DEF_STRING");
                    throw new RuntimeException();
                }
                String checksumStr = strTok.nextToken();
                checksumStr = StringUtils.removeEnd(checksumStr, ";");

                int checksum = Integer.parseInt(checksumStr);

                if (checksum != linesInBlock) {
                    LOG.error("Check Sum String is not same as Lines in Block");
                    throw new RuntimeException();
                }
                break;
            } else {
                LOG.error("Cannot find any fields");
                throw new RuntimeException();
            }

            line = bufReader.readLine();
        }
        return reconciliationBlock;
    }

    /**
     * Removes comments and trims whitespace
     *
     * @param line the line
     * @return stripped and trimmed line
     */
    protected String stripCommentsAndTrim(String line) {
        int commentIndex = line.indexOf(GeneralLedgerConstants.Reconciliation.COMMENT_STRING);
        if (commentIndex > -1) {
            // chop off comments
            line = line.substring(0, commentIndex);
        }

        line = line.trim();
        return line;
    }
}
