/*
 * 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.jackrabbit.oak.query.ast;

import static org.apache.jackrabbit.guava.common.collect.Lists.newArrayList;
import static org.apache.jackrabbit.guava.common.collect.Sets.newHashSet;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import javax.jcr.PropertyType;

import org.apache.jackrabbit.oak.api.PropertyValue;
import org.apache.jackrabbit.oak.query.ValueConverter;
import org.apache.jackrabbit.oak.query.index.FilterImpl;
import org.apache.jackrabbit.oak.plugins.memory.PropertyValues;

/**
 * A "in" comparison operation.
 */
public class InImpl extends ConstraintImpl {

    private final DynamicOperandImpl operand1;
    private final List<StaticOperandImpl> operand2;

    public InImpl(DynamicOperandImpl operand1, List<StaticOperandImpl> operand2) {
        this.operand1 = operand1;
        this.operand2 = operand2;
    }

    public DynamicOperandImpl getOperand1() {
        return operand1;
    }

    public List<StaticOperandImpl> getOperand2() {
        return operand2;
    }

    @Override
    public ConstraintImpl simplify() {
        if (operand2.size() == 1) {
            return new ComparisonImpl(
                    operand1, Operator.EQUAL, operand2.iterator().next());
        }

        Set<StaticOperandImpl> set = newHashSet(operand2);
        if (set.size() == 1) {
            return new ComparisonImpl(
                    operand1, Operator.EQUAL, set.iterator().next());
        } else if (set.size() != operand2.size()) {
            return new InImpl(operand1, newArrayList(set));
        } else {
            return this;
        }
    }

    @Override
    public Set<PropertyExistenceImpl> getPropertyExistenceConditions() {
        PropertyExistenceImpl p = operand1.getPropertyExistence();
        if (p == null) {
            return Collections.emptySet();
        }
        return Collections.singleton(p);
    }
    
    @Override
    public Set<SelectorImpl> getSelectors() {
        return operand1.getSelectors();
    }

    @Override
    public boolean evaluate() {
        // JCR 2.0 spec, 6.7.16 Comparison:
        // "operand1 may evaluate to an array of values"
        PropertyValue p1 = operand1.currentProperty();
        if (p1 == null) {
            return false;
        }
        for (StaticOperandImpl s : operand2) {
            PropertyValue p2 = s.currentValue();
            if (p2 == null) {
                // if the property doesn't exist, the result is false
                continue;
            }
            // "the value of operand2 is converted to the
            // property type of the value of operand1"
            p2 = convertValueToType(p2, p1);
            if (PropertyValues.match(p1, p2)) {
                return true;
            }
        }
        return false;
    }

    @Override
    boolean accept(AstVisitor v) {
        return v.visit(this);
    }

    @Override
    public String toString() {
        StringBuilder buff = new StringBuilder();
        buff.append(operand1).append(" in(");
        int i = 0;
        for (StaticOperandImpl s : operand2) {
            if (i++ > 0) {
                buff.append(", ");
            }
            buff.append(s);
        }
        buff.append(")");
        return buff.toString();
    }

    @Override
    public void restrict(FilterImpl f) {
        ArrayList<PropertyValue> list = new ArrayList<PropertyValue>();
        for (StaticOperandImpl s : operand2) {
            if (!ValueConverter.canConvert(
                    s.getPropertyType(),
                    operand1.getPropertyType())) {
                throw new IllegalArgumentException(
                        "Unsupported conversion from property type " + 
                                PropertyType.nameFromValue(s.getPropertyType()) + 
                                " to property type " +
                                PropertyType.nameFromValue(operand1.getPropertyType()));
            }
            list.add(s.currentValue());
        }
        if (list != null) {
            operand1.restrictList(f, list);
        }
    }

    @Override
    public void restrictPushDown(SelectorImpl s) {
        for (StaticOperandImpl op : operand2) {
            if (op.currentValue() == null) {
                // one unknown value means it is not pushed down
                return;
            }
        }
        if (operand1.canRestrictSelector(s)) {
            s.restrictSelector(this);
        }
    }

    @Override
    public boolean equals(Object that) {
        if (this == that) {
            return true;
        } else if (that instanceof InImpl) {
            return operand1.equals(((InImpl) that).operand1)
                    && newHashSet(operand2).equals(newHashSet(((InImpl) that).operand2));
        } else {
            return false;
        }
    }

    @Override
    public int hashCode() {
        return operand1.hashCode();
    }

    @Override
    public AstElement copyOf() {
        return new InImpl(operand1.createCopy(), operand2);
    }
}
