/*
 * 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.brooklyn.policy.loadbalancing;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.Group;
import org.apache.brooklyn.api.sensor.Sensor;
import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.api.sensor.SensorEventListener;
import org.apache.brooklyn.entity.group.AbstractGroup;
import org.apache.brooklyn.entity.group.DynamicGroupImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Predicate;

/**
 * A group of items that are contained within a given (dynamically changing) set of containers.
 * 
 * The {@link setContainers(Group)} sets the group of containers. The membership of that group
 * is dynamically tracked.
 * 
 * When containers are added/removed, or when an items is added/removed, or when an {@link Moveable} item 
 * is moved then the membership of this group of items is automatically updated accordingly.
 * 
 * For example: in Monterey, this could be used to track the actors that are within a given cluster of venues.
 */
public class ItemsInContainersGroupImpl extends DynamicGroupImpl implements ItemsInContainersGroup {

    // TODO Inefficient: will not scale to many 1000s of items

    private static final Logger LOG = LoggerFactory.getLogger(ItemsInContainersGroup.class);
    
    private Group containerGroup;
    
    private final SensorEventListener<Object> eventHandler = new SensorEventListener<Object>() {
        @Override
        public void onEvent(SensorEvent<Object> event) {
            Entity source = event.getSource();
            Object value = event.getValue();
            Sensor sensor = event.getSensor();
            
            if (sensor.equals(AbstractGroup.MEMBER_ADDED)) {
                onContainerAdded((Entity) value);
            } else if (sensor.equals(AbstractGroup.MEMBER_REMOVED)) {
                onContainerRemoved((Entity) value);
            } else if (sensor.equals(Movable.CONTAINER)) {
                onItemMoved((Movable)source, (BalanceableContainer<?>) value);
            } else {
                throw new IllegalStateException("Unhandled event type "+sensor+": "+event);
            }
        }
    };

    public ItemsInContainersGroupImpl() {
    }
    
    @Override
    public void init() {
        super.init();
        setEntityFilter(new Predicate<Entity>() {
            @Override public boolean apply(Entity e) {
                return acceptsEntity(e);
            }});
    }
    
    protected Predicate<? super Entity> getItemFilter() {
        return getConfig(ITEM_FILTER);
    }
    
    @Override
    protected boolean acceptsEntity(Entity e) {
        if (e instanceof Movable) {
            return acceptsItem((Movable)e, ((Movable)e).getAttribute(Movable.CONTAINER));
        }
        return false;
    }

    boolean acceptsItem(Movable e, BalanceableContainer c) {
        return (containerGroup != null && c != null) ? getItemFilter().apply(e) && containerGroup.hasMember(c) : false;
    }

    @Override
    public void setContainers(Group containerGroup) {
        this.containerGroup = containerGroup;
        subscriptions().subscribe(containerGroup, AbstractGroup.MEMBER_ADDED, eventHandler);
        subscriptions().subscribe(containerGroup, AbstractGroup.MEMBER_REMOVED, eventHandler);
        subscriptions().subscribe(null, Movable.CONTAINER, eventHandler);
        
        if (LOG.isTraceEnabled()) LOG.trace("{} scanning entities on container group set", this);
        rescanEntities();
    }
    
    private void onContainerAdded(Entity newContainer) {
        if (LOG.isTraceEnabled()) LOG.trace("{} rescanning entities on container {} added", this, newContainer);
        rescanEntities();
    }
    
    private void onContainerRemoved(Entity oldContainer) {
        if (LOG.isTraceEnabled()) LOG.trace("{} rescanning entities on container {} removed", this, oldContainer);
        rescanEntities();
    }
    
    @Override
    protected void onEntityAdded(Entity item) {
        if (acceptsEntity(item)) {
            if (LOG.isDebugEnabled()) LOG.debug("{} adding new item {}", this, item);
            addMember(item);
        }
    }
    
    @Override
    protected void onEntityRemoved(Entity item) {
        if (removeMember(item)) {
            if (LOG.isDebugEnabled()) LOG.debug("{} removing deleted item {}", this, item);
        }
    }
    
    private void onItemMoved(Movable item, BalanceableContainer container) {
        if (LOG.isTraceEnabled()) LOG.trace("{} processing moved item {}, to container {}", new Object[] {this, item, container});
        if (hasMember(item)) {
            if (!acceptsItem(item, container)) {
                if (LOG.isDebugEnabled()) LOG.debug("{} removing moved item {} from group, as new container {} is not a member", new Object[] {this, item, container});
                removeMember(item);
            }
        } else {
            if (acceptsItem(item, container)) {
                if (LOG.isDebugEnabled()) LOG.debug("{} adding moved item {} to group, as new container {} is a member", new Object[] {this, item, container});
                addMember(item);
            }
        }
    }
}
