/*
 * 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.geode.cache.query.internal.cq;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import org.apache.geode.SystemFailure;
import org.apache.geode.cache.CacheCallback;
import org.apache.geode.cache.query.CqAttributes;
import org.apache.geode.cache.query.CqAttributesMutator;
import org.apache.geode.cache.query.CqListener;
import org.apache.geode.internal.i18n.LocalizedStrings;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.internal.logging.log4j.LocalizedMessage;

import org.apache.logging.log4j.Logger;

public class CqAttributesImpl implements CqAttributes, CqAttributesMutator, Cloneable {
  private static final Logger logger = LogService.getLogger();

  private volatile ArrayList<CqListener> cqListeners = null;

  private static final CqListener[] EMPTY_LISTENERS = new CqListener[0];

  /**
   * Used to synchronize access to cqListeners
   */
  private final Object clSync = new Object();

  /**
   * Returns the CqListeners set with the CQ
   * 
   * @return CqListener[]
   */
  public CqListener[] getCqListeners() {
    final ArrayList listeners = this.cqListeners;
    if (listeners == null) {
      return CqAttributesImpl.EMPTY_LISTENERS;
    }

    CqListener[] result = null;
    synchronized (this.clSync) {
      result = new CqListener[listeners.size()];
      listeners.toArray(result);
    }
    return result;

  }

  /**
   * Returns the CqListener set with the CQ
   * 
   * @return CqListener
   */
  public CqListener getCqListener() {
    ArrayList<CqListener> listeners = this.cqListeners;
    if (listeners == null) {
      return null;
    }
    synchronized (this.clSync) {
      if (listeners.size() == 0) {
        return null;
      }
      if (listeners.size() == 1) {
        return listeners.get(0);
      }
    }
    throw new IllegalStateException(
        LocalizedStrings.CqAttributesFactory_MORE_THAN_ONE_CQLISTENER_EXISTS.toLocalizedString());
  }

  @Override
  public Object clone() {
    try {
      return super.clone();
    } catch (CloneNotSupportedException e) {
      throw new InternalError(
          LocalizedStrings.CqAttributesFactory_CLONENOTSUPPORTEDEXCEPTION_THROWN_IN_CLASS_THAT_IMPLEMENTS_CLONEABLE
              .toLocalizedString());
    }
  }

  /**
   * Adds a Cqlistener to the end of the list of Cqlisteners on this CqQuery.
   * 
   * @param cql the user defined cq listener to add to the CqQuery.
   * @throws IllegalArgumentException if <code>aListener</code> is null
   */
  public void addCqListener(CqListener cql) {
    if (cql == null) {
      throw new IllegalArgumentException(
          LocalizedStrings.CqAttributesFactory_ADDCQLISTENER_PARAMETER_WAS_NULL
              .toLocalizedString());
    }
    synchronized (this.clSync) {
      ArrayList<CqListener> oldListeners = this.cqListeners;
      if (oldListeners == null || oldListeners.size() == 0) {
        ArrayList<CqListener> al = new ArrayList<CqListener>(1);
        al.add(cql);
        this.setCqListeners(al);
      } else {
        if (!oldListeners.contains(cql)) {
          oldListeners.add(cql);
        }
      }
    }
  }

  /**
   * Removes all Cqlisteners, calling on each of them, and then adds each listener in the specified
   * array.
   * 
   * @param addedListeners a possibly null or empty array of listeners to add to this CqQuery.
   * @throws IllegalArgumentException if the <code>newListeners</code> array has a null element
   */
  public void initCqListeners(CqListener[] addedListeners) {
    ArrayList<CqListener> oldListeners;
    synchronized (this.clSync) {
      oldListeners = this.cqListeners;
      if (addedListeners == null || addedListeners.length == 0) {
        this.setCqListeners(null);
      } else { // we have some listeners to add
        List nl = Arrays.asList(addedListeners);
        if (nl.contains(null)) {
          throw new IllegalArgumentException(
              LocalizedStrings.CqAttributesFactory_INITCQLISTENERS_PARAMETER_HAD_A_NULL_ELEMENT
                  .toLocalizedString());
        }
        this.setCqListeners(new ArrayList(nl));
      }
    }

    if (oldListeners != null) {
      CqListener cql = null;
      for (Iterator<CqListener> iter = oldListeners.iterator(); iter.hasNext();) {
        try {
          cql = iter.next();
          cql.close();
          // Handle client side exceptions.
        } catch (Exception ex) {
          logger.warn(LocalizedMessage.create(
              LocalizedStrings.CqAttributesFactory_EXCEPTION_OCCURRED_WHILE_CLOSING_CQ_LISTENER_ERROR_0,
              ex.getLocalizedMessage()));
          if (logger.isDebugEnabled()) {
            logger.debug(ex.getMessage(), ex);
          }
        } catch (VirtualMachineError err) {
          SystemFailure.initiateFailure(err);
          // If this ever returns, rethrow the error. We're poisoned
          // now, so don't let this thread continue.
          throw err;
        } catch (Throwable t) {
          // Whenever you catch Error or Throwable, you must also
          // catch VirtualMachineError (see above). However, there is
          // _still_ a possibility that you are dealing with a cascading
          // error condition, so you also need to check to see if the JVM
          // is still usable:
          SystemFailure.checkFailure();
          logger.warn(LocalizedMessage.create(
              LocalizedStrings.CqAttributesFactory_RUNTIME_EXCEPTION_OCCURRED_WHILE_CLOSING_CQ_LISTENER_ERROR_0,
              t.getLocalizedMessage()));
          if (logger.isDebugEnabled()) {
            logger.debug(t.getMessage(), t);
          }
        }
      }
    }
  }

  /**
   * Removes a Cqlistener from the list of Cqlisteners on this CqQuery. Does nothing if the
   * specified listener has not been added. If the specified listener has been added then
   * {@link CacheCallback#close()} will be called on it; otherwise does nothing.
   * 
   * @param cql the Cqlistener to remove from the CqQuery.
   * @throws IllegalArgumentException if <code>cl</code> is null
   */
  public void removeCqListener(CqListener cql) {
    if (cql == null) {
      throw new IllegalArgumentException(
          LocalizedStrings.CqAttributesFactory_REMOVECQLISTENER_PARAMETER_WAS_NULL
              .toLocalizedString());
    }
    synchronized (this.clSync) {
      ArrayList<CqListener> oldListeners = this.cqListeners;
      if (oldListeners != null) {
        if (oldListeners.remove(cql)) {
          if (oldListeners.isEmpty()) {
            this.setCqListeners(null);
          }
          try {
            cql.close();
            // Handle client side exceptions.
          } catch (Exception ex) {
            logger.warn(LocalizedMessage.create(
                LocalizedStrings.CqAttributesFactory_EXCEPTION_CLOSING_CQ_LISTENER_ERROR_0,
                ex.getLocalizedMessage()));
            if (logger.isDebugEnabled()) {
              logger.debug(ex.getMessage(), ex);
            }
          } catch (VirtualMachineError err) {
            SystemFailure.initiateFailure(err);
            // If this ever returns, rethrow the error. We're poisoned
            // now, so don't let this thread continue.
            throw err;
          } catch (Throwable t) {
            // Whenever you catch Error or Throwable, you must also
            // catch VirtualMachineError (see above). However, there is
            // _still_ a possibility that you are dealing with a cascading
            // error condition, so you also need to check to see if the JVM
            // is still usable:
            SystemFailure.checkFailure();
            logger.warn(LocalizedMessage.create(
                LocalizedStrings.CqAttributesFactory_RUNTIME_EXCEPTION_OCCURRED_CLOSING_CQ_LISTENER_ERROR_0,
                t.getLocalizedMessage()));
            if (logger.isDebugEnabled()) {
              logger.debug(t.getMessage(), t);
            }
          }
        }
      }
    }
  }

  public void setCqListeners(ArrayList<CqListener> cqListeners) {
    synchronized (this.clSync) {
      this.cqListeners = cqListeners;
    }
  }
}
