package io.whz.synapse.component;

import android.Manifest;
import android.app.Activity;
import android.app.ActivityOptions;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.provider.Settings;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;

import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import io.whz.synapse.R;
import io.whz.synapse.element.Global;
import io.whz.synapse.element.VerticalGap;
import io.whz.synapse.neural.MNISTUtil;
import io.whz.synapse.pojo.constant.PreferenceCons;
import io.whz.synapse.pojo.constant.TrackCons;
import io.whz.synapse.pojo.dao.DBModel;
import io.whz.synapse.pojo.dao.DBModelDao;
import io.whz.synapse.pojo.neural.Model;
import io.whz.synapse.pojo.event.MANEvent;
import io.whz.synapse.pojo.event.ModelDeletedEvent;
import io.whz.synapse.pojo.event.TrainEvent;
import io.whz.synapse.pojo.multiple.binder.PlayViewBinder;
import io.whz.synapse.pojo.multiple.binder.TrainedModelViewBinder;
import io.whz.synapse.pojo.multiple.binder.TrainingModelViewBinder;
import io.whz.synapse.pojo.multiple.binder.WelcomeViewBinder;
import io.whz.synapse.pojo.multiple.item.PlayItem;
import io.whz.synapse.pojo.multiple.item.TrainedModelItem;
import io.whz.synapse.pojo.multiple.item.TrainingModelItem;
import io.whz.synapse.pojo.multiple.item.WelcomeItem;
import io.whz.synapse.track.ExceptionHelper;
import io.whz.synapse.track.Tracker;
import io.whz.synapse.transition.FabTransform;
import io.whz.synapse.util.DbHelper;
import io.whz.synapse.util.Precondition;
import me.drakeet.multitype.MultiTypeAdapter;

import static android.support.v4.content.PermissionChecker.PERMISSION_GRANTED;
import static io.whz.synapse.R.id.fab;
import static io.whz.synapse.pojo.constant.TrackCons.Main.CLICK_DOWNLOAD;
import static io.whz.synapse.pojo.constant.TrackCons.Main.CLICK_FAB;
import static io.whz.synapse.pojo.constant.TrackCons.Main.CLICK_PLAY;
import static io.whz.synapse.pojo.constant.TrackCons.Main.CLICK_TRAINED;
import static io.whz.synapse.pojo.constant.TrackCons.Main.CLICK_TRAINING;

public class MainActivity extends WrapperActivity implements View.OnClickListener {
    private static final int REQUEST_EXTERNAL_STORAGE = 0x01;
    private static final String TAG = App.TAG + "-MainActivity";

    private final ItemsButler mButler = new ItemsButler();

    private RecyclerView mRecyclerView;
    private FloatingActionButton mFab;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().setBackgroundDrawableResource(R.color.window_background);
        setContentView(R.layout.activity_main);

        if (!checkDownloadManager()) {
            return;
        }

        mRecyclerView = findViewById(R.id.rv);
        mRecyclerView.setAdapter(mButler.getAdapter());
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mRecyclerView.addOnScrollListener(new OnScrollTracker());
        mRecyclerView.addItemDecoration(new VerticalGap(getResources().getDimensionPixelOffset(R.dimen.list_item_vertical_gap)));
        mFab = findViewById(fab);
        mFab.setOnClickListener(this);

        checkPermissionOrSolveData();
    }

    private boolean checkDownloadManager() {
        if (getSystemService(DOWNLOAD_SERVICE) == null) {
            showNeedDownloadManagerDialog();
            return false;
        }
        
        return true;
    }

    private void showNeedDownloadManagerDialog() {
        final Activity that = this;
        
        new AlertDialog.Builder(this)
                .setTitle(R.string.text_dialog_lack_download_manager_title)
                .setMessage(R.string.text_dialog_lack_download_manager_msg)
                .setNegativeButton(R.string.text_dialog_finish_action, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        if (!that.isFinishing()) {
                            finish();
                        }
                    }
                }).show();
    }

    private void checkPermissionOrSolveData() {
        if (Global.getInstance().isDirSet()) {
            solveData();
        } else if (checkStoragePermission()) {
            final File root;

            if ((root = getExternalFilesDir(null)) != null) {
                Global.getInstance().setRootDir(root);
                solveData();
            } else {
                new AlertDialog.Builder(this)
                        .setTitle(R.string.text_dialog_external_not_found_title)
                        .setMessage(R.string.text_dialog_external_not_found_msg)
                        .setCancelable(false)
                        .setNegativeButton(R.string.text_dialog_finish_action, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialogInterface, int i) {
                                finish();
                            }
                        }).show();
            }
        } else if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
            new AlertDialog.Builder(this)
                    .setTitle(R.string.text_dialog_need_external_title)
                    .setMessage(R.string.text_dialog_need_external_msg)
                    .setCancelable(false)
                    .setPositiveButton(R.string.dialog_positive, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            if (!MainActivity.this.isFinishing()) {
                                requestExternalStoragePermission();
                            }
                        }
                    }).setNegativeButton(R.string.text_dialog_finish_action, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    finish();
                }
            }).show();
        } else {
            requestExternalStoragePermission();
        }
    }

    private void requestExternalStoragePermission() {
        ActivityCompat.requestPermissions(MainActivity.this, new String[]{
                Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_EXTERNAL_STORAGE);
    }

    private boolean checkStoragePermission() {
        return ContextCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        switch (requestCode) {
            case REQUEST_EXTERNAL_STORAGE:
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    final File dir =  getExternalFilesDir(null);

                    if (dir != null) {
                        Global.getInstance()
                                .setRootDir(dir);

                        solveData();
                        break;
                    }
                }

                checkPermissionOrSolveData();
                break;

            default:
                break;
        }
    }

    private boolean checkDataReadyRoughly() {
        return Global.getInstance()
                .getPreference()
                .getBoolean(PreferenceCons.IS_DATA_SET_READY, false);
    }

    public boolean isDataSetReady() {
        final boolean ready = checkDataReadyRoughly();

        if (!ready) {
            return false;
        }

        final File[] files = Global.getInstance().getDirs().train.listFiles();

        if (files == null
                || files.length != (MNISTUtil.MAX_TRAINING_SIZE / MNISTUtil.PRE_FILE_SIZE)) {
            Global.getInstance()
                    .getPreference()
                    .edit()
                    .putBoolean(PreferenceCons.IS_DATA_SET_READY, false)
                    .apply();

            return false;
        }

        return true;
    }

    private void solveData() {
        mButler.clear();

        final boolean ready = isDataSetReady();

        if (ready) {
            final LayoutAnimationController controller =
                    AnimationUtils.loadLayoutAnimation(this, R.anim.layout_animation_from_bottom);

            mRecyclerView.setLayoutAnimation(controller);
            mRecyclerView.scheduleLayoutAnimation();

            dataSetReady();
        } else {
            dataSetUnready();
        }
    }

    private void dataSetReady() {
        mRecyclerView.addOnScrollListener(new FabVisibleHandler(mFab));
        mFab.show();

        mButler.setWelcome(null)
                .setPlayItem(new PlayItem())
                .setTrainedModes(getAllTrainedModelItems())
                .notifyDataSetChanged();
    }

    @NonNull
    @CheckResult
    private WelcomeItem getDataSetItem(@WelcomeItem.State int state) {
        return new WelcomeItem(state);
    }

    @CheckResult
    @Nullable
    private List<TrainedModelItem> getAllTrainedModelItems() {
        final List<Model> models = new ArrayList<>();

        try {
            final List<DBModel> dbModels = Global.getInstance()
                    .getSession()
                    .getDBModelDao()
                    .queryBuilder()
                    .orderDesc(DBModelDao.Properties.CreatedTime)
                    .list();

            for (int i = 0, len = dbModels.size(); i < len; ++i) {
                models.add(DbHelper.dbModel2Model(dbModels.get(i)));
            }

        } catch (Exception e) {
            ExceptionHelper.getInstance()
                    .caught(e);
        }

        if (models.isEmpty()) {
            return null;
        }

        final List<TrainedModelItem> items = new ArrayList<>(models.size());

        for (Model model : models) {
            items.add(new TrainedModelItem(model));
        }

        return items;
    }

    private void dataSetUnready() {
        mButler.setWelcome(getDataSetItem(WelcomeItem.UNREADY))
                .notifyDataSetChanged();
    }

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

        EventBus.getDefault()
                .register(this);
    }

    @Override
    protected void onStop() {
        super.onStop();

        EventBus.getDefault()
                .unregister(this);
    }

    @SuppressWarnings("unused")
    @Subscribe(sticky = true, priority = -1, threadMode = ThreadMode.MAIN)
    public void onModelDeleted(@NonNull ModelDeletedEvent event) {
        Global.getInstance()
                .getBus()
                .removeStickyEvent(event);

        mButler.setTrainedModes(getAllTrainedModelItems())
                .notifyDataSetChanged();
    }

    @SuppressWarnings("unused")
    @Subscribe(sticky = true, priority = -1, threadMode = ThreadMode.MAIN)
    public void onTraining(@NonNull final TrainEvent event) {
        @TrainEvent.Type final int what = event.what;

        switch (what) {
            case TrainEvent.START:
                mRecyclerView.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        final Model model = (Model) event.obj;
                        mButler.setTrainingModel(getTrainingModelItem(model))
                                .notifyDataSetChanged();
                    }
                }, 300);

                mRecyclerView.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mRecyclerView.smoothScrollToPosition(0);
                    }
                }, 400);
                break;

            case TrainEvent.UPDATE:
                final Model model = (Model) event.obj;
                mButler.setTrainingModel(getTrainingModelItem(model))
                        .notifyDataSetChanged();
                break;

            case TrainEvent.COMPLETE:
            case TrainEvent.ERROR:
            case TrainEvent.INTERRUPTED:
                mButler.setTrainingModel(getTrainingModelItem(null))
                        .setTrainedModes(getAllTrainedModelItems())
                        .notifyDataSetChanged();

                Global.getInstance()
                        .getBus()
                        .removeStickyEvent(event);
                break;

            case TrainEvent.EVALUATE:
            default:
                break;
        }
    }

    @CheckResult
    @Nullable
    private TrainingModelItem getTrainingModelItem(@Nullable Model model) {
        return model == null ?
                null : new TrainingModelItem(model);
    }

    @SuppressWarnings("unused")
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onNormalEvent(MANEvent event) {
        @MANEvent.Event final int what = event.what;

        switch (what) {
            case MANEvent.CLICK_DOWNLOAD:
                showDownloadRequestDialog(this);
                break;

            case MANEvent.DOWNLOAD_COMPLETE:
                handleDownloadComplete(event);
                break;

            case MANEvent.DECOMPRESS_COMPLETE:
                handleDecompressComplete(event);
                break;

            case MANEvent.REJECT_MSG:
                handleRejectMsg(event);
                break;

            case MANEvent.JUMP_TO_PLAY:
                handleJump2Play(event);
                break;

            case MANEvent.JUMP_TO_TRAINED:
                handleJump2Trained(event);
                break;

            case MANEvent.JUMP_TO_TRAINING:
                handleJump2Training(event);
                break;

            default:
                break;
        }
    }

    private void handleJump2Training(MANEvent event) {
        final Intent intent = new Intent(this, ModelDetailActivity.class);
        intent.putExtra(ModelDetailActivity.INTENT_TYPE, ModelDetailActivity.IS_TRAINING);

        startActivity(intent);

        Tracker.getInstance()
                .logEvent(CLICK_TRAINING);
    }

    private void handleJump2Trained(MANEvent event) {
        final Long id = (Long) event.obj;

        if (id == null) {
            return;
        }

        final Intent intent = new Intent(this, ModelDetailActivity.class);
        intent.putExtra(ModelDetailActivity.INTENT_TYPE, ModelDetailActivity.IS_TRAINED);
        intent.putExtra(ModelDetailActivity.TRAINED_ID, id);

        startActivity(intent);

        Tracker.getInstance()
                .logEvent(CLICK_TRAINED);
    }

    private void handleJump2Play(MANEvent event) {
        final Intent intent = new Intent(this, PlayActivity.class);

        startActivity(intent);

        Tracker.getInstance()
                .logEvent(CLICK_PLAY);
    }

    private void handleRejectMsg(MANEvent event) {
        final String text = String.valueOf(event.obj);

        if (TextUtils.isEmpty(text)) {
            return;
        }

        Snackbar.make(mRecyclerView, text, Snackbar.LENGTH_SHORT)
                .show();
    }

    private void handleDownloadComplete(MANEvent event) {
        final boolean success = event.obj != null ? (Boolean) event.obj : false;

        @StringRes int res = R.string.text_download_success;

        if (!success) {
            mButler.setWelcome(getDataSetItem(WelcomeItem.UNREADY))
                    .notifyDataSetChanged();
            res = R.string.text_download_error;
        }

        Snackbar.make(mRecyclerView, res, Snackbar.LENGTH_SHORT)
                .show();
    }

    private void handleDecompressComplete(MANEvent event) {
        final boolean success = event.obj != null ? (Boolean) event.obj : false;
        @StringRes final int res;

        if (success) {
            mButler.setWelcome(new WelcomeItem(WelcomeItem.READY))
                    .notifyDataSetChanged();

            mRecyclerView.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (!MainActivity.this.isFinishing()) {
                        solveData();
                    }
                }
            }, 1500);

            res = R.string.text_decompress_success;
        } else {
            mButler.setWelcome(getDataSetItem(WelcomeItem.UNREADY))
                    .notifyDataSetChanged();
            res = R.string.text_decompress_error;
        }

        Snackbar.make(mRecyclerView, res, Snackbar.LENGTH_SHORT)
                .show();
    }

    private void requestDownload() {
        mButler.setWelcome(getDataSetItem(WelcomeItem.WAITING))
                .notifyDataSetChanged();

        final Intent intent = new Intent(this, MainService.class);
        intent.putExtra(MainService.ACTION_KEY, MainService.ACTION_DOWNLOAD);

        startService(intent);

        Tracker.getInstance()
                .logEvent(CLICK_DOWNLOAD);
    }

    private void showDownloadRequestDialog(@NonNull final Activity that) {
        final ConnectivityManager manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        final NetworkInfo network = manager.getActiveNetworkInfo();

        if (network == null
                || !network.isConnected()) {
            new AlertDialog.Builder(that)
                    .setTitle(R.string.text_dialog_network_unavailable)
                    .setMessage(R.string.text_dialog_need_wifi_msg)
                    .setPositiveButton(R.string.text_dialog_download_setting_network, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            toSetting();
                        }
                    }).show();
        } else if (network.getType() == ConnectivityManager.TYPE_WIFI) {
            requestDownload();
        } else {
            new AlertDialog.Builder(that)
                    .setTitle(R.string.text_dialog_need_wifi_title)
                    .setMessage(R.string.text_dialog_need_wifi_msg)
                    .setPositiveButton(R.string.text_dialog_download_direct, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            if (that.isFinishing()) {
                                return;
                            }

                            requestDownload();
                        }
                    }).setNegativeButton(R.string.text_dialog_download_setting_network, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    toSetting();
                }
            }).show();
        }
    }

    private void toSetting() {
        try {
            final Intent intent = new Intent(Settings.ACTION_WIFI_SETTINGS);
            startActivity(intent);
        } catch (Exception e) {
            ExceptionHelper.getInstance()
                    .caught(e);
        }
    }

    @Override
    public void onClick(View view) {
        final int id = view.getId();

        switch (id) {
            case fab:
                startNeuralNetworkConfig(view);
                Tracker.getInstance()
                        .logEvent(CLICK_FAB);
                break;

            default:
                break;
        }
    }

    /**
     * Transition animation may cause exception
     */
    private void startNeuralNetworkConfig(@NonNull final View view) {
        view.setClickable(false);
        view.postDelayed(new Runnable() {
            @Override
            public void run() {
                view.setClickable(true);
            }
        }, 1000);

        try {
            final Intent intent = new Intent(this, NeuralModelActivity.class);

            FabTransform.addExtras(intent,
                    ContextCompat.getColor(this, R.color.color_accent),
                    R.drawable.ic_add_white_24dp);

            final ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
                    this, view, getString(R.string.transition_neural_config));

            startActivity(intent, options.toBundle());
        } catch (Exception e) {
            ExceptionHelper.getInstance()
                    .caught(e);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.ac_main_menu, menu);

        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        final int id = item.getItemId();

        switch (id) {
            case R.id.about:
                new AboutDialog()
                        .show(getSupportFragmentManager(), getString(R.string.text_about));

                Tracker.getInstance()
                        .logEvent(TrackCons.Main.CLICK_ABOUT);

                return true;

            default:
                return super.onOptionsItemSelected(item);
        }
    }

    private static final class ItemsButler {
        private final RecyclerView.Adapter mAdapter;
        private final List<Object> mOldItems;
        private final List<Object> mCurItems;
        private final DiffUtil.Callback mDiffCallback;

        private WelcomeItem mWelcome;
        private PlayItem mPlayItem;
        private TrainingModelItem mTrainingModel;
        private List<TrainedModelItem> mTrainedModes;

        ItemsButler() {
            mOldItems = new ArrayList<>();
            mCurItems = new ArrayList<>();
            mDiffCallback = new DiffCallback();

            final MultiTypeAdapter adapter = new MultiTypeAdapter(mCurItems);
            registerItems(adapter);

            mAdapter = adapter;
        }

        private void registerItems(@NonNull MultiTypeAdapter adapter) {
            adapter.register(WelcomeItem.class, new WelcomeViewBinder());
            adapter.register(TrainedModelItem.class, new TrainedModelViewBinder());
            adapter.register(TrainingModelItem.class, new TrainingModelViewBinder());
            adapter.register(PlayItem.class, new PlayViewBinder());
        }

        RecyclerView.Adapter getAdapter() {
            return mAdapter;
        }

        ItemsButler setWelcome(@Nullable WelcomeItem item) {
            mWelcome = item;
            return this;
        }

        ItemsButler setPlayItem(@Nullable PlayItem item) {
            mPlayItem = item;
            return this;
        }

        ItemsButler setTrainingModel(@Nullable TrainingModelItem item) {
            mTrainingModel = item;
            return this;
        }

        ItemsButler setTrainedModes(@Nullable List<TrainedModelItem> items) {
            mTrainedModes = items;
            return this;
        }

        ItemsButler clear() {
            mWelcome = null;
            mTrainedModes = null;
            return this;
        }

        private void solveItemChange() {
            mOldItems.clear();
            mOldItems.addAll(mCurItems);
            mCurItems.clear();

            if (mWelcome != null) {
                mCurItems.add(mWelcome);
            }

            if (mTrainingModel != null) {
                mCurItems.add(mTrainingModel);
            }

            if (mPlayItem != null) {
                mCurItems.add(mPlayItem);
            }

            if (mTrainedModes != null
                    && !mTrainedModes.isEmpty()) {
                mCurItems.addAll(mTrainedModes);
            }
        }

        void notifyDataSetChanged() {
            solveItemChange();

            DiffUtil.calculateDiff(mDiffCallback, true)
                    .dispatchUpdatesTo(mAdapter);
        }

        private class DiffCallback extends DiffUtil.Callback {
            @Override
            public int getOldListSize() {
                return mOldItems.size();
            }

            @Override
            public int getNewListSize() {
                return mCurItems.size();
            }

            @Override
            public boolean areItemsTheSame(int i, int i1) {
                return mOldItems.get(i).getClass().equals(mCurItems.get(i1).getClass());
            }

            @Override
            public boolean areContentsTheSame(int i, int i1) {
                return mOldItems.get(i).equals(mCurItems.get(i1));
            }
        }
    }

    private static final class FabVisibleHandler extends RecyclerView.OnScrollListener {
        private static final int RESPOND_RANGE = 10;

        private final FloatingActionButton mFab;

        FabVisibleHandler(@NonNull FloatingActionButton fab) {
            mFab = Precondition.checkNotNull(fab);
        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            if (Math.abs(dy) > RESPOND_RANGE) {
                if (dy < 0
                        && !mFab.isShown()) {
                    mFab.show();
                } else if (dy > 0
                        && mFab.isShown()){
                    mFab.hide();
                }
            }
        }
    }

    private static final class OnScrollTracker extends RecyclerView.OnScrollListener {
        private static final int AVAILABLE = 5;
        private final Tracker mTrack = Tracker.getInstance();

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            if (newState != RecyclerView.SCROLL_STATE_IDLE
                    || !(recyclerView.getLayoutManager() instanceof LinearLayoutManager)
                    || recyclerView.getChildCount() < AVAILABLE) {
                return;
            }

            final LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager();

            final int totalItemCount = recyclerView.getAdapter().getItemCount();
            final int lastVisibleItemPosition = manager.findLastVisibleItemPosition();

            if (lastVisibleItemPosition == totalItemCount - 1) {
                mTrack.logEvent(TrackCons.Main.SCROLL_BOTTOM);
            }
        }
    }
}