package org.fdroid.fdroid.views.swap;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.widget.CursorAdapter;
import android.support.v7.widget.SearchView;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;

import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService;

import java.util.Timer;
import java.util.TimerTask;

public class SwapAppsView extends ListView implements
        SwapWorkflowActivity.InnerView,
        LoaderManager.LoaderCallbacks<Cursor>,
        SearchView.OnQueryTextListener {

    private DisplayImageOptions displayImageOptions;

    public SwapAppsView(Context context) {
        super(context);
    }

    public SwapAppsView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SwapAppsView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(21)
    public SwapAppsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    private SwapWorkflowActivity getActivity() {
        return (SwapWorkflowActivity) getContext();
    }

    private static final int LOADER_SWAPABLE_APPS = 759283741;
    private static final String TAG = "SwapAppsView";

    private Repo repo;
    private AppListAdapter adapter;
    private String mCurrentFilterString;

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        repo = getActivity().getState().getPeerRepo();

        /*
        if (repo == null) {
            TODO: Uh oh, something stuffed up for this to happen.
            TODO: What is the best course of action from here?
        }
        */

        adapter = new AppListAdapter(getContext(), getContext().getContentResolver().query(
                AppProvider.getRepoUri(repo), Schema.AppMetadataTable.Cols.ALL, null, null, null));

        setAdapter(adapter);

        // either reconnect with an existing loader or start a new one
        getActivity().getSupportLoaderManager().initLoader(LOADER_SWAPABLE_APPS, null, this);

        displayImageOptions = Utils.getImageLoadingOptions().build();

        LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
                pollForUpdatesReceiver, new IntentFilter(UpdateService.LOCAL_ACTION_STATUS));

        schedulePollForUpdates();
    }

    /**
     * Remove relevant listeners/receivers/etc so that they do not receive and process events
     * when this view is not in use.
     */
    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(pollForUpdatesReceiver);
    }

    private void pollForUpdates() {
        if (adapter.getCount() > 1 ||
                (adapter.getCount() == 1 && !new App((Cursor) adapter.getItem(0)).packageName.equals("org.fdroid.fdroid"))) {
            Utils.debugLog(TAG, "Not polling for new apps from swap repo, because we already have more than one.");
            return;
        }

        Utils.debugLog(TAG, "Polling swap repo to see if it has any updates.");
        getActivity().getService().refreshSwap();
    }

    private void schedulePollForUpdates() {
        Utils.debugLog(TAG, "Scheduling poll for updated swap repo in 5 seconds.");
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                Looper.prepare();
                pollForUpdates();
                Looper.loop();
            }
        }, 5000);
    }

    @Override
    public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) {

        inflater.inflate(R.menu.swap_search, menu);

        SearchView searchView = new SearchView(getActivity());

        MenuItem searchMenuItem = menu.findItem(R.id.action_search);
        MenuItemCompat.setActionView(searchMenuItem, searchView);
        MenuItemCompat.setShowAsAction(searchMenuItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS);

        searchView.setOnQueryTextListener(this);
        return true;
    }

    @Override
    public int getStep() {
        return SwapService.STEP_SUCCESS;
    }

    @Override
    public int getPreviousStep() {
        return SwapService.STEP_INTRO;
    }

    @ColorRes
    public int getToolbarColour() {
        return R.color.swap_bright_blue;
    }

    @Override
    public String getToolbarTitle() {
        return getResources().getString(R.string.swap_success);
    }

    @Override
    public CursorLoader onCreateLoader(int id, Bundle args) {
        Uri uri = TextUtils.isEmpty(mCurrentFilterString)
                ? AppProvider.getRepoUri(repo)
                : AppProvider.getSearchUri(repo, mCurrentFilterString);

        return new CursorLoader(getActivity(), uri, Schema.AppMetadataTable.Cols.ALL, null, null, Schema.AppMetadataTable.Cols.NAME);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        adapter.swapCursor(cursor);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        adapter.swapCursor(null);
    }

    @Override
    public boolean onQueryTextChange(String newText) {
        String newFilter = !TextUtils.isEmpty(newText) ? newText : null;
        if (mCurrentFilterString == null && newFilter == null) {
            return true;
        }
        if (mCurrentFilterString != null && mCurrentFilterString.equals(newFilter)) {
            return true;
        }
        mCurrentFilterString = newFilter;
        getActivity().getSupportLoaderManager().restartLoader(LOADER_SWAPABLE_APPS, null, this);
        return true;
    }

    @Override
    public boolean onQueryTextSubmit(String query) {
        // this is not needed since we respond to every change in text
        return true;
    }

    private class AppListAdapter extends CursorAdapter {

        private class ViewHolder {

            private final LocalBroadcastManager localBroadcastManager;
            private App app;

            ProgressBar progressView;
            TextView nameView;
            ImageView iconView;
            Button btnInstall;
            TextView statusInstalled;
            TextView statusIncompatible;

            private final BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    switch (intent.getAction()) {
                        case Downloader.ACTION_STARTED:
                            resetView();
                            break;
                        case Downloader.ACTION_PROGRESS:
                            if (progressView.getVisibility() != View.VISIBLE) {
                                showProgress();
                            }
                            int read = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0);
                            int total = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0);
                            if (total > 0) {
                                int progress = (int) ((double) read / total * 100);
                                progressView.setIndeterminate(false);
                                progressView.setMax(100);
                                progressView.setProgress(progress);
                            } else {
                                progressView.setIndeterminate(true);
                            }
                            break;
                        case Downloader.ACTION_COMPLETE:
                            resetView();
                            break;
                        case Downloader.ACTION_INTERRUPTED:
                            if (intent.hasExtra(Downloader.EXTRA_ERROR_MESSAGE)) {
                                String msg = intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE)
                                        + " " + intent.getDataString();
                                Toast.makeText(context, R.string.download_error, Toast.LENGTH_SHORT).show();
                                Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
                            } else { // user canceled
                                Toast.makeText(context, R.string.details_notinstalled, Toast.LENGTH_LONG).show();
                            }
                            resetView();
                            break;
                        default:
                            throw new RuntimeException("intent action not handled!");
                    }

                }
            };

            private final ContentObserver appObserver = new ContentObserver(new Handler()) {
                @Override
                public void onChange(boolean selfChange) {
                    Activity activity = getActivity();
                    if (activity != null) {
                        app = AppProvider.Helper.findByPackageName(getActivity().getContentResolver(), app.packageName);
                        resetView();
                    }
                }
            };

            ViewHolder() {
                localBroadcastManager = LocalBroadcastManager.getInstance(getContext());
            }

            public void setApp(@NonNull App app) {
                if (this.app == null || !this.app.packageName.equals(app.packageName)) {
                    this.app = app;

                    Context context = getContext();
                    Apk apk = ApkProvider.Helper.find(context, app.packageName, app.suggestedVersionCode);
                    String urlString = apk.getUrl();

                    // TODO unregister receivers? or will they just die with this instance
                    localBroadcastManager.registerReceiver(downloadReceiver,
                            DownloaderService.getIntentFilter(urlString));

                    // NOTE: Instead of continually unregistering and re-registering the observer
                    // (with a different URI), this could equally be done by only having one
                    // registration in the constructor, and using the ContentObserver.onChange(boolean, URI)
                    // method and inspecting the URI to see if it matches. However, this was only
                    // implemented on API-16, so leaving like this for now.
                    getActivity().getContentResolver().unregisterContentObserver(appObserver);
                    getActivity().getContentResolver().registerContentObserver(
                            AppProvider.getContentUri(this.app.packageName), true, appObserver);
                }
                resetView();
            }

            private void resetView() {

                if (app == null) {
                    return;
                }

                progressView.setVisibility(View.GONE);
                progressView.setIndeterminate(true);

                if (app.name != null) {
                    nameView.setText(app.name);
                }

                ImageLoader.getInstance().displayImage(app.iconUrl, iconView, displayImageOptions);

                if (app.hasUpdates()) {
                    btnInstall.setText(R.string.menu_upgrade);
                    btnInstall.setVisibility(View.VISIBLE);
                    statusIncompatible.setVisibility(View.GONE);
                    statusInstalled.setVisibility(View.GONE);
                } else if (app.isInstalled()) {
                    btnInstall.setVisibility(View.GONE);
                    statusIncompatible.setVisibility(View.GONE);
                    statusInstalled.setVisibility(View.VISIBLE);
                } else if (!app.compatible) {
                    btnInstall.setVisibility(View.GONE);
                    statusIncompatible.setVisibility(View.VISIBLE);
                    statusInstalled.setVisibility(View.GONE);
                } else {
                    btnInstall.setText(R.string.menu_install);
                    btnInstall.setVisibility(View.VISIBLE);
                    statusIncompatible.setVisibility(View.GONE);
                    statusInstalled.setVisibility(View.GONE);
                }

                OnClickListener installListener = new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (app.hasUpdates() || app.compatible) {
                            getActivity().install(app);
                            showProgress();
                        }
                    }
                };

                btnInstall.setOnClickListener(installListener);
            }

            private void showProgress() {
                progressView.setVisibility(View.VISIBLE);
                btnInstall.setVisibility(View.GONE);
                statusInstalled.setVisibility(View.GONE);
                statusIncompatible.setVisibility(View.GONE);
            }
        }

        @Nullable
        private LayoutInflater inflater;

        AppListAdapter(@NonNull Context context, @Nullable Cursor c) {
            super(context, c, FLAG_REGISTER_CONTENT_OBSERVER);
        }

        @NonNull
        private LayoutInflater getInflater(Context context) {
            if (inflater == null) {
                inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            }
            return inflater;
        }

        @Override
        public View newView(Context context, Cursor cursor, ViewGroup parent) {
            View view = getInflater(context).inflate(R.layout.swap_app_list_item, parent, false);

            ViewHolder holder = new ViewHolder();

            holder.progressView = (ProgressBar) view.findViewById(R.id.progress);
            holder.nameView = (TextView) view.findViewById(R.id.name);
            holder.iconView = (ImageView) view.findViewById(android.R.id.icon);
            holder.btnInstall = (Button) view.findViewById(R.id.btn_install);
            holder.statusInstalled = (TextView) view.findViewById(R.id.status_installed);
            holder.statusIncompatible = (TextView) view.findViewById(R.id.status_incompatible);

            view.setTag(holder);
            bindView(view, context, cursor);
            return view;
        }

        @Override
        public void bindView(final View view, final Context context, final Cursor cursor) {
            ViewHolder holder = (ViewHolder) view.getTag();
            final App app = new App(cursor);
            holder.setApp(app);
        }
    }

    private final BroadcastReceiver pollForUpdatesReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            int statusCode = intent.getIntExtra(UpdateService.EXTRA_STATUS_CODE, -1);
            switch (statusCode) {
                case UpdateService.STATUS_COMPLETE_WITH_CHANGES:
                    Utils.debugLog(TAG, "Swap repo has updates, notifying the list adapter.");
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            adapter.notifyDataSetChanged();
                        }
                    });
                    break;

                case UpdateService.STATUS_ERROR_GLOBAL:
                    // TODO: Well, if we can't get the index, we probably can't swapp apps.
                    // Tell the user something helpful?
                    break;

                case UpdateService.STATUS_COMPLETE_AND_SAME:
                    schedulePollForUpdates();
                    break;
            }
        }
    };

}
