/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU 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 General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 *    DefaultAssociationRule.java
 *    Copyright (C) 2010-2012 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.associations;

import java.io.Serializable;
import java.util.Collection;

import weka.core.Tag;
import weka.core.Utils;

/**
 * Class for storing and manipulating an association rule.
 * 
 * @author Mark Hall (mhall{[at]}pentaho{[dot]}com).
 */
public class DefaultAssociationRule extends AssociationRule 
  implements Serializable {
  
  /** For serialization */
  private static final long serialVersionUID = -661269018702294489L;

  /** Enum for holding different metric types */
  public static enum METRIC_TYPE {
    CONFIDENCE("conf") {
      double compute(int premiseSupport, int consequenceSupport, 
          int totalSupport, int totalTransactions) {
        
        return (double)totalSupport / (double)premiseSupport;
      }
    },
    LIFT("lift") {
      double compute(int premiseSupport, int consequenceSupport, 
          int totalSupport, int totalTransactions) {
        
        double confidence = 
          METRIC_TYPE.CONFIDENCE.compute(premiseSupport, consequenceSupport, 
              totalSupport, totalTransactions);
        return confidence / ((double)consequenceSupport /
            (double)totalTransactions);
      }
    },
    LEVERAGE("lev") {
      double compute(int premiseSupport, int consequenceSupport, 
          int totalSupport, int totalTransactions) {
        
        double coverageForItemSet = (double)totalSupport /
          (double)totalTransactions;
        double expectedCoverageIfIndependent = 
          ((double)premiseSupport / (double)totalTransactions) *
          ((double)consequenceSupport / (double)totalTransactions);
        return coverageForItemSet - expectedCoverageIfIndependent;
      }
    },
    CONVICTION("conv") {
      double compute(int premiseSupport, int consequenceSupport, 
          int totalSupport, int totalTransactions) {
        
        double num = 
          (double)premiseSupport * (double)(totalTransactions - consequenceSupport) /
          (double)totalTransactions;
        double denom = premiseSupport - totalSupport + 1;
        return num / denom;
      }
    };
    
    private final String m_stringVal;
    METRIC_TYPE(String name) {
      m_stringVal = name;
    }
    
    abstract double compute(int premiseSupport, int consequenceSupport, 
        int totalSupport, int totalTransactions);
    
    public String toString() {
      return m_stringVal;
    }
    
    public String toStringMetric(int premiseSupport, int consequenceSupport,
        int totalSupport, int totalTransactions) {
      return m_stringVal + ":(" + Utils.doubleToString(compute(premiseSupport, consequenceSupport,
          totalSupport, totalTransactions), 2) + ")";
    }
    
    public String toXML(int premiseSupport, int consequenceSupport,
        int totalSupport, int totalTransactions) {
      String result = "<CRITERE name=\"" + m_stringVal + "\" value=\" " +
        Utils.doubleToString(compute(premiseSupport, consequenceSupport,
          totalSupport, totalTransactions), 2) + "\"/>";
      
      return result;
    }
  }
  
  /** Tags for display in the GUI */
  public static final Tag[] TAGS_SELECTION = {
    new Tag(METRIC_TYPE.CONFIDENCE.ordinal(), "Confidence"),
    new Tag(METRIC_TYPE.LIFT.ordinal(), "Lift"),
    new Tag(METRIC_TYPE.LEVERAGE.ordinal(), "Leverage"),
    new Tag(METRIC_TYPE.CONVICTION.ordinal(), "Conviction")
  };
  
  /** The metric type for this rule */
  protected DefaultAssociationRule.METRIC_TYPE m_metricType = METRIC_TYPE.CONFIDENCE;
  
  /** The premise of the rule */
  protected Collection<Item> m_premise;
  
  /** The consequence of the rule */
  protected Collection<Item> m_consequence;
  
  /** The support for the premise */
  protected int m_premiseSupport;
  
  /** The support for the consequence */
  protected int m_consequenceSupport;
  
  /** The total support for the item set (premise + consequence) */
  protected int m_totalSupport;
  
  /** The total number of transactions in the data */
  protected int m_totalTransactions;
  
  /**
   * Construct a new default association rule.
   * 
   * @param premise the premise of the rule
   * @param consequence the consequence of the rule
   * @param metric the metric for the rule
   * @param premiseSupport the support of the premise
   * @param consequenceSupport the support of the consequence
   * @param totalSupport the total support of the rule
   * @param totalTransactions the number of transactions in the data
   */
  public DefaultAssociationRule(Collection<Item> premise, 
      Collection<Item> consequence, METRIC_TYPE metric,
      int premiseSupport, int consequenceSupport,
      int totalSupport, int totalTransactions) {
    m_premise = premise;
    m_consequence = consequence;
    m_metricType = metric;
    m_premiseSupport = premiseSupport;
    m_consequenceSupport = consequenceSupport;
    m_totalSupport = totalSupport;
    m_totalTransactions = totalTransactions;
  }
  
  /* (non-Javadoc)
   * @see weka.associations.AssociationRule#getPremise()
   */
  public Collection<Item> getPremise() {
    return m_premise;
  }
  
  /* (non-Javadoc)
   * @see weka.associations.AssociationRule#getConsequence()
   */
  public Collection<Item> getConsequence() {
    return m_consequence;
  }
  
  /* (non-Javadoc)
   * @see weka.associations.AssociationRule#getPrimaryMetricName()
   */
  public String getPrimaryMetricName() {
    return TAGS_SELECTION[m_metricType.ordinal()].getReadable();
  }
  
  /* (non-Javadoc)
   * @see weka.associations.AssociationRule#getPrimaryMetricValue()
   */
  public double getPrimaryMetricValue() {
    return m_metricType.compute(m_premiseSupport, m_consequenceSupport, 
        m_totalSupport, m_totalTransactions);
  }
  
  /* (non-Javadoc)
   * @see weka.associations.AssociationRule#getNamedMetricValue(java.lang.String)
   */
  public double getNamedMetricValue(String metricName) throws Exception {
    
    DefaultAssociationRule.METRIC_TYPE requested = null;
    
    for (DefaultAssociationRule.METRIC_TYPE m : METRIC_TYPE.values()) {
      if (TAGS_SELECTION[m.ordinal()].getReadable().equals(metricName)) {
        requested = m;
      }
    }
    
    if (requested == null) {
      throw new Exception("[AssociationRule] Unknown metric: " + metricName);
    }
    
    return requested.compute(m_premiseSupport, m_consequenceSupport, 
        m_totalSupport, m_totalTransactions);
  }
  
  /* (non-Javadoc)
   * @see weka.associations.AssociationRule#getNumberOfMetricsForRule()
   */
  public int getNumberOfMetricsForRule() {
    return METRIC_TYPE.values().length;
  }
  
  /* (non-Javadoc)
   * @see weka.associations.AssociationRule#getMetricNamesForRule()
   */
  public String[] getMetricNamesForRule() {
    String[] metricNames = new String[TAGS_SELECTION.length];
    
    for (int i = 0; i < TAGS_SELECTION.length; i++) {
      metricNames[i] = TAGS_SELECTION[i].getReadable();
    }
    
    return metricNames;
  }
  
  /* (non-Javadoc)
   * @see weka.associations.AssociationRule#getMetricValuesForRule()
   */
  public double[] getMetricValuesForRule() throws Exception {
    double[] values = new double[TAGS_SELECTION.length];
    
    for (int i = 0; i < TAGS_SELECTION.length; i++) {
      values[i] = getNamedMetricValue(TAGS_SELECTION[i].getReadable());
    }
    
    return values;
  }
  
  /* (non-Javadoc)
   * @see weka.associations.AssociationRule#getPremiseSupport()
   */
  public int getPremiseSupport() {
    return m_premiseSupport;
  }
  
  /* (non-Javadoc)
   * @see weka.associations.AssociationRule#getConsequenceSupport()
   */
  public int getConsequenceSupport() {
    return m_consequenceSupport;
  }
  
  /* (non-Javadoc)
   * @see weka.associations.AssociationRule#getTotalSupport()
   */
  public int getTotalSupport() {
    return m_totalSupport;
  }
  
  /* (non-Javadoc)
   * @see weka.associations.AssociationRule#getTotalTransactions()
   */
  public int getTotalTransactions() {
    return m_totalTransactions;
  }
              
  /**
   * Get a textual description of this rule.
   * 
   * @return a textual description of this rule.
   */
  public String toString() {
    StringBuffer result = new StringBuffer();
    
    result.append(m_premise.toString() + ": " + m_premiseSupport 
        + " ==> " + m_consequence.toString() + ": " + m_totalSupport 
        + "   ");
    for (DefaultAssociationRule.METRIC_TYPE m : METRIC_TYPE.values()) {
      if (m.equals(m_metricType)) {
        result.append("<" + 
            m.toStringMetric(m_premiseSupport, m_consequenceSupport, 
                m_totalSupport, m_totalTransactions) + "> ");
      } else {
        result.append("" + 
            m.toStringMetric(m_premiseSupport, m_consequenceSupport, 
                m_totalSupport, m_totalTransactions) + " ");
      }
    }
    return result.toString();
  }
}