package org.jbpm.persistence.scripts;

import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;

import bitronix.tm.resource.jdbc.PoolingDataSource;
import org.drools.core.impl.KnowledgeBaseImpl;
import org.drools.persistence.jta.JtaTransactionManager;
import org.jbpm.persistence.map.impl.ProcessCreatorForHelp;
import org.jbpm.persistence.scripts.oldentities.ProcessInstanceInfo;
import org.jbpm.persistence.scripts.oldentities.SessionInfo;
import org.jbpm.persistence.scripts.oldentities.TaskImpl;
import org.jbpm.persistence.scripts.util.SQLCommandUtil;
import org.jbpm.persistence.scripts.util.SQLScriptUtil;
import org.jbpm.persistence.scripts.util.TestsUtil;
import org.jbpm.persistence.util.PersistenceUtil;
import org.kie.api.KieBase;
import org.kie.api.runtime.Environment;
import org.kie.api.runtime.EnvironmentName;
import org.kie.api.task.model.I18NText;
import org.kie.api.task.model.OrganizationalEntity;
import org.kie.internal.KnowledgeBase;
import org.kie.internal.KnowledgeBaseFactory;
import org.kie.internal.persistence.jpa.JPAKnowledgeService;
import org.kie.internal.runtime.StatefulKnowledgeSession;
import org.kie.internal.task.api.TaskModelProvider;
import org.kie.internal.task.api.model.InternalI18NText;
import org.kie.internal.task.api.model.InternalOrganizationalEntity;
import org.kie.internal.task.api.model.InternalPeopleAssignments;
import org.kie.internal.task.api.model.InternalTaskData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Central context that hides persistence from tests, so there is no need to work with persistence in the tests
 * (transactions etc).
 */
public final class TestPersistenceContext {

    private static final Logger logger = LoggerFactory.getLogger(TestPersistenceContext.class);
    private HashMap<String, Object> context;
    private EntityManagerFactory entityManagerFactory;
    private JtaTransactionManager transactionManager;
    private Environment environment;

    private final Properties dataSourceProperties;
    private final DatabaseType databaseType;

    public TestPersistenceContext() {
        this.dataSourceProperties = PersistenceUtil.getDatasourceProperties();
        this.databaseType = TestsUtil.getDatabaseType(dataSourceProperties);
    }

    /**
     * Initializes persistence context from specified persistence unit.
     * @param persistenceUnit Persistence unit which is used to initialize this persistence context.
     */
    public void init(final PersistenceUnit persistenceUnit) {
        try {
            context = PersistenceUtil.setupWithPoolingDataSource(persistenceUnit.getName(), persistenceUnit
                    .getDataSourceName());
            entityManagerFactory = (EntityManagerFactory) context.get(EnvironmentName.ENTITY_MANAGER_FACTORY);
            environment = PersistenceUtil.createEnvironment(context);
            Object tm = this.environment.get(EnvironmentName.TRANSACTION_MANAGER);
            transactionManager = new JtaTransactionManager(environment.get(EnvironmentName.TRANSACTION),
                    environment.get(EnvironmentName.TRANSACTION_SYNCHRONIZATION_REGISTRY),
                    tm);
        } catch (RuntimeException ex) {
            // log the whole exception stacktrace as for some reason junit is not able to do so and only prints
            // the highest level exception, which makes debugging very hard
            logger.error("Failed to initialize persistence unit {}", persistenceUnit, ex);
            if (entityManagerFactory != null) {
                entityManagerFactory.close();
            }
            throw ex;
        }
    }

    /**
     * Cleans up this persistence context. Closes all instances that need to be closed.
     */
    public void clean() {
        PersistenceUtil.cleanUp(context);
    }

    /**
     * Executes SQL scripts from specified root SQL scripts folder. Selects appropriate scripts from root folder
     * by using dialect that is defined in datasource.properties file.
     * @param scriptsRootFolder Root folder containing folders with SQL scripts for all supported database systems.
     * @throws IOException
     */
    public void executeScripts(final File scriptsRootFolder) throws IOException, SQLException {
        executeScripts(scriptsRootFolder, null);
    }
    
    public void executeScripts(final File scriptsRootFolder, String type) throws IOException, SQLException {
        testIsInitialized();
        final File[] sqlScripts = TestsUtil.getDDLScriptFilesByDatabaseType(scriptsRootFolder, databaseType, true);
        final Connection connection = ((PoolingDataSource) context.get(PersistenceUtil.DATASOURCE)).getConnection();
        connection.setAutoCommit(false);
        try {
            for (File script : sqlScripts) {
                if (type == null || script.getName().startsWith(type)) {
                    logger.debug("Executing script {}", script.getName());
                    final List<String> scriptCommands = SQLScriptUtil.getCommandsFromScript(script, databaseType);
                    for (String command : scriptCommands) {
                        logger.debug(command);
                        final PreparedStatement statement;
                        if (databaseType == DatabaseType.SQLSERVER || databaseType == DatabaseType.SQLSERVER2008) {
                            statement = connection.prepareStatement(
                                    SQLCommandUtil.preprocessCommandSqlServer(command, dataSourceProperties));
                        } else {
                            statement = connection.prepareStatement(command);
                        }
                        statement.execute();
                        statement.close();
                    }
                }
            }
            connection.commit();
        } catch (SQLException ex) {
            connection.rollback();
            throw new RuntimeException(ex.getMessage(), ex);
        } finally {
            connection.close();
        }
    }

    /**
     * Starts and persists a basic simple process using current database entities.
     * @param processId Process identifier. This identifier is also used to generate KieBase
     * (process with this identifier is part of generated KieBase).
     */
    public void startAndPersistSomeProcess(final String processId) {
        testIsInitialized();
        final StatefulKnowledgeSession session;
        final KieBase kbase = createKieBase(processId);

        session = JPAKnowledgeService.newStatefulKnowledgeSession(kbase, null, environment);
        session.startProcess(processId);
    }
    
    public void createSomeTask() {
        testIsInitialized();
        TaskImpl task = new TaskImpl();
        InternalI18NText name = (InternalI18NText) TaskModelProvider.getFactory().newI18NText();
        name.setText("Some Task");
        List<I18NText> names = new ArrayList<I18NText>();
        names.add(name);
        task.setNames(names);
        InternalTaskData taskData = (InternalTaskData) TaskModelProvider.getFactory().newTaskData();        
        taskData.setWorkItemId(12);
        taskData.setProcessInstanceId(1);
        taskData.setProcessId("someprocess");
        taskData.setDeploymentId("org.jbpm.test:someprocess:1.0");
        taskData.setProcessSessionId(1);
        task.setTaskData(taskData);
        InternalPeopleAssignments peopleAssignments = 
            (InternalPeopleAssignments) TaskModelProvider.getFactory().newPeopleAssignments();
        peopleAssignments.setPotentialOwners(new ArrayList<OrganizationalEntity>());
        peopleAssignments.setBusinessAdministrators(new ArrayList<OrganizationalEntity>());
        peopleAssignments.setExcludedOwners(new ArrayList<OrganizationalEntity>());
        peopleAssignments.setRecipients(new ArrayList<OrganizationalEntity>());
        peopleAssignments.setTaskStakeholders(new ArrayList<OrganizationalEntity>());
        InternalOrganizationalEntity jdoe = 
            (InternalOrganizationalEntity) TaskModelProvider.getFactory().newUser();
        jdoe.setId("jdoe");
        peopleAssignments.getPotentialOwners().add(jdoe);
        peopleAssignments.getBusinessAdministrators().add(jdoe);
        task.setPeopleAssignments(peopleAssignments);
        final boolean txOwner = transactionManager.begin();
        try {
            EntityManager em = entityManagerFactory.createEntityManager();
            em.persist(jdoe);
            em.persist(task);
            transactionManager.commit(txOwner);
        } catch (Exception ex) {
            ex.printStackTrace();
            transactionManager.rollback(txOwner);
            throw new RuntimeException(ex.getMessage(), ex);
        }
    }

    /**
     * Loads persisted session from database.
     * @param sessionId Unique identifier of the session.
     * @param processIdForKieBase Process identifier for KieBase generation. A KieBase is generated for
     * loaded session and this KieBase contains process with this identifier.
     * @return Session that is stored in database.
     */
    public StatefulKnowledgeSession loadPersistedSession(final Long sessionId, final String processIdForKieBase) {
        testIsInitialized();
        return JPAKnowledgeService.loadStatefulKnowledgeSession(sessionId, createKieBase(processIdForKieBase),
                null, environment);
    }

    /**
     * Persists a process and a session using entites from jBPM 6.0. Persists
     * process and session using their database identifiers so be aware that you can
     * rewrite some of your data. This method should be used only to populate inital data for tests.
     * @param sessionId Unique identifier of the session.
     * @param processId Identifier of the process (name).
     * @param processInstanceId Unique identifier of the process.
     */
    public void persistOldProcessAndSession(final Integer sessionId, final String processId,
            final Long processInstanceId) {
        testIsInitialized();
        final boolean txOwner = transactionManager.begin();
        final EntityManager entityManager = entityManagerFactory.createEntityManager();
        try {
            entityManager.merge(getOldProcessInstanceInfo(processId, processInstanceId));
            entityManager.merge(getOldSessionInfo(sessionId));
            entityManager.flush();
            entityManager.close();
            transactionManager.commit(txOwner);
        } catch (Exception ex) {
            ex.printStackTrace();
            transactionManager.rollback(txOwner);
            throw new RuntimeException(ex.getMessage(), ex);
        }
    }

    /**
     * Reads stored processes count from database.
     * @return Stored processes count.
     */
    public int getStoredProcessesCount() {
        return getStoredEntitiesCount("ProcessInstanceInfo");
    }

    /**
     * Reads stored sessions count from database.
     * @return Stored sessions count.
     */
    public int getStoredSessionsCount() {
        return getStoredEntitiesCount("SessionInfo");
    }

    /**
     * Reads stored entities count from database.
     * @param entityClassName Class name of entity.
     * @return Stored entities count.
     */
    private int getStoredEntitiesCount(final String entityClassName) {
        testIsInitialized();
        final boolean txOwner = transactionManager.begin();
        final EntityManager entityManager = entityManagerFactory.createEntityManager();
        try {
            final List entitiesList = entityManager.createQuery("SELECT p FROM " + entityClassName + " p")
                    .getResultList();
            if (entitiesList == null) {
                return 0;
            } else {
                return entitiesList.size();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            transactionManager.rollback(txOwner);
            throw new RuntimeException(ex.getMessage(), ex);
        } finally {
            entityManager.close();
            transactionManager.commit(txOwner);
        }
    }

    /**
     * Checks if this persistence context is initialized.
     */
    private void testIsInitialized() {
        if (context == null) {
            throw new IllegalStateException("TestContext is not initialized! Call TestContext.init() before using it.");
        }
    }

    /**
     * Creates very basic KieBase that contains processes with specified processIds.
     * @param processIds ProcessIds of processes that are contained within resulting KieBase.
     * @return Basic KieBase.
     */
    private KieBase createKieBase(final String... processIds) {
        final KnowledgeBase kbase = KnowledgeBaseFactory.newKnowledgeBase();
        for (String processId : processIds) {
            ((KnowledgeBaseImpl) kbase).addProcess(ProcessCreatorForHelp.newSimpleEventProcess(processId, "test"));
        }
        return kbase;
    }

    /**
     * Returns ProcessInstanceInfo entity for jBPM 6.0 version.
     * @param processId Process identifier (name).
     * @param processInstanceId Unique identifier of the process. Database identifier.
     * @return ProcessInstanceInfo entity for jBPM 6.0 version filled with default data.
     * @throws ParseException
     */
    private ProcessInstanceInfo getOldProcessInstanceInfo(final String processId, final Long processInstanceId)
            throws ParseException {
        final DateFormat dateFormat = getDateFormat();
        final ProcessInstanceInfo result = new ProcessInstanceInfo();
        result.setProcessInstanceId(processInstanceId);
        result.setEventTypes(getOldProcessEventTypes());
        result.setLastModificationDate(dateFormat.parse("2015-08-25 13:43:25.760"));
        result.setLastReadDate(dateFormat.parse("2015-08-25 13:43:25.210"));
        result.setProcessId(processId);
        result.setStartDate(dateFormat.parse("2015-08-25 13:43:25.190"));
        result.setState(1);
        result.setVersion(2);
        result.setProcessInstanceByteArray(
                TestsUtil.hexStringToByteArray(
                        "ACED00057769000852756C65466C6F770A0608061004180052550A0852756C65466C6F7710011A0E6D696E696D616C50726F63657373200128023A0A0801100222020805280160006A0E5F6A62706D2D756E697175652D3072120A0E5F6A62706D2D756E697175652D311001800101"));
        return result;
    }

    /**
     * Return default process event types.
     * @return Default process event types.
     */
    private Set<String> getOldProcessEventTypes() {
        final Set<String> resultSet = new HashSet<String>(1);
        resultSet.add("test");
        return resultSet;
    }

    /**
     * Returns SessionInfo entity for jBPM 6.0 version.
     * @param sessionId Unique identifier of the session. Database identifier.
     * @return SessionInfo entity for jBPM 6.0 version filled with default data.
     * @throws ParseException
     */
    private SessionInfo getOldSessionInfo(final Integer sessionId) throws ParseException {
        final DateFormat dateFormat = getDateFormat();
        final SessionInfo result = new SessionInfo();
        result.setId(sessionId);
        result.setLastModificationDate(dateFormat.parse("2015-08-25 13:43:25.248"));
        result.setStartDate(dateFormat.parse("2015-08-25 13:43:24.858"));
        result.setVersion(2);
        result.setData(
                TestsUtil.hexStringToByteArray(
                        "ACED0005777C0A060806100418005272080010001A6818002000320608011000180042231A190A044D41494E10001801200028FFFFFFFFFFFFFFFFFF01400022060A044D41494E52350A0744454641554C54222A0A266F72672E64726F6F6C732E636F72652E726574656F6F2E496E697469616C46616374496D706C100022026800"));
        return result;
    }

    private DateFormat getDateFormat() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    }
}
