/*
 * Copyright (C) 2012 The K-9 Dog Walkers
 * Copyright (C) 2006 The Android Open Source Project
 *
 * 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.fsck.k9.helper;

import java.util.Comparator;

import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.database.CharArrayBuffer;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;


/**
 * This class can be used to combine multiple {@link Cursor}s into one.
 */
public class MergeCursor implements Cursor {
    /**
     * List of the cursors combined in this object.
     */
    protected final Cursor[] mCursors;

    /**
     * The currently active cursor.
     */
    protected Cursor mActiveCursor;

    /**
     * The index of the currently active cursor in {@link #mCursors}.
     *
     * @see #mActiveCursor
     */
    protected int mActiveCursorIndex;

    /**
     * The cursor's current position.
     */
    protected int mPosition;

    /**
     * Used to cache the value of {@link #getCount()}.
     */
    private int mCount = -1;

    /**
     * The comparator that is used to decide how the individual cursors are merged.
     */
    private final Comparator<Cursor> mComparator;


    /**
     * Constructor
     *
     * @param cursors
     *         The list of cursors this {@code MultiCursor} should combine.
     * @param comparator
     *         A comparator that is used to decide in what order the individual cursors are merged.
     */
    public MergeCursor(Cursor[] cursors, Comparator<Cursor> comparator) {
        mCursors = cursors.clone();
        mComparator = comparator;

        resetCursors();
    }

    private void resetCursors() {
        mActiveCursorIndex = -1;
        mActiveCursor = null;
        mPosition = -1;

        for (int i = 0, len = mCursors.length; i < len; i++) {
            Cursor cursor = mCursors[i];
            if (cursor != null) {
                cursor.moveToPosition(-1);

                if (mActiveCursor == null) {
                    mActiveCursorIndex = i;
                    mActiveCursor = mCursors[mActiveCursorIndex];
                }
            }
        }
    }

    @Override
    public void close() {
        for (Cursor cursor : mCursors) {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    @Override
    public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
        mActiveCursor.copyStringToBuffer(columnIndex, buffer);
    }

    @Override
    public void deactivate() {
        for (Cursor cursor : mCursors) {
            if (cursor != null) {
                cursor.deactivate();
            }
        }
    }

    @Override
    public byte[] getBlob(int columnIndex) {
        return mActiveCursor.getBlob(columnIndex);
    }

    @Override
    public int getColumnCount() {
        return mActiveCursor.getColumnCount();
    }

    @Override
    public int getColumnIndex(String columnName) {
        return mActiveCursor.getColumnIndex(columnName);
    }

    @Override
    public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
        return mActiveCursor.getColumnIndexOrThrow(columnName);
    }

    @Override
    public String getColumnName(int columnIndex) {
        return mActiveCursor.getColumnName(columnIndex);
    }

    @Override
    public String[] getColumnNames() {
        return mActiveCursor.getColumnNames();
    }

    @Override
    public int getCount() {
        // CursorLoaders seem to call getCount() a lot. So we're caching the aggregated count.
        if (mCount == -1) {
            int count = 0;
            for (Cursor cursor : mCursors) {
                if (cursor != null) {
                    count += cursor.getCount();
                }
            }

            mCount = count;
        }

        return mCount;
    }

    @Override
    public double getDouble(int columnIndex) {
        return mActiveCursor.getDouble(columnIndex);
    }

    @Override
    public float getFloat(int columnIndex) {
        return mActiveCursor.getFloat(columnIndex);
    }

    @Override
    public int getInt(int columnIndex) {
        return mActiveCursor.getInt(columnIndex);
    }

    @Override
    public long getLong(int columnIndex) {
        return mActiveCursor.getLong(columnIndex);
    }

    @Override
    public int getPosition() {
        return mPosition;
    }

    @Override
    public short getShort(int columnIndex) {
        return mActiveCursor.getShort(columnIndex);
    }

    @Override
    public String getString(int columnIndex) {
        return mActiveCursor.getString(columnIndex);
    }

    @TargetApi(11)
    @Override
    public int getType(int columnIndex) {
        return mActiveCursor.getType(columnIndex);
    }

    @Override
    public boolean getWantsAllOnMoveCalls() {
        return mActiveCursor.getWantsAllOnMoveCalls();
    }

    @Override
    public boolean isAfterLast() {
        int count = getCount();
        if (count == 0) {
            return true;
        }

        return (mPosition == count);
    }

    @Override
    public boolean isBeforeFirst() {
        if (getCount() == 0) {
            return true;
        }

        return (mPosition == -1);
    }

    @Override
    public boolean isClosed() {
        return mActiveCursor.isClosed();
    }

    @Override
    public boolean isFirst() {
        if (getCount() == 0) {
            return false;
        }

        return (mPosition == 0);
    }

    @Override
    public boolean isLast() {
        int count = getCount();
        if (count == 0) {
            return false;
        }

        return (mPosition == (count - 1));
    }

    @Override
    public boolean isNull(int columnIndex) {
        return mActiveCursor.isNull(columnIndex);
    }

    @Override
    public boolean move(int offset) {
        return moveToPosition(mPosition + offset);
    }

    @Override
    public boolean moveToFirst() {
        return moveToPosition(0);
    }

    @Override
    public boolean moveToLast() {
        return moveToPosition(getCount() - 1);
    }

    @Override
    public boolean moveToNext() {
        int count = getCount();
        if (mPosition == count) {
            return false;
        }

        if (mPosition == count - 1) {
            mActiveCursor.moveToNext();
            mPosition++;
            return false;
        }

        int smallest = -1;
        for (int i = 0, len = mCursors.length; i < len; i++) {
            if (mCursors[i] == null || mCursors[i].getCount() == 0 || mCursors[i].isLast()) {
                continue;
            }

            if (smallest == -1) {
                smallest = i;
                mCursors[smallest].moveToNext();
                continue;
            }

            Cursor left = mCursors[smallest];
            Cursor right = mCursors[i];

            right.moveToNext();

            int result = mComparator.compare(left, right);
            if (result > 0) {
                smallest = i;
                left.moveToPrevious();
            } else {
                right.moveToPrevious();
            }
        }

        mPosition++;
        if (smallest != -1) {
            mActiveCursorIndex = smallest;
            mActiveCursor = mCursors[mActiveCursorIndex];
        }

        return true;
    }

    @Override
    public boolean moveToPosition(int position) {
        // Make sure position isn't past the end of the cursor
        final int count = getCount();
        if (position >= count) {
            mPosition = count;
            return false;
        }

        // Make sure position isn't before the beginning of the cursor
        if (position < 0) {
            mPosition = -1;
            return false;
        }

        // Check for no-op moves, and skip the rest of the work for them
        if (position == mPosition) {
            return true;
        }

        if (position > mPosition) {
            for (int i = 0, end = position - mPosition; i < end; i++) {
                if (!moveToNext()) {
                    return false;
                }
            }
        } else {
            for (int i = 0, end = mPosition - position; i < end; i++) {
                if (!moveToPrevious()) {
                    return false;
                }
            }
        }

        return true;
    }

    @Override
    public boolean moveToPrevious() {
        if (mPosition < 0) {
            return false;
        }

        mActiveCursor.moveToPrevious();

        if (mPosition == 0) {
            mPosition = -1;
            return false;
        }

        int greatest = -1;
        for (int i = 0, len = mCursors.length; i < len; i++) {
            if (mCursors[i] == null || mCursors[i].isBeforeFirst()) {
                continue;
            }

            if (greatest == -1) {
                greatest = i;
                continue;
            }

            Cursor left = mCursors[greatest];
            Cursor right = mCursors[i];

            int result = mComparator.compare(left, right);
            if (result <= 0) {
                greatest = i;
            }
        }

        mPosition--;
        if (greatest != -1) {
            mActiveCursorIndex = greatest;
            mActiveCursor = mCursors[mActiveCursorIndex];
        }

        return true;
    }

    @Override
    public void registerContentObserver(ContentObserver observer) {
        for (Cursor cursor : mCursors) {
            cursor.registerContentObserver(observer);
        }
    }

    @Override
    public void registerDataSetObserver(DataSetObserver observer) {
        for (Cursor cursor : mCursors) {
            cursor.registerDataSetObserver(observer);
        }
    }

    @Deprecated
    @Override
    public boolean requery() {
        boolean success = true;
        for (Cursor cursor : mCursors) {
            success &= cursor.requery();
        }

        return success;
    }

    @Override
    public void setNotificationUri(ContentResolver cr, Uri uri) {
        for (Cursor cursor : mCursors) {
            cursor.setNotificationUri(cr, uri);
        }
    }

    @Override
    public void unregisterContentObserver(ContentObserver observer) {
        for (Cursor cursor : mCursors) {
            cursor.unregisterContentObserver(observer);
        }
    }

    @Override
    public void unregisterDataSetObserver(DataSetObserver observer) {
        for (Cursor cursor : mCursors) {
            cursor.unregisterDataSetObserver(observer);
        }
    }

    @Override
    public Bundle getExtras() {
        throw new RuntimeException("Not implemented");
    }

    @Override
    public Bundle respond(Bundle extras) {
        throw new RuntimeException("Not implemented");
    }
}
