/*
 * Copyright 2012 JBoss, a division of Red Hat.
 *
 * Licensed 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.jboss.errai.databinding.client;

import java.util.ArrayList;
import java.util.List;

import org.jboss.errai.common.client.api.Assert;
import org.jboss.errai.databinding.client.api.PropertyChangeEvent;
import org.jboss.errai.databinding.client.api.PropertyChangeHandler;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;

/**
 * This is a utility class that can be used by implementations of {@link HasPropertyChangeHandlers}. It manages a list
 * of handlers and dispatches {@link PropertyChangeEvent}s.
 *
 * @author David Cracauer <dcracauer@gmail.com>
 * @author Christian Sadilek <csadilek@redhat.com>
 */
public class PropertyChangeHandlerSupport {

  private final List<PropertyChangeHandler<?>> handlers = new ArrayList<PropertyChangeHandler<?>>();
  private final Multimap<String, PropertyChangeHandler<?>> specificPropertyHandlers = ArrayListMultimap.create();

  /**
   * Adds a {@link PropertyChangeHandler} that will be notified when any
   * property of the bound object changes. Multiple handlers can be registered.
   * If the same handler instance is passed multiple times, it will be notified
   * multiple times.
   *
   * @param handler
   *          The {@link PropertyChangeHandler} to add, must not be null.
   */
  public void addPropertyChangeHandler(PropertyChangeHandler<?> handler) {
    handlers.add(Assert.notNull(handler));
  }

  /**
   * Adds a {@link PropertyChangeHandler} that will be notified only when the
   * given property of the bound object changes. Multiple handlers can be
   * registered for each property. If the same handler instance is passed for
   * the same property multiple times, it will be notified multiple times. If
   * the property name does not correspond to a property of the bound object, no
   * exception is thrown, but no events will ever be delivered to the handler.
   *
   * @param name
   *          The property name for which notifications should be sent.
   * @param handler
   *          The {@link PropertyChangeHandler} to add, must not be null.
   */
  public void addPropertyChangeHandler(String name, PropertyChangeHandler<?> handler) {
    specificPropertyHandlers.put(name, Assert.notNull(handler));
  }

  /**
   * Removes a {@link PropertyChangeHandler} from the list of handlers. If the handler was added more than once to the
   * same event source, it will be notified one less time after being removed. If listener is null, or was never added,
   * no exception is thrown and no action is taken.
   *
   * @param handler
   *          The {@link PropertyChangeHandler} to remove.
   */
  public void removePropertyChangeHandler(PropertyChangeHandler<?> handler) {
    handlers.remove(handler);
  }

  /**
   * Removes a {@link PropertyChangeHandler}, causing it no longer to be
   * notified only when the given property of the bound object changes. If the
   * same handler instance was added for the same property multiple times, it
   * will be notified one less time per change than before. If listener is null,
   * was never added, or the property name does not correspond to a property of
   * the bound object, no exception is thrown and no action is taken.
   *
   * @param name
   *          The property name for which notifications should be sent.
   * @param handler
   *          The {@link PropertyChangeHandler} to add, must not be null.
   */
  public void removePropertyChangeHandler(String name, PropertyChangeHandler<?> handler) {
    specificPropertyHandlers.remove(name, Assert.notNull(handler));
  }

  /**
   * Notify registered {@link PropertyChangeHandlers} of a {@link PropertyChangeEvent}. Will only dispatch events that
   * represent a change. If oldValue and newValue are equal, the event will be ignored.
   *
   * @param event
   *          {@link the PropertyChangeEvent} to provide to handlers.
   */
  @SuppressWarnings({ "rawtypes", "unchecked" })
  public void notifyHandlers(PropertyChangeEvent<?> event) {
    if (!acceptEvent(event)) {
      return;
    }

    for (PropertyChangeHandler handler : specificPropertyHandlers.get(event.getPropertyName())) {
      handler.onPropertyChange(event);
    }

    for (PropertyChangeHandler handler : handlers) {
      handler.onPropertyChange(event);
    }
  }

  private boolean acceptEvent(PropertyChangeEvent<?> event) {
    if (event == null) {
      return false;
    }

    if (event.getOldValue() == null) {
      return event.getNewValue() != null;
    }

    if (event.getNewValue() == null) {
      return event.getOldValue() != null;
    }

    return !event.getOldValue().equals(event.getNewValue());
  }
}