package org.drools.decisiontable.parser;

/*
 * Copyright 2005 JBoss Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.drools.decisiontable.parser.xls.PropertiesSheetListener;
import org.drools.template.model.Condition;
import org.drools.template.model.Consequence;
import org.drools.template.model.Global;
import org.drools.template.model.Import;
import org.drools.template.model.Package;
import org.drools.template.model.Rule;
import org.drools.template.parser.DecisionTableParseException;

/**
 * @author <a href="mailto:shaun.addison@gmail.com"> Shaun Addison </a><a
 *         href="mailto:michael.neale@gmail.com"> Michael Neale </a>
 *
 * Define a ruleset spreadsheet which contains one or more decision tables.
 *
 * Stay calm, deep breaths... this is a little bit scary, its where it all
 * happens.
 *
 * A table is identifed by a cell beginning with the text "RuleTable". The first
 * row after the table identifier defines the column type: either a condition
 * ("C") or consequence ("A" for action), and so on.
 *
 * The second row contains ObjectType declarations (optionally, or can be left blank).
 * If cells are merged, then all snippets below the merged bit will become part of
 * the same column as seperate constraints.
 *
 * The third row identifies the java code block associated with the condition
 * or consequence. This code block will include a parameter marker for the
 * attribute defined by that column.
 *
 * The third row is a label for the attribute associated with that column.
 *
 * All subsequent rows identify rules with the set.
 */
public class DefaultRuleSheetListener
    implements
    RuleSheetListener {

    //keywords
    public static final String            FUNCTIONS_TAG          = "Functions";
    public static final String            IMPORT_TAG             = "Import";
    public static final String            SEQUENTIAL_FLAG        = "Sequential";
    public static final String            VARIABLES_TAG          = "Variables";
    public static final String            RULE_TABLE_TAG         = "RuleTable";
    public static final String            RULESET_TAG            = "RuleSet";
    private static final int              ACTION_ROW             = 1;
    private static final int              OBJECT_TYPE_ROW        = 2;
    private static final int              CODE_ROW               = 3;
    private static final int              LABEL_ROW              = 4;

    //state machine variables for this parser
    private boolean                       _isInRuleTable         = false;
    private int                           _ruleRow;
    private int                           _ruleStartColumn;
    private int                           _ruleStartRow;
    private Rule                          _currentRule;
    private String                        _currentRulePrefix;
    private boolean                       _currentSequentialFlag = false;                        // indicates that we are in sequential mode

    //accumulated output
    private Map<Integer, ActionType>       _actions;
    private final HashMap<Integer, String> _cellComments          = new HashMap<Integer, String>();
    private final List<Rule>               _ruleList              = new LinkedList<Rule>();

    //need to keep an ordered list of this to make conditions appear in the right order
    private List<SourceBuilder>            sourceBuilders         = new ArrayList<SourceBuilder>();

    private final PropertiesSheetListener _propertiesListner     = new PropertiesSheetListener();

	private boolean showPackage;

    public DefaultRuleSheetListener() {
        this( true );
    }

    public DefaultRuleSheetListener(boolean showPackage) {
        this.showPackage = showPackage;
    }

    /* (non-Javadoc)
     * @see org.drools.decisiontable.parser.RuleSheetListener#getProperties()
     */
    public Properties getProperties() {
        return this._propertiesListner.getProperties();
    }

    /* (non-Javadoc)
     * @see org.drools.decisiontable.parser.RuleSheetListener#getRuleSet()
     */
    public Package getRuleSet() {
        if ( this._ruleList.isEmpty() ) {
            throw new DecisionTableParseException( "No RuleTable's were found in spreadsheet." );
        }
        final Package ruleset = buildRuleSet();
        return ruleset;
    }

    /**
     * Add a new rule to the current list of rules
     * @param rule
     */
    protected void addRule(final Rule newRule) {
        this._ruleList.add( newRule );
    }

    private Package buildRuleSet() {
        final String defaultPackageName = "rule_table";
        final String rulesetName = getProperties().getProperty( RULESET_TAG,
                                                                defaultPackageName );

        final Package ruleset = new Package( (showPackage) ? rulesetName : null );
        for ( Rule rule : this._ruleList ) {
            ruleset.addRule( rule );
        }
	        final List<Import> importList = RuleSheetParserUtil.getImportList( getProperties().getProperty( IMPORT_TAG ) );
	        for ( Import import1 : importList ) {
	            ruleset.addImport( import1 );
	        }
	        final List<Global> variableList = RuleSheetParserUtil.getVariableList( getProperties().getProperty( VARIABLES_TAG ) ); // Set the list of variables to
	        // be added to the
	        // application-data tags
	        for ( Global global : variableList ) {
	            ruleset.addVariable( global );
	        }

        final String functions = getProperties().getProperty( FUNCTIONS_TAG );
        ruleset.addFunctions( functions );
        return ruleset;
    }

    /*
     * (non-Javadoc)
     *
     * @see my.hssf.util.SheetListener#startSheet(java.lang.String)
     */
    public void startSheet(final String name) {
        // nothing to see here... move along..
    }

    /*
     * (non-Javadoc)
     *
     * @see my.hssf.util.SheetListener#finishSheet()
     */
    public void finishSheet() {
        this._propertiesListner.finishSheet();
        finishRuleTable();
        flushRule();
    }

    /*
     * (non-Javadoc)
     *
     * @see my.hssf.util.SheetListener#newRow()
     */
    public void newRow(final int rowNumber,
                       final int columns) {
        if ( _currentRule != null ) flushRule();
        // nothing to see here... these aren't the droids your looking for..
        // move along...
    }

    /**
     * This makes sure that the rules have all their components added.
     * As when there are merged/spanned cells, they may be left out.
     */
    private void flushRule() {
        for ( Iterator<SourceBuilder> iter = sourceBuilders.iterator(); iter.hasNext(); ) {
            SourceBuilder src = iter.next();
            if ( src.hasValues() ) {
                if ( src instanceof LhsBuilder ) {
                    Condition con = new Condition();
                    con.setSnippet( src.getResult() );
                    _currentRule.addCondition( con );
                } else if ( src instanceof RhsBuilder ) {
                    Consequence con = new Consequence();
                    con.setSnippet( src.getResult() );
                    _currentRule.addConsequence( con );
                }
                src.clearValues();
            }
        }

    }

    /*
     * (non-Javadoc)
     *
     * @see my.hssf.util.SheetListener#newCell(int, int, java.lang.String)
     */
    public void newCell(final int row,
                        final int column,
                        final String value,
                        int mergedColStart) {
        if ( isCellValueEmpty( value ) ) {
            return;
        }
        if ( _isInRuleTable && row == this._ruleStartRow ) {
            return;
        }
        if ( this._isInRuleTable ) {
            processRuleCell( row,
                             column,
                             value,
                             mergedColStart );
        } else {
            processNonRuleCell( row,
                                column,
                                value );
        }
    }

    /**
     * This gets called each time a "new" rule table is found.
     */
    private void initRuleTable(final int row,
                               final int column,
                               final String value) {
        preInitRuleTable( row,
                          column,
                          value );
        this._isInRuleTable = true;
        this._actions = new HashMap<Integer, ActionType>();
        this.sourceBuilders = new ArrayList<SourceBuilder>();
        this._ruleStartColumn = column;
        this._ruleStartRow = row;
        this._ruleRow = row + LABEL_ROW + 1;

        // setup stuff for the rules to come.. (the order of these steps are
        // important !)
        this._currentRulePrefix = RuleSheetParserUtil.getRuleName( value );
        this._currentSequentialFlag = getSequentialFlag();

        this._currentRule = createNewRuleForRow( this._ruleRow );

        this._ruleList.add( this._currentRule );
        postInitRuleTable( row,
                           column,
                           value );

    }

    /**
     * Called before rule table initialisation. Subclasses may
     * override this method to do additional processing.
     */
    protected void preInitRuleTable(int row,
                                    int column,
                                    String value) {
    }

    protected Rule getCurrentRule() {
        return _currentRule;
    }

    /**
     * Called after rule table initialisation. Subclasses may
     * override this method to do additional processing.
     */
    protected void postInitRuleTable(int row,
                                     int column,
                                     String value) {
    }

    private boolean getSequentialFlag() {
        final String seqFlag = getProperties().getProperty( SEQUENTIAL_FLAG );
        return RuleSheetParserUtil.isStringMeaningTrue( seqFlag );
    }

    private void finishRuleTable() {
        if ( this._isInRuleTable ) {
            this._currentSequentialFlag = false;
            this._isInRuleTable = false;

        }
    }

    private void processNonRuleCell(final int row,
                                    final int column,
                                    final String value) {
        if ( value.startsWith( RULE_TABLE_TAG ) ) {
            initRuleTable( row,
                           column,
                           value );
        } else {
            this._propertiesListner.newCell( row,
                                             column,
                                             value,
                                             RuleSheetListener.NON_MERGED );
        }
    }

    private void processRuleCell(final int row,
                                 final int column,
                                 final String value,
                                 final int mergedColStart) {
        if ( value.startsWith( RULE_TABLE_TAG ) ) {
            finishRuleTable();
            initRuleTable( row,
                           column,
                           value );
            return;
        }

        // Ignore any comments cells preceeding the first rule table column
        if ( column < this._ruleStartColumn ) {
            return;
        }

        // Ignore any further cells from the rule def row
        if ( row == this._ruleStartRow ) {
            return;
        }

        switch ( row - this._ruleStartRow ) {
            case ACTION_ROW :
                ActionType.addNewActionType( this._actions,
                                             value,
                                             column,
                                             row );
                break;
            case OBJECT_TYPE_ROW :
                objectTypeRow( row,
                               column,
                               value,
                               mergedColStart );
                break;
            case CODE_ROW :
                codeRow( row,
                         column,
                         value );
                break;
            case LABEL_ROW :
                labelRow( row,
                          column,
                          value );
                break;
            default :
                nextDataCell( row,
                              column,
                              value );
                break;
        }
    }

    /**
     * This is for handling a row where an object declaration may appear,
     * this is the row immediately above the snippets.
     * It may be blank, but there has to be a row here.
     *
     * Merged cells have "special meaning" which is why this is so freaking hard.
     * A future refactor may be to move away from an "event" based listener.
     */
    private void objectTypeRow(final int row,
                               final int column,
                               final String value,
                               final int mergedColStart) {
        if ( value.indexOf( "$param" ) > -1 || value.indexOf( "$1" ) > -1 ) {
            throw new DecisionTableParseException( "It looks like you have snippets in the row that is " + "meant for column declarations." + " Please insert an additional row before the snippets." + " Row number: " + (row + 1) );
        }
        ActionType action = getActionForColumn( row,
                                                column );
        if ( mergedColStart == RuleSheetListener.NON_MERGED ) {
            if ( action.type == ActionType.CONDITION ) {
                SourceBuilder src = new LhsBuilder( value );
                action.setSourceBuilder( src );
                this.sourceBuilders.add( src );

            } else if ( action.type == ActionType.ACTION ) {
                SourceBuilder src = new RhsBuilder( value );
                action.setSourceBuilder( src );
                this.sourceBuilders.add( src );
            }
        } else {
            if ( column == mergedColStart ) {
                if ( action.type == ActionType.CONDITION ) {
                    action.setSourceBuilder( new LhsBuilder( value ) );
                    this.sourceBuilders.add( action.getSourceBuilder() );
                } else if ( action.type == ActionType.ACTION ) {
                    action.setSourceBuilder( new RhsBuilder( value ) );
                    this.sourceBuilders.add( action.getSourceBuilder() );
                }
            } else {
                ActionType startOfMergeAction = getActionForColumn( row,
                                                                    mergedColStart );
                action.setSourceBuilder( startOfMergeAction.getSourceBuilder() );
            }

        }
    }

    private void codeRow(final int row,
                         final int column,
                         final String value) {
        final ActionType actionType = getActionForColumn( row,
                                                          column );
        if ( actionType.getSourceBuilder() == null ) {
            if ( actionType.type == ActionType.CONDITION ) {
                actionType.setSourceBuilder( new LhsBuilder( null ) );
                this.sourceBuilders.add( actionType.getSourceBuilder() );
            } else if ( actionType.type == ActionType.ACTION ) {
                actionType.setSourceBuilder( new RhsBuilder( null ) );
                this.sourceBuilders.add( actionType.getSourceBuilder() );
            } else if ( actionType.type == ActionType.PRIORITY ) {
                actionType.setSourceBuilder( new LhsBuilder( null ) );
                this.sourceBuilders.add( actionType.getSourceBuilder() );
            }
        }
        if ( value.trim().equals( "" ) && (actionType.type == ActionType.ACTION || actionType.type == ActionType.CONDITION) ) {
            throw new DecisionTableParseException( "Code description - row:" + (row + 1) + " cell number:" + (column + 1) + " - does not contain any code specification. It should !" );
        }

        actionType.addTemplate( column,
                                value );
    }

    private void labelRow(final int row,
                          final int column,
                          final String value) {
        final ActionType actionType = getActionForColumn( row,
                                                          column );

        if ( !value.trim().equals( "" ) && (actionType.type == ActionType.ACTION || actionType.type == ActionType.CONDITION) ) {
            this._cellComments.put( new Integer( column ),
                                    value );
        } else {
            this._cellComments.put( new Integer( column ),
                                    "From column: " + Rule.convertColNumToColName( column ) );
        }
    }

    private ActionType getActionForColumn(final int row,
                                          final int column) {
        final ActionType actionType = this._actions.get( new Integer( column ) );

        if ( actionType == null ) {
            throw new DecisionTableParseException( "Code description - row number:" + (row + 1) + " cell number:" + (column + 1) + " - does not have an 'ACTION' or 'CONDITION' column header." );
        }

        return actionType;
    }

    private void nextDataCell(final int row,
                          final int column,
                          final String value) {
        final ActionType actionType = getActionForColumn( row,
                                                    column );

        if ( row - this._ruleRow > 1 ) {
            // Encountered a row gap from the last rule.
            // This is not part of the ruleset.
            finishRuleTable();
            processNonRuleCell( row,
                                column,
                                value );
            return;
        }

        if ( row > this._ruleRow ) {
            // In a new row/rule
            this._currentRule = createNewRuleForRow( row );

            this._ruleList.add( this._currentRule );
            this._ruleRow++;
        }

        //if the rule set is not sequential and the actionType type is PRIORITY then set the current Rule's salience paramenter with the value got from the cell
        if ( actionType.type == ActionType.PRIORITY && !this._currentSequentialFlag ) {
            this._currentRule.setSalience( new Integer( value ) );
        } else if ( actionType.type == ActionType.NAME ) // if the actionType
        // type is PRIORITY then
        // set the current
        // Rule's name
        // paramenter with the
        // value got from the
        // cell
        {
            this._currentRule.setName( value );
        } else if ( actionType.type == ActionType.DESCRIPTION ) // if the
        // actionType
        // type is
        // DESCRIPTION
        // then set the
        // current
        // Rule's
        // description
        // paramenter
        // with the
        // value got
        // from the cell
        {
            this._currentRule.setDescription( value );
        } else if ( actionType.type == ActionType.ACTIVATIONGROUP ) // if the actionType
        // type is NOLOOP
        // then set the
        // current Rule's
        // no-loop
        // paramenter with
        // the value got
        // from the cell
        {
            this._currentRule.setActivationGroup( value );
        } else if ( actionType.type == ActionType.NOLOOP ) // if the actionType
        // type is NOLOOP
        // then set the
        // current Rule's
        // no-loop
        // paramenter with
        // the value got
        // from the cell
        {
            this._currentRule.setNoLoop( value );
        } else if ( actionType.type == ActionType.RULEFLOWGROUP ) {
            this._currentRule.setRuleFlowGroup( value );
        }
        else if ( actionType.type == ActionType.DURATION ) // if the actionType
        // type is DURATION
        // then creates a
        // new duration tag
        // with the value
        // got from the cell
        {
            createDuration( column,
                            value,
                            actionType );
        } else if ( actionType.type == ActionType.CONDITION || actionType.type == ActionType.ACTION ) {
            actionType.addCellValue( column, value );
        }

    }

    private Rule createNewRuleForRow(final int row) {

        Integer salience = null;
        if ( this._currentSequentialFlag ) {
            salience = new Integer( Rule.calcSalience( row ) );
        }
        final int spreadsheetRow = row + 1;
        final String name = this._currentRulePrefix + "_" + spreadsheetRow;
        final Rule rule = new Rule( name,
                                    salience,
                                    spreadsheetRow );
        rule.setComment( "From row number: " + (spreadsheetRow) );

        return rule;

    }

    // 08 - 16 - 2005 RIK: This function creates a new DURATION
    private void createDuration(final int column,
                                final String value,
                                final ActionType actionType) {

        this._currentRule.setDuration( value );
    }

    private boolean isCellValueEmpty(final String value) {
        return value == null || "".equals( value.trim() );
    }

}