package org.ecoinformatics.datamanager.sample;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ResourceBundle;

import org.ecoinformatics.datamanager.DataManager;
import org.ecoinformatics.datamanager.database.Condition;
import org.ecoinformatics.datamanager.database.ConnectionNotAvailableException;
import org.ecoinformatics.datamanager.database.DatabaseConnectionPoolInterface;
import org.ecoinformatics.datamanager.database.Query;
import org.ecoinformatics.datamanager.database.SelectionItem;
import org.ecoinformatics.datamanager.database.TableItem;
import org.ecoinformatics.datamanager.database.WhereClause;
import org.ecoinformatics.datamanager.download.DataStorageInterface;
import org.ecoinformatics.datamanager.download.EcogridEndPointInterface;
import org.ecoinformatics.datamanager.parser.Attribute;
import org.ecoinformatics.datamanager.parser.AttributeList;
import org.ecoinformatics.datamanager.parser.DataPackage;
import org.ecoinformatics.datamanager.parser.Entity;
import org.ecoinformatics.datamanager.quality.QualityReport;

  /**
   * This class is a sample calling application to demonstrate use of the
   * Data Manager Library API. 
   * 
   * This class implements DatabaseConnectionPoolInterface to provide
   * database connections to the Data Manager code. This class is also
   * associated with the SampleDataStorage class, which illustrates an
   * implementation of the DataStorageInterface, and the EcogridEndPoint
   * class, which illustrates an implementation of the
   * EcogridEndPointInterface.
   * 
   * Database information in this class will be read from a properties file,
   * determined by the values of the CONFIG_DIR and CONFIG_NAME constants.
   * 
   * @author dcosta
   *
   */
public class SampleCallingApp implements DatabaseConnectionPoolInterface {

  /*
   * Class fields
   */
  
  /*
   * Configuration directory and file name for the properties file. You can
   * edit the properties in this file to change the database connection
   * information as well as the sample metadata document used by this 
   * application.
   */
  private static final String CONFIG_NAME = "datamanager";

  /*
   * These fields will be assigned values when the properties file is loaded. 
   */
  private static ResourceBundle options = null;
  private static String dbDriver = null;
  private static String dbURL = null;
  private static String dbUser = null;
  private static String dbPassword = null;
  private static String databaseAdapterName = null;
  private static String documentURL = null;
  private static String entityName = null;
  private static String packageID = null;
  private static Boolean qualityReporting = new Boolean("false");  // default value
    
  
  /*
   * Instance fields
   */

  // An instance of the DataManager class. This object provides the 
  // calling application access to all the public methods exposed by the 
  // Data Manager Library API.
  private DataManager dataManager;

  // InputStream object used to read metadata from the remote location
  private InputStream metadataInputStream = null;

  // A dataPackage object is returned after parsing the metadata
  // (see method testParseMetadata() ). We save it in this field so that it
  // can be subsequently used by other methods.
  private DataPackage dataPackage;
  
  // A DataStorageInterface object that this class is associated with.
  // The calling application must use an object of this type to interact
  // with the Data Manager's download manager (see testDownloadData()).
  private DataStorageInterface dsi = null;

  // The calling application needs to be associated with an
  // EcogridEndPointInterface object for loading data to the database.
  // (See testLoadDataToDB()).
  private EcogridEndPointInterface eepi = null;
  
  
  
  /*
   * Constructors
   */
        
  /**
   * Constructor. Load database parameters and sample document name from 
   * properties file, get an instance of the DataManager class, and construct 
   * a DataStorageInterface object and an EcogridEndPointInterface object.
   */
  public SampleCallingApp() {
    loadOptions();
    dataManager = DataManager.getInstance(this, databaseAdapterName);
    dsi = new SampleDataStorage();
    eepi = new EcogridEndPoint();
  }

  
  /*
   * Class methods
   */
 
  /**
   * Main method. Creates an instance of the calling application object,
   * initializes it, and runs a number of calls to the DataManager that
   * test the various use cases. Each use case builds on the previous
   * use cases.
   */
  public static void main(String[] args)
    throws MalformedURLException, IOException, Exception {
    boolean success = true;

    SampleCallingApp dmm = new SampleCallingApp();
    dmm.setUp();
    success = success && dmm.testParseMetadata();   // Use Case #1
    success = success && dmm.testDownloadData();    // Use Case #2
    success = success && dmm.testLoadDataToDB();    // Use Case #3
    success = success && dmm.testSelectData();      // Use Case #4
    success = success && dmm.testEnumerationMethods();  // Miscellaneous other
    success = success && dmm.testQualityReport();       // Miscellaneous other
    System.err.println("Finished all tests, success = " + success + "\n");
    dmm.tearDown();  // clean-up tables
  }
  
  
  /**
   * Loads Data Manager options from a configuration file.
   */
  private static void loadOptions() {

    try {
      // Load database connection options
      options = ResourceBundle.getBundle(CONFIG_NAME);
      dbDriver = options.getString("dbDriver");
      dbURL = options.getString("dbURL");
      dbUser = options.getString("dbUser");
      dbPassword = options.getString("dbPassword");
      databaseAdapterName = options.getString("dbAdapter");
      
      // Load sample document and Metacat server options
      documentURL = options.getString("documentURL");
      entityName = options.getString("entityName");
      packageID = options.getString("packageID");
      
      /* Check the value of the qualityReporting property and call 
       * QualityReport.setQualityReporting() accordingly. This controls whether
       * the Data Manager library will execute with quality reporting turned
       * on or off.
       */
      String qualityReportingStr = options.getString("qualityReporting");
      String qualityReportTemplate = options.getString("qualityReportTemplate");
      if (qualityReportingStr != null) {
        if (qualityReportingStr.equalsIgnoreCase("true")) {
          QualityReport.setQualityReporting(true, qualityReportTemplate);
          String emlDereferencerXSLT = options.getString("emlDereferencerXSLT");
          QualityReport.setEmlDereferencerXSLTPath(emlDereferencerXSLT);
        }
        else if (qualityReportingStr.equalsIgnoreCase("false")) {
          QualityReport.setQualityReporting(false, null);
        }
      }
    } 
    catch (Exception e) {
      System.err.println("Error in loading options: " + e.getMessage());
    }
  }


  /*
   * Instance methods
   */
  
  /**
   * Get database adpater name. Implementation of this method is required by
   * the DatabaseConnectionPoolInterface.
   * 
   * @return database adapter name, for example, "PostgresAdapter"
   */
  public String getDBAdapterName() {
    return databaseAdapterName;
  }


  /**
   * Gets a database connection from the pool. Implementation of this method is 
   * required by the DatabaseConnectionPoolInterface. Note that in this
   * example, there is no actual pool of connections. A full-fledged
   * application should manage a pool of connections that can be re-used.
   * 
   * @return checked out connection
   * @throws SQLException
   */
  public Connection getConnection()
          throws SQLException, ConnectionNotAvailableException {
    Connection connection = null;

    try {
      Class.forName(dbDriver);
    } 
    catch (java.lang.ClassNotFoundException e) {
      System.err.print("ClassNotFoundException: ");
      System.err.println(e.getMessage());
      throw (new SQLException(e.getMessage()));
    }

    try {
      connection = DriverManager.getConnection(dbURL, dbUser, dbPassword);
    } 
    catch (SQLException e) {
      System.err.println("SQLException: " + e.getMessage());
      throw (e);
    }

    return connection;
  }


  /**
   * Returns checked out dabase connection to the pool.
   * Implementation of this method is required by the 
   * DatabaseConnectionPoolInterface.
   * Note that in this example, there is no actual pool of connections
   * to return the connection to. A full-fledged
   * application should manage a pool of connections that can be re-used.
   * 
   * @param  conn, Connection that is being returned
   * @return boolean indicator if the connection was returned successfully
   */
  public boolean returnConnection(Connection conn) {
    boolean success = false;

    try {
      conn.close();
      success = true;
    } 
    catch (Exception e) {
      success = false;
    }

    return success;
  }
  

  /**
   * Initialize the calling application by opening an input stream to the
   * metadata document.
   * 
   * @throws MalformedURLException
   * @throws IOException
   * @throws Exception
   */
  private void setUp() throws MalformedURLException, IOException, Exception {
    try {
      URL url = new URL(documentURL);
      metadataInputStream = url.openStream();
    } 
    catch (MalformedURLException e) {
      e.printStackTrace();
      throw (e);
    } 
    catch (IOException e) {
      e.printStackTrace();
      throw (e);
    } 
    catch (Exception e) {
      e.printStackTrace();
      throw (e);
    }
  }


  /**
   * Clean-up after the calling application has completed its tests
   * of the various use cases. Drop tables from the database.
   * 
   * @throws ClassNotFoundException
   * @throws SQLException
   * @throws Exception
   */
  private void tearDown() throws ClassNotFoundException, SQLException,
      Exception {
    if (dataPackage != null) {
      dataManager.dropTables(dataPackage); // Clean-up test tables
      System.err.println("Finished dropping tables.\n");
    }
  }
  

  /**
   * Tests Use Case #2, downloading data from the remote location to the
   * local store.
   * 
   * @return   success, true if the download was successful, else false
   */
  public boolean testDownloadData() {
    boolean success = false;
    
    // We need to provide the Data Manager with a list of all 
    // DataStorageInterface objects that want to download the data
    // to their local data store (in this case there is only one).
    DataStorageInterface[] dataStorageList = new DataStorageInterface[1];
    
    dataStorageList[0] = dsi;

    // Download the data
    if (dataPackage != null) {
      success = dataManager.downloadData(dataPackage, eepi, dataStorageList);
    }
 
    System.err.println("Finished testDownloadData(), success = " + success + 
                       "\n");
    return success;
  }


  /**
   * Tests methods in the API to enumerate an entity's database table name,
   * or an attribute's database field names. There is no Use Case number
   * associated with these methods at present, so we can think of these
   * as "miscellaneous other" use cases.
   * 
   * @return  success, true if successful, else false
   * 
   * @throws MalformedURLException
   * @throws IOException
   * @throws Exception
   */
  public boolean testEnumerationMethods()
          throws MalformedURLException, IOException, Exception {
    boolean success = false;

    // Get the database table name for a given packageID and entityName
    String tableName = DataManager.getDBTableName(packageID, entityName);
    System.err.println("tableName: " + tableName);

    // Get a list of field names corresponding to the attrbutes for a
    // given packageID and entityName
    String[] fieldNames = DataManager.getDBFieldNames(packageID, entityName);

    // Print out the field names that were returned
    if (fieldNames != null) {
      for (int i = 0; i < fieldNames.length; i++) {
        System.err.println("  fieldNames[" + i + "]: " + fieldNames[i]);
      }
    }

    // We succeeded if we found both a table name and a list of field names
    success = ((tableName != null) && (fieldNames != null));
    System.err.println("Finished testEnumerationMethods(), success = " + 
                       success + "\n");

    return success;
  }

 
  /**
   * Tests Use Case #3, loading data into the database.
   * 
   * @return success, true if successful, else false
   * 
   * @throws MalformedURLException
   * @throws IOException
   * @throws Exception
   */
  public boolean testLoadDataToDB() 
         throws MalformedURLException, IOException, Exception {
    boolean success = false;

    // Load the data for this data package. We also need to provide an
    // EcogridEndPointInterface object to the Data Manager.
    if (dataPackage != null) {
      success = dataManager.loadDataToDB(dataPackage, eepi);
    }
 
    System.err.println("Finished testLoadDataToDB(), success = " + success
                       + "\n");
    return success;
  }
  
  
  /**
   * Tests Use Case #1, parsing a metadata document.
   * 
   * @return  success, true if successful, else false
   */
  public boolean testParseMetadata() {
    boolean success = false;
    
    if (metadataInputStream != null) {
      try {
        // Parse the metadata that is being read from the input stream
        dataPackage = dataManager.parseMetadata(metadataInputStream);
        // If a DataPackage was returned, we succeeded in parsing
        success = (dataPackage != null);
      }
      catch (Exception e){
        e.printStackTrace();
      }
    }
    
    System.err.println("Finished testParseMetadata(), success = " + success
                       + "\n");
    return success;
  }

  
  /**
   * Test whether a quality report can be generated and stored.
   * 
   * @return  success, true if successful, else false
   */
  private boolean testQualityReport() {
    boolean success = false;

    // Assumes that the DataPackage object has already been created in the
    // previous tests and saved in the 'dataPackage' instance field.
    if (dataPackage != null) {
      QualityReport qualityReport = dataPackage.getQualityReport();
      if (qualityReport != null) {
        File qualityReportFile = new File("/tmp/quality_report.xml");
        try {
          success = qualityReport.storeQualityReport(qualityReportFile);
        }
        catch (IOException e) {
          System.err.println("Error storing quality report file: " + e.getMessage());
        }
      }
    }
    
    System.err.println("Finished testQualityReport(), success = " + success
        + "\n");
    return success;
  }


  /**
   * Tests Use Case #5, selecting data from a table.
   * Tests the creation and use of a Query object for querying a data table. 
   * Runs a query with a conditional WHERE clause and prints the result set.
   * 
   * Note that if the properties file is edited to use a different test
   * document than the default document, this method will need to be re-written
   * with a different query. This method was specifically written to query
   * the default test document.
   */
  public boolean testSelectData() throws Exception {
    AttributeList attributeList;
    Attribute attribute;                   // Used in where clause condition
    Entity entity = null;
    String operator = ">";                 // Used in where clause condition
    boolean success = false;
    int intValue = 1;                      // Used in where clause condition
    Integer value = new Integer(intValue); // Used in where clause condition
    ResultSet resultSet = null;
  
    // Assumes that the DataPackage object has already been created in the
    // previous tests and saved in the 'dataPackage' instance field.
    if (dataPackage != null) {
      Entity[] entityList = dataPackage.getEntityList();
      entity = entityList[0];
      attributeList = entity.getAttributeList();
      Attribute[] attributes = attributeList.getAttributes();
      attribute = attributes[2];

    /*
     * Now build a test query, execute it, and compare the result set to
     * known values.
     */
      DataPackage[] dataPackages = new DataPackage[1];
      dataPackages[0] = dataPackage;
      Query query = new Query();
      /* SELECT clause */
      SelectionItem selectionItem = new SelectionItem(entity, attribute);
      query.addSelectionItem(selectionItem);
      /* FROM clause */
      TableItem tableItem = new TableItem(entity);
      query.addTableItem(tableItem);
      /* WHERE clause with relational operator condition */
      Condition condition = new Condition(entity, attribute, operator, value);
      WhereClause whereClause = new WhereClause(condition);
      query.setWhereClause(whereClause);
      
      /* Print out the SQL string */
      System.err.println("Query SQL = '" + query.toSQLString() + "'");

      try {
        /* Run the query */
        resultSet = dataManager.selectData(query, dataPackages);
        
        /* Output the result set */
        if (resultSet != null) {
          success = true;
          int i = 1;

          System.err.println(
                         "Printing all records with 'count' value greater than " 
                         + intValue
                            );
          while (resultSet.next()) {;
            int count = resultSet.getInt(1);
            System.err.println("resultSet[" + i + "], count =  " + count);
            i++;
          }
        }
        else {
          throw new Exception("resultSet is null");
        }
      } 
      catch (Exception e) {
        System.err.println("Exception in DataManager.selectData()"
                           + e.getMessage()
                          );
        throw (e);
      }  
      finally {
        if (resultSet != null) resultSet.close();
      }
    } 
    
    System.err.println("Finished testSelectData(), success = " + success
                       + "\n");
    return success;
  }
  
}
