package org.schabi.newpipe.fragments.list.search;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.TooltipCompat;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
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.view.animation.DecelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.TextView;

import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SearchEngine;
import org.schabi.newpipe.extractor.search.SearchResult;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.history.HistoryListener;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.StateSaver;

import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;

import icepick.State;
import io.reactivex.Notification;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.functions.Predicate;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;

import static org.schabi.newpipe.util.AnimationUtils.animateView;

public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor.NextItemsResult> {

    /*//////////////////////////////////////////////////////////////////////////
    // Search
    //////////////////////////////////////////////////////////////////////////*/

    /**
     * The suggestions will appear only if the query meet this threshold (>=).
     */
    private static final int THRESHOLD_SUGGESTION = 3;

    /**
     * How much time have to pass without emitting a item (i.e. the user stop typing) to fetch/show the suggestions, in milliseconds.
     */
    private static final int SUGGESTIONS_DEBOUNCE = 150; //ms

    @State
    protected int filterItemCheckedId = -1;
    private SearchEngine.Filter filter = SearchEngine.Filter.ANY;

    @State
    protected int serviceId = -1;
    @State
    protected String searchQuery = "";
    @State
    protected boolean wasSearchFocused = false;

    private int currentPage = 0;
    private int currentNextPage = 0;
    private String searchLanguage;
    private boolean showSuggestions = true;

    private PublishSubject<String> suggestionPublisher = PublishSubject.create();
    private Disposable searchDisposable;
    private Disposable suggestionWorkerDisposable;

    private SuggestionListAdapter suggestionListAdapter;

    /*//////////////////////////////////////////////////////////////////////////
    // Views
    //////////////////////////////////////////////////////////////////////////*/

    private View searchToolbarContainer;
    private AutoCompleteTextView searchEditText;
    private View searchClear;

    /*////////////////////////////////////////////////////////////////////////*/

    public static SearchFragment getInstance(int serviceId, String query) {
        SearchFragment searchFragment = new SearchFragment();
        searchFragment.setQuery(serviceId, query);
        searchFragment.searchOnResume();
        return searchFragment;
    }

    /**
     * Set wasLoading to true so when the fragment onResume is called, the initial search is done.
     * (it will only start searching if the query is not null or empty)
     */
    private void searchOnResume() {
        if (!TextUtils.isEmpty(searchQuery)) {
            wasLoading.set(true);
        }
    }

    /*//////////////////////////////////////////////////////////////////////////
    // Fragment's LifeCycle
    //////////////////////////////////////////////////////////////////////////*/

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        suggestionListAdapter = new SuggestionListAdapter(activity);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_search, container, false);
    }

    @Override
    public void onPause() {
        super.onPause();
        wasSearchFocused = searchEditText.hasFocus();

        if (searchDisposable != null) searchDisposable.dispose();
        if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
        hideSoftKeyboard(searchEditText);
    }

    @Override
    public void onResume() {
        if (DEBUG) Log.d(TAG, "onResume() called");
        super.onResume();

        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
        showSuggestions = preferences.getBoolean(getString(R.string.show_search_suggestions_key), true);
        searchLanguage = preferences.getString(getString(R.string.search_language_key), getString(R.string.default_language_value));

        if (!TextUtils.isEmpty(searchQuery)) {
            if (wasLoading.getAndSet(false)) {
                if (currentNextPage > currentPage) loadMoreItems();
                else search(searchQuery);
            } else if (infoListAdapter.getItemsList().size() == 0) {
                if (savedState == null) {
                    search(searchQuery);
                } else if (!isLoading.get() && !wasSearchFocused) {
                    infoListAdapter.clearStreamItemList();
                    showEmptyState();
                }
            }
        }

        if (suggestionWorkerDisposable == null || suggestionWorkerDisposable.isDisposed()) initSuggestionObserver();
    }

    @Override
    public void onDestroyView() {
        if (DEBUG) Log.d(TAG, "onDestroyView() called");
        unsetSearchListeners();
        super.onDestroyView();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (!activity.isChangingConfigurations()) StateSaver.onDestroy(savedState);

        if (searchDisposable != null) searchDisposable.dispose();
        if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case ReCaptchaActivity.RECAPTCHA_REQUEST:
                if (resultCode == Activity.RESULT_OK && searchQuery.length() != 0) {
                    search(searchQuery);
                } else Log.e(TAG, "ReCaptcha failed");
                break;

            default:
                Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
                break;
        }
    }

    /*//////////////////////////////////////////////////////////////////////////
    // State Saving
    //////////////////////////////////////////////////////////////////////////*/

    @Override
    public void writeTo(Queue<Object> objectsToSave) {
        super.writeTo(objectsToSave);
        objectsToSave.add(currentPage);
        objectsToSave.add(currentNextPage);
    }

    @Override
    public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
        super.readFrom(savedObjects);
        currentPage = (int) savedObjects.poll();
        currentNextPage = (int) savedObjects.poll();
    }

    @Override
    public void onSaveInstanceState(Bundle bundle) {
        searchQuery = searchEditText != null && !TextUtils.isEmpty(searchEditText.getText().toString())
                ? searchEditText.getText().toString() : searchQuery;
        super.onSaveInstanceState(bundle);
    }

    /*//////////////////////////////////////////////////////////////////////////
    // Init's
    //////////////////////////////////////////////////////////////////////////*/

    @Override
    public void reloadContent() {
        if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
            search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString());
        } else {
            if (searchEditText != null) {
                searchEditText.setText("");
                showSoftKeyboard(searchEditText);
            }
            animateView(errorPanelRoot, false, 200);
        }
    }

    /*//////////////////////////////////////////////////////////////////////////
    // Menu
    //////////////////////////////////////////////////////////////////////////*/

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);

        ActionBar supportActionBar = activity.getSupportActionBar();
        if (supportActionBar != null) {
            supportActionBar.setDisplayShowTitleEnabled(false);
            supportActionBar.setDisplayHomeAsUpEnabled(true);
        }

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

        searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
        searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
        searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
        setupSearchView();

        restoreFilterChecked(menu, filterItemCheckedId);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.menu_filter_all:
            case R.id.menu_filter_video:
            case R.id.menu_filter_channel:
            case R.id.menu_filter_playlist:
                changeFilter(item, getFilterFromMenuId(item.getItemId()));
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    private void restoreFilterChecked(Menu menu, int itemId) {
        if (itemId != -1) {
            MenuItem item = menu.findItem(itemId);
            if (item == null) return;

            item.setChecked(true);
            filter = getFilterFromMenuId(itemId);
        }
    }

    private SearchEngine.Filter getFilterFromMenuId(int itemId) {
        switch (itemId) {
            case R.id.menu_filter_all:
                return SearchEngine.Filter.ANY;
            case R.id.menu_filter_video:
                return SearchEngine.Filter.STREAM;
            case R.id.menu_filter_channel:
                return SearchEngine.Filter.CHANNEL;
            case R.id.menu_filter_playlist:
                return SearchEngine.Filter.PLAYLIST;
            default:
                return SearchEngine.Filter.ANY;
        }
    }

    /*//////////////////////////////////////////////////////////////////////////
    // Search
    //////////////////////////////////////////////////////////////////////////*/

    private TextWatcher textWatcher;

    private void setupSearchView() {
        searchEditText.setText(searchQuery != null ? searchQuery : "");
        searchEditText.setAdapter(suggestionListAdapter);

        if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) {
            searchToolbarContainer.setTranslationX(100);
            searchToolbarContainer.setAlpha(0f);
            searchToolbarContainer.setVisibility(View.VISIBLE);
            searchToolbarContainer.animate().translationX(0).alpha(1f).setDuration(200).setInterpolator(new DecelerateInterpolator()).start();
        } else {
            searchToolbarContainer.setTranslationX(0);
            searchToolbarContainer.setAlpha(1f);
            searchToolbarContainer.setVisibility(View.VISIBLE);
        }

        initSearchListeners();

        if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) showSoftKeyboard(searchEditText);
        else hideSoftKeyboard(searchEditText);
        wasSearchFocused = false;
    }

    private void initSearchListeners() {
        searchClear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
                if (TextUtils.isEmpty(searchEditText.getText())) {
                    NavigationHelper.gotoMainFragment(getFragmentManager());
                    return;
                }

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                    searchEditText.setText("", false);
                } else searchEditText.setText("");
                suggestionListAdapter.updateAdapter(new ArrayList<String>());
                showSoftKeyboard(searchEditText);
            }
        });

        TooltipCompat.setTooltipText(searchClear, getString(R.string.clear));

        searchEditText.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
                searchEditText.showDropDown();
            }
        });

        searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]");
                if (hasFocus) searchEditText.showDropDown();
            }
        });

        searchEditText.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                if (DEBUG) {
                    Log.d(TAG, "onItemClick() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
                }
                String s = suggestionListAdapter.getSuggestion(position);
                if (DEBUG) Log.d(TAG, "onItemClick text = " + s);
                submitQuery(s);
            }
        });
        searchEditText.setThreshold(THRESHOLD_SUGGESTION);

        if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
        textWatcher = new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }

            @Override
            public void afterTextChanged(Editable s) {
                String newText = searchEditText.getText().toString();
                if (!TextUtils.isEmpty(newText)) suggestionPublisher.onNext(newText);
            }
        };
        searchEditText.addTextChangedListener(textWatcher);

        searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (DEBUG)
                    Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
                if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
                    submitQuery(searchEditText.getText().toString());
                    return true;
                }
                return false;
            }
        });

        if (suggestionWorkerDisposable == null || suggestionWorkerDisposable.isDisposed()) initSuggestionObserver();
    }

    private void unsetSearchListeners() {
        searchClear.setOnClickListener(null);
        searchClear.setOnLongClickListener(null);
        searchEditText.setOnClickListener(null);
        searchEditText.setOnItemClickListener(null);
        searchEditText.setOnFocusChangeListener(null);
        searchEditText.setOnEditorActionListener(null);

        if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
        textWatcher = null;
    }

    private void showSoftKeyboard(View view) {
        if (DEBUG) Log.d(TAG, "showSoftKeyboard() called with: view = [" + view + "]");
        if (view == null) return;

        if (view.requestFocus()) {
            InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
        }
    }

    private void hideSoftKeyboard(View view) {
        if (DEBUG) Log.d(TAG, "hideSoftKeyboard() called with: view = [" + view + "]");
        if (view == null) return;

        InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);

        view.clearFocus();
    }

    public void giveSearchEditTextFocus() {
        showSoftKeyboard(searchEditText);
    }

    private void initSuggestionObserver() {
        if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
        final Predicate<String> checkEnabledAndLength = new Predicate<String>() {
            @Override
            public boolean test(@io.reactivex.annotations.NonNull String s) throws Exception {
                boolean lengthCheck = s.length() >= THRESHOLD_SUGGESTION;
                // Clear the suggestions adapter if the length check fails
                if (!lengthCheck && !suggestionListAdapter.isEmpty()) {
                    suggestionListAdapter.updateAdapter(new ArrayList<String>());
                }
                // Only pass through if suggestions is enabled and the query length is equal or greater than THRESHOLD_SUGGESTION
                return showSuggestions && lengthCheck;
            }
        };

        suggestionWorkerDisposable = suggestionPublisher
                .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
                .startWith(!TextUtils.isEmpty(searchQuery) ? searchQuery : "")
                .filter(checkEnabledAndLength)
                .switchMap(new Function<String, Observable<Notification<List<String>>>>() {
                    @Override
                    public Observable<Notification<List<String>>> apply(@io.reactivex.annotations.NonNull String query) throws Exception {
                        return ExtractorHelper.suggestionsFor(serviceId, query, searchLanguage).toObservable().materialize();
                    }
                })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Notification<List<String>>>() {
                    @Override
                    public void accept(@io.reactivex.annotations.NonNull Notification<List<String>> listNotification) throws Exception {
                        if (listNotification.isOnNext()) {
                            handleSuggestions(listNotification.getValue());
                            if (errorPanelRoot.getVisibility() == View.VISIBLE) {
                                hideLoading();
                            }
                        } else if (listNotification.isOnError()) {
                            Throwable error = listNotification.getError();
                            if (!ExtractorHelper.isInterruptedCaused(error)) {
                                onSuggestionError(error);
                            }
                        }
                    }
                });
    }

    @Override
    protected void doInitialLoadLogic() {
        // no-op
    }

    private void search(final String query) {
        if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "]");

        hideSoftKeyboard(searchEditText);
        this.searchQuery = query;
        this.currentPage = 0;
        infoListAdapter.clearStreamItemList();

        if (activity instanceof HistoryListener) {
            ((HistoryListener) activity).onSearch(serviceId, query);
        }

        final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
        final String searchLanguageKey = getContext().getString(R.string.search_language_key);
        searchLanguage = sharedPreferences.getString(searchLanguageKey, getContext().getString(R.string.default_language_value));
        startLoading(false);
    }

    @Override
    public void startLoading(boolean forceLoad) {
        super.startLoading(forceLoad);
        if (searchDisposable != null) searchDisposable.dispose();
        searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, searchLanguage, filter)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<SearchResult>() {
                    @Override
                    public void accept(@NonNull SearchResult result) throws Exception {
                        isLoading.set(false);
                        handleResult(result);
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(@NonNull Throwable throwable) throws Exception {
                        isLoading.set(false);
                        onError(throwable);
                    }
                });
    }

    @Override
    protected void loadMoreItems() {
        isLoading.set(true);
        showListFooter(true);
        if (searchDisposable != null) searchDisposable.dispose();
        currentNextPage = currentPage + 1;
        searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, searchLanguage, filter)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<ListExtractor.NextItemsResult>() {
                    @Override
                    public void accept(@NonNull ListExtractor.NextItemsResult result) throws Exception {
                        isLoading.set(false);
                        handleNextItems(result);
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(@NonNull Throwable throwable) throws Exception {
                        isLoading.set(false);
                        onError(throwable);
                    }
                });
    }

    @Override
    protected boolean hasMoreItems() {
        // TODO: No way to tell if search has more items in the moment
        return true;
    }

    @Override
    protected void onItemSelected(InfoItem selectedItem) {
        super.onItemSelected(selectedItem);
        hideSoftKeyboard(searchEditText);
    }

    /*//////////////////////////////////////////////////////////////////////////
    // Utils
    //////////////////////////////////////////////////////////////////////////*/

    private void changeFilter(MenuItem item, SearchEngine.Filter filter) {
        this.filter = filter;
        this.filterItemCheckedId = item.getItemId();
        item.setChecked(true);
        if (searchQuery != null && !searchQuery.isEmpty()) search(searchQuery);
    }

    private void submitQuery(String query) {
        if (DEBUG) Log.d(TAG, "submitQuery() called with: query = [" + query + "]");
        if (query.isEmpty()) return;
        search(query);
    }

    private void setQuery(int serviceId, String searchQuery) {
        this.serviceId = serviceId;
        this.searchQuery = searchQuery;
    }

    @Override
    public void showError(String message, boolean showRetryButton) {
        super.showError(message, showRetryButton);
        hideSoftKeyboard(searchEditText);
    }

    /*//////////////////////////////////////////////////////////////////////////
    // Suggestion Results
    //////////////////////////////////////////////////////////////////////////*/

    public void handleSuggestions(@NonNull List<String> suggestions) {
        if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
        suggestionListAdapter.updateAdapter(suggestions);
    }

    public void onSuggestionError(Throwable exception) {
        if (DEBUG) Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]");
        if (super.onError(exception)) return;

        int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error;
        onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, NewPipe.getNameOfService(serviceId), searchQuery, errorId);
    }

    /*//////////////////////////////////////////////////////////////////////////
    // Contract
    //////////////////////////////////////////////////////////////////////////*/

    @Override
    public void hideLoading() {
        super.hideLoading();
        showListFooter(false);
    }

    /*//////////////////////////////////////////////////////////////////////////
    // Search Results
    //////////////////////////////////////////////////////////////////////////*/

    @Override
    public void handleResult(@NonNull SearchResult result) {
        if (!result.errors.isEmpty()) {
            showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, 0);
        }

        if (infoListAdapter.getItemsList().size() == 0) {
            if (result.resultList.size() > 0) {
                infoListAdapter.addInfoItemList(result.resultList);
            } else {
                infoListAdapter.clearStreamItemList();
                showEmptyState();
                return;
            }
        }

        super.handleResult(result);
    }

    @Override
    public void handleNextItems(ListExtractor.NextItemsResult result) {
        showListFooter(false);
        currentPage = Integer.parseInt(result.nextItemsUrl);
        infoListAdapter.addInfoItemList(result.nextItemsList);

        if (!result.errors.isEmpty()) {
            showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId)
                    , "\"" + searchQuery + "\" → page " + currentPage, 0);
        }
        super.handleNextItems(result);
    }

    @Override
    protected boolean onError(Throwable exception) {
        if (super.onError(exception)) return true;

        if (exception instanceof SearchEngine.NothingFoundException) {
            infoListAdapter.clearStreamItemList();
            showEmptyState();
        } else {
            int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error;
            onUnrecoverableError(exception, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, errorId);
        }

        return true;
    }
}
