/*
 * Copyright (c) 2004-2020 The YAWL Foundation. All rights reserved.
 * The YAWL Foundation is a collaboration of individuals and
 * organisations who are committed to improving workflow technology.
 *
 * This file is part of YAWL. YAWL is free software: you can
 * redistribute it and/or modify it under the terms of the GNU Lesser
 * General Public License as published by the Free Software Foundation.
 *
 * YAWL 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 Lesser General
 * Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with YAWL. If not, see <http://www.gnu.org/licenses/>.
 */

package org.yawlfoundation.yawl.scheduling.persistence;

import org.jdom2.Document;
import org.jdom2.Element;
import org.yawlfoundation.yawl.scheduling.Case;
import org.yawlfoundation.yawl.scheduling.Mapping;
import org.yawlfoundation.yawl.scheduling.util.Utils;
import org.yawlfoundation.yawl.util.HibernateEngine;
import org.yawlfoundation.yawl.util.JDOMUtil;

import java.text.ParseException;
import java.util.*;


/**
 * This class encapsulates the database. It exclusively uses stored procedures to
 * access the persistence layer, thereby maintaining the concept of logical data
 * independence. As long as the signature of the stored procedure and their
 * semantics do not change, data source and application code may be developed
 * independently from each other.
 * 
 * Specifically, DataMapper facilitates the persistence of resource utilisation
 * plans (RUPs) and of mappings.
 * 
 * A mapping is a bijective function that assigns to every YAWL workitem Id a
 * unique request key, where the latter is used by the custom service. In
 * effect, the mapping table binds the operations of the Scheduling Service to
 * the YAWL engine in a non-ambiguous way.
 * 
 * @author tbe, jku
 * @version $Id$
 * 
 */
public class DataMapper {
    private HibernateEngine _db;

	public DataMapper()	{

        // setup database connection
        Set<Class> persistedClasses = new HashSet<Class>();
        persistedClasses.add(Mapping.class);
        persistedClasses.add(Case.class);
        _db = new HibernateEngine(true, persistedClasses);
    }

	/**
	 * Saves or updates a mapping to the database for recovery of failed YAWL
	 * requests
	 * 
	 * @param mapping
	 *           object
	 *
	 */
	public void saveMapping(Mapping mapping) {
        Mapping exists = (Mapping) _db.get(Mapping.class, mapping.getWorkItemId());
        if (exists != null) {
            exists.setRequestKey(mapping.getRequestKey());
            exists.setWorkItemStatus(mapping.getWorkItemStatus());
            exists.setLocked(mapping.isLocked());
            _db.exec(exists, HibernateEngine.DB_UPDATE, true);
        }
        else {
            _db.exec(mapping, HibernateEngine.DB_INSERT, true);
        }
	}


	/**
	 * removes a mapping from database
	 * 
	 * @param mapping
	 *           Object
	 */
	public void removeMapping(Mapping mapping) {
        _db.exec(mapping, HibernateEngine.DB_DELETE, true);
	}


	/**
	 * Removes all mappings with specified "workItemId"
	 *
	 * @author tbe, jku
	 * @param workItemId - YAWL's unique work item identifier
	 */
	public void removeMapping(String workItemId) {
        Mapping mapping = (Mapping) _db.get(Mapping.class, workItemId);
        if (mapping != null) removeMapping(mapping);
	}


	/**
	 * Get all mappings from the database
	 * 
	 * @return ArrayList<Mapping>
	 */
	public List<Mapping> getMappings() {
        List<Mapping> mappingList = new ArrayList<Mapping>();
        for (Object o : _db.getObjectsForClass("Mapping")) {
            mappingList.add((Mapping) o);
        }
        return mappingList;
	}

	
    public void saveRup(Case theCase) {
        Case exists = (Case) _db.get(Case.class, theCase.getCaseId());
        if (exists != null) {
            exists.setCaseName(theCase.getCaseName());
            exists.setDescription(theCase.getDescription());
            exists.setTimestamp(theCase.getTimestamp());
            exists.setSavedBy(theCase.getSavedBy());
            exists.setRupAsString(theCase.getRupAsString());
            _db.exec(exists, HibernateEngine.DB_UPDATE, true);
        }
        else {
            _db.exec(theCase, HibernateEngine.DB_INSERT, true);
        }
    }
    

	/**
	 * Get all RUPs from the database that start after "from" and end before
	 * "to". Allow for specifying a set of Yawl case Ids that are to be excluded
	 * from the result. Also, it is possible to select only RUPs that are active.
	 * 
	 * @author jku, tbe
	 * @param from
	 * @param to
	 * @param yCaseIdsToExclude
	 * @param activeOnly
	 * @return List<Case> all cases with RUPs that meet the selection criteria
	 */
	public List<Case> getRupsByInterval(Date from, Date to,
                                   List<String> yCaseIdsToExclude, boolean activeOnly) {

        if (yCaseIdsToExclude == null) yCaseIdsToExclude = new ArrayList<String>();
        List<Case> caseList = new ArrayList<Case>();
        for (Case c : getAllRups()) {
            if ((activeOnly && (! c.isActive())) || yCaseIdsToExclude.contains(c.getCaseId())) {
                continue;
            }
            Document rup = c.getRUP();
            long fromTime = getTime(rup, "//Activity/From");
            long toTime = getTime(rup, "//Activity/To");
            if ((fromTime > -1) && (toTime > -1) && (fromTime >= from.getTime()) && 
                    (toTime <= to.getTime())) {
                caseList.add(c);
            }
        }
        return caseList;
	}


	public List<Case> getRupByCaseId(String caseId)	{
        List<Case> caseList = new ArrayList<Case>();
        if (caseId != null) {
            for (Case c : getAllRups()) {
                if (caseId.equals(c.getCaseId())) {
                    caseList.add(c);
                }
            }
        }
        return caseList;
	}

	/**
	 * @author jku, tbe
	 * @param
	 * @return
	 */
	public List<Case> getAllRups() {
        List list = _db.getObjectsForClass("Case");
        List<Case> caseList = new ArrayList<Case>();
        if (list != null) {
            for (Object o : list) {
                caseList.add((Case) o);
            }
        }
        return caseList;
    }


	/**
	 * @author jku, tbe
	 * @param timestamp
	 * @return
	 */
	public List<Case> getActiveRups(String timestamp) {
        List<Case> caseList = new ArrayList<Case>();
        for (Case c : getAllRups()) {
            if (c.isActive()) {
                Document rup = c.getRUP();
                Element phaseElem = JDOMUtil.selectElement(rup, "//Activity/RequestType");
                if (phaseElem != null && "SOU".equals(phaseElem.getText())) {
                    long toTime = getTime(rup, "//Activity/To");
                    if (toTime > -1 && toTime < getTime(timestamp)) {
                        caseList.add(c);
                    }
                }
            }
        }
        return caseList;
    }

	/**
	 * @author jku, tbe
	 * @param activityName
	 * @param activityType
	 * @param nodeName
	 * @return
	 */
	public List<List<Element>> getRupNodes(String activityName, String activityType,
                                           String nodeName)	{
        List<List<Element>> nodesList = new ArrayList<List<Element>>();
        for (Case c : getRupsByActivity(activityName)) {
            Document rup = c.getRUP();
            Element elem = JDOMUtil.selectElement(rup, "//Activity/ActivityType");
            if (elem != null) {
                if (activityType.equals(elem.getText())) {
                    Element node = JDOMUtil.selectElement(rup, "//Activity/" + nodeName);
                    if (node != null) {
                        nodesList.add(Utils.string2Elements(JDOMUtil.elementToString(node)));
                    }
                }
            }
        }
        return nodesList;
	}


	/**
	 * @author jku, tbe
	 * @param activityName
	 * @return
	 */
	public List<Case> getRupsByActivity(String activityName) {
        List<Case> caseList = new ArrayList<Case>();
        for (Case c : getAllRups()) {
            Document rup = c.getRUP();
            Element elem = JDOMUtil.selectElement(rup, "//Activity/ActivityName");
            if (elem != null) {
                if (activityName.equals(elem.getText())) {
                    caseList.add(c);
                }
            }
        }
        return caseList;
	}

	/**
	 * @param activityName
	 * @return
	 */
	public List<String> getRupActivityTypes(String activityName) {
        List<String> activityTypes = new ArrayList<String>();
        for (Case c : getRupsByActivity(activityName)) {
            Document rup = c.getRUP();
            Element elem = JDOMUtil.selectElement(rup, "//Activity/ActivityType");
            if (elem != null) {
                if (elem.getText() != null) {
                    activityTypes.add(elem.getText());
                }
            }
        }
        return activityTypes;
	}

	/**
	 * Update RUP, set "active status"
	 * 
	 * @param caseId
	 * @param active
	 * @return
	 */
	public void updateRup(String caseId, boolean active) {
        List caseList = _db.getObjectsForClassWhere("Case", "caseId='" + caseId + "'");
        if (caseList != null && (! caseList.isEmpty())) {
            Case caseWithRup = (Case) caseList.get(0);
            caseWithRup.setActive(active);
            _db.exec(caseWithRup, HibernateEngine.DB_UPDATE, true);
        }
	}


    private long getTime(Document rup, String xpath) {
        Element elem = JDOMUtil.selectElement(rup, xpath);
        return elem != null ? getTime(elem.getText()) : -1;
    }


    private long getTime(String dateText) {
        if (dateText != null) {
            try {
                Date date = Utils.string2Date(dateText, Utils.DATETIME_PATTERN_XML);
                return date.getTime();
            }
            catch (ParseException pe) {
                // fall through to default return
            }
        }
        return -1;
    }

}