/**
 * Copyright (c) 2012 Todoroo Inc
 *
 * See the file "LICENSE" for the full license governing this code.
 */
package com.todoroo.astrid.core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.ActionBar;
import android.support.v4.app.FragmentActivity;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnCreateContextMenuListener;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;

import com.timsu.astrid.R;
import com.todoroo.andlib.data.Property.CountProperty;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Field;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.sql.UnaryCriterion;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.actfm.TagSettingsActivity;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.CustomFilterCriterion;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.MultipleSelectCriterion;
import com.todoroo.astrid.api.PermaSql;
import com.todoroo.astrid.api.TextInputCriterion;
import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskApiDao.TaskCriteria;
import com.todoroo.astrid.service.StatisticsService;
import com.todoroo.astrid.service.ThemeService;
import com.todoroo.astrid.utility.AstridPreferences;

/**
 * Activity that allows users to build custom filters
 *
 * @author Tim Su <tim@todoroo.com>
 *
 */
public class CustomFilterActivity extends FragmentActivity {

    private static final String IDENTIFIER_TITLE = "title"; //$NON-NLS-1$
    private static final String IDENTIFIER_IMPORTANCE = "importance"; //$NON-NLS-1$
    private static final String IDENTIFIER_DUEDATE = "dueDate"; //$NON-NLS-1$
    private static final String IDENTIFIER_UNIVERSE = "active"; //$NON-NLS-1$

    static final int MENU_GROUP_FILTER = 0;
    static final int MENU_GROUP_CONTEXT_TYPE = 1;
    static final int MENU_GROUP_CONTEXT_DELETE = 2;

    // --- hierarchy of filter classes

    public static class CriterionInstance {
        public static final int TYPE_ADD = 0;
        public static final int TYPE_SUBTRACT = 1;
        public static final int TYPE_INTERSECT = 2;
        public static final int TYPE_UNIVERSE = 3;

        /** criteria for this instance */
        public CustomFilterCriterion criterion;

        /** which of the entries is selected (MultipleSelect) */
        public int selectedIndex = -1;

        /** text of selection (TextInput) */
        public String selectedText = null;

        /** type of join */
        public int type = TYPE_INTERSECT;

        /** statistics for {@link FilterView} */
        public int start, end, max;

        @SuppressWarnings("nls")
        public String getTitleFromCriterion() {
            if(criterion instanceof MultipleSelectCriterion) {
                if(selectedIndex >= 0 && ((MultipleSelectCriterion)criterion).entryTitles != null &&
                        selectedIndex < ((MultipleSelectCriterion)criterion).entryTitles.length) {
                    String title = ((MultipleSelectCriterion)criterion).entryTitles[selectedIndex];
                    return criterion.text.replace("?", title);
                }
                return criterion.text;
            } else if(criterion instanceof TextInputCriterion) {
                if(selectedText == null)
                    return criterion.text;
                return criterion.text.replace("?", selectedText);
            }
            throw new UnsupportedOperationException("Unknown criterion type"); //$NON-NLS-1$
        }

        public String getValueFromCriterion() {
            if(type == TYPE_UNIVERSE)
                return null;
            if(criterion instanceof MultipleSelectCriterion) {
                if(selectedIndex >= 0 && ((MultipleSelectCriterion)criterion).entryValues != null &&
                        selectedIndex < ((MultipleSelectCriterion)criterion).entryValues.length) {
                    return ((MultipleSelectCriterion)criterion).entryValues[selectedIndex];
                }
                return criterion.text;
            } else if(criterion instanceof TextInputCriterion) {
                return selectedText;
            }
            throw new UnsupportedOperationException("Unknown criterion type"); //$NON-NLS-1$
        }
    }

    private ListView listView;
    private TextView filterName;
    private boolean isDialog;

    private CustomFilterAdapter adapter;
    private final Map<String,CustomFilterCriterion> criteria = Collections.synchronizedMap(new LinkedHashMap<String,CustomFilterCriterion>());

    private final FilterCriteriaReceiver filterCriteriaReceiver = new FilterCriteriaReceiver();

    // --- activity

    @Autowired
    Database database;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setupForDialogOrFullscreen();
        super.onCreate(savedInstanceState);
        ContextManager.setContext(this);

        ActionBar ab = getSupportActionBar();
        if (ab != null)
            ab.setDisplayHomeAsUpEnabled(true);

        setContentView(R.layout.custom_filter_activity);
        setTitle(R.string.CFA_title);

        listView = (ListView) findViewById(android.R.id.list);

        DependencyInjectionService.getInstance().inject(this);
        database.openForReading();
        populateCriteria();

        filterName = (TextView)findViewById(R.id.filterName);
        List<CriterionInstance> startingCriteria = new ArrayList<CriterionInstance>();
        startingCriteria.add(getStartingUniverse());
        adapter = new CustomFilterAdapter(this, startingCriteria);
        listView.setAdapter(adapter);
        updateList();

        setUpListeners();
    }

    private void setupForDialogOrFullscreen() {
        isDialog = AstridPreferences.useTabletLayout(this);
        if (isDialog)
            setTheme(ThemeService.getDialogTheme());
        else
            ThemeService.applyTheme(this);
    }

    /**
     * Populate criteria list with built in and plugin criteria. The request is sent to every application
     * registered to listen for this broadcast. Each plugin can then add criteria to this activity.
     */
    @SuppressWarnings("nls")
    private void populateCriteria() {
        Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_REQUEST_CUSTOM_FILTER_CRITERIA);
        sendOrderedBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);

        Resources r = getResources();

        // built in criteria: due date
        {
            String[] entryValues = new String[] {
                    "0",
                    PermaSql.VALUE_EOD_YESTERDAY,
                    PermaSql.VALUE_EOD,
                    PermaSql.VALUE_EOD_TOMORROW,
                    PermaSql.VALUE_EOD_DAY_AFTER,
                    PermaSql.VALUE_EOD_NEXT_WEEK,
                    PermaSql.VALUE_EOD_NEXT_MONTH,
            };
            ContentValues values = new ContentValues();
            values.put(Task.DUE_DATE.name, "?");
            CustomFilterCriterion criterion = new MultipleSelectCriterion(
                    IDENTIFIER_DUEDATE,
                    getString(R.string.CFC_dueBefore_text),
                    Query.select(Task.ID).from(Task.TABLE).where(
                            Criterion.and(
                                    TaskCriteria.activeVisibleMine(),
                                    Criterion.or(
                                            Field.field("?").eq(0),
                                            Task.DUE_DATE.gt(0)),
                                    Task.DUE_DATE.lte("?"))).toString(),
                    values, r.getStringArray(R.array.CFC_dueBefore_entries),
                    entryValues, ((BitmapDrawable)r.getDrawable(R.drawable.tango_calendar)).getBitmap(),
                    getString(R.string.CFC_dueBefore_name));
            criteria.put(IDENTIFIER_DUEDATE, criterion);
        }

        // built in criteria: importance
        {
            String[] entryValues = new String[] {
                            Integer.toString(Task.IMPORTANCE_DO_OR_DIE),
                            Integer.toString(Task.IMPORTANCE_MUST_DO),
                            Integer.toString(Task.IMPORTANCE_SHOULD_DO),
                            Integer.toString(Task.IMPORTANCE_NONE),
                    };
            String[] entries = new String[] {
                    "!!!", "!!", "!", "o"
            };
            ContentValues values = new ContentValues();
            values.put(Task.IMPORTANCE.name, "?");
            CustomFilterCriterion criterion = new MultipleSelectCriterion(
                    IDENTIFIER_IMPORTANCE,
                    getString(R.string.CFC_importance_text),
                    Query.select(Task.ID).from(Task.TABLE).where(
                            Criterion.and(TaskCriteria.activeVisibleMine(),
                                    Task.IMPORTANCE.lte("?"))).toString(),
                    values, entries,
                    entryValues, ((BitmapDrawable)r.getDrawable(R.drawable.tango_warning)).getBitmap(),
                    getString(R.string.CFC_importance_name));
            criteria.put(IDENTIFIER_IMPORTANCE, criterion);
        }

        // built in criteria: title containing X
        {
            ContentValues values = new ContentValues();
            values.put(Task.TITLE.name, "?");
            CustomFilterCriterion criterion = new TextInputCriterion(
                    IDENTIFIER_TITLE,
                    getString(R.string.CFC_title_contains_text),
                    Query.select(Task.ID).from(Task.TABLE).where(
                            Criterion.and(TaskCriteria.activeVisibleMine(),
                                    Task.TITLE.like("%?%"))).toString(),
                        null, getString(R.string.CFC_title_contains_name), "",
                        ((BitmapDrawable)r.getDrawable(R.drawable.tango_alpha)).getBitmap(),
                        getString(R.string.CFC_title_contains_name));
            criteria.put(IDENTIFIER_TITLE, criterion);
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
    }

    @Override
    protected void onStop() {
        StatisticsService.sessionStop(this);
        super.onStop();
    }

    @Override
    protected void onResume() {
        super.onResume();
        StatisticsService.sessionStart(this);
        registerReceiver(filterCriteriaReceiver, new IntentFilter(AstridApiConstants.BROADCAST_SEND_CUSTOM_FILTER_CRITERIA));
        populateCriteria();
    }

    @Override
    protected void onPause() {
        StatisticsService.sessionPause();
        super.onPause();
        unregisterReceiver(filterCriteriaReceiver);
    }

    private CriterionInstance getStartingUniverse() {
        CriterionInstance instance = new CriterionInstance();
        instance.criterion = new MultipleSelectCriterion(IDENTIFIER_UNIVERSE,
                getString(R.string.CFA_universe_all),
                null, null, null, null, null, null);
        instance.type = CriterionInstance.TYPE_UNIVERSE;
        return instance;
    }

    private void setUpListeners() {
        ((Button)findViewById(R.id.add)).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                listView.showContextMenu();
            }
        });

        final Button saveAndView = ((Button)findViewById(R.id.saveAndView));
        saveAndView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                saveAndView();
            }
        });

        filterName.addTextChangedListener(new TextWatcher() {
            @Override
            public void afterTextChanged(Editable s) {
                if(s.length() == 0) {
                    saveAndView.setText(R.string.CFA_button_view);
                } else {
                    saveAndView.setText(R.string.CFA_button_save);
                }
            }
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count,
                    int after) {
                //
            }
            @Override
            public void onTextChanged(CharSequence s, int start, int before,
                    int count) {
                //
            }
        });

        listView.setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
            @Override
            public void onCreateContextMenu(ContextMenu menu, View v,
                    ContextMenuInfo menuInfo) {
                if(menu.hasVisibleItems()) {
                    /* If it has items already, then the user did not click on the "Add Criteria" button, but instead
                       long held on a row in the list view, which caused CustomFilterAdapter.onCreateContextMenu
                       to be invoked before this onCreateContextMenu method was invoked.
                     */
                    return;
                }

                int i = 0;
                for (CustomFilterCriterion item : criteria.values()) {
                    try {
                        menu.add(CustomFilterActivity.MENU_GROUP_FILTER,
                                i, 0, item.name);
                    } catch (NullPointerException e) {
                        throw new NullPointerException("One of the criteria is null. Criteria: " + criteria); //$NON-NLS-1$
                    }
                    i++;
                }
            }
        });
    }

    @Override
    public void finish() {
        super.finish();
        if (!AstridPreferences.useTabletLayout(this))
            AndroidUtilities.callOverridePendingTransition(this, R.anim.slide_right_in, R.anim.slide_right_out);
    }


    // --- listeners and action events

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
        if(menu.size() > 0)
            menu.clear();

        // view holder
        if(v.getTag() != null) {
            adapter.onCreateContextMenu(menu, v);
        }
    }

    @SuppressWarnings("nls")
    void saveAndView() {
        StringBuilder sql = new StringBuilder(" WHERE ");
        StringBuilder suggestedTitle = new StringBuilder();
        ContentValues values = new ContentValues();
        for(int i = 0; i < adapter.getCount(); i++) {
            CriterionInstance instance = adapter.getItem(i);
            String value = instance.getValueFromCriterion();
            if(value == null && instance.criterion.sql != null && instance.criterion.sql.contains("?"))
                value = "";

            String title = instance.getTitleFromCriterion();

            switch(instance.type) {
            case CriterionInstance.TYPE_ADD:
                sql.append("OR ");
                suggestedTitle.append(getString(R.string.CFA_type_add)).append(' ').
                    append(title).append(' ');
                break;
            case CriterionInstance.TYPE_SUBTRACT:
                sql.append("AND NOT ");
                suggestedTitle.append(getString(R.string.CFA_type_subtract)).append(' ').
                    append(title).append(' ');
                break;
            case CriterionInstance.TYPE_INTERSECT:
                sql.append("AND ");
                suggestedTitle.append(title).append(' ');
                break;
            case CriterionInstance.TYPE_UNIVERSE:
            }


            // special code for all tasks universe
            if(instance.criterion.sql == null)
                sql.append(TaskCriteria.activeVisibleMine()).append(' ');
            else {
                String subSql = instance.criterion.sql.replace("?", UnaryCriterion.sanitize(value));
                sql.append(Task.ID).append(" IN (").append(subSql).append(") ");
            }

            if(instance.criterion.valuesForNewTasks != null &&
                    instance.type == CriterionInstance.TYPE_INTERSECT) {
                for(Entry<String, Object> entry : instance.criterion.valuesForNewTasks.valueSet()) {
                    values.put(entry.getKey().replace("?", value),
                            entry.getValue().toString().replace("?", value));
                }
            }
        }

        String title;
        if(filterName.getText().length() > 0) {
            // persist saved filter
            title = filterName.getText().toString().trim();
            SavedFilter.persist(adapter, title, sql.toString(), values);
        } else {
            // temporary
            title = suggestedTitle.toString();
        }

        // view
        Filter filter = new Filter(title, title, sql.toString(), values);
        setResult(RESULT_OK, new Intent().putExtra(TagSettingsActivity.TOKEN_NEW_FILTER, filter));
        finish();
    }

    /**
     * Recalculate all sizes
     */
    @SuppressWarnings("nls")
    void updateList() {
        int max = 0, last = -1;

        StringBuilder sql = new StringBuilder(Query.select(new CountProperty()).from(Task.TABLE).toString()).
            append(" WHERE ");

        for(int i = 0; i < adapter.getCount(); i++) {
            CriterionInstance instance = adapter.getItem(i);
            String value = instance.getValueFromCriterion();
            if(value == null && instance.criterion.sql != null && instance.criterion.sql.contains("?"))
                value = "";

            switch(instance.type) {
            case CriterionInstance.TYPE_ADD:
                sql.append("OR ");
                break;
            case CriterionInstance.TYPE_SUBTRACT:
                sql.append("AND NOT ");
                break;
            case CriterionInstance.TYPE_INTERSECT:
                sql.append("AND ");
                break;
            case CriterionInstance.TYPE_UNIVERSE:
            }

            // special code for all tasks universe
            if(instance.criterion.sql == null)
                sql.append(TaskCriteria.activeVisibleMine()).append(' ');
            else {
                String subSql = instance.criterion.sql.replace("?", UnaryCriterion.sanitize(value));
                subSql = PermaSql.replacePlaceholders(subSql);
                sql.append(Task.ID).append(" IN (").append(subSql).append(") ");
            }

            Cursor cursor = database.getDatabase().rawQuery(sql.toString(), null);
            try {
                cursor.moveToNext();
                instance.start = last == -1 ? cursor.getInt(0) : last;
                instance.end = cursor.getInt(0);
                last = instance.end;
                max = Math.max(max, last);
            } finally {
                cursor.close();
            }
        }

        for(int i = 0; i < adapter.getCount(); i++) {
            CriterionInstance instance = adapter.getItem(i);
            instance.max = max;
        }

        adapter.notifyDataSetInvalidated();
    }

    @SuppressWarnings("nls")
    private <V> V getNth(int index, Map<?,V> map) {
        int i = 0;
        for (V v : map.values()) {
            if (i == index) return v;
            i++;
        }
        throw new IllegalArgumentException("out of bounds");
    }

    @Override
    public boolean onOptionsItemSelected(android.support.v4.view.MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            finish();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        if(item.getGroupId() == MENU_GROUP_FILTER) {
            // give an initial value for the row before adding it
            CustomFilterCriterion criterion = getNth(item.getItemId(), criteria);
            final CriterionInstance instance = new CriterionInstance();
            instance.criterion = criterion;
            adapter.showOptionsFor(instance, new Runnable() {
                public void run() {
                    adapter.add(instance);
                    updateList();
                }
            });
            return true;
        }

        // item type context item
        else if(item.getGroupId() == MENU_GROUP_CONTEXT_TYPE) {
            CriterionInstance instance = adapter.getItem(item.getOrder());
            instance.type = item.getItemId();
            updateList();
        }

        // delete context item
        else if(item.getGroupId() == MENU_GROUP_CONTEXT_DELETE) {
            CriterionInstance instance = adapter.getItem(item.getOrder());
            adapter.remove(instance);
            updateList();
        }

        return super.onContextItemSelected(item);
    }

    public class FilterCriteriaReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            try {
                final Parcelable[] filters = intent.getExtras().
                    getParcelableArray(AstridApiConstants.EXTRAS_RESPONSE);
                for (Parcelable filter : filters) {
                    CustomFilterCriterion filterCriterion = (CustomFilterCriterion) filter;
                    criteria.put(filterCriterion.identifier, filterCriterion);
                }
            } catch (Exception e) {
                String addon;
                try {
                    addon = intent.getStringExtra(AstridApiConstants.EXTRAS_ADDON);
                } catch (Exception e1) {
                    Log.e("receive-custom-filter-criteria-error-retrieving-addon", //$NON-NLS-1$
                            e.toString(), e);
                    return;
                }
                Log.e("receive-custom-filter-criteria-" +  //$NON-NLS-1$
                        addon,
                        e.toString(), e);
            }
        }
    }
}
