package uk.ac.ebi.atlas.model;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.KryoSerializable;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.Math.max;
import static java.util.stream.Collectors.toList;

public abstract class Profile<
        DataColumnDescriptor extends DescribesDataColumns,
        Expr extends Expression,
        Self extends Profile<DataColumnDescriptor, Expr, Self>>

implements KryoSerializable {

    protected Map<DataColumnDescriptor, Expr> expressionsByCondition = new HashMap<>();
    private String id;
    private String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Profile)) return false;
        Profile<?, ?, ?> profile = (Profile<?, ?, ?>) o;
        return Objects.equal(expressionsByCondition, profile.expressionsByCondition) &&
                Objects.equal(getId(), profile.getId()) &&
                Objects.equal(getName(), profile.getName());
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(expressionsByCondition, getId(), getName());
    }

    protected Profile() {}

    protected Profile(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public boolean isEmpty() {
        return expressionsByCondition.isEmpty();
    }

    public boolean hasAllExpressionsEqualZero(){
        return expressionsByCondition.values().stream().noneMatch(expr -> expr.getLevel() != 0);
    }

    public long getSpecificity() {
        return expressionsByCondition.values().stream().filter(expr -> expr.getLevel() != 0).count();
    }

    public long getSpecificity(Collection<AssayGroup> assayGroups) {
        List<String> assayGroupIds = assayGroups.stream().map(DescribesDataColumns::getId).collect(toList());

        return expressionsByCondition.values().stream()
                .filter(expr -> assayGroupIds.contains(expr.getDataColumnDescriptorId()))
                .filter(expr -> expr.getLevel() != 0)
                .count();
    }


    @Nullable
    public Double getExpressionLevel(DataColumnDescriptor condition) {
        Expression expression = expressionsByCondition.get(condition);
        if (expression != null) {
            return expression.getLevel();
        }
        return null;
    }

    public double getMaxExpressionLevelOn(Collection<DataColumnDescriptor> conditions) {
        checkArgument(!CollectionUtils.isEmpty(conditions));
        double expressionLevel = 0D;

        for (DataColumnDescriptor condition : conditions) {
            Double level = getExpressionLevel(condition);
            if (level != null) {
                expressionLevel = max(expressionLevel, Math.abs(level));
            }
        }
        return expressionLevel;
    }

    public double getAverageExpressionLevelOn(Collection<DataColumnDescriptor> conditions) {
        checkArgument(!CollectionUtils.isEmpty(conditions));

        double expressionLevel = 0D;

        for (DataColumnDescriptor condition : conditions) {
            Double level = getExpressionLevel(condition);
            if (level != null) {
                expressionLevel += Math.abs(level);
            }
        }

        return expressionLevel / conditions.size();
    }

    public boolean isExpressedOnAnyOf(Collection<DataColumnDescriptor> conditions) {
        for (DataColumnDescriptor condition : conditions) {
            Double level = getExpressionLevel(condition);
            if (level != null && level > 0) {
                return true;
            }
        }

        return false;
    }

    public Set<DataColumnDescriptor> getConditions() {
        return Sets.newHashSet(expressionsByCondition.keySet());
    }

    public void add(DataColumnDescriptor condition, Expr expression) {
        expressionsByCondition.put(condition, expression);
    }

    public Expr getExpression(DataColumnDescriptor condition) {
        return expressionsByCondition.get(condition);
    }

    public String getName() {
        if (StringUtils.isBlank(name)) {
            return id;
        }

        return name;
    }

    public Self filter(Predicate<Expr> keepExpressions){
        Self result = createEmptyCopy();
        expressionsByCondition.entrySet().stream().filter(e -> keepExpressions.apply(e.getValue())).forEach(e -> {
            result.add(e.getKey(), e.getValue());
        });

        return result;
    }

    protected abstract Self createEmptyCopy();

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
                .add("expressionsByCondition", expressionsByCondition.values())
                .add("id", id)
                .add("name", getName())
                .toString();
    }

    public Map<String,String> properties(){
        return ImmutableMap.of("id", id, "name", getName());
    }

    public String[] identifiers(){
        return new String[]{id, getName()};
    }


    @Override
    public void write(Kryo kryo, Output output) {
        output.writeString(id);
        output.writeString(name);
        kryo.writeObject(output, expressionsByCondition);
    }

    @Override
    public void read(Kryo kryo, Input input) {
        id = input.readString();
        name = input.readString();
        expressionsByCondition = kryo.readObject(input, HashMap.class);
    }
}

@@ -4163,17 +4163,18 @@
 & level 
-%3E
+!=
  0) %7B%0A  

Fix the fix: Log fold change can be negative