/**
 * Copyright 2008-2016 Evernote Corporation. All rights reserved.
 */
package com.evernote.android.data;

import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;

import com.evernote.android.data.sel.Sel;
import com.evernote.android.data.sel.Selection;

import java.util.Collection;
import java.util.concurrent.Callable;

import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;

/**
 * @author xlepaul
 * @author rwondratschek
 * @since 2015-06-29
 */
public abstract class QueryBuilder<Source, Queryable, Self extends QueryBuilder<Source, Queryable, Self>> {

    /**
     * @return a QueryBuilder for {@link ContentResolver} queries
     */
    public static CR cr() {
        return new CR();
    }

    public static CR cr(Uri uri) {
        return new CR().source(uri);
    }

    /**
     * @return a QueryBuilder for {@link SQLiteDatabase} queries
     */
    public static DB db() {
        return new DB();
    }

    public static DB db(String table) {
        return new DB().source(table);
    }

    protected Source source;
    protected String[] projection;
    protected String selection;
    protected String[] selectionArgs;
    protected String sortOrder;

    protected QueryBuilder() {
    }

    protected abstract Self self();

    public Self source(Source source) {
        checkNull(this.source, "source");
        this.source = source;
        return self();
    }

    public Self projection(String... projection) {
        checkNull(this.projection, "projection");
        this.projection = projection;
        return self();
    }

    public Self selection(String selection) {
        checkNull(this.selection, "selection");
        this.selection = selection;
        return self();
    }

    public Self selectionArgs(String... selectionArgs) {
        checkNull(this.selectionArgs, "selectionArgs");
        this.selectionArgs = selectionArgs;
        return self();
    }

    public Self selectionArgs(Collection<String> selectionArgs) {
        return selectionArgs(selectionArgs.toArray(new String[selectionArgs.size()]));
    }

    public Self select(String columnName, String value) {
        return select(Sel.filter(columnName, value));
    }

    /**
     * @throws IllegalArgumentException if there are no values.
     */
    public Self select(String columnName, String... values) {
        return select(Sel.filter(columnName, values));
    }

    /**
     * @throws IllegalArgumentException if the list is empty.
     */
    public Self select(String columnName, Collection<String> values) {
        return select(Sel.filter(columnName, values));
    }

    public Self select(Selection selection) {
        return selection(selection.sql()).selectionArgs(selection.params());
    }

    public Self sortOrder(String sortOrder) {
        checkNull(this.sortOrder, "sortOrder");
        this.sortOrder = sortOrder;
        return self();
    }

    protected final void checkNull(Object field, String name) {
        if (field != null) {
            throw new IllegalStateException(name + " has already been set");
        }
    }

    public abstract Cursor query(Queryable queryable);

    public final Maybe<Cursor> queryStream(final Queryable queryable) {
        return Maybe.fromCallable(new Callable<Cursor>() {
            @Override
            public Cursor call() throws Exception {
                return query(queryable);
            }
        });
    }

    public final Fetcher fetch(Queryable queryable) {
        return Fetcher.of(query(queryable));
    }

    public final <ResultT> Observable<ResultT> fetchStream(final Queryable queryable, final Converter<ResultT> converter) {
        return Observable.create(new ObservableOnSubscribe<ResultT>() {
            @Override
            public void subscribe(ObservableEmitter<ResultT> e) throws Exception {
                fetch(queryable).subscribe(converter, e);
            }
        });
    }

    public static class CR extends QueryBuilder<Uri, ContentResolver, CR> {
        @Override
        protected CR self() {
            return this;
        }

        @Override
        public Cursor query(ContentResolver cr) {
            return cr.query(source, projection, selection, selectionArgs, sortOrder);
        }

        public Cursor query(Context context) {
            return query(context.getContentResolver());
        }

        public Maybe<Cursor> queryStream(Context context) {
            return queryStream(context.getContentResolver());
        }

        public Fetcher fetch(Context context) {
            return fetch(context.getContentResolver());
        }

        public <ResultT> Observable<ResultT> fetchStream(Context context, Converter<ResultT> converter) {
            return fetchStream(context.getContentResolver(), converter);
        }
    }

    public static class DB extends QueryBuilder<String, SQLiteDatabase, DB> {

        private boolean distinct;
        private String groupBy;
        private String having;
        private String limit;

        @Override
        protected DB self() {
            return this;
        }

        public DB distinctValues() {
            this.distinct = true;
            return this;
        }

        public DB groupBy(String groupBy) {
            checkNull(this.groupBy, "groupBy");
            this.groupBy = groupBy;
            return this;
        }

        public DB having(String having) {
            checkNull(this.having, "having");
            this.having = having;
            return this;
        }

        public DB limit(String limit) {
            checkNull(this.limit, "limit");
            this.limit = limit;
            return this;
        }

        @Override
        public Cursor query(SQLiteDatabase db) {
            return db.query(distinct, source, projection, selection, selectionArgs,
                    groupBy, having, sortOrder, limit);
        }
    }

}