/*
 * 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.activemq.artemis.ra;

import javax.jms.ConnectionMetaData;
import javax.resource.ResourceException;
import javax.resource.spi.ConnectionManager;
import javax.resource.spi.ConnectionRequestInfo;
import javax.resource.spi.ManagedConnection;
import javax.resource.spi.ManagedConnectionFactory;
import javax.resource.spi.ResourceAdapter;
import javax.resource.spi.ResourceAdapterAssociation;
import javax.security.auth.Subject;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
import org.apache.activemq.artemis.service.extensions.xa.recovery.XARecoveryConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.invoke.MethodHandles;

public final class ActiveMQRAManagedConnectionFactory implements ManagedConnectionFactory, ResourceAdapterAssociation {

   private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

   static final long serialVersionUID = -1452379518562456741L;

   private ActiveMQResourceAdapter ra;

   private ConnectionManager cm;

   private final ActiveMQRAMCFProperties mcfProperties;

   private ActiveMQConnectionFactory recoveryConnectionFactory;

   private XARecoveryConfig resourceRecovery;

   public ActiveMQRAManagedConnectionFactory() {
      logger.trace("constructor()");

      ra = null;
      cm = null;
      mcfProperties = new ActiveMQRAMCFProperties();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public Object createConnectionFactory() throws ResourceException {
      logger.debug("createConnectionFactory()");

      return createConnectionFactory(new ActiveMQRAConnectionManager());
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public Object createConnectionFactory(final ConnectionManager cxManager) throws ResourceException {
      logger.trace("createConnectionFactory({})", cxManager);

      cm = cxManager;

      ActiveMQRAConnectionFactory cf = new ActiveMQRAConnectionFactoryImpl(this, cm);

      logger.trace("Created connection factory: {}, using connection manager: {}", cf, cm);

      return cf;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public ManagedConnection createManagedConnection(final Subject subject,
                                                    final ConnectionRequestInfo cxRequestInfo) throws ResourceException {
      logger.trace("createManagedConnection({}, {})", subject, cxRequestInfo);

      ActiveMQRAConnectionRequestInfo cri = getCRI((ActiveMQRAConnectionRequestInfo) cxRequestInfo);

      ActiveMQRACredential credential = ActiveMQRACredential.getCredential(this, subject, cri);

      logger.trace("jms credential: {}", credential);

      ActiveMQRAManagedConnection mc = new ActiveMQRAManagedConnection(this, cri, ra, credential.getUserName(), credential.getPassword());

      logger.trace("created new managed connection: {}", mc);

      registerRecovery();

      return mc;
   }

   private synchronized void registerRecovery() {
      if (recoveryConnectionFactory == null) {
         recoveryConnectionFactory = ra.createRecoveryActiveMQConnectionFactory(mcfProperties);

         Map<String, String> recoveryConfProps = new HashMap<>();
         recoveryConfProps.put(XARecoveryConfig.JNDI_NAME_PROPERTY_KEY, ra.getJndiName());
         resourceRecovery = ra.getRecoveryManager().register(recoveryConnectionFactory, null, null, recoveryConfProps);
      }
   }

   public XARecoveryConfig getResourceRecovery() {
      return resourceRecovery;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public ManagedConnection matchManagedConnections(final Set connectionSet,
                                                    final Subject subject,
                                                    final ConnectionRequestInfo cxRequestInfo) throws ResourceException {
      if (logger.isTraceEnabled()) {
         logger.trace("matchManagedConnections({}, {}, {})", connectionSet, subject, cxRequestInfo);
      }

      ActiveMQRAConnectionRequestInfo cri = getCRI((ActiveMQRAConnectionRequestInfo) cxRequestInfo);
      ActiveMQRACredential credential = ActiveMQRACredential.getCredential(this, subject, cri);

      logger.trace("Looking for connection matching credentials: {}", credential);

      for (Object obj : connectionSet) {
         if (obj instanceof ActiveMQRAManagedConnection mc) {
            ManagedConnectionFactory mcf = mc.getManagedConnectionFactory();

            if ((mc.getUserName() == null || mc.getUserName() != null && mc.getUserName().equals(credential.getUserName())) && mcf.equals(this)) {
               if (cri.equals(mc.getCRI())) {
                  logger.trace("Found matching connection: {}", mc);

                  return mc;
               }
            }
         }
      }

      logger.trace("No matching connection was found");

      return null;
   }

   /**
    * <b>NOT SUPPORTED</b>
    * <p>
    * {@inheritDoc}
    */
   @Override
   public void setLogWriter(final PrintWriter out) throws ResourceException {
      logger.trace("setLogWriter({})", out);
   }

   /**
    * <b>NOT SUPPORTED</b>
    * <p>
    * {@inheritDoc}
    */
   @Override
   public PrintWriter getLogWriter() throws ResourceException {
      logger.trace("getLogWriter()");

      return null;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public ResourceAdapter getResourceAdapter() {
      logger.trace("getResourceAdapter()");

      return ra;
   }

   public boolean isIgnoreJTA() {
      return ra.isIgnoreJTA();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void setResourceAdapter(final ResourceAdapter ra) throws ResourceException {
      logger.trace("setResourceAdapter({})", ra);

      if (ra == null || !(ra instanceof ActiveMQResourceAdapter)) {
         throw new ResourceException("Resource adapter is " + ra);
      }

      this.ra = (ActiveMQResourceAdapter) ra;
      this.ra.setManagedConnectionFactory(this);
   }

   @Override
   public boolean equals(final Object obj) {
      logger.trace("equals({})", obj);

      if (obj == null) {
         return false;
      }

      if (obj instanceof ActiveMQRAManagedConnectionFactory other) {

         return mcfProperties.equals(other.getProperties()) && ra.equals(other.getResourceAdapter());
      } else {
         return false;
      }
   }

   @Override
   public int hashCode() {
      logger.trace("hashCode()");

      int hash = mcfProperties.hashCode();
      hash += 31 * (ra != null ? ra.hashCode() : 0);

      return hash;
   }

   public String getSessionDefaultType() {
      logger.trace("getSessionDefaultType()");

      return mcfProperties.getSessionDefaultType();
   }

   /**
    * Set the default session type
    *
    * @param type either {@literal javax.jms.Topic} or {@literal javax.jms.Queue}
    */
   public void setSessionDefaultType(final String type) {
      logger.trace("setSessionDefaultType({})", type);

      mcfProperties.setSessionDefaultType(type);
   }

   public String getConnectionParameters() {
      return mcfProperties.getStrConnectionParameters();
   }

   public void setConnectionParameters(final String configuration) {
      mcfProperties.setConnectionParameters(configuration);
   }

   public String getConnectorClassName() {
      return mcfProperties.getConnectorClassName();
   }

   public void setConnectorClassName(final String value) {
      mcfProperties.setConnectorClassName(value);
   }

   public String getConnectionLoadBalancingPolicyClassName() {
      return mcfProperties.getConnectionLoadBalancingPolicyClassName();
   }

   public void setConnectionLoadBalancingPolicyClassName(final String connectionLoadBalancingPolicyClassName) {
      mcfProperties.setConnectionLoadBalancingPolicyClassName(connectionLoadBalancingPolicyClassName);
   }

   public String getDiscoveryAddress() {
      return mcfProperties.getDiscoveryAddress();
   }

   public void setDiscoveryAddress(final String discoveryAddress) {
      mcfProperties.setDiscoveryAddress(discoveryAddress);
   }

   public Integer getDiscoveryPort() {
      return mcfProperties.getDiscoveryPort();
   }

   public void setDiscoveryPort(final Integer discoveryPort) {
      mcfProperties.setDiscoveryPort(discoveryPort);
   }

   public Long getDiscoveryRefreshTimeout() {
      return mcfProperties.getDiscoveryRefreshTimeout();
   }

   public void setDiscoveryRefreshTimeout(final Long discoveryRefreshTimeout) {
      mcfProperties.setDiscoveryRefreshTimeout(discoveryRefreshTimeout);
   }

   public Long getDiscoveryInitialWaitTimeout() {
      return mcfProperties.getDiscoveryInitialWaitTimeout();
   }

   public void setDiscoveryInitialWaitTimeout(final Long discoveryInitialWaitTimeout) {
      mcfProperties.setDiscoveryInitialWaitTimeout(discoveryInitialWaitTimeout);
   }

   public String getClientID() {
      return mcfProperties.getClientID();
   }

   public void setClientID(final String clientID) {
      mcfProperties.setClientID(clientID);
   }

   public Integer getDupsOKBatchSize() {
      return mcfProperties.getDupsOKBatchSize();
   }

   public void setDupsOKBatchSize(final Integer dupsOKBatchSize) {
      mcfProperties.setDupsOKBatchSize(dupsOKBatchSize);
   }

   public Integer getTransactionBatchSize() {
      return mcfProperties.getTransactionBatchSize();
   }

   public void setTransactionBatchSize(final Integer transactionBatchSize) {
      mcfProperties.setTransactionBatchSize(transactionBatchSize);
   }

   public Long getClientFailureCheckPeriod() {
      return mcfProperties.getClientFailureCheckPeriod();
   }

   public void setClientFailureCheckPeriod(final Long clientFailureCheckPeriod) {
      mcfProperties.setClientFailureCheckPeriod(clientFailureCheckPeriod);
   }

   public Long getConnectionTTL() {
      return mcfProperties.getConnectionTTL();
   }

   public void setConnectionTTL(final Long connectionTTL) {
      mcfProperties.setConnectionTTL(connectionTTL);
   }

   public Long getCallTimeout() {
      return mcfProperties.getCallTimeout();
   }

   public void setCallTimeout(final Long callTimeout) {
      mcfProperties.setCallTimeout(callTimeout);
   }

   public Integer getConsumerWindowSize() {
      return mcfProperties.getConsumerWindowSize();
   }

   public void setConsumerWindowSize(final Integer consumerWindowSize) {
      mcfProperties.setConsumerWindowSize(consumerWindowSize);
   }

   public Integer getConsumerMaxRate() {
      return mcfProperties.getConsumerMaxRate();
   }

   public void setConsumerMaxRate(final Integer consumerMaxRate) {
      mcfProperties.setConsumerMaxRate(consumerMaxRate);
   }

   public Integer getConfirmationWindowSize() {
      return mcfProperties.getConfirmationWindowSize();
   }

   public void setConfirmationWindowSize(final Integer confirmationWindowSize) {
      mcfProperties.setConfirmationWindowSize(confirmationWindowSize);
   }

   public Integer getProducerMaxRate() {
      return mcfProperties.getProducerMaxRate();
   }

   public void setProducerMaxRate(final Integer producerMaxRate) {
      mcfProperties.setProducerMaxRate(producerMaxRate);
   }

   public Integer getMinLargeMessageSize() {
      return mcfProperties.getMinLargeMessageSize();
   }

   public void setMinLargeMessageSize(final Integer minLargeMessageSize) {
      mcfProperties.setMinLargeMessageSize(minLargeMessageSize);
   }

   /**
    * A getter as well as a setter for those servers that use introspection
    */
   public Boolean getBlockOnAcknowledge() {
      return mcfProperties.isBlockOnAcknowledge();
   }

   public Boolean isBlockOnAcknowledge() {
      return mcfProperties.isBlockOnAcknowledge();
   }

   public void setBlockOnAcknowledge(final Boolean blockOnAcknowledge) {
      mcfProperties.setBlockOnAcknowledge(blockOnAcknowledge);
   }

   /**
    * A getter as well as a setter for those servers that use introspection
    */
   public Boolean getBlockOnNonDurableSend() {
      return mcfProperties.isBlockOnNonDurableSend();
   }

   public Boolean isBlockOnNonDurableSend() {
      return mcfProperties.isBlockOnNonDurableSend();
   }

   public void setBlockOnNonDurableSend(final Boolean blockOnNonDurableSend) {
      mcfProperties.setBlockOnNonDurableSend(blockOnNonDurableSend);
   }

   /**
    * A getter as well as a setter for those servers that use introspection
    */
   public Boolean getBlockOnDurableSend() {
      return mcfProperties.isBlockOnDurableSend();
   }

   public Boolean isBlockOnDurableSend() {
      return mcfProperties.isBlockOnDurableSend();
   }

   public void setBlockOnDurableSend(final Boolean blockOnDurableSend) {
      mcfProperties.setBlockOnDurableSend(blockOnDurableSend);
   }

   /**
    * A getter as well as a setter for those servers that use introspection
    */
   public Boolean getAutoGroup() {
      return mcfProperties.isAutoGroup();
   }

   public Boolean isAutoGroup() {
      return mcfProperties.isAutoGroup();
   }

   public void setAutoGroup(final Boolean autoGroup) {
      mcfProperties.setAutoGroup(autoGroup);
   }

   /**
    * A getter as well as a setter for those servers that use introspection
    */
   public Boolean getPreAcknowledge() {
      return mcfProperties.isPreAcknowledge();
   }

   public Boolean isPreAcknowledge() {
      return mcfProperties.isPreAcknowledge();
   }

   public void setPreAcknowledge(final Boolean preAcknowledge) {
      mcfProperties.setPreAcknowledge(preAcknowledge);
   }

   public Long getRetryInterval() {
      return mcfProperties.getRetryInterval();
   }

   public void setRetryInterval(final Long retryInterval) {
      mcfProperties.setRetryInterval(retryInterval);
   }

   public Double getRetryIntervalMultiplier() {
      return mcfProperties.getRetryIntervalMultiplier();
   }

   public void setRetryIntervalMultiplier(final Double retryIntervalMultiplier) {
      mcfProperties.setRetryIntervalMultiplier(retryIntervalMultiplier);
   }

   public Integer getReconnectAttempts() {
      return mcfProperties.getReconnectAttempts();
   }

   public void setReconnectAttempts(final Integer reconnectAttempts) {
      mcfProperties.setReconnectAttempts(reconnectAttempts);
   }

   /**
    * A getter as well as a setter for those servers that use introspection
    */
   public Boolean getUseGlobalPools() {
      return mcfProperties.isUseGlobalPools();
   }

   public Boolean isUseGlobalPools() {
      return mcfProperties.isUseGlobalPools();
   }

   public void setUseGlobalPools(final Boolean useGlobalPools) {
      mcfProperties.setUseGlobalPools(useGlobalPools);
   }

   /**
    * A getter as well as a setter for those servers that use introspection
    */
   public Boolean getCacheDestinations() {
      return mcfProperties.isCacheDestinations();
   }

   public Boolean isCacheDestinations() {
      return mcfProperties.isCacheDestinations();
   }

   public void setCacheDestinations(final Boolean cacheDestinations) {
      mcfProperties.setCacheDestinations(cacheDestinations);
   }

   /**
    * A getter as well as a setter for those servers that use introspection
    */
   public Boolean getEnable1xPrefixes() {
      return mcfProperties.isEnable1xPrefixes();
   }

   public Boolean isEnable1xPrefixes() {
      return mcfProperties.isEnable1xPrefixes();
   }

   public void setEnable1xPrefixes(final Boolean enable1xPrefixes) {
      mcfProperties.setEnable1xPrefixes(enable1xPrefixes);
   }

   public Integer getScheduledThreadPoolMaxSize() {
      return mcfProperties.getScheduledThreadPoolMaxSize();
   }

   public void setScheduledThreadPoolMaxSize(final Integer scheduledThreadPoolMaxSize) {
      mcfProperties.setScheduledThreadPoolMaxSize(scheduledThreadPoolMaxSize);
   }

   public Integer getThreadPoolMaxSize() {
      return mcfProperties.getThreadPoolMaxSize();
   }

   public void setThreadPoolMaxSize(final Integer threadPoolMaxSize) {
      mcfProperties.setThreadPoolMaxSize(threadPoolMaxSize);
   }

   /**
    * A getter as well as a setter for those servers that use introspection
    */
   public Boolean getHA() {
      return mcfProperties.isHA();
   }

   public Boolean isHA() {
      return mcfProperties.isHA();
   }

   public void setAllowLocalTransactions(Boolean allowLocalTransactions) {
      mcfProperties.setAllowLocalTransactions(allowLocalTransactions);
   }

   /**
    * A getter as well as a setter for those servers that use introspection
    */
   public Boolean getAllowLocalTransactions() {
      return mcfProperties.isAllowLocalTransactions();
   }

   public Boolean isAllowLocalTransactions() {
      return mcfProperties.isAllowLocalTransactions();
   }

   /**
    * A getter as well as a setter for those servers that use introspection
    */
   public Boolean getInJtaTransaction() {
      return mcfProperties.isInJtaTransaction();
   }

   public Boolean isInJtaTransaction() {
      return mcfProperties.isInJtaTransaction();
   }

   public void setInJtaTransaction(Boolean inJtaTransaction) {
      mcfProperties.setInJtaTransaction(inJtaTransaction);
   }

   public void setHA(Boolean ha) {
      mcfProperties.setHA(ha);
   }

   public Boolean isCompressLargeMessage() {
      return mcfProperties.isCompressLargeMessage();
   }

   public void setCompressLargeMessage(final Boolean compressLargeMessage) {
      mcfProperties.setCompressLargeMessage(compressLargeMessage);
   }

   public Integer getCompressionLevel() {
      return mcfProperties.getCompressionLevel();
   }

   public void setCompressionLevel(final Integer compressionLevel) {
      mcfProperties.setCompressionLevel(compressionLevel);
   }

   public Integer getInitialConnectAttempts() {
      return mcfProperties.getInitialConnectAttempts();
   }

   public void setInitialConnectAttempts(final Integer initialConnectAttempts) {
      mcfProperties.setInitialConnectAttempts(initialConnectAttempts);
   }

   public Integer getUseTryLock() {
      logger.trace("getUseTryLock()");

      return mcfProperties.getUseTryLock();
   }

   public void setUseTryLock(final Integer useTryLock) {
      logger.trace("setUseTryLock({})", useTryLock);

      mcfProperties.setUseTryLock(useTryLock);
   }

   public ConnectionMetaData getMetaData() {
      logger.trace("getMetadata()");

      return new ActiveMQRAConnectionMetaData();
   }

   protected ActiveMQRAMCFProperties getProperties() {
      logger.trace("getProperties()");

      return mcfProperties;
   }

   private ActiveMQRAConnectionRequestInfo getCRI(final ActiveMQRAConnectionRequestInfo info) {
      logger.trace("getCRI({})", info);

      if (info == null) {
         // Create a default one
         return new ActiveMQRAConnectionRequestInfo(ra.getProperties(), mcfProperties.getType());
      } else {
         // Fill the one with any defaults
         info.setDefaults(ra.getProperties());
         return info;
      }
   }

   // this should be called when ActiveMQResourceAdapter.stop() is called since this MCF is registered with it
   public void stop() {
      if (resourceRecovery != null) {
         ra.getRecoveryManager().unRegister(resourceRecovery);
      }

      if (recoveryConnectionFactory != null) {
         recoveryConnectionFactory.close();
         recoveryConnectionFactory = null;
      }
   }
}
