/*
 * 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.cassandra.cql;

import java.nio.charset.CharacterCodingException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.utils.ByteBufferUtil;

/**
 * WhereClauses encapsulate all of the predicates of a SELECT query.
 *
 */
public class WhereClause
{
    // all relations (except for `<key> IN (.., .., ..)` which can be directly interpreted) from parser
    // are stored into this array and are filtered to the keys/columns by extractKeysFromColumns(...)
    private final List<Relation> clauseRelations = new ArrayList<Relation>();
    private final List<Relation> columns = new ArrayList<Relation>();

    // added to either by the parser from an IN clause or by extractKeysFromColumns
    private final Set<Term> keys = new LinkedHashSet<Term>();
    private Term startKey, finishKey;
    private boolean includeStartKey = false, includeFinishKey = false, multiKey = false;
    // set by extractKeysFromColumns
    private String keyAlias = null;

    /**
     * Create a new WhereClause with the first parsed relation.
     *
     * @param firstRelation key or column relation
     */
    public WhereClause(Relation firstRelation)
    {
        and(firstRelation);
    }

    public WhereClause()
    {}

    /**
     * Add an additional relation to this WHERE clause.
     *
     * @param relation the relation to add.
     */
    public void and(Relation relation)
    {
        clauseRelations.add(relation);
    }

    /**
     * The same as KEY = <key> to avoid using Relation object
     * @param key key to include into clause
     */
    public void andKeyEquals(Term key)
    {
        keys.add(key);
    }

    public List<Relation> getColumnRelations()
    {
        return columns;
    }

    public boolean isKeyRange()
    {
        return startKey != null;
    }

    public Term getStartKey()
    {
        return startKey;
    }

    public Term getFinishKey()
    {
        return finishKey;
    }

    public Set<Term> getKeys()
    {
        return keys;
    }

    public boolean includeStartKey()
    {
        return includeStartKey;
    }

    public boolean includeFinishKey()
    {
        return includeFinishKey;
    }

    public void setKeyAlias(String alias)
    {
        keyAlias = alias.toUpperCase();
    }

    public boolean isMultiKey() {
        return multiKey;
    }

    public void setMultiKey(boolean multiKey)
    {
        this.multiKey = multiKey;
    }

    public String getKeyAlias()
    {
        // TODO fix special casing here, key alias should always be set post-extract
        // key alias as not related to keys in here, it can be unset when we have a query like
        // SELECT * FROM <CF> WHERE key = 1 and col > 2 and col < 3;
        // it will be always set when statement looks like this
        // SELECT * FROM <CF> WHERE <key> IN (.., .., ..);
        // key is NULL when KEY keyword is used or when key alias given by user was not recognized
        // validateKeyAlias will throw an exception for us in that case
        return keyAlias == null ? QueryProcessor.DEFAULT_KEY_NAME : keyAlias;
    }

    public void extractKeysFromColumns(CFMetaData cfm)
    {
        String realKeyAlias = null;
        try
        {
            // ThriftValidation ensures that key_alias is ascii
            realKeyAlias = ByteBufferUtil.string(cfm.getKeyName()).toUpperCase();
        }
        catch (CharacterCodingException e)
        {
            throw new RuntimeException(e);
        }

        if (!keys.isEmpty())
            return; // we already have key(s) set (<key> IN (.., ...) construction used)

        for (Relation relation : clauseRelations)
        {
            String name = relation.getEntity().getText().toUpperCase();
            if (name.equals(realKeyAlias))
            {
                if (keyAlias == null) // setting found key as an alias
                    keyAlias = name;

                if (relation.operator() == RelationType.EQ)
                {
                    keys.add(relation.getValue());
                }
                else if ((relation.operator() == RelationType.GT) || (relation.operator() == RelationType.GTE))
                {
                    startKey = relation.getValue();
                    includeStartKey = relation.operator() == RelationType.GTE;
                }
                else if ((relation.operator() == RelationType.LT) || (relation.operator() == RelationType.LTE))
                {
                    finishKey = relation.getValue();
                    includeFinishKey = relation.operator() == RelationType.LTE;
                }
            }
            else
            {
                columns.add(relation);
            }
        }
    }

    public String toString()
    {
        return String.format("WhereClause [keys=%s, startKey=%s, finishKey=%s, columns=%s, includeStartKey=%s, includeFinishKey=%s, multiKey=%s, keyAlias=%s]",
                             keys,
                             startKey,
                             finishKey,
                             columns,
                             includeStartKey,
                             includeFinishKey,
                             multiKey,
                             keyAlias);
    }
}
