/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache 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.apache.org/licenses/LICENSE-2.0
 *
 * 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.apache.ode.dao.jpa;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.xml.namespace.QName;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ode.bpel.common.ProcessState;
import org.apache.ode.bpel.dao.ActivityRecoveryDAO;
import org.apache.ode.bpel.dao.BpelDAOConnection;
import org.apache.ode.bpel.dao.CorrelationSetDAO;
import org.apache.ode.bpel.dao.CorrelatorDAO;
import org.apache.ode.bpel.dao.FaultDAO;
import org.apache.ode.bpel.dao.MessageExchangeDAO;
import org.apache.ode.bpel.dao.ProcessDAO;
import org.apache.ode.bpel.dao.ProcessInstanceDAO;
import org.apache.ode.bpel.dao.ScopeDAO;
import org.apache.ode.bpel.dao.ScopeStateEnum;
import org.apache.ode.bpel.dao.XmlDataDAO;
import org.apache.ode.bpel.evt.ProcessInstanceEvent;
import org.apache.ode.bpel.iapi.ProcessConf.CLEANUP_CATEGORY;
import org.w3c.dom.Element;

import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;

@Entity
@Table(name="ODE_PROCESS_INSTANCE")
@NamedQueries({
    @NamedQuery(name=ProcessInstanceDAOImpl.DELETE_INSTANCES_BY_PROCESS, query="delete from ProcessInstanceDAOImpl as i where i._process = :process"),
    @NamedQuery(name=ProcessInstanceDAOImpl.SELECT_INSTANCE_IDS_BY_PROCESS, query="select i._instanceId from ProcessInstanceDAOImpl as i where i._process = :process"),
    @NamedQuery(name=ProcessInstanceDAOImpl.COUNT_INSTANCE_IDS_BY_PROCESS, query="select count(i._instanceId) from ProcessInstanceDAOImpl as i where i._process = :process"),
    @NamedQuery(name=ProcessInstanceDAOImpl.SELECT_FAULT_IDS_BY_PROCESS, query="select i._faultId from ProcessInstanceDAOImpl as i where i._process = :process and i._faultId is not null"),
    @NamedQuery(name=ProcessInstanceDAOImpl.COUNT_FAILED_INSTANCES_BY_STATUS_AND_PROCESS_ID, 
            query="select count(i._instanceId), max(i._lastRecovery) from ProcessInstanceDAOImpl as i where i._process._processId = :processId and i._state in(:states) and exists(select r from ActivityRecoveryDAOImpl r where i = r._instance)"),
    @NamedQuery(name=ProcessInstanceDAOImpl.COUNT_FAILED_INSTANCES_BY_STATUS_AND_PROCESS_IDS,
            query="select i._process._processId, count(i._instanceId), max(i._lastRecovery) from ProcessInstanceDAOImpl as i where i._process._processId in (:processIds) and i._state = 20 and exists(select r from ActivityRecoveryDAOImpl r where i = r._instance) group by i._process._processId"),
    @NamedQuery(name=ProcessInstanceDAOImpl.COUNT_INSTANCES_BY_PROCESSES_IDS_AND_STATES, 
            query="select i._process._processId, count(i._instanceId) from ProcessInstanceDAOImpl as i where i._process._processId in (:processIds) and i._state in(:states) group by i._process._processId")
})
public class ProcessInstanceDAOImpl extends OpenJPADAO implements ProcessInstanceDAO {
    private static final Log __log = LogFactory.getLog(ProcessInstanceDAOImpl.class);

    public final static String DELETE_INSTANCES_BY_PROCESS = "DELETE_INSTANCES_BY_PROCESS";
    public final static String SELECT_INSTANCE_IDS_BY_PROCESS = "SELECT_INSTANCE_IDS_BY_PROCESS";
    public final static String COUNT_INSTANCE_IDS_BY_PROCESS = "COUNT_INSTANCE_IDS_BY_PROCESS";

    public final static String SELECT_FAULT_IDS_BY_PROCESS = "SELECT_FAULT_IDS_BY_PROCESS";
    public final static String COUNT_FAILED_INSTANCES_BY_STATUS_AND_PROCESS_ID = "COUNT_FAILED_INSTANCES_BY_STATUS_AND_PROCESS_ID";
    public final static String COUNT_FAILED_INSTANCES_BY_STATUS_AND_PROCESS_IDS = "COUNT_FAILED_INSTANCES_BY_STATUS_AND_PROCESS_IDS";    
    public static final String COUNT_INSTANCES_BY_PROCESSES_IDS_AND_STATES = "COUNT_INSTANCES_BY_PROCESSES_IDS_AND_STATES";

    // Hazelcast based cache for oracle rac issue
    private static final String WSO2_HZ_INSTANCE_NAME = "WSO2_HZ_INSTANCE_NAME";
    private static final String USE_STATE_CACHE="WSO2_USE_STATE_CACHE";
    private static final String HAZELCAST_CACHE_EXECUTION_STATE = "ExecutionState";


    @Transient
    private static boolean useStateCache = false;
    @Transient
    private static String hzInstanceName = null;
    
    @Id @Column(name="ID")
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long _instanceId;
    @Basic @Column(name="LAST_RECOVERY_DATE")
    private Date _lastRecovery;
    @Basic @Column(name="LAST_ACTIVE_TIME")
    private Date _lastActive;
    @Basic @Column(name="INSTANCE_STATE")
    private short _state;
    @Basic @Column(name="PREVIOUS_STATE")
    private short _previousState;
    @Lob @Column(name="EXECUTION_STATE")
    private byte[] _executionState;
    @Basic @Column(name="SEQUENCE")
    private long _sequence;
    @Basic @Column(name="DATE_CREATED")
    private Date _dateCreated = new Date();

    @OneToOne(fetch=FetchType.LAZY,cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}) @Column(name="ROOT_SCOPE_ID")
    private ScopeDAOImpl _rootScope;
    @OneToMany(targetEntity=ScopeDAOImpl.class,mappedBy="_processInstance",fetch=FetchType.LAZY,cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.DETACH})
    private Collection<ScopeDAO> _scopes = new ArrayList<ScopeDAO>();
    @OneToMany(targetEntity=ActivityRecoveryDAOImpl.class,mappedBy="_instance",fetch=FetchType.LAZY,cascade={CascadeType.ALL})
    private Collection<ActivityRecoveryDAO> _recoveries = new ArrayList<ActivityRecoveryDAO>();

    @SuppressWarnings("unused")
    @Basic @Column(name="FAULT_ID", insertable=false, updatable=false, nullable=true)
    private long _faultId;
    @OneToOne(fetch=FetchType.LAZY,cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}) @Column(name="FAULT_ID")
    private FaultDAOImpl _fault;
    @ManyToOne(fetch=FetchType.EAGER,cascade={CascadeType.PERSIST}) @Column(name="PROCESS_ID")
    private ProcessDAOImpl _process;
    @ManyToOne(fetch=FetchType.LAZY,cascade={CascadeType.PERSIST}) @Column(name="INSTANTIATING_CORRELATOR_ID")
    private CorrelatorDAOImpl _instantiatingCorrelator;

    @OneToMany(targetEntity=MessageExchangeDAOImpl.class,mappedBy="_processInst",fetch=FetchType.LAZY)
    @SuppressWarnings("unused")
    private Collection<MessageExchangeDAO> _messageExchanges = new ArrayList<MessageExchangeDAO>();
    
    @OneToMany(targetEntity=EventDAOImpl.class,mappedBy="_instance",fetch=FetchType.LAZY)
    @SuppressWarnings("unused")
    private List<EventDAOImpl> events = new ArrayList<EventDAOImpl>();

    private transient int _activityFailureCount = -1;

    static {
        String  cache = System.getProperty(USE_STATE_CACHE);
        if(cache != null && cache.toLowerCase().equals("true")) {
            useStateCache = true;
            hzInstanceName = System.getProperty(WSO2_HZ_INSTANCE_NAME);
        }
    }

    public ProcessInstanceDAOImpl() { }

    public ProcessInstanceDAOImpl(CorrelatorDAOImpl correlator, ProcessDAOImpl process) {
        _instantiatingCorrelator = correlator;
        _process = process;
    }

    public void createActivityRecovery(String channel, long activityId,
            String reason, Date dateTime, Element data, String[] actions,
            int retries) {
        ActivityRecoveryDAOImpl ar = new ActivityRecoveryDAOImpl(channel, activityId, reason, dateTime, data, actions, retries);
        _recoveries.add(ar);
        ar.setInstance(this);
        _lastRecovery = dateTime;
    }

    public ScopeDAO createScope(ScopeDAO parentScope, String name, int scopeModelId) {
        ScopeDAOImpl ret = new ScopeDAOImpl((ScopeDAOImpl)parentScope,name,scopeModelId,this);
        ret.setState(ScopeStateEnum.ACTIVE);
        //_scopes.add(ret); // Commented for BPS-682
        _rootScope = (parentScope == null)?ret:_rootScope;

        // Must persist the scope to generate a scope ID
        getEM().persist(ret);
        return ret;
    }

    @SuppressWarnings("unchecked")
    public Collection<CorrelationSetDAO> selectCorrelationSets(Collection<ProcessInstanceDAO> instances) {
        return getEM().createNamedQuery(CorrelationSetDAOImpl.SELECT_CORRELATION_SETS_BY_INSTANCES).setParameter("instances", instances).getResultList();
    }

    public void delete(Set<CLEANUP_CATEGORY> cleanupCategories) {
        delete(cleanupCategories, true);
    }

    public void delete(Set<CLEANUP_CATEGORY> cleanupCategories, boolean deleteMessageExchanges) {
        if(__log.isDebugEnabled()) __log.debug("Cleaning up instance Data with " + cleanupCategories);

        // remove jacob state
        setExecutionState(null);
        if (getEM() != null) {
            if( !cleanupCategories.isEmpty() ) {
                // by default, we do not flush before select; flush it, so we can delete no matter if an entity is loaded up
                // or not; more importantly, OpenJPA will secretly load from the entire table if some entities reside only
                // in memory
                getEM().flush();
            }

            if (cleanupCategories.contains(CLEANUP_CATEGORY.EVENTS)) {
                deleteEvents();
            }
            if (cleanupCategories.contains(CLEANUP_CATEGORY.CORRELATIONS)) {
                deleteCorrelations();
            }
            if( cleanupCategories.contains(CLEANUP_CATEGORY.MESSAGES) ) {
                deleteMessageRoutes();
            }
            if (cleanupCategories.contains(CLEANUP_CATEGORY.VARIABLES)) {
                deleteVariables();
            }
            if (cleanupCategories.contains(CLEANUP_CATEGORY.INSTANCE)) {
                deleteInstance();
            }

            if(deleteMessageExchanges){
                deleteMessageExchanges();
            }

            getEM().flush();
        }
    }

    private void deleteInstance() {
        if( _fault != null ) {
            getEM().remove(_fault);
        }
        getEM().remove(this); // This deletes ActivityRecoveryDAO

        if(useStateCache && hzInstanceName != null) {
        HazelcastInstance hazelcast_cache = Hazelcast.getHazelcastInstanceByName(hzInstanceName);
        if(hazelcast_cache != null)
            hazelcast_cache.getMap(HAZELCAST_CACHE_EXECUTION_STATE).remove(_instanceId);
            if(__log.isDebugEnabled()) {
                __log.debug(" Removing the side cache instance state iid" + _instanceId);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void deleteVariables() {
        Collection xmlDataIds = getEM().createNamedQuery(XmlDataDAOImpl.SELECT_XMLDATA_IDS_BY_INSTANCE).setParameter("instance", this).getResultList();
        batchUpdateByIds(xmlDataIds.iterator(), getEM().createNamedQuery(XmlDataProperty.DELETE_XML_DATA_PROPERTIES_BY_XML_DATA_IDS), "xmlDataIds");
        Collection scopeIds = getEM().createNamedQuery(ScopeDAOImpl.SELECT_SCOPE_IDS_BY_INSTANCE).setParameter("instance", this).getResultList();
        batchUpdateByIds(scopeIds.iterator(), getEM().createNamedQuery(XmlDataDAOImpl.DELETE_XMLDATA_BY_SCOPE_IDS), "scopeIds");

        batchUpdateByIds(scopeIds.iterator(), getEM().createNamedQuery(PartnerLinkDAOImpl.DELETE_PARTNER_LINKS_BY_SCOPE_IDS), "scopeIds");
        // TODO is this the right thing to do
        this._rootScope = null;
        batchUpdateByIds(scopeIds.iterator(), getEM().createNamedQuery(ScopeDAOImpl.DELETE_SCOPES_BY_SCOPE_IDS), "ids");
    }

    private void deleteMessageRoutes() {
        getEM().createNamedQuery(MessageRouteDAOImpl.DELETE_MESSAGE_ROUTES_BY_INSTANCE).setParameter ("instance", this).executeUpdate();
    }

    private void deleteMessageExchanges() {
        for(MessageExchangeDAO messageExchangeDAO : _messageExchanges){
            messageExchangeDAO.setPartnerLink(null);
        }
        getEM().createNamedQuery(MessageExchangeDAOImpl.DELETE_MEXS_BY_INSTANCE).setParameter("instance", this).executeUpdate();
    }

    @SuppressWarnings("unchecked")
    private void deleteCorrelations() {
        Collection corrSetIds = getEM().createNamedQuery(CorrelationSetDAOImpl.SELECT_CORRELATION_SET_IDS_BY_INSTANCE).setParameter("instance", this).getResultList();
        batchUpdateByIds(corrSetIds.iterator(), getEM().createNamedQuery(CorrSetProperty.DELETE_CORSET_PROPERTIES_BY_PROPERTY_IDS), "corrSetIds");
        batchUpdateByIds(corrSetIds.iterator(), getEM().createNamedQuery(CorrelationSetDAOImpl.DELETE_CORRELATION_SETS_BY_IDS), "ids");
    }

    private void deleteEvents() {
        getEM().createNamedQuery(EventDAOImpl.DELETE_EVENTS_BY_INSTANCE).setParameter ("instance", this).executeUpdate();
    }

    public void deleteActivityRecovery(String channel) {
        ActivityRecoveryDAOImpl toRemove = null;
        for (ActivityRecoveryDAO _recovery : _recoveries) {
            ActivityRecoveryDAOImpl arElement = (ActivityRecoveryDAOImpl) _recovery;
            if (arElement.getChannel().equals(channel)) {
                toRemove = arElement;
                break;
            }
        }
        if (toRemove != null) {
            getEM().remove(toRemove);
            _recoveries.remove(toRemove);
        }

    }

    public void finishCompletion() {
        // make sure we have completed.
        assert (ProcessState.isFinished(this.getState()));
        // let our process know that we've done our work.
    }

    public long genMonotonic() {
        return _sequence++;
    }

    public int getActivityFailureCount() {
        if( _activityFailureCount == -1 ) {
            _activityFailureCount = _recoveries.size();
        }

        return _activityFailureCount;
    }

    public void setActivityFailureCount(int activityFailureCount) {
        _activityFailureCount = activityFailureCount;
    }

    public Date getActivityFailureDateTime() {
        return _lastRecovery;
    }

    public Collection<ActivityRecoveryDAO> getActivityRecoveries() {
        return _recoveries;
    }
    
    public void setActivityRecoveries(Collection<ActivityRecoveryDAO> recoveries){
    	_recoveries = recoveries;
    }

    public CorrelationSetDAO getCorrelationSet(String name) {
        //    TODO: should this method be deprecated?

        //  Its not clear where the correlation set for the process is used
        //  or populated.

        throw new UnsupportedOperationException();

        //return null;
    }

    public Set<CorrelationSetDAO> getCorrelationSets() {
        //    TODO: should this method be deprecated?
        //  Its not clear where the correlation set for the process is used
        //  or populated.
        return new HashSet<CorrelationSetDAO>();
    }

    public Date getCreateTime() {
        return _dateCreated;
    }

    public EventsFirstLastCountTuple getEventsFirstLastCount() {
        // TODO Auto-generated method stub
        return null;
    }

    public byte[] getExecutionState() {

        if(useStateCache && hzInstanceName != null && _executionState == null){
            HazelcastInstance hazelcast_cache = Hazelcast.getHazelcastInstanceByName(hzInstanceName);
            if(hazelcast_cache != null)
                _executionState = (byte[])(hazelcast_cache.getMap(HAZELCAST_CACHE_EXECUTION_STATE).get(_instanceId));
            if(__log.isDebugEnabled()) {
                __log.debug(" Getting the execution state from hazel cast cache iid= "+ _instanceId);
            }
        }

        return _executionState;
    }

    public FaultDAO getFault() {
        return _fault;
    }

    public Long getInstanceId() {
        return _instanceId;
    }

    public CorrelatorDAO getInstantiatingCorrelator() {
        return _instantiatingCorrelator;
    }

    public Date getLastActiveTime() {
        return _lastActive;
    }

    public short getPreviousState() {
        return _previousState;
    }

    public ProcessDAO getProcess() {
        return _process;
    }

    public ScopeDAO getRootScope() {
        return _rootScope;
    }

    public ScopeDAO getScope(Long scopeInstanceId) {
        return getEM().find(ScopeDAOImpl.class, scopeInstanceId);
    }

    public Collection<ScopeDAO> getScopes(String scopeName) {
        Collection<ScopeDAO> ret = new ArrayList<ScopeDAO>();

        for (ScopeDAO sElement : _scopes) {
            if ( sElement.getName().equals(scopeName)) ret.add(sElement);
        }
        return ret;
    }

    public Collection<ScopeDAO> getScopes() {
        return _scopes;
    }

    public short getState() {
        return _state;
    }

    public XmlDataDAO[] getVariables(String variableName, int scopeModelId) {

        //TODO: This method is not used and should be considered a deprecation candidate.

        List<XmlDataDAO> results = new ArrayList<XmlDataDAO>();

        for (ScopeDAO sElement : _scopes) {
            if ( sElement.getModelId() == scopeModelId) {
                XmlDataDAO var = sElement.getVariable(variableName);
                if ( var != null ) results.add(var);
            }
        }
        return results.toArray(new XmlDataDAO[results.size()]);
    }

    public void insertBpelEvent(ProcessInstanceEvent event) {
        getConn().insertBpelEvent(event, getProcess(), this);
    }

    public void setExecutionState(byte[] execState) {
        _executionState = execState;

        if(useStateCache && hzInstanceName != null) {
          HazelcastInstance hazelcast_cache = Hazelcast.getHazelcastInstanceByName(hzInstanceName);
           if(hazelcast_cache != null) {
               if(execState != null) {
                   hazelcast_cache.getMap(HAZELCAST_CACHE_EXECUTION_STATE).put(_instanceId, _executionState);
                   if(__log.isDebugEnabled()) {
                       __log.debug(" Adding execution state to hzmap iid" + _instanceId);
                   }
               }else  {
                   hazelcast_cache.getMap(HAZELCAST_CACHE_EXECUTION_STATE).remove(_instanceId);
                   if(__log.isDebugEnabled()) {
                       __log.debug(" Removing execution state from hzmap iid" + _instanceId);
                   }
               }
           }
        }
    }

    public void setFault(FaultDAO fault) {
        _fault = (FaultDAOImpl)fault;
    }

    public void setFault(QName faultName, String explanation, int faultLineNo,
            int activityId, Element faultMessage) {
        _fault = new FaultDAOImpl(faultName,explanation,faultLineNo,activityId,faultMessage);
    }

    public void setLastActiveTime(Date dt) {
        _lastActive = dt;
    }

    public void setState(short state) {
        _previousState = _state;
        _state = state;
        if(state==ProcessState.STATE_TERMINATED) {
          deleteMessageRoutes();
        }
    }

    void removeRoutes(String routeGroupId) {
        _process.removeRoutes(routeGroupId, this);
    }

    public BpelDAOConnection getConnection() {
        return new BPELDAOConnectionImpl(getEM());
    }

    public Collection<String> getMessageExchangeIds() {
        Collection<String> c = new HashSet<String>();
        for (MessageExchangeDAO m : _messageExchanges) {
            c.add(m.getMessageExchangeId());
        }
        return c;
    }

    public void addMessageExchange(MessageExchangeDAO dao) {
        if (getEM().find(MessageExchangeDAOImpl.class, dao.getMessageExchangeId()) == null) {
            _messageExchanges.add(dao);
            if (__log.isDebugEnabled()) {
                __log.debug("MessageExchangeDAO with id:" + dao.getMessageExchangeId() + " is in-cooperated with " +
                            this.getInstanceId());
            }
        } else {
            if (__log.isDebugEnabled()) {
                __log.debug("MessageExchangeDAO with id:" + dao.getMessageExchangeId() + " is already in-cooperated " +
                            "with " + this.getInstanceId());
            }
        }
    }

	public List<EventDAOImpl> getEvents() {
		return events;
	}

	public void setEvents(List<EventDAOImpl> events) {
		this.events = events;
	}

	
	
    
    
}
