/*
 * Copyright (C) 2014 Indeed Inc.
 *
 * 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 com.indeed.flamdex.query;

import com.google.common.base.Joiner;

import java.util.Arrays;
import java.util.List;

/**
 * @author jsgroth
 */
public class Query {
    private final BooleanOp operator;
    private final List<Query> operands;

    private final Term startTerm;
    private final Term endTerm;
    private final boolean isMaxInclusive;

    private Query(BooleanOp operator, List<Query> operands) {
        this.operator = operator;
        this.operands = operands;
        startTerm = null;
        endTerm = null;
        isMaxInclusive = false;
    }

    private Query(Term startTerm) {
        operator = null;
        operands = null;
        this.startTerm = startTerm;
        endTerm = null;
        isMaxInclusive = false;
    }

    private Query(Term startTerm, Term endTerm, boolean maxInclusive) {
        operator = null;
        operands = null;
        this.startTerm = startTerm;
        this.endTerm = endTerm;
        isMaxInclusive = maxInclusive;
    }

    public static Query newBooleanQuery(BooleanOp operator, List<Query> operands) {
        if (operator == null || operands == null || operands.isEmpty()) {
            throw new IllegalArgumentException("invalid arguments: operator=" + operator + ", operands = "+operands);
        }
        if (operator == BooleanOp.NOT && operands.size() > 1) {
            return new Query(BooleanOp.NOT, Arrays.asList(newBooleanQuery(BooleanOp.OR, operands)));
        }
        return new Query(operator, operands);
    }

    public static Query newTermQuery(Term term) {
        if (term == null) {
            throw new IllegalArgumentException("term cannot be null");
        }
        return new Query(term);
    }

    public static Query newRangeQuery(String field, long startTerm, long endTerm, boolean isMaxInclusive) {
        return newRangeQuery(new Term(field, true, startTerm, ""), new Term(field, true, endTerm, ""), isMaxInclusive);
    }

    public static Query newRangeQuery(String field, String startTerm, String endTerm, boolean isMaxInclusive) {
        return newRangeQuery(new Term(field, false, 0, startTerm), new Term(field, false, 0, endTerm), isMaxInclusive);
    }

    public static Query newRangeQuery(Term startTerm, Term endTerm, boolean isMaxInclusive) {
        if (startTerm == null || endTerm == null) {
            throw new IllegalArgumentException("term arguments cannot be null");
        }
        if (!startTerm.getFieldName().equals(endTerm.getFieldName())) {
            throw new IllegalArgumentException("field names do not match");
        }
        if (startTerm.isIntField() != endTerm.isIntField()) {
            throw new IllegalArgumentException("terms do not have the same field type");
        }
        if (endTerm.getTermIntVal() < startTerm.getTermIntVal()) {
            throw new IllegalArgumentException("end term must be >= start term");
        }
        return new Query(startTerm, endTerm, isMaxInclusive);
    }

    public QueryType getQueryType() {
        if (operator != null) return QueryType.BOOLEAN;
        if (endTerm != null) return QueryType.RANGE;
        return QueryType.TERM;
    }

    public BooleanOp getOperator() {
        return operator;
    }

    public List<Query> getOperands() {
        return operands;
    }

    public Term getStartTerm() {
        return startTerm;
    }

    public Term getEndTerm() {
        return endTerm;
    }

    public boolean isMaxInclusive() {
        return isMaxInclusive;
    }

    @Override
    public String toString() {
        switch (getQueryType()) {
            case TERM:
                return startTerm.toString();
            case BOOLEAN:
                if (operator == BooleanOp.NOT) {
                    return "NOT (" + Joiner.on(", ").join(operands) + ")";
                } else {
                    return "(" + Joiner.on(" " + operator.toString() + " ").join(operands) + ")";
                }
            case RANGE:
                return (startTerm.isIntField() ? "int:" : "str:") + startTerm.getFieldName() + ":[" +
                        (startTerm.isIntField() ? startTerm.getTermIntVal() : startTerm.getTermStringVal()) + " TO " +
                        (endTerm.isIntField() ? endTerm.getTermIntVal() : endTerm.getTermStringVal()) + (isMaxInclusive ? "]" : ")");
            default:
                return super.toString();
        }
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Query)) return false;

        Query other = (Query) o;

        final QueryType queryType = getQueryType();
        if (other.getQueryType() != queryType) return false;

        switch (queryType) {
            case TERM:
                return startTerm.equals(other.startTerm);
            case BOOLEAN:
                return operator == other.operator && operands.equals(other.operands);
            case RANGE:
                return startTerm.equals(other.startTerm) && endTerm.equals(other.endTerm)
                        && isMaxInclusive == other.isMaxInclusive;
            default:
                throw new AssertionError("can't happen");
        }
    }

    @Override
    public int hashCode() {
        final QueryType queryType = getQueryType();
        int hashCode = queryType.hashCode();

        switch (queryType) {
            case TERM:
                hashCode *= 31;
                hashCode += startTerm.hashCode();
                break;

            case BOOLEAN:
                hashCode *= 31;
                hashCode += operator.hashCode();
                hashCode *= 31;
                hashCode += operands.hashCode();
                break;

            case RANGE:
                hashCode *= 31;
                hashCode += startTerm.hashCode();
                hashCode *= 31;
                hashCode += endTerm.hashCode();
                hashCode *= 31;
                hashCode += isMaxInclusive ? 1231 : 1237;
                break;

            default:
                throw new AssertionError("can't happen");
        }

        return hashCode;
    }
}
