/*
 * <!--This file is part of LSPosed.
 *
 * LSPosed is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * LSPosed is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with LSPosed.  If not, see <https://www.gnu.org/licenses/>.
 *
 * Copyright (C) 2021 LSPosed Contributors-->
 */

package org.lsposed.manager.ui.fragment;

import android.annotation.SuppressLint;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
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.webkit.WebView;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.view.MenuProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import org.lsposed.manager.App;
import org.lsposed.manager.R;
import org.lsposed.manager.databinding.FragmentRepoBinding;
import org.lsposed.manager.databinding.ItemOnlinemoduleBinding;
import org.lsposed.manager.repo.RepoLoader;
import org.lsposed.manager.repo.model.OnlineModule;
import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
import org.lsposed.manager.util.ModuleUtil;

import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import rikka.core.util.LabelComparator;
import rikka.core.util.ResourceUtils;
import rikka.recyclerview.RecyclerViewKt;

public class RepoFragment extends BaseFragment implements RepoLoader.RepoListener, ModuleUtil.ModuleListener, MenuProvider {
    protected FragmentRepoBinding binding;
    protected SearchView searchView;
    private SearchView.OnQueryTextListener mSearchListener;
    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private boolean preLoadWebview = true;

    private final RepoLoader repoLoader = RepoLoader.getInstance();
    private final ModuleUtil moduleUtil = ModuleUtil.getInstance();
    private RepoAdapter adapter;
    private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() {
        @Override
        public void onChanged() {
            binding.swipeRefreshLayout.setRefreshing(!adapter.isLoaded());
        }
    };

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        mSearchListener = new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                adapter.getFilter().filter(query);
                return false;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                adapter.getFilter().filter(newText);
                return false;
            }
        };
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        binding = FragmentRepoBinding.inflate(getLayoutInflater(), container, false);
        binding.appBar.setLiftable(true);
        binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> binding.appBar.setLifted(!top));
        setupToolbar(binding.toolbar, binding.clickView, R.string.module_repo, R.menu.menu_repo);
        binding.toolbar.setNavigationIcon(null);
        adapter = new RepoAdapter();
        adapter.setHasStableIds(true);
        adapter.registerAdapterDataObserver(observer);
        binding.recyclerView.setAdapter(adapter);
        binding.recyclerView.setHasFixedSize(true);
        binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
        RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
        binding.swipeRefreshLayout.setOnRefreshListener(adapter::fullRefresh);
        binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset());
        View.OnClickListener l = v -> {
            if (searchView.isIconified()) {
                binding.recyclerView.smoothScrollToPosition(0);
                binding.appBar.setExpanded(true, true);
            }
        };
        binding.toolbar.setOnClickListener(l);
        binding.clickView.setOnClickListener(l);
        repoLoader.addListener(this);
        moduleUtil.addListener(this);
        onRepoLoaded();
        return binding.getRoot();
    }

    private void updateRepoSummary() {
        final int[] count = new int[]{0};
        HashSet<String> processedModules = new HashSet<>();
        var modules = moduleUtil.getModules();
        if (modules != null && repoLoader.isRepoLoaded()) {
            modules.forEach((k, v) -> {
                        if (!processedModules.contains(k.first)) {
                            var ver = repoLoader.getModuleLatestVersion(k.first);
                            if (ver != null && ver.upgradable(v.versionCode, v.versionName)) {
                                ++count[0];
                            }
                            processedModules.add(k.first);
                        }
                    }
            );
        } else {
            count[0] = -1;
        }
        runOnUiThread(() -> {
            if (binding != null) {
                if (count[0] > 0) {
                    binding.toolbar.setSubtitle(getResources().getQuantityString(R.plurals.module_repo_upgradable, count[0], count[0]));
                } else if (count[0] == 0) {
                    binding.toolbar.setSubtitle(getResources().getString(R.string.module_repo_up_to_date));
                } else {
                    binding.toolbar.setSubtitle(getResources().getString(R.string.loading));
                }
                binding.toolbarLayout.setSubtitle(binding.toolbar.getSubtitle());
            }
        });
    }

    @Override
    public void onPrepareMenu(Menu menu) {
        searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
        if (searchView != null) {
            searchView.setOnQueryTextListener(mSearchListener);
            searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(@NonNull View arg0) {
                    binding.appBar.setExpanded(false, true);
                    binding.recyclerView.setNestedScrollingEnabled(false);
                }

                @Override
                public void onViewDetachedFromWindow(@NonNull View v) {
                    binding.recyclerView.setNestedScrollingEnabled(true);
                }
            });
            searchView.findViewById(androidx.appcompat.R.id.search_edit_frame).setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT);
        }
        int sort = App.getPreferences().getInt("repo_sort", 0);
        if (sort == 0) {
            menu.findItem(R.id.item_sort_by_name).setChecked(true);
        } else if (sort == 1) {
            menu.findItem(R.id.item_sort_by_update_time).setChecked(true);
        }
        menu.findItem(R.id.item_upgradable_first).setChecked(App.getPreferences().getBoolean("upgradable_first", true));
    }

    @Override
    public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();

        mHandler.removeCallbacksAndMessages(null);
        repoLoader.removeListener(this);
        moduleUtil.removeListener(this);
        adapter.unregisterAdapterDataObserver(observer);
        binding = null;
    }

    @Override
    public void onResume() {
        super.onResume();
        adapter.refresh();
        if (preLoadWebview) {
            mHandler.postDelayed(() -> new WebView(requireContext()), 500);
            preLoadWebview = false;
        }
    }

    @Override
    public void onRepoLoaded() {
        if (adapter != null) {
            adapter.refresh();
        }
        updateRepoSummary();
    }

    @Override
    public void onThrowable(Throwable t) {
        showHint(getString(R.string.repo_load_failed, t.getLocalizedMessage()), true);
        updateRepoSummary();
    }

    @Override
    public void onModulesReloaded() {
        updateRepoSummary();
    }

    @Override
    public boolean onMenuItemSelected(@NonNull MenuItem item) {
        int itemId = item.getItemId();
        if (itemId == R.id.item_sort_by_name) {
            item.setChecked(true);
            App.getPreferences().edit().putInt("repo_sort", 0).apply();
            adapter.refresh();
        } else if (itemId == R.id.item_sort_by_update_time) {
            item.setChecked(true);
            App.getPreferences().edit().putInt("repo_sort", 1).apply();
            adapter.refresh();
        } else if (itemId == R.id.item_upgradable_first) {
            item.setChecked(!item.isChecked());
            App.getPreferences().edit().putBoolean("upgradable_first", item.isChecked()).apply();
            adapter.refresh();
        } else {
            return false;
        }
        return true;
    }

    private class RepoAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<RepoAdapter.ViewHolder> implements Filterable {
        private List<OnlineModule> fullList, showList;
        private final LabelComparator labelComparator = new LabelComparator();
        private boolean isLoaded = false;
        private final Resources resources = App.getInstance().getResources();
        private final String[] channels = resources.getStringArray(R.array.update_channel_values);
        private String channel;
        private final RepoLoader repoLoader = RepoLoader.getInstance();

        RepoAdapter() {
            fullList = showList = Collections.emptyList();
        }

        @NonNull
        @Override
        public RepoAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            return new ViewHolder(ItemOnlinemoduleBinding.inflate(getLayoutInflater(), parent, false));
        }

        RepoLoader.ModuleVersion getUpgradableVer(OnlineModule module) {
            ModuleUtil.InstalledModule installedModule = moduleUtil.getModule(module.getName());
            if (installedModule != null) {
                var ver = repoLoader.getModuleLatestVersion(installedModule.packageName);
                if (ver != null && ver.upgradable(installedModule.versionCode, installedModule.versionName))
                    return ver;
            }
            return null;
        }

        @Override
        public void onBindViewHolder(@NonNull RepoAdapter.ViewHolder holder, int position) {
            OnlineModule module = showList.get(position);
            holder.appName.setText(module.getDescription());
            holder.appPackageName.setText(module.getName());
            Instant instant;
            channel = App.getPreferences().getString("update_channel", channels[0]);
            var latestReleaseTime = repoLoader.getLatestReleaseTime(module.getName(), channel);
            instant = Instant.parse(latestReleaseTime != null ? latestReleaseTime : module.getLatestReleaseTime());
            var formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
                    .withLocale(App.getLocale()).withZone(ZoneId.systemDefault());
            holder.publishedTime.setText(String.format(getString(R.string.module_repo_updated_time), formatter.format(instant)));
            SpannableStringBuilder sb = new SpannableStringBuilder();

            String summary = module.getSummary();
            if (summary != null) {
                sb.append(summary);
            }
            holder.appDescription.setVisibility(View.VISIBLE);
            holder.appDescription.setText(sb);
            sb = new SpannableStringBuilder();
            var upgradableVer = getUpgradableVer(module);
            if (upgradableVer != null) {
                String hint = getString(R.string.update_available, upgradableVer.versionName);
                sb.append(hint);
                final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), com.google.android.material.R.attr.colorPrimary));
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                    final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL));
                    sb.setSpan(typefaceSpan, sb.length() - hint.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
                } else {
                    final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
                    sb.setSpan(styleSpan, sb.length() - hint.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
                }
                sb.setSpan(foregroundColorSpan, sb.length() - hint.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
            } else if (moduleUtil.getModule(module.getName()) != null) {
                String installed = getString(R.string.installed);
                sb.append(installed);
                final StyleSpan styleSpan = new StyleSpan(Typeface.ITALIC);
                sb.setSpan(styleSpan, sb.length() - installed.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
                final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), com.google.android.material.R.attr.colorSecondary));
                sb.setSpan(foregroundColorSpan, sb.length() - installed.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
            }
            if (sb.length() > 0) {
                holder.hint.setVisibility(View.VISIBLE);
                holder.hint.setText(sb);
            } else {
                holder.hint.setVisibility(View.GONE);
            }

            holder.itemView.setOnClickListener(v -> {
                searchView.clearFocus();
                safeNavigate(RepoFragmentDirections.actionRepoFragmentToRepoItemFragment(module.getName()));
            });
            holder.itemView.setTooltipText(module.getDescription());
        }

        @Override
        public int getItemCount() {
            return showList.size();
        }

        @SuppressLint("NotifyDataSetChanged")
        private void setLoaded(List<OnlineModule> list, boolean isLoaded) {
            runOnUiThread(() -> {
                if (list != null) showList = list;
                this.isLoaded = isLoaded;
                notifyDataSetChanged();
            });
        }

        public void setData(Collection<OnlineModule> modules) {
            if (modules == null) return;
            setLoaded(null, false);
            channel = App.getPreferences().getString("update_channel", channels[0]);
            int sort = App.getPreferences().getInt("repo_sort", 0);
            boolean upgradableFirst = App.getPreferences().getBoolean("upgradable_first", true);
            ConcurrentHashMap<String, Boolean> upgradable = new ConcurrentHashMap<>();
            fullList = modules.parallelStream().filter((onlineModule -> !onlineModule.isHide() && !(repoLoader.getReleases(onlineModule.getName()) != null && repoLoader.getReleases(onlineModule.getName()).isEmpty())))
                    .sorted((a, b) -> {
                        if (upgradableFirst) {
                            var aUpgrade = upgradable.computeIfAbsent(a.getName(), n -> getUpgradableVer(a) != null);
                            var bUpgrade = upgradable.computeIfAbsent(b.getName(), n -> getUpgradableVer(b) != null);
                            if (aUpgrade && !bUpgrade) return -1;
                            else if (!aUpgrade && bUpgrade) return 1;
                        }
                        if (sort == 0) {
                            return labelComparator.compare(a.getDescription(), b.getDescription());
                        } else {
                            return Instant.parse(repoLoader.getLatestReleaseTime(b.getName(), channel)).compareTo(Instant.parse(repoLoader.getLatestReleaseTime(a.getName(), channel)));
                        }
                    }).collect(Collectors.toList());
            String queryStr = searchView != null ? searchView.getQuery().toString() : "";
            runOnUiThread(() -> getFilter().filter(queryStr));
        }

        public void fullRefresh() {
            runAsync(() -> {
                setLoaded(null, false);
                repoLoader.loadRemoteData();
                refresh();
            });
        }

        public void refresh() {
            runAsync(() -> adapter.setData(repoLoader.getOnlineModules()));
        }

        @Override
        public long getItemId(int position) {
            return showList.get(position).getName().hashCode();
        }

        @Override
        public Filter getFilter() {
            return new RepoAdapter.ModuleFilter();
        }

        @Override
        public boolean isLoaded() {
            return isLoaded && repoLoader.isRepoLoaded();
        }

        static class ViewHolder extends RecyclerView.ViewHolder {
            ConstraintLayout root;
            TextView appName;
            TextView appPackageName;
            TextView appDescription;
            TextView hint;
            TextView publishedTime;

            ViewHolder(ItemOnlinemoduleBinding binding) {
                super(binding.getRoot());
                root = binding.itemRoot;
                appName = binding.appName;
                appPackageName = binding.appPackageName;
                appDescription = binding.description;
                hint = binding.hint;
                publishedTime = binding.publishedTime;
            }
        }

        class ModuleFilter extends Filter {

            private boolean lowercaseContains(String s, String filter) {
                return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter);
            }

            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                FilterResults filterResults = new FilterResults();
                ArrayList<OnlineModule> filtered = new ArrayList<>();
                String filter = constraint.toString().toLowerCase();
                for (OnlineModule info : fullList) {
                    if (lowercaseContains(info.getDescription(), filter) ||
                            lowercaseContains(info.getName(), filter) ||
                            lowercaseContains(info.getSummary(), filter)) {
                        filtered.add(info);
                    }
                }
                filterResults.values = filtered;
                filterResults.count = filtered.size();
                return filterResults;
            }

            @Override
            protected void publishResults(CharSequence constraint, FilterResults results) {
                //noinspection unchecked
                setLoaded((List<OnlineModule>) results.values, true);
            }
        }
    }
}
