// 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
// 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.cloud.utils.db;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.cloud.utils.Pair;
import com.cloud.utils.db.SearchBase.Condition;
import com.cloud.utils.db.SearchBase.Select;

/**
 * big joins or high performance searches, it is much better to
 */
public class SearchCriteria<K> {
    public enum Op {
        GT(" > ? ", 1), GTEQ(" >= ? ", 1), LT(" < ? ", 1), LTEQ(" <= ? ", 1), EQ(" = ? ", 1), NEQ(" != ? ", 1), BETWEEN(" BETWEEN ? AND ? ", 2), NBETWEEN(
                " NOT BETWEEN ? AND ? ",
                2), IN(" IN () ", -1), NOTIN(" NOT IN () ", -1), LIKE(" LIKE ? ", 1), NLIKE(" NOT LIKE ? ", 1), NIN(" NOT IN () ", -1), NULL(" IS NULL ", 0), NNULL(
                " IS NOT NULL ",
                0), SC(" () ", 1), TEXT("  () ", 1), RP("", 0), AND(" AND ", 0), OR(" OR ", 0), NOT(" NOT ", 0), FIND_IN_SET(" ) ", 1);

        private final String op;
        int params;

        Op(String op, int params) {
            this.op = op;
            this.params = params;
        }

        @Override
        public String toString() {
            return op;
        }

        public int getParams() {
            return params;
        }
    }

    public enum Func {
        NATIVE("@", 1), MAX("MAX(@)", 1), MIN("MIN(@)", 1), FIRST("FIRST(@)", 1), LAST("LAST(@)", 1), SUM("SUM(@)", 1), COUNT("COUNT(@)", 1), DISTINCT("DISTINCT(@)", 1);

        private String func;
        private int count;

        Func(String func, int params) {
            this.func = func;
            this.count = params;
        }

        @Override
        public String toString() {
            return func;
        }

        public int getCount() {
            return count;
        }
    }

    public enum SelectType {
        Fields, Entity, Single, Result
    }

    private final Map<String, Attribute> _attrs;
    private final ArrayList<Condition> _conditions;
    private ArrayList<Condition> _additionals = null;
    private HashMap<String, Object[]> _params = new HashMap<String, Object[]>();
    private int _counter;
    private HashMap<String, JoinBuilder<SearchCriteria<?>>> _joins;
    private final ArrayList<Select> _selects;
    private final GroupBy<? extends SearchBase<?, ?, K>, ?, K> _groupBy;
    private final List<Object> _groupByValues;
    private final Class<K> _resultType;
    private final SelectType _selectType;

    protected SearchCriteria(SearchBase<?, ?, K> sb) {
        this._attrs = sb._attrs;
        this._conditions = sb._conditions;
        this._additionals = new ArrayList<Condition>();
        this._counter = 0;
        this._joins = null;
        if (sb._joins != null) {
            _joins = new HashMap<String, JoinBuilder<SearchCriteria<?>>>(sb._joins.size());
            for (Map.Entry<String, JoinBuilder<SearchBase<?, ?, ?>>> entry : sb._joins.entrySet()) {
                JoinBuilder<SearchBase<?, ?, ?>> value = entry.getValue();
                _joins.put(entry.getKey(),
                    new JoinBuilder<SearchCriteria<?>>(value.getT().create(), value.getFirstAttribute(), value.getSecondAttribute(), value.getType()));
            }
        }
        _selects = sb._selects;
        _groupBy = sb._groupBy;
        if (_groupBy != null) {
            _groupByValues = new ArrayList<Object>();
        } else {
            _groupByValues = null;
        }
        _resultType = sb._resultType;
        _selectType = sb._selectType;
    }

    protected void setParameters(HashMap<String, Object[]> parameters) {
        _params = parameters;
    }

    public SelectType getSelectType() {
        return _selectType;
    }

    public void getSelect(StringBuilder str, int insertAt) {
        if (_selects == null || _selects.size() == 0) {
            return;
        }

        for (Select select : _selects) {
            String func = select.func.toString() + ",";
            if (select.attr == null) {
                func = func.replace("@", "*");
            } else {
                func = func.replace("@", select.attr.table + "." + select.attr.columnName);
            }
            str.insert(insertAt, func);
            insertAt += func.length();
            if (select.field == null) {
                break;
            }
        }

        str.delete(insertAt - 1, insertAt);
    }

    public List<Field> getSelectFields() {
        List<Field> fields = new ArrayList<Field>(_selects.size());
        for (Select select : _selects) {
            fields.add(select.field);
        }

        return fields;
    }

    public void setParameters(String conditionName, Object... params) {
        assert _conditions.contains(new Condition(conditionName)) || _additionals.contains(new Condition(conditionName)) : "Couldn't find " + conditionName;
        _params.put(conditionName, params);
    }

    public boolean isSelectAll() {
        return _selects == null || _selects.size() == 0;
    }

    protected JoinBuilder<SearchCriteria<?>> findJoin(Map<String, JoinBuilder<SearchCriteria<?>>> jbmap, String joinName) {
        JoinBuilder<SearchCriteria<?>> jb = jbmap.get(joinName);
        if (jb != null) {
            return jb;
        }

        for (JoinBuilder<SearchCriteria<?>> j2 : jbmap.values()) {
            SearchCriteria<?> sc = j2.getT();
            if (sc._joins != null)
                jb = findJoin(sc._joins, joinName);
            if (jb != null) {
                return jb;
            }
        }

        assert (false) : "Unable to find a join by the name " + joinName;
        return null;
    }

    public void setJoinParameters(String joinName, String conditionName, Object... params) {
        JoinBuilder<SearchCriteria<?>> join = findJoin(_joins, joinName);
        assert (join != null) : "Incorrect join name specified: " + joinName;
        join.getT().setParameters(conditionName, params);

    }

    public SearchCriteria<?> getJoin(String joinName) {
        return _joins.get(joinName).getT();
    }

    public Pair<GroupBy<?, ?, ?>, List<Object>> getGroupBy() {
        return _groupBy == null ? null : new Pair<GroupBy<?, ?, ?>, List<Object>>(_groupBy, _groupByValues);
    }

    public void setGroupByValues(Object... values) {
        for (Object value : values) {
            _groupByValues.add(value);
        }
    }

    public Class<K> getResultType() {
        return _resultType;
    }

    @Deprecated
    public void addAnd(String field, Op op, Object... values) {
        String name = Integer.toString(_counter++);
        addCondition(name, " AND ", field, op);
        setParameters(name, values);
    }

    @Deprecated
    public void addAnd(Attribute attr, Op op, Object... values) {
        String name = Integer.toString(_counter++);
        addCondition(name, " AND ", attr, op);
        setParameters(name, values);
    }

    @Deprecated
    public void addOr(String field, Op op, Object... values) {
        String name = Integer.toString(_counter++);
        addCondition(name, " OR ", field, op);
        setParameters(name, values);
    }

    protected void addCondition(String conditionName, String cond, String fieldName, Op op) {
        Attribute attr = _attrs.get(fieldName);
        assert attr != null : "Unable to find field: " + fieldName;
        addCondition(conditionName, cond, attr, op);
    }

    protected void addCondition(String conditionName, String cond, Attribute attr, Op op) {
        Condition condition = new Condition(conditionName, /*(_conditions.size() + _additionals.size()) == 0 ? "" : */cond, attr, op);
        _additionals.add(condition);
    }

    public String getWhereClause() {
        StringBuilder sql = new StringBuilder();
        int i = 0;
        for (Condition condition : _conditions) {
            if (condition.isPreset()) {
                _params.put(condition.name, condition.presets);
            }
            Object[] params = _params.get(condition.name);
            if ((condition.op == null || condition.op.params == 0) || (params != null)) {
                condition.toSql(sql, params, i++);
            }
        }

        for (Condition condition : _additionals) {
            if (condition.isPreset()) {
                _params.put(condition.name, condition.presets);
            }
            Object[] params = _params.get(condition.name);
            if ((condition.op.params == 0) || (params != null)) {
                condition.toSql(sql, params, i++);
            }
        }

        return sql.toString();
    }

    public List<Pair<Attribute, Object>> getValues() {
        ArrayList<Pair<Attribute, Object>> params = new ArrayList<Pair<Attribute, Object>>(_params.size());
        for (Condition condition : _conditions) {
            Object[] objs = _params.get(condition.name);
            if (condition.op != null && condition.op.params != 0 && objs != null) {
                getParams(params, condition, objs);
            }
        }

        for (Condition condition : _additionals) {
            Object[] objs = _params.get(condition.name);
            if ((condition.op.params == 0) || (objs != null)) {
                getParams(params, condition, objs);
            }
        }

        return params;
    }

    public Collection<JoinBuilder<SearchCriteria<?>>> getJoins() {
        return _joins != null ? _joins.values() : null;
    }

    private void getParams(ArrayList<Pair<Attribute, Object>> params, Condition condition, Object[] objs) {
        if (condition.op == Op.SC) {
            assert (objs != null && objs.length > 0) : " Where's your search criteria object? " + condition.name;
            params.addAll(((SearchCriteria<?>)objs[0]).getValues());
            return;
        }

        if (objs != null && objs.length > 0) {
            for (Object obj : objs) {
                if ((condition.op != Op.EQ && condition.op != Op.NEQ) || (obj != null)) {
                    params.add(new Pair<Attribute, Object>(condition.attr, obj));
                }
            }
        }
    }

    public Pair<String, ArrayList<Object>> toSql() {
        StringBuilder sql = new StringBuilder();

        return new Pair<String, ArrayList<Object>>(sql.toString(), null);
    }
}
