/**
 * Copyright 2005-2018 The Kuali Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.opensource.org/licenses/ecl2.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.kuali.rice.kew.engine.node.dao.impl;

import org.kuali.rice.core.api.criteria.QueryByCriteria;
import org.kuali.rice.kew.api.KEWPropertyConstants;
import org.kuali.rice.kew.doctype.bo.DocumentType;
import org.kuali.rice.kew.engine.RouteHelper;
import org.kuali.rice.kew.engine.node.NodeState;
import org.kuali.rice.kew.engine.node.RouteNode;
import org.kuali.rice.kew.engine.node.RouteNodeInstance;
import org.kuali.rice.kew.engine.node.dao.RouteNodeDAO;
import org.kuali.rice.kew.service.KEWServiceLocator;
import org.kuali.rice.krad.data.DataObjectService;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCallback;
import org.springframework.jdbc.core.PreparedStatementCreator;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import static org.kuali.rice.core.api.criteria.PredicateFactory.equal;

public class RouteNodeDAOJpa implements RouteNodeDAO {

    private EntityManager entityManager;
    private DataObjectService dataObjectService;

    public static final String FIND_INITIAL_NODE_INSTANCES_NAME = "RouteNodeInstance.FindInitialNodeInstances";
    public static final String FIND_INITIAL_NODE_INSTANCES_QUERY = "select d.initialRouteNodeInstances from "
            + "DocumentRouteHeaderValue d where d.documentId = :documentId";

    /**
     * @return the entityManager
     */
    public EntityManager getEntityManager() {
        return this.entityManager;
    }

    /**
     * @param entityManager the entityManager to set
     */
    public void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public RouteNodeInstance findRouteNodeInstanceById(String nodeInstanceId) {
        QueryByCriteria.Builder queryByCriteria = QueryByCriteria.Builder.create().setPredicates(
                equal(KEWPropertyConstants.ROUTE_NODE_INSTANCE_ID, nodeInstanceId)
        );

        List<RouteNodeInstance> routeNodeInstances = getDataObjectService().findMatching(
                RouteNodeInstance.class, queryByCriteria.build()).getResults();
        if (routeNodeInstances != null && routeNodeInstances.size() > 0) {
            return routeNodeInstances.get(0);
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    public List<RouteNodeInstance> getActiveNodeInstances(String documentId) {
        QueryByCriteria.Builder queryByCriteria = QueryByCriteria.Builder.create().setPredicates(
                equal(KEWPropertyConstants.DOCUMENT_ID, documentId),
                equal(KEWPropertyConstants.ACTIVE, true)
        );
        return getDataObjectService().findMatching(RouteNodeInstance.class,
                queryByCriteria.build()).getResults();
    }

    private static final String CURRENT_ROUTE_NODE_NAMES_SQL = "SELECT rn.nm, rn.typ" +
            " FROM krew_rte_node_t rn," +
            "      krew_rte_node_instn_t rni" +
            " LEFT JOIN krew_rte_node_instn_lnk_t rnl" +
            "   ON rnl.from_rte_node_instn_id = rni.rte_node_instn_id" +
            " WHERE rn.rte_node_id = rni.rte_node_id AND" +
            "       rni.doc_hdr_id = ? AND" +
            "       rnl.from_rte_node_instn_id IS NULL";

    @Override
    public List<String> getCurrentRouteNodeNames(final String documentId) {
        final DataSource dataSource = KEWServiceLocator.getDataSource();
        JdbcTemplate template = new JdbcTemplate(dataSource);
        List<String> names = template.execute(new PreparedStatementCreator() {
                                                  public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
                                                      return connection.prepareStatement(CURRENT_ROUTE_NODE_NAMES_SQL);
                                                  }
                                              }, new PreparedStatementCallback<List<String>>() {
                                                  public List<String> doInPreparedStatement(
                                                          PreparedStatement statement) throws SQLException, DataAccessException {
                                                      List<String> routeNodeNames = new ArrayList<String>();
                                                      statement.setString(1, documentId);
                                                      ResultSet rs = statement.executeQuery();
                                                      try {
                                                          while (rs.next()) {
                                                              String name = rs.getString("nm");
                                                              routeNodeNames.add(name);
                                                          }
                                                      } finally {
                                                          if (rs != null) {
                                                              rs.close();
                                                          }
                                                      }
                                                      return routeNodeNames;
                                                  }
                                              }
        );
        return names;
    }

    @Override
    public List<String> getCurrentSimpleRouteNodeNames(final String documentId) {
        final DataSource dataSource = KEWServiceLocator.getDataSource();
        JdbcTemplate template = new JdbcTemplate(dataSource);
        List<String> names = template.execute(new PreparedStatementCreator() {
                                                  public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
                                                      return connection.prepareStatement(CURRENT_ROUTE_NODE_NAMES_SQL);
                                                  }
                                              }, new PreparedStatementCallback<List<String>>() {
                                                  public List<String> doInPreparedStatement(
                                                          PreparedStatement statement) throws SQLException, DataAccessException {
                                                      List<String> routeNodeNames = new ArrayList<String>();
                                                      statement.setString(1, documentId);
                                                      ResultSet rs = statement.executeQuery();
                                                      try {
                                                          while (rs.next()) {
                                                              String name = rs.getString("nm");
                                                              String type = rs.getString("typ");
                                                              if (isSimpleNode(type)) {
                                                                  routeNodeNames.add(name);
                                                              }
                                                          }
                                                      } finally {
                                                          if (rs != null) {
                                                              rs.close();
                                                          }
                                                      }
                                                      return routeNodeNames;
                                                  }
                                              }
        );
        return names;
    }


    @Override
    public List<String> getActiveRouteNodeNames(final String documentId) {
        final DataSource dataSource = KEWServiceLocator.getDataSource();
        JdbcTemplate template = new JdbcTemplate(dataSource);
        List<String> names = template.execute(
                new PreparedStatementCreator() {
                    public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
                        PreparedStatement statement = connection.prepareStatement(
                                "SELECT rn.nm FROM krew_rte_node_t rn, krew_rte_node_instn_t rni WHERE rn.rte_node_id "
                                        + "= rni.rte_node_id AND rni.doc_hdr_id = ? AND rni.actv_ind = ?");
                        return statement;
                    }
                },
                new PreparedStatementCallback<List<String>>() {
                    public List<String> doInPreparedStatement(PreparedStatement statement) throws SQLException, DataAccessException {
                        List<String> routeNodeNames = new ArrayList<String>();
                        statement.setString(1, documentId);
                        statement.setBoolean(2, Boolean.TRUE);
                        ResultSet rs = statement.executeQuery();
                        try {
                            while (rs.next()) {
                                String name = rs.getString("nm");
                                routeNodeNames.add(name);
                            }
                        } finally {
                            if (rs != null) {
                                rs.close();
                            }
                        }
                        return routeNodeNames;
                    }
                });
        return names;
    }

    @Override
    public List<String> getActiveSimpleRouteNodeNames(final String documentId) {
        final DataSource dataSource = KEWServiceLocator.getDataSource();
        JdbcTemplate template = new JdbcTemplate(dataSource);
        List<String> names = template.execute(
                new PreparedStatementCreator() {
                    public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
                        PreparedStatement statement = connection.prepareStatement(
                                "SELECT rn.nm, rn.typ FROM krew_rte_node_t rn, krew_rte_node_instn_t rni WHERE rn.rte_node_id "
                                        + "= rni.rte_node_id AND rni.doc_hdr_id = ? AND rni.actv_ind = ?");
                        return statement;
                    }
                },
                new PreparedStatementCallback<List<String>>() {
                    public List<String> doInPreparedStatement(PreparedStatement statement) throws SQLException, DataAccessException {
                        List<String> routeNodeNames = new ArrayList<String>();
                        statement.setString(1, documentId);
                        statement.setBoolean(2, Boolean.TRUE);
                        ResultSet rs = statement.executeQuery();
                        try {
                            while (rs.next()) {
                                String name = rs.getString("nm");
                                String type = rs.getString("typ");
                                if (isSimpleNode(type)) {
                                    routeNodeNames.add(name);
                                }
                            }
                        } finally {
                            if (rs != null) {
                                rs.close();
                            }
                        }
                        return routeNodeNames;
                    }
                });
        return names;
    }

    /**
     * This method is a bit of a hack because in order to check if a node is "simple" or not, we actually
     * need to instantiate it's type and check the java class type. RouteHelper can do this for us but it
     * needs an actual RouteNode object to do this work. To avoid the full cost of loading the full
     * RouteNode object and it's associated DocumentType, we will mock out what we need here to get the job
     * done.
     */
    private boolean isSimpleNode(String type) {
        // construct a "mock" RouteNode so that we can fool the RouteHelper
        RouteNode routeNode = new RouteNode();
        routeNode.setNodeType(type);
        // document type must be non-null since route helper will attempt to get the application id off of it
        // though it's ok if the application id is null
        routeNode.setDocumentType(new DocumentType());
        return new RouteHelper().isSimpleNode(routeNode);
    }

    @SuppressWarnings("unchecked")
    public List<RouteNodeInstance> getTerminalNodeInstances(String documentId) {
        QueryByCriteria.Builder queryByCriteria = QueryByCriteria.Builder.create().setPredicates(
                equal(KEWPropertyConstants.DOCUMENT_ID, documentId),
                equal(KEWPropertyConstants.ACTIVE, false),
                equal(KEWPropertyConstants.COMPLETE, true)
        );

        //FIXME: Can we do this better using just the JPQL query?
        List<RouteNodeInstance> terminalNodes = new ArrayList<RouteNodeInstance>();
        List<RouteNodeInstance> routeNodeInstances = getDataObjectService().
                findMatching(RouteNodeInstance.class, queryByCriteria.build()).getResults();
        for (RouteNodeInstance routeNodeInstance : routeNodeInstances) {
            if (routeNodeInstance.getNextNodeInstances().isEmpty()) {
                terminalNodes.add(routeNodeInstance);
            }
        }
        return terminalNodes;
    }

    @Override
    public List<String> getTerminalRouteNodeNames(final String documentId) {
        final DataSource dataSource = KEWServiceLocator.getDataSource();
        JdbcTemplate template = new JdbcTemplate(dataSource);
        List<String> names = template.execute(new PreparedStatementCreator() {
                                                  public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
                                                      PreparedStatement statement = connection.prepareStatement("SELECT rn.nm" +
                                                              "  FROM krew_rte_node_t rn," +
                                                              "       krew_rte_node_instn_t rni" +
                                                              "  LEFT JOIN krew_rte_node_instn_lnk_t rnl" +
                                                              "    ON rnl.from_rte_node_instn_id = rni.rte_node_instn_id" +
                                                              "  WHERE rn.rte_node_id = rni.rte_node_id AND" +
                                                              "        rni.doc_hdr_id = ? AND" +
                                                              "        rni.actv_ind = ? AND" +
                                                              "        rni.cmplt_ind = ? AND" +
                                                              "        rnl.from_rte_node_instn_id IS NULL");
                                                      return statement;
                                                  }
                                              }, new PreparedStatementCallback<List<String>>() {
                                                  public List<String> doInPreparedStatement(
                                                          PreparedStatement statement) throws SQLException, DataAccessException {
                                                      List<String> routeNodeNames = new ArrayList<String>();
                                                      statement.setString(1, documentId);
                                                      statement.setBoolean(2, Boolean.FALSE);
                                                      statement.setBoolean(3, Boolean.TRUE);
                                                      ResultSet rs = statement.executeQuery();
                                                      try {
                                                          while (rs.next()) {
                                                              String name = rs.getString("nm");
                                                              routeNodeNames.add(name);
                                                          }
                                                      } finally {
                                                          if (rs != null) {
                                                              rs.close();
                                                          }
                                                      }
                                                      return routeNodeNames;
                                                  }
                                              }
        );
        return names;
    }

    public List getInitialNodeInstances(String documentId) {
        //FIXME: Not sure this query is returning what it needs to
        Query query = entityManager.createNamedQuery(FIND_INITIAL_NODE_INSTANCES_NAME);
        query.setParameter(KEWPropertyConstants.DOCUMENT_ID, documentId);
        return (List) query.getResultList();
    }

    public NodeState findNodeState(Long nodeInstanceId, String key) {
        QueryByCriteria.Builder queryByCriteria = QueryByCriteria.Builder.create().setPredicates(
                equal(KEWPropertyConstants.ROUTE_NODE_INSTANCE_ID, nodeInstanceId.toString()),
                equal(KEWPropertyConstants.KEY, key)
        );

        List<NodeState> nodeStates = getDataObjectService().findMatching(
                NodeState.class, queryByCriteria.build()).getResults();
        if (nodeStates != null && nodeStates.size() > 0) {
            return nodeStates.get(0);
        }
        return null;
    }

    public RouteNode findRouteNodeByName(String documentTypeId, String name) {
        QueryByCriteria.Builder queryByCriteria = QueryByCriteria.Builder.create().setPredicates(
                equal(KEWPropertyConstants.DOCUMENT_TYPE_ID, documentTypeId),
                equal(KEWPropertyConstants.ROUTE_NODE_NAME, name)
        );
        List<RouteNode> routeNodes = getDataObjectService().findMatching(
                RouteNode.class, queryByCriteria.build()).getResults();
        if (routeNodes != null && routeNodes.size() > 0) {
            return routeNodes.get(0);
        }
        return null;
    }

    public List<RouteNode> findFinalApprovalRouteNodes(String documentTypeId) {
        QueryByCriteria.Builder queryByCriteria = QueryByCriteria.Builder.create().setPredicates(
                equal(KEWPropertyConstants.DOCUMENT_TYPE_ID, documentTypeId),
                equal(KEWPropertyConstants.FINAL_APPROVAL, Boolean.TRUE)
        );
        return getDataObjectService().findMatching(RouteNode.class, queryByCriteria.build()).getResults();
    }

    public List findProcessNodeInstances(RouteNodeInstance process) {
        QueryByCriteria.Builder queryByCriteria = QueryByCriteria.Builder.create().setPredicates(
                equal(KEWPropertyConstants.PROCESS_ID, process.getRouteNodeInstanceId())
        );
        return getDataObjectService().findMatching(RouteNodeInstance.class, queryByCriteria.build()).getResults();
    }

    public List findRouteNodeInstances(String documentId) {
        QueryByCriteria.Builder queryByCriteria = QueryByCriteria.Builder.create().setPredicates(
                equal(KEWPropertyConstants.DOCUMENT_ID, documentId)
        );
        return getDataObjectService().findMatching(RouteNodeInstance.class, queryByCriteria.build()).getResults();
    }

    public void deleteLinksToPreNodeInstances(RouteNodeInstance routeNodeInstance) {
        List<RouteNodeInstance> preNodeInstances = routeNodeInstance.getPreviousNodeInstances();
        for (Iterator<RouteNodeInstance> preNodeInstanceIter = preNodeInstances.iterator(); preNodeInstanceIter.hasNext(); ) {
            RouteNodeInstance preNodeInstance = (RouteNodeInstance) preNodeInstanceIter.next();
            List<RouteNodeInstance> nextInstances = preNodeInstance.getNextNodeInstances();
            nextInstances.remove(routeNodeInstance);
            getEntityManager().merge(preNodeInstance);
        }
    }

    public void deleteRouteNodeInstancesHereAfter(RouteNodeInstance routeNodeInstance) {
        RouteNodeInstance rnInstance = findRouteNodeInstanceById(routeNodeInstance.getRouteNodeInstanceId());
        entityManager.remove(rnInstance);
    }

    public void deleteNodeStateById(Long nodeStateId) {
        QueryByCriteria.Builder queryByCriteria = QueryByCriteria.Builder.create().setPredicates(
                equal(KEWPropertyConstants.ROUTE_NODE_STATE_ID, nodeStateId)
        );
        List<NodeState> nodeStates = getDataObjectService().findMatching(
                NodeState.class, queryByCriteria.build()).getResults();
        NodeState nodeState = null;
        if (nodeStates != null && nodeStates.size() > 0) {
            nodeState = nodeStates.get(0);
        }
        getDataObjectService().delete(nodeState);
    }

    public void deleteNodeStates(List statesToBeDeleted) {
        for (Iterator stateToBeDeletedIter = statesToBeDeleted.iterator(); stateToBeDeletedIter.hasNext(); ) {
            Long stateId = (Long) stateToBeDeletedIter.next();
            deleteNodeStateById(stateId);
        }
    }


    public DataObjectService getDataObjectService() {
        return dataObjectService;
    }

    @Required
    public void setDataObjectService(DataObjectService dataObjectService) {
        this.dataObjectService = dataObjectService;
    }


}
