/*
 * Copyright
 *   2008 axYus - www.axyus.com
 *   2008 C.Marchand - christophe.marchand@axyus.com
 *
 * This file is part of XEMELIOS.
 *
 * XEMELIOS 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.
 *
 * XEMELIOS 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 XEMELIOS; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package fr.gouv.finances.cp.xemelios.data.impl.mysql;

import fr.gouv.finances.cp.utils.Pair;
import fr.gouv.finances.cp.xemelios.common.config.ElementModel;
import fr.gouv.finances.cp.xemelios.common.config.EtatModel;
import fr.gouv.finances.cp.xemelios.common.config.ListeResultatModel;
import fr.gouv.finances.cp.xemelios.data.DataAccessException;
import fr.gouv.finances.cp.xemelios.data.DataConfigurationException;
import fr.gouv.finances.cp.xemelios.data.DataResultSet;
import fr.gouv.finances.cp.xemelios.data.impl.MySqlDataLayer;
import fr.gouv.finances.cp.xemelios.data.CachedData;
import fr.gouv.finances.cp.xemelios.data.DataHandler;
import java.io.File;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * This class is provided to dynamically generate DOM documents from a request.
 * User provides a request, and for each row of ResultSet, a DOM document is generated
 * throw the provided xsl. Then, this document is used to display fields
 * @author chm
 */
public class MySqlDataResultSetDocGenerator implements DataResultSet {
    private static Logger logger = Logger.getLogger(MySqlDataResultSetDocGenerator.class);
    protected ElementModel elementModel;
    protected Pair collectivite, budget;
    protected String xPath, sql;
    protected ListeResultatModel lrm;
    protected MySqlDataLayer impl;
    protected File xslFile = null;
    protected int cacheSize = 100;
    protected HashMap<String,Document> cachedDocuments;
    protected ResultSet rs;
    protected int currentPage = 0;
    protected NodeList currentList=null;
    protected Document currentDoc=null;
    protected ResultSetMetaData rsmd=null;
    protected Transformer transformer=null;
    protected Document sourceDoc;
    protected XPath xp;
    protected int currentItemInList=-1;
    protected int estimatedSize = -1;
    protected String currentDocId;
    protected int currentCount = 0;
    protected boolean hasNextItem = false;
    protected CachedData cache=null;
    protected int estimatedPageCount=-1;
    protected int pageSize = 0;
    protected Connection conToRelease = null;
    
//    private boolean alreadyFetched = false;

    public MySqlDataResultSetDocGenerator(ElementModel elementModel,Pair collectivite,Pair budget,String xPath,String sql,ListeResultatModel lrm,MySqlDataLayer impl, File xslFile) throws SQLException, DataConfigurationException {
        super();
        this.xslFile=xslFile;
        this.elementModel=elementModel;
        this.collectivite=collectivite;
        this.budget=budget;
        this.xPath=xPath;
        this.sql=sql;
        this.lrm=lrm;
        this.impl=impl;
        TransformerFactory tf = TransformerFactory.newInstance();
        try {
            transformer = tf.newTransformer(new StreamSource(new FileInputStream(xslFile)));
            sourceDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            sourceDoc.appendChild(sourceDoc.createElement("document"));
            XPathFactory xpf = XPathFactory.newInstance();
            xp = xpf.newXPath();
            xp.setNamespaceContext(elementModel.getParent().getParent().getNamespaces());
        } catch(Exception ex) {
            logger.error("in <init>",ex);
        }
        cacheSize = elementModel.getMaxDisplay();
        cachedDocuments = new HashMap<String,Document>();
        executeRequest();
    }

    public int getCount() {
        return estimatedSize;
    }

    public int getEstimatedSize() {
        return estimatedSize;
    }

    public boolean hasNextPage() {
        return currentPage<getEstimatedPageCount();
    }

    public boolean hasPreviousPage() {
        return currentPage>0;
    }

    public void setEnvironment(ListeResultatModel lrm, CachedData cache) {
        this.lrm=lrm;
        this.cache=cache;
    }

    public ListeResultatModel getListeResultatModel() {
        return lrm;
    }

    public void setListeResultatModel(ListeResultatModel l) {
        this.lrm=l;
    }

    public boolean hasNext() {
        return hasNextItem;
    }

    public EtatModel getEtatModel() {
        return elementModel.getParent();
    }

    public Pair getCollectivite() {
        return collectivite;
    }

    public Pair getBudget() {
        return budget;
    }

    public void nextPage() throws DataAccessException, DataConfigurationException {
        if(hasNextPage()) {
            currentPage++;
            currentList=null;
            if(rs!=null) {
                resetResultSet();
            }
            try {
                executeRequest();
            } catch(SQLException sqlEx) {
                throw new DataAccessException(sqlEx);
            }
        }
    }

    public void lastPage() throws DataAccessException, DataConfigurationException {
        if(hasNextPage()) {
            currentPage=getEstimatedPageCount()-1;
            currentList=null;
            if(rs!=null) {
                resetResultSet();
            }
            try {
                executeRequest();
            } catch(SQLException sqlEx) {
                throw new DataAccessException(sqlEx);
            }
        }
    }

    public void previousPage() throws DataAccessException, DataConfigurationException {
        if(hasPreviousPage()) {
            currentPage--;
            currentList=null;
            if(rs!=null) {
                resetResultSet();
            }
            try {
                executeRequest();
            } catch(SQLException sqlEx) {
                throw new DataAccessException(sqlEx);
            }
        }
    }

    public void firstPage() throws DataAccessException, DataConfigurationException {
        currentPage=0;
        currentList=null;
        if(rs!=null) {
            resetResultSet();
        }
        try {
            executeRequest();
        } catch(SQLException sqlEx) {
            throw new DataAccessException(sqlEx);
        }
    }

    public void setPage(int page) throws DataAccessException, DataConfigurationException {
        currentPage=page;
        currentList=null;
        if(rs!=null) {
            resetResultSet();
        }
        try {
            executeRequest();
        } catch(SQLException sqlEx) {
            throw new DataAccessException(sqlEx);
        }
    }

    public int getEstimatedPageCount() {
        if(estimatedPageCount==(-1)) {
            if(cacheSize==0 || estimatedSize==0) {
                estimatedPageCount=0;
            } else {
                estimatedPageCount=estimatedSize/cacheSize;
                if(estimatedSize % cacheSize >0) estimatedPageCount++;
            }
        }
        return estimatedPageCount;
    }

    public int getCurrentPage() {
        return currentPage;
    }

    public Document getDocument(String docId) throws DataConfigurationException, DataAccessException {
        return cachedDocuments.get(docId);
    }

    public String getDocumentEncoding(String docId) throws DataConfigurationException, DataAccessException {
        return "UTF-8";
    }

    public void clear() {
        cleanCaches();
    }

    public DataHandler next() {
        try {
            if(currentList==null) {
                currentDoc = getNextDoc();
                currentList = (NodeList)xp.evaluate(xPath, currentDoc, XPathConstants.NODESET);
                currentItemInList=-1;
            }
        } catch(Exception ex) {
            logger.error("in next()",ex);
        }
        Element el = null;
        try {
            el = (Element)currentList.item(++currentItemInList);
        } catch(IndexOutOfBoundsException iEx) {
        } catch(Exception ex) {
            logger.error(this.toString(),ex);
        }
        if(currentItemInList==currentList.getLength()-1) {
            currentList=null;
            hasNextItem=currentCount<pageSize;
        }
        if(el!=null)
            return new DataHandler(lrm,el,this,currentDocId,cache);
        else {
            hasNextItem=false;
            return null;
        }
    }

    public void remove() {
        throw new UnsupportedOperationException("this operation is not allowed on MySqlDataResultSetDocGenerator");
    }
    private void cleanAllCaches() {
        if(cache!=null) cache.flush();
        if(cachedDocuments!=null) cachedDocuments.clear();
    }
    protected void cleanCaches() {
        if(cache!=null) cache.flush();
        //if(cachedDocuments!=null) cachedDocuments.clear();
    }
    protected void executeRequest() throws SQLException {
        StringBuffer request = new StringBuffer();
        request.append("SELECT SQL_CALC_FOUND_ROWS ").append(sql.substring(6)).append(" LIMIT ");
        request.append(currentPage*cacheSize).append(", ").append((currentPage+1)*cacheSize);
//logger.debug(request.toString());
        rs = impl.execute(request.toString());
        hasNextItem = true;
        currentCount=0;
        cleanAllCaches();
        rsmd = rs.getMetaData();
        conToRelease = rs.getStatement().getConnection();
        if(estimatedSize==-1) {
            Statement ps = conToRelease.createStatement();
            ResultSet rs2 = ps.executeQuery("SELECT FOUND_ROWS()");
            if(rs2.next()) {
                estimatedSize = rs2.getInt(1);
//                currentPage = 0;
                getEstimatedPageCount();
            }
            rs2.close();
            ps.close();
        }
        if(currentPage==(getEstimatedPageCount()-1)) pageSize = estimatedSize % cacheSize;
        else pageSize = cacheSize;

    }
    protected Document getNextDoc() throws SQLException, TransformerException {
        if(rs==null) executeRequest();
        if(rs.next()) {
            currentCount++;
            currentDocId = Integer.toString(currentCount);
            for(int i=1;i<=rsmd.getColumnCount();i++) {
                String pName = rsmd.getColumnName(i);
                String pValue = rs.getString(pName);
                transformer.setParameter(pName, pValue);
            }
            DOMResult dr = new DOMResult();
            transformer.transform(new DOMSource(sourceDoc), dr);
            Document doc = (Document)dr.getNode();
            cachedDocuments.put(currentDocId, doc);
            return doc;
        } else {
            impl.releaseConnection(conToRelease);
            conToRelease = null;
        }
        return null;
    }

    @Override
    protected void finalize() throws Throwable {
        if(conToRelease!=null) impl.releaseConnection(conToRelease);
        conToRelease = null;
        super.finalize();
    }
    protected void resetResultSet() {
        try {
            rs.close();
        } catch(Exception ex) {}
        rs=null;
        impl.releaseConnection(conToRelease);
        conToRelease=null;
    }

    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append("cacheSize=").append(cacheSize).append("\ncurrentCount=").append(currentCount).append("\ncurrentItemInList=").append(currentItemInList).append("\ncurrentPage=").append(currentPage).append("\nestimatedPageCount=").append(estimatedPageCount).append("\nestimatedSize=").append(estimatedSize).append("\npageSize=").append(pageSize);
        return sb.toString();
    }
    
}
