package me.chyxion.tigon.mybatis;

import java.util.*;
import java.io.Serializable;
import java.lang.reflect.Array;
import org.springframework.util.Assert;
import me.chyxion.tigon.model.BaseModel;
import org.apache.commons.lang3.StringUtils;

/**
 * @version 0.0.1
 * @since 0.0.1
 * @author Shaun Chyxion <br>
 * chyxion@163.com <br>
 * May 12, 2015 3:00:40 PM
 */
public class Search implements Serializable {
    private static final long serialVersionUID = 1L;

    private final List<Criterion> criteria =
        new LinkedList<Criterion>();
    private String table;
    private Integer offset;
    private Integer limit;
    private final Map<String, String> orders =
        new LinkedHashMap<String, String>();
    /**
     * extended data
     */
    private final Map<String, Object> attrs =
        new HashMap<String, Object>();

    private static final List<SearchProcessor> PROCESSORS;
    static {
        List<SearchProcessor> processors = new LinkedList<SearchProcessor>();
        processors.add(new EqualSearchProcessor());
        processors.add(new IsNullSearchProcessor());

        processors.add(new InSearchProcessor());
        processors.add(new NotInSearchProcessor());

        processors.add(new NotEqualSearchProcessor());
        processors.add(new IsNotNullSearchProcessor());

        processors.add(new GtSearchProcessor());
        processors.add(new GteSearchProcessor());

        processors.add(new LtSearchProcessor());
        processors.add(new LteSearchProcessor());

        processors.add(new BetweenSearchProcessor());
        processors.add(new LikeSearchProcessor());

        processors.add(new AndSearchProcessor());
        processors.add(new OrSearchProcessor());
        PROCESSORS = Collections.unmodifiableList(processors);
    }

    /**
     * default construct
     */
    public Search() {
    }
    
    /**
     * @param value id value
     */
    public Search(Object value) {
        eq(BaseModel.ID, value);
    }

    /**
     * construct by eq
     * @param col col name
     * @param value value
     */
    public Search(String col, Object value) {
        eq(col, value);
    }

    /**
     * construct by eq
     * @param col col name
     * @param values values
     */
    public Search(String col, Collection<?> values) {
        in(col, values);
    }

    /**
     * construct by eq
     * @param col col name
     * @param values values
     */
    public Search(String col, Object[] values) {
        in(col, values);
    }

    /**
     * set table
     * @param table table
     * @return this
     */
    public Search table(String table) {
        this.table = table;
        return this;
    }

    /**
     * clear criteria
     * @return search
     */
    public Search clearCriteria() {
        criteria.clear();
        return this;
    }

    /**
     * clear orders
     * @return search
     */
    public Search clearOrders() {
        orders.clear();
        return this;
    }

    /**
     * col eq
     * @param col col name
     * @param value value
     * @return this
     */
    public Search eq(String col, Object value) {
        if (value == null) {
            criteria.add(new Criterion(CriterionType.IS_NULL, col, value));
        }
        else if (value instanceof Collection) {
            in(col, (Collection<?>) value);
        }
        else if (value.getClass().isArray()) {
            // may primitive, (Object[]) causes exception
            in(col, toList(value));
        }
        else {
            criteria.add(new Criterion(CriterionType.EQ, col, value));
        }
        return this;
    }

    /**
     * same as #eq(col, null)
     * @param col col name
     * @return this
     */
    public Search isNull(String col) {
        return eq(col, null);
    }

    /**
     * col in values array
     * @param col col name
     * @param values values
     * @return this
     */
    public Search in(String col, Object[] values) {
        criteria.add(new Criterion(CriterionType.IN, col, values));
        return this;
    }

    /**
     * col in values list
     * @param col col name
     * @param values values
     * @return this
     */
    public Search in(String col, Collection<?> values) {
        if (values != null && !values.isEmpty()) {
            criteria.add(new Criterion(CriterionType.IN, col, values));
        }
        return this;
    }

    /**
     * col like value
     * @param col col name
     * @param value value
     * @return this
     */
    public Search like(String col, String value) {
        criteria.add(new Criterion(CriterionType.LIKE, col, value));
        return this;
    }

    /**
     * @param col col name
     * @param value value
     * @param wrapValue wrap value with %
     * @return this
     */
    public Search like(String col, String value, boolean wrapValue) {
        return like(col, wrapValue ? "%" + value + "%" : value);
    }

    /**
     * @param col col name
     * @param bottom bottom value
     * @param top top value
     * @return this
     */
    public Search between(String col, Object bottom, Object top) {
        criteria.add(new Criterion(
            CriterionType.BETWEEN,
            col, 
            Arrays.asList(bottom, top)));
        return this;
    }

    /**
     * @param col col name
     * @param value value
     * @return this
     */
    public Search ne(String col, Object value) {
        criteria.add(new Criterion(
            value != null ? CriterionType.NE :
                CriterionType.IS_NOT_NULL, col, value));
        return this;
    }

    /**
     * col not null
     * @param col col
     * @return search
     */
    public Search notNull(String col) {
        criteria.add(new Criterion(
            CriterionType.IS_NOT_NULL, col, (Object) null));
        return this;
    }

    /**
     * @param col col name
     * @param values values
     * @return this
     */
    public Search notIn(String col, Object[] values) {
        return notIn(col, Arrays.asList(values));
    }
    
    /**
     * @param col col name
     * @param values values
     * @return this
     */
    public Search notIn(String col, Collection<?> values) {
        criteria.add(new Criterion(CriterionType.NOT_IN, col, values));
        return this;
    }
    
    /**
     * @param col col name
     * @param value value
     * @return this
     */
    public Search lt(String col, Object value) {
        criteria.add(new Criterion(CriterionType.LT, col, value));
        return this;
    }

    /**
     * @param col col name
     * @param value value
     * @return this
     */
    public Search lte(String col, Object value) {
        criteria.add(new Criterion(CriterionType.LTE, col, value));
        return this;
    }

    /**
     * @param col col name
     * @param value value
     * @return this
     */
    public Search gt(String col, Object value) {
        criteria.add(new Criterion(CriterionType.GT, col, value));
        return this;
    }

    /**
     * @param col col name
     * @param value value
     * @return this
     */
    public Search gte(String col, Object value) {
        criteria.add(new Criterion(CriterionType.GTE, col, value));
        return this;
    }

    /**
     * @param search search
     * @return this
     */
    public Search and(Search search) {
        Assert.notNull(search, "And Search Could Not Be Null");
        if (StringUtils.isBlank(search.table)) {
            search.table = table;
        }
        Assert.state(this != search, "And Search Could Not Be Self");
        criteria.add(new Criterion(CriterionType.AND, search));
        return this;
    }

    /**
     * @param search search
     * @return this
     */
    public Search or(Search search) {
        Assert.notNull(search, "Or Search Could Not Be Null");
        if (StringUtils.isBlank(search.table)) {
            search.table = table;
        }
        Assert.state(this != search, "Or Search Could Not Be Self");
        criteria.add(new Criterion(CriterionType.OR, search));
        return this;
    }

    /**
     * @param col col name
     * @param value value
     * @return this
     */
    public Search or(String col, Object value) {
        return or(new Search(col, value).table(table));
    }

    /**
     * @param col col name
     * @return this
     */
    public Search asc(String col) {
        return order(col, "asc");
    }

    /**
     * @param col col name
     * @return this
     */
    public Search desc(String col) {
        return order(col, "desc");
    }
    
    /**
     * @param offset offset
     * @return this
     */
    public Search offset(Integer offset) {
        if (offset != null) {
            Assert.state(offset >= 0, "Offset May Not Be Negative");
        }
        this.offset = offset;
        return this;
    }

    /**
     * @param offset offset
     * @return this
     */
    public Search offset(int offset) {
        Assert.state(offset >= 0, "Offset May Not Be Negative");
        this.offset = offset;
        return this;
    }

    /**
     * @return offset
     */
    public Integer offset() {
        return offset;
    }

    /**
     * @param limit limit
     * @return this
     */
    public Search limit(Integer limit) {
        if (limit != null) {
            Assert.state(limit > 0, "Limit May Not Be Negative Or Zero");
        }
        this.limit = limit;
        return this;
    }

    /**
     * @param limit limit
     * @return this
     */
    public Search limit(int limit) {
        Assert.state(limit > 0, "Limit May Not Be Negative Or Zero");
        this.limit = limit;
        return this;
    }

    /**
     * @return limit
     */
    public Integer limit() {
        return limit;
    }

    /**
     * @return orders
     */
    public Map<String, String> orders() {
        final Map<String, String> ordersRtn =
            new LinkedHashMap<String, String>(orders.size());
        boolean hasTable = StringUtils.isNotBlank(table);
        for (final Map.Entry<String, String> order : orders.entrySet()) {
            final String col = order.getKey();
            ordersRtn.put(hasTable && !col.contains(".") ?
                table + "." + col : col, order.getValue());
        }
        return ordersRtn;
    }

    /**
     * assemble to sql and value list
     * @return sql and value list
     */
    public List<Object> assemble() {
        return assemble(false);
    }

    /**
     * @return true if has criterion
     */
    public boolean hasCriterion() {
        return !criteria.isEmpty();
    }

    /**
     * @return true if has no criterion
     */
    public boolean hasNoCriterion() {
        return criteria.isEmpty();
    }

    /**
     * @return true if has order
     */
    public boolean hasOrder() {
        return !orders.isEmpty();
    }

    /**
     * @return true if has no order
     */
    public boolean hasNoOrder() {
        return orders.isEmpty();
    }

    /**
     * get attrs data
     * @param name name
     * @param <T> data type
     * @return data
     */
    @SuppressWarnings("unchecked")
    public <T> T getAttr(String name) {
        return (T) attrs.get(name);
    }

    /**
     * set attrs data
     * @param name name
     * @param value value
     * @return this
     */
    public Search setAttr(String name, Object value) {
        attrs.put(name, value);
        return this;
    }

    // --
    // private methods

    /**
     * @param col col name
     * @param direction direction
     * @return this
     */
    Search order(final String col, final String direction) {
        Assert.hasText(col, "Order Column Could Not Be Blank");
        orders.put(col, direction);
        return this;
    }

    /**
     * get table
     * @return table
     */
    String table() {
        return this.table;
    }

    List<Object> assemble(boolean subSearch) {
        List<Object> result = new LinkedList<Object>();
        final SearchProcessArgs args = new SearchProcessArgs(table, result);
        for (final Criterion criterion : criteria) {
            args.setCriterion(criterion);
            for (final SearchProcessor processor : PROCESSORS) {
                if (processor.accept(args)) {
                    processor.process(args);
                    break;
                }
            }
            if (!args.isHasPrevCol()) {
                args.setHasPrevCol(true);
            }
        }

        if (subSearch && criteria.size() > 1) {
            result.add(0, new SqlFragment("("));
            result.add(new SqlFragment(")"));
        }
        return result;
    }

    List<Object> toList(Object source) {
        final int length = Array.getLength(source);
        final List<Object> list = new ArrayList<Object>(length);
        for (int i = 0; i < length; ++i) {
            list.add(Array.get(source, i));
        }
        return list;
    }
}
