/*
 * 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.core.feed;

import static com.google.common.base.Preconditions.checkNotNull;

import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.feed.http.HttpPollConfig;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.guava.Functionals;
import org.apache.brooklyn.util.javalang.JavaClassNames;
import org.apache.brooklyn.util.text.Strings;

import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;

/**
 * Configuration for a poll, or a subscription etc, that is being added to a feed.
 * 
 * @author aled
 */
public class FeedConfig<V, T, F extends FeedConfig<V, T, F>> {

    /** The onSuccess or onError functions can return this value to indicate that the sensor should not change. 
     * @deprecated since 0.7.0 use UNCHANGED */
    @Deprecated
    public static final Object UNSET = Entities.UNCHANGED;
    /** The onSuccess or onError functions can return this value to indicate that the sensor should not change. */ 
    public static final Object UNCHANGED = Entities.UNCHANGED;
    /** The onSuccess or onError functions can return this value to indicate that the sensor value should be removed
     * (cf 'null', but useful in dynamic situations) */ 
    public static final Object REMOVE = Entities.REMOVE;
    
    /** Indicates that no sensor is being used here. This sensor is suppressed,
     * but is useful where you want to use the feeds with custom success/exception/failure functions
     * which directly set multiple sensors, e.g. dynamically based on the poll response.
     * <p>
     * See {@link HttpPollConfig#forMultiple()} and its usages.
     * (It can work for any poll config, but conveniences have not been supplied for others.)  */
    public static final AttributeSensor<Void> NO_SENSOR = Sensors.newSensor(Void.class, "brooklyn.no.sensor");
    
    private final AttributeSensor<T> sensor;
    private Function<? super V, T> onsuccess;
    private Function<? super V, T> onfailure;
    private Function<? super Exception, T> onexception;
    private Predicate<? super V> checkSuccess;
    private boolean suppressDuplicates;
    private boolean enabled = true;
    
    public FeedConfig(AttributeSensor<T> sensor) {
        this.sensor = checkNotNull(sensor, "sensor");
    }

    public FeedConfig(FeedConfig<V, T, F> other) {
        this.sensor = other.sensor;
        this.onsuccess = other.onsuccess;
        this.onfailure = other.onfailure;
        this.onexception = other.onexception;
        this.checkSuccess = other.checkSuccess;
        this.suppressDuplicates = other.suppressDuplicates;
        this.enabled = other.enabled;
    }

    @SuppressWarnings("unchecked")
    protected F self() {
        return (F) this;
    }
    
    public AttributeSensor<T> getSensor() {
        return sensor;
    }

    public Predicate<? super V> getCheckSuccess() {
        return checkSuccess;
    }
    
    public Function<? super V, T> getOnSuccess() {
        return onsuccess;
    }

    public Function<? super V, T> getOnFailure() {
        return onfailure;
    }
    
    public Function<? super Exception, T> getOnException() {
        return onexception;
    }

    public boolean getSupressDuplicates() {
        return suppressDuplicates;
    }
    
    public boolean isEnabled() {
        return enabled;
    }
    
    /** sets the predicate used to check whether a feed run is successful */
    public F checkSuccess(Predicate<? super V> val) {
        this.checkSuccess = checkNotNull(val, "checkSuccess");
        return self();
    }
    /** as {@link #checkSuccess(Predicate)} */
    public F checkSuccess(final Function<? super V,Boolean> val) {
        return checkSuccess(Functionals.predicate(val));
    }

    @SuppressWarnings("unused")
    /** @deprecated since 0.7.0, kept for rebind */ @Deprecated
    private F checkSuccessLegacy(final Function<? super V,Boolean> val) {
        return checkSuccess(new Predicate<V>() {
            @Override
            public boolean apply(V input) {
                return val.apply(input);
            }
        });
    }

    public F onSuccess(Function<? super V,T> val) {
        this.onsuccess = checkNotNull(val, "onSuccess");
        return self();
    }

    public F setOnSuccess(T val) {
        return onSuccess(Functions.constant(val));
    }

    /**
     * A failure is when the connection is fine (no exception) but the other end returns a result object V
     * which the feed can tell indicates a failure (e.g. HTTP code 404)
     */
    public F onFailure(Function<? super V,T> val) {
        this.onfailure = checkNotNull(val, "onFailure");
        return self();
    }

    /** @see #onFailure(Function) */
    public F setOnFailure(T val) {
        return onFailure(Functions.constant(val));
    }

    /**
     * Registers a callback to be used by {@link #onSuccess(Function)} and {@link #onFailure(Function)},
     * i.e. whenever a result comes back, but not in case of exceptions being thrown (ie problems communicating)
     */
    public F onResult(Function<? super V, T> val) {
        onSuccess(val);
        return onFailure(val);
    }

    /** @see #onResult(Function) */
    public F setOnResult(T val) {
        return onResult(Functions.constant(val));
    }

    /**
     * An exception is when there is an error in the communication
     */
    public F onException(Function<? super Exception,T> val) {
        this.onexception = checkNotNull(val, "onException");
        return self();
    }

    /** @see #onException(Function) */
    public F setOnException(T val) {
        return onException(Functions.constant(val));
    }

    /**
     * A convenience for indicating a behaviour to occur for both {@link #onException(Function)}
     * (error connecting) and {@link #onFailure(Function)} (successful communication but failure
     * report from remote end)
     */
    public F onFailureOrException(Function<Object,T> val) {
        onFailure(val);
        return onException(val);
    }

    /** @see #onFailureOrException(Function) */
    public F setOnFailureOrException(T val) {
        return onFailureOrException(Functions.constant(val));
    }

    public F suppressDuplicates(boolean val) {
        suppressDuplicates = val;
        return self();
    }

    /**
     * Whether this feed is enabled (defaulting to true).
     */
    public F enabled(boolean val) {
        enabled = val;
        return self();
    }

    public boolean hasSuccessHandler() {
        return this.onsuccess != null;
    }

    public boolean hasFailureHandler() {
        return this.onfailure != null;
    }

    public boolean hasExceptionHandler() {
        return this.onexception != null;
    }

    public boolean hasCheckSuccessHandler() {
        return this.checkSuccess != null;
    }
    
    @Override
    public String toString() {
        StringBuilder result = new StringBuilder();
        result.append(toStringBaseName());
        result.append("[");
        boolean contents = false;
        Object source = toStringPollSource();
        AttributeSensor<T> s = getSensor();
        if (Strings.isNonBlank(Strings.toString(source))) {
            result.append(Strings.toUniqueString(source, 40));
            if (s!=null) {
                result.append("->");
                result.append(s.getName());
            }
            contents = true;
        } else if (s!=null) {
            result.append(s.getName());
            contents = true;
        }
        MutableList<Object> fields = toStringOtherFields();
        if (fields!=null) {
            for (Object field: fields) {
                if (Strings.isNonBlank(Strings.toString(field))) {
                    if (contents) result.append(";");
                    contents = true;
                    result.append(field);
                }
            }
        }
        result.append("]");
        return result.toString();
    }

    /** can be overridden to supply a simpler base name than the class name */
    protected String toStringBaseName() {
        return JavaClassNames.simpleClassName(this);
    }
    /** can be overridden to supply add'l info for the {@link #toString()}; subclasses can add to the returned value */
    protected MutableList<Object> toStringOtherFields() {
        return MutableList.<Object>of();
    }
    /** can be overridden to supply add'l info for the {@link #toString()}, placed before the sensor with -> */
    protected Object toStringPollSource() {
        return null;
    }
    /** all configs should supply a unique tag element, inserted into the feed */
    protected String getUniqueTag() {
        return toString();
    }

    /** returns fields which should be used for equality, including by default {@link #toStringOtherFields()} and {@link #toStringPollSource()};
     * subclasses can add to the returned value */
    protected MutableList<Object> equalsFields() {
        MutableList<Object> result = MutableList.of().appendIfNotNull(getSensor()).appendIfNotNull(toStringPollSource());
        for (Object field: toStringOtherFields()) result.appendIfNotNull(field);
        return result;
    }

    @Override
    public int hashCode() { 
        int hc = super.hashCode();
        for (Object f: equalsFields())
            hc = Objects.hashCode(hc, f);
        return hc;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!super.equals(obj)) return false;
        PollConfig<?,?,?> other = (PollConfig<?,?,?>) obj;
        if (!Objects.equal(getUniqueTag(), other.getUniqueTag())) return false;
        if (!Objects.equal(equalsFields(), other.equalsFields())) return false;
        return true;
    }

}
