/*
 * 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 org.totschnig.myexpenses.ui;

import java.lang.reflect.Field;

import org.totschnig.myexpenses.MyApplication;

import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.ResourceCursorTreeAdapter;
import android.widget.TextView;

/**
 * An easy adapter to map columns from a cursor to TextViews or ImageViews
 * defined in an XML file. You can specify which columns you want, which views
 * you want to display the columns, and the XML file that defines the appearance
 * of these views. Separate XML files for child and groups are possible.
 *
 * Binding occurs in two phases. First, if a
 * {@link android.widget.SimpleCursorTreeAdapter.ViewBinder} is available,
 * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
 * is invoked. If the returned value is true, binding has occurred. If the
 * returned value is false and the view to bind is a TextView,
 * {@link #setViewText(TextView, String)} is invoked. If the returned value
 * is false and the view to bind is an ImageView,
 * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
 * binding can be found, an {@link IllegalStateException} is thrown.
 */
public abstract class SimpleCursorTreeAdapter extends ResourceCursorTreeAdapter {
    
    /** The name of the columns that contain the data to display for a group. */
    private String[] mGroupFromNames;
    
    /** The indices of columns that contain data to display for a group. */
    private int[] mGroupFrom;
    /**
     * The View IDs that will display a group's data fetched from the
     * corresponding column.
     */
    private int[] mGroupTo;

    /** The name of the columns that contain the data to display for a child. */
    private String[] mChildFromNames;
    
    /** The indices of columns that contain data to display for a child. */
    private int[] mChildFrom;
    /**
     * The View IDs that will display a child's data fetched from the
     * corresponding column.
     */
    private int[] mChildTo;
    
    /**
     * View binder, if supplied
     */
    private ViewBinder mViewBinder;

    /**
     * Constructor.
     * 
     * @param context The context where the {@link ExpandableListView}
     *            associated with this {@link SimpleCursorTreeAdapter} is
     *            running
     * @param cursor The database cursor
     * @param collapsedGroupLayout The resource identifier of a layout file that
     *            defines the views for a collapsed group. The layout file
     *            should include at least those named views defined in groupTo.
     * @param expandedGroupLayout The resource identifier of a layout file that
     *            defines the views for an expanded group. The layout file
     *            should include at least those named views defined in groupTo.
     * @param groupFrom A list of column names that will be used to display the
     *            data for a group.
     * @param groupTo The group views (from the group layouts) that should
     *            display column in the "from" parameter. These should all be
     *            TextViews or ImageViews. The first N views in this list are
     *            given the values of the first N columns in the from parameter.
     * @param childLayout The resource identifier of a layout file that defines
     *            the views for a child (except the last). The layout file
     *            should include at least those named views defined in childTo.
     * @param lastChildLayout The resource identifier of a layout file that
     *            defines the views for the last child within a group. The
     *            layout file should include at least those named views defined
     *            in childTo.
     * @param childFrom A list of column names that will be used to display the
     *            data for a child.
     * @param childTo The child views (from the child layouts) that should
     *            display column in the "from" parameter. These should all be
     *            TextViews or ImageViews. The first N views in this list are
     *            given the values of the first N columns in the from parameter.
     */
    public SimpleCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout,
            int expandedGroupLayout, String[] groupFrom, int[] groupTo, int childLayout,
            int lastChildLayout, String[] childFrom, int[] childTo) {
        super(context, cursor, collapsedGroupLayout, expandedGroupLayout, childLayout,
                lastChildLayout);
        init(groupFrom, groupTo, childFrom, childTo);
    }

    /**
     * Constructor.
     * 
     * @param context The context where the {@link ExpandableListView}
     *            associated with this {@link SimpleCursorTreeAdapter} is
     *            running
     * @param cursor The database cursor
     * @param collapsedGroupLayout The resource identifier of a layout file that
     *            defines the views for a collapsed group. The layout file
     *            should include at least those named views defined in groupTo.
     * @param expandedGroupLayout The resource identifier of a layout file that
     *            defines the views for an expanded group. The layout file
     *            should include at least those named views defined in groupTo.
     * @param groupFrom A list of column names that will be used to display the
     *            data for a group.
     * @param groupTo The group views (from the group layouts) that should
     *            display column in the "from" parameter. These should all be
     *            TextViews or ImageViews. The first N views in this list are
     *            given the values of the first N columns in the from parameter.
     * @param childLayout The resource identifier of a layout file that defines
     *            the views for a child. The layout file
     *            should include at least those named views defined in childTo.
     * @param childFrom A list of column names that will be used to display the
     *            data for a child.
     * @param childTo The child views (from the child layouts) that should
     *            display column in the "from" parameter. These should all be
     *            TextViews or ImageViews. The first N views in this list are
     *            given the values of the first N columns in the from parameter.
     */
    public SimpleCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout,
            int expandedGroupLayout, String[] groupFrom, int[] groupTo,
            int childLayout, String[] childFrom, int[] childTo) {
        super(context, cursor, collapsedGroupLayout, expandedGroupLayout, childLayout);
        init(groupFrom, groupTo, childFrom, childTo);
    }

    /**
     * Constructor.
     * 
     * @param context The context where the {@link ExpandableListView}
     *            associated with this {@link SimpleCursorTreeAdapter} is
     *            running
     * @param cursor The database cursor
     * @param groupLayout The resource identifier of a layout file that defines
     *            the views for a group. The layout file should include at least
     *            those named views defined in groupTo.
     * @param groupFrom A list of column names that will be used to display the
     *            data for a group.
     * @param groupTo The group views (from the group layouts) that should
     *            display column in the "from" parameter. These should all be
     *            TextViews or ImageViews. The first N views in this list are
     *            given the values of the first N columns in the from parameter.
     * @param childLayout The resource identifier of a layout file that defines
     *            the views for a child. The layout file should include at least
     *            those named views defined in childTo.
     * @param childFrom A list of column names that will be used to display the
     *            data for a child.
     * @param childTo The child views (from the child layouts) that should
     *            display column in the "from" parameter. These should all be
     *            TextViews or ImageViews. The first N views in this list are
     *            given the values of the first N columns in the from parameter.
     */
    public SimpleCursorTreeAdapter(Context context, Cursor cursor, int groupLayout,
            String[] groupFrom, int[] groupTo, int childLayout, String[] childFrom,
            int[] childTo) {
        super(context, cursor, groupLayout, childLayout);
        init(groupFrom, groupTo, childFrom, childTo);
    }

    private void init(String[] groupFromNames, int[] groupTo, String[] childFromNames,
            int[] childTo) {
        
        mGroupFromNames = groupFromNames;
        mGroupTo = groupTo;
        
        mChildFromNames = childFromNames;
        mChildTo = childTo;
        try {
          // use reflection to disable auto-requery thus preventing strict mode violations
          Class cls = Class.forName("android.widget.CursorTreeAdapter");
          Field field = cls.getDeclaredField("mAutoRequery");
          field.setAccessible(true);
          field.set(this, false);
        } catch (Exception e) {
          Log.e(MyApplication.TAG, e.getMessage());
        }
    }
    
    /**
     * Returns the {@link ViewBinder} used to bind data to views.
     *
     * @return a ViewBinder or null if the binder does not exist
     *
     * @see #setViewBinder(android.widget.SimpleCursorTreeAdapter.ViewBinder)
     */
    public ViewBinder getViewBinder() {
        return mViewBinder;
    }

    /**
     * Sets the binder used to bind data to views.
     *
     * @param viewBinder the binder used to bind data to views, can be null to
     *        remove the existing binder
     *
     * @see #getViewBinder()
     */
    public void setViewBinder(ViewBinder viewBinder) {
        mViewBinder = viewBinder;
    }

    private void bindView(View view, Context context, Cursor cursor, int[] from, int[] to) {
        final ViewBinder binder = mViewBinder;
        
        for (int i = 0; i < to.length; i++) {
            View v = view.findViewById(to[i]);
            if (v != null) {
                boolean bound = false;
                if (binder != null) {
                    bound = binder.setViewValue(v, cursor, from[i]);
                }
                
                if (!bound) {
                    String text = cursor.getString(from[i]);
                    if (text == null) {
                        text = "";
                    }
                    if (v instanceof TextView) {
                        setViewText((TextView) v, text);
                    } else if (v instanceof ImageView) {
                        setViewImage((ImageView) v, text);
                    } else {
                        throw new IllegalStateException("SimpleCursorTreeAdapter can bind values" +
                                " only to TextView and ImageView!");
                    }
                }
            }
        }
    }
    
    private void initFromColumns(Cursor cursor, String[] fromColumnNames, int[] fromColumns) {
        for (int i = fromColumnNames.length - 1; i >= 0; i--) {
            fromColumns[i] = cursor.getColumnIndexOrThrow(fromColumnNames[i]);
        }
    }
    
    @Override
    protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
        if (mChildFrom == null) {
            mChildFrom = new int[mChildFromNames.length];
            initFromColumns(cursor, mChildFromNames, mChildFrom);
        }
        
        bindView(view, context, cursor, mChildFrom, mChildTo);
    }

    @Override
    protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
        if (mGroupFrom == null) {
            mGroupFrom = new int[mGroupFromNames.length];
            initFromColumns(cursor, mGroupFromNames, mGroupFrom);
        }
        
        bindView(view, context, cursor, mGroupFrom, mGroupTo);
    }

    /**
     * Called by bindView() to set the image for an ImageView. By default, the
     * value will be treated as a Uri. Intended to be overridden by Adapters
     * that need to filter strings retrieved from the database.
     * 
     * @param v ImageView to receive an image
     * @param value the value retrieved from the cursor
     */
    protected void setViewImage(ImageView v, String value) {
        try {
            v.setImageResource(Integer.parseInt(value));
        } catch (NumberFormatException nfe) {
            v.setImageURI(Uri.parse(value));
        }
    }

    /**
     * Called by bindView() to set the text for a TextView but only if
     * there is no existing ViewBinder or if the existing ViewBinder cannot
     * handle binding to an TextView.
     *
     * Intended to be overridden by Adapters that need to filter strings
     * retrieved from the database.
     * 
     * @param v TextView to receive text
     * @param text the text to be set for the TextView
     */
    public void setViewText(TextView v, String text) {
        v.setText(text);
    }

    /**
     * This class can be used by external clients of SimpleCursorTreeAdapter
     * to bind values from the Cursor to views.
     *
     * You should use this class to bind values from the Cursor to views
     * that are not directly supported by SimpleCursorTreeAdapter or to
     * change the way binding occurs for views supported by
     * SimpleCursorTreeAdapter.
     *
     * @see SimpleCursorTreeAdapter#setViewImage(ImageView, String) 
     * @see SimpleCursorTreeAdapter#setViewText(TextView, String)
     */
    public static interface ViewBinder {
        /**
         * Binds the Cursor column defined by the specified index to the specified view.
         *
         * When binding is handled by this ViewBinder, this method must return true.
         * If this method returns false, SimpleCursorTreeAdapter will attempts to handle
         * the binding on its own.
         *
         * @param view the view to bind the data to
         * @param cursor the cursor to get the data from
         * @param columnIndex the column at which the data can be found in the cursor
         *
         * @return true if the data was bound to the view, false otherwise
         */
        boolean setViewValue(View view, Cursor cursor, int columnIndex);
    }
}
