/*
 *    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 2 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, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 *    Array.java
 *    Copyright (C) 2009 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.core.pmml;

import java.io.Serializable;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.util.ArrayList;

import org.w3c.dom.Element;

/**
 * Class for encapsulating a PMML Array element.
 * 
 * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
 * @version $Revision: 5563 $
 */
public class Array implements Serializable {
  
  /**
   * Utility method to check if an XML element is an array.
   * 
   * @param arrayE the XML element to check
   * @return returns true if the XML element is an array
   */
  public static boolean isArray(Element arrayE) {
    String name = arrayE.getTagName();

    if (name.equals("Array") || name.equals("NUM-ARRAY") || name.equals("INT-ARRAY")
        || name.equals("REAL-ARRAY") || name.equals("STRING-ARRAY") 
        || isSparseArray(arrayE)) {
      return true;
    }
    return false;
  }
  
  /**
   * Utility method to check if an XML element is a sparse array.
   * 
   * @param arrayE the XML element to check.
   * @return true if the XML element is a sparse array.
   */
  private static boolean isSparseArray(Element arrayE) {
    String name = arrayE.getTagName();
    
    if (name.equals("INT-SparseArray") || name.equals("REAL-SparseArray")) {
      return true;
    }
    
    return false;
  }
  
  /**
   * Static factory method for creating non-sparse or sparse
   * array types as needed.
   * 
   * @param arrayE the XML element encapsulating the array
   * @return an appropriate Array type
   * @throws Exception if there is a problem when constructing the array
   */
  public static Array create(Element arrayE) throws Exception {
    if (!isArray(arrayE)) {
      throw new Exception("[Array] the supplied element does not contain an array!");
    }
    
    if (isSparseArray(arrayE)) {
      // TODO: implement sparse array subclass :-)
    }
     
    return new Array(arrayE);
  }
  
  public static enum ArrayType {
    NUM("NUM-ARRAY"),
    INT("INT-ARRAY"),
    REAL("REAL-ARRAY"),
    STRING("STRING-ARRAY");
    
    private final String m_stringVal;
    
    ArrayType(String name) {
      m_stringVal = name;
    }
    
    public String toString() {
      return m_stringVal;
    }
  }
  
  /** The values of the array */
  protected ArrayList<String> m_values = new ArrayList<String>();
  
  /** The type of the array */
  protected ArrayType m_type = ArrayType.NUM;
  
  protected Array(Element arrayE) throws Exception {
    String arrayS = arrayE.getTagName();
    
    // get the type of the array
    if (arrayS.equals("Array")) {
      String type = arrayE.getAttribute("type"); 
      if (type.equals("int")) {
        m_type = ArrayType.INT;
      } else if (type.equals("real")) {
        m_type = ArrayType.REAL;
      } else if (type.equals("string")) {
        m_type = ArrayType.STRING;
      }
    } else {
      for (ArrayType a : ArrayType.values()) {
        if (a.toString().equals(arrayS)) {
          m_type = a;
          break;
        }
      }
    }
    // now read the values
    String contents = arrayE.getChildNodes().item(0).getNodeValue();
    StringReader sr = new StringReader(contents);
    StreamTokenizer st = new StreamTokenizer(sr);
    st.resetSyntax();
    st.whitespaceChars(0, ' ');
    st.wordChars(' '+1,'\u00FF');
    st.whitespaceChars(' ',' ');
    st.quoteChar('"');
    st.quoteChar('\'');
    //m_Tokenizer.eolIsSignificant(true);
    
    st.nextToken();
    while (st.ttype != StreamTokenizer.TT_EOF && 
        st.ttype != StreamTokenizer.TT_EOL) {
      m_values.add(st.sval);
      st.nextToken();
    }
  }
  
  /**
   * Get the type of this array.
   * 
   * @return the type of the array.
   */
  public ArrayType getType() {
    return m_type;
  }
  
  /**
   * Is this array a SparseArray?
   * 
   * @return true if this array is sparse.
   */
  public boolean isSparse() {
    return false;
  }
  
  /**
   * Get the number of values in this array.
   * 
   * @return the number of values in this array.
   */
  public int numValues() {
    return m_values.size();
  }
  
  /**
   * Returns true if the array contains this string value.
   * 
   * @param value the value to check for.
   * @return true if the array contains this string value
   */
  public boolean contains(String value) {
    return m_values.contains(value);
  }
  
  /**
   * Returns true if the array contains this integer value.
   * 
   * @param value the value to check for
   * @return true if the array contains this integer value
   */
  public boolean contains(int value) {
    return contains(new Integer(value).toString());
  }
  
  /**
   * Returns true if the array contains this real value.
   * 
   * @param value the value to check for
   * @return true if the array contains this real value
   */
  public boolean contains(double value) {
    return contains(new Double(value).toString());
  }
  
  /**
   * Returns true if the array contains this real value.
   * 
   * @param value the value to check for
   * @return true if the array contains this real value
   */
  public boolean contains(float value) {
    return contains(new Float(value).toString());
  }
  
  private void checkInRange(int index) throws Exception {
    if (index >= m_values.size() || index < 0) {
      throw new IllegalArgumentException("[Array] index out of range " + index);
    }
  }
  
  /**
   * Gets the value at index from the array.
   * 
   * @param index the index of the value to get.
   * @return the value at index in the array as as String.
   * @throws Exception if index is out of bounds.
   */
  public String value(int index) throws Exception {
    checkInRange(index);
    
    return m_values.get(index);
  }
  
  /**
   * Gets the value at index from the array as a String. Calls
   * value().
   * 
   * @param index the index of the value to get.
   * @return the value at index in the array as a String.
   * @throws Exception if index is out of bounds.
   */
  public String valueString(int index) throws Exception {
    return value(index);
  }
  
  /**
   * Gets the value at index from the array as a double.
   * 
   * @param index the index of the value to get.
   * @return the value at index in the array as a double.
   * @throws Exception if index is out of bounds.
   */
  public double valueDouble(int index) throws Exception {
    if (m_type == ArrayType.STRING) {
      throw new Exception("[Array] Array does not contain numbers!");
    }
    return Double.parseDouble(value(index));
  }
  
  /**
   * Gets the value at index from the array as a float.
   * 
   * @param index the index of the value to get.
   * @return the value at index in the array as a float.
   * @throws Exception if index is out of bounds.
   */
  public float valueFloat(int index) throws Exception {
    if (m_type == ArrayType.STRING) {
      throw new Exception("[Array] Array does not contain numbers!");
    }
    return Float.parseFloat(value(index));
  }
  
  /**
   * Gets the value at index from the array as an int.
   * 
   * @param index the index of the value to get.
   * @return the value at index in the array as an int.
   * @throws Exception if index is out of bounds.
   */
  public int valueInt(int index) throws Exception {
    if (m_type == ArrayType.INT) {
      throw new Exception("[Array] Array does not contain integers!");
    }
    return Integer.parseInt(value(index));
  }
  
  /**
   * Gets the value at indexOfIndex from the array. Does the
   * same as value() if this array is not sparse.
   * 
   * @param indexOfIndex the index of the index of the value to get.
   * @return a value from the array as a String.
   * @throws Exception if indexOfIndex is out of bounds.
   */
  public String valueSparse(int indexOfIndex) throws Exception {
    return value(indexOfIndex);
  }
  
  /**
   * Gets the value at indexOfIndex from the array. Does the
   * same as value() if this array is not sparse.
   * 
   * @param indexOfIndex the index of the index of the value to get.
   * @return a value from the array as a String.
   * @throws Exception if indexOfIndex is out of bounds.
   */
  public String valueSparseString(int indexOfIndex) throws Exception {
    return valueSparse(indexOfIndex);
  }
  
  /**
   * Gets the value at indexOfIndex from the array. Does the
   * same as value() if this array is not sparse.
   * 
   * @param indexOfIndex the index of the index of the value to get.
   * @return a value from the array as a double.
   * @throws Exception if indexOfIndex is out of bounds.
   */
  public double valueSparseDouble(int indexOfIndex) throws Exception {
    return valueDouble(indexOfIndex);
  }
  
  /**
   * Gets the value at indexOfIndex from the array. Does the
   * same as value() if this array is not sparse.
   * 
   * @param indexOfIndex the index of the index of the value to get.
   * @return a value from the array as a float.
   * @throws Exception if indexOfIndex is out of bounds.
   */
  public float valueSparseFloat(int indexOfIndex) throws Exception {
    return valueFloat(indexOfIndex);
  }
  
  /**
   * Gets the value at indexOfIndex from the array. Does the
   * same as value() if this array is not sparse.
   * 
   * @param indexOfIndex the index of the index of the value to get.
   * @return a value from the array as an int.
   * @throws Exception if indexOfIndex is out of bounds.
   */
  public int valueSparseInt(int indexOfIndex) throws Exception {
    return valueInt(indexOfIndex);
  }
  
  public String toString() {
    StringBuffer text = new StringBuffer();
    
    text.append("[");
    for (int i = 0; i < m_values.size(); i++) {
      text.append(m_values.get(i));
      if (i < m_values.size() - 1) {
        text.append(",");
      }
    }
    
    text.append("]");
    return text.toString();
  }
}
