/*
 * 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.ActivityNotFoundException;
import android.os.Bundle;
import android.util.Log;
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.HorizontalScrollView;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.MenuProvider;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.adapter.FragmentStateAdapter;

import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import com.google.android.material.textview.MaterialTextView;

import org.lsposed.manager.App;
import org.lsposed.manager.ConfigManager;
import org.lsposed.manager.R;
import org.lsposed.manager.databinding.FragmentPagerBinding;
import org.lsposed.manager.databinding.ItemLogTextviewBinding;
import org.lsposed.manager.databinding.SwiperefreshRecyclerviewBinding;
import org.lsposed.manager.receivers.LSPManagerServiceHolder;
import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
import org.lsposed.manager.util.AccessibilityUtils;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import rikka.material.app.LocaleDelegate;
import rikka.recyclerview.RecyclerViewKt;

public class LogsFragment extends BaseFragment implements MenuProvider {
    private FragmentPagerBinding binding;
    private LogPageAdapter adapter;
    private MenuItem wordWrap;

    interface OptionsItemSelectListener {
        boolean onOptionsItemSelected(@NonNull MenuItem item);
    }

    private OptionsItemSelectListener optionsItemSelectListener;

    private final ActivityResultLauncher<String> saveLogsLauncher = registerForActivityResult(
            new ActivityResultContracts.CreateDocument("application/zip"),
            uri -> {
                if (uri == null) return;
                runAsync(() -> {
                    var context = requireContext();
                    var cr = context.getContentResolver();
                    try (var zipFd = cr.openFileDescriptor(uri, "wt")) {
                        showHint(context.getString(R.string.logs_saving), false);
                        LSPManagerServiceHolder.getService().getLogs(zipFd);
                        showHint(context.getString(R.string.logs_saved), true);
                    } catch (Throwable e) {
                        var cause = e.getCause();
                        var message = cause == null ? e.getMessage() : cause.getMessage();
                        var text = context.getString(R.string.logs_save_failed2, message);
                        showHint(text, false);
                        Log.w(App.TAG, "save log", e);
                    }
                });
            });

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        binding = FragmentPagerBinding.inflate(inflater, container, false);
        binding.appBar.setLiftable(true);
        setupToolbar(binding.toolbar, binding.clickView, R.string.Logs, R.menu.menu_logs);
        binding.toolbar.setNavigationIcon(null);
        binding.toolbar.setSubtitle(ConfigManager.isVerboseLogEnabled() ? R.string.enabled_verbose_log : R.string.disabled_verbose_log);
        adapter = new LogPageAdapter(this);
        binding.viewPager.setAdapter(adapter);

        var isAnimationEnabled = AccessibilityUtils.isAnimationEnabled(requireContext().getContentResolver());
        new TabLayoutMediator(
            binding.tabLayout,
            binding.viewPager,
            // `autoRefresh = true` by default. Update the tabs automatically when the data set of the view pager's
            // adapter changes.
            true,
            isAnimationEnabled,
            (tab, position) -> tab.setText((int) adapter.getItemId(position))
        ).attach();

        binding.tabLayout.addOnLayoutChangeListener((view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
            ViewGroup vg = (ViewGroup) binding.tabLayout.getChildAt(0);
            int tabLayoutWidth = IntStream.range(0, binding.tabLayout.getTabCount()).map(i -> vg.getChildAt(i).getWidth()).sum();
            if (tabLayoutWidth <= binding.getRoot().getWidth()) {
                binding.tabLayout.setTabMode(TabLayout.MODE_FIXED);
                binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
            }
        });

        return binding.getRoot();
    }

    public void setOptionsItemSelectListener(OptionsItemSelectListener optionsItemSelectListener) {
        this.optionsItemSelectListener = optionsItemSelectListener;
    }

    @Override
    public boolean onMenuItemSelected(@NonNull MenuItem item) {
        var itemId = item.getItemId();
        if (itemId == R.id.menu_save) {
            save();
            return true;
        } else if (itemId == R.id.menu_word_wrap) {
            item.setChecked(!item.isChecked());
            App.getPreferences().edit().putBoolean("enable_word_wrap", item.isChecked()).apply();
            binding.viewPager.setUserInputEnabled(item.isChecked());
            adapter.refresh();
            return true;
        }
        if (optionsItemSelectListener != null) {
            return optionsItemSelectListener.onOptionsItemSelected(item);
        }
        return false;
    }

    @Override
    public void onPrepareMenu(@NonNull Menu menu) {
        wordWrap = menu.findItem(R.id.menu_word_wrap);
        wordWrap.setChecked(App.getPreferences().getBoolean("enable_word_wrap", false));
        binding.viewPager.setUserInputEnabled(wordWrap.isChecked());
    }

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

    }

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

        binding = null;
    }

    private void save() {
        LocalDateTime now = LocalDateTime.now();
        String filename = String.format(LocaleDelegate.getDefaultLocale(), "LSPosed_%s.zip", now.toString());
        try {
            saveLogsLauncher.launch(filename);
        } catch (ActivityNotFoundException e) {
            showHint(R.string.enable_documentui, true);
        }
    }

    public static class LogFragment extends BaseFragment {
        public static final int SCROLL_THRESHOLD = 500;
        protected boolean verbose;
        protected SwiperefreshRecyclerviewBinding binding;
        protected LogAdaptor adaptor;
        protected LinearLayoutManager layoutManager;

        class LogAdaptor extends EmptyStateRecyclerView.EmptyStateAdapter<LogAdaptor.ViewHolder> {
            private List<CharSequence> log = Collections.emptyList();
            private boolean isLoaded = false;

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

            @Override
            public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
                holder.item.setText(log.get(position));
            }

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

            @SuppressLint("NotifyDataSetChanged")
            void refresh(List<CharSequence> log) {
                runOnUiThread(() -> {
                    isLoaded = true;
                    this.log = log;
                    notifyDataSetChanged();
                });
            }

            void fullRefresh() {
                runAsync(() -> {
                    isLoaded = false;
                    List<CharSequence> tmp;
                    try (var parcelFileDescriptor = ConfigManager.getLog(verbose);
                         var br = new BufferedReader(new InputStreamReader(new FileInputStream(parcelFileDescriptor != null ? parcelFileDescriptor.getFileDescriptor() : null)))) {
                        tmp = br.lines().parallel().collect(Collectors.toList());
                    } catch (Throwable e) {
                        tmp = Arrays.asList(Log.getStackTraceString(e).split("\n"));
                    }
                    refresh(tmp);
                });
            }

            @Override
            public boolean isLoaded() {
                return isLoaded;
            }

            class ViewHolder extends RecyclerView.ViewHolder {
                final MaterialTextView item;

                public ViewHolder(ItemLogTextviewBinding binding) {
                    super(binding.getRoot());
                    item = binding.logItem;
                }
            }
        }

        protected LogAdaptor createAdaptor() {
            return new LogAdaptor();
        }

        @Nullable
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            binding = SwiperefreshRecyclerviewBinding.inflate(getLayoutInflater(), container, false);
            var arguments = getArguments();
            if (arguments == null) return null;
            verbose = arguments.getBoolean("verbose");
            adaptor = createAdaptor();
            binding.recyclerView.setAdapter(adaptor);
            layoutManager = new LinearLayoutManager(requireActivity());
            binding.recyclerView.setLayoutManager(layoutManager);
            // ltr even for rtl languages because of log format
            binding.recyclerView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
            binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset());
            RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
            binding.swipeRefreshLayout.setOnRefreshListener(adaptor::fullRefresh);
            adaptor.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
                @Override
                public void onChanged() {
                    binding.swipeRefreshLayout.setRefreshing(!adaptor.isLoaded());
                }
            });
            adaptor.fullRefresh();
            return binding.getRoot();
        }

        public void scrollToTop(LogsFragment logsFragment) {
            logsFragment.binding.appBar.setExpanded(true, true);
            if (layoutManager.findFirstVisibleItemPosition() > SCROLL_THRESHOLD) {
                binding.recyclerView.scrollToPosition(0);
            } else {
                binding.recyclerView.smoothScrollToPosition(0);
            }
        }

        public void scrollToBottom(LogsFragment logsFragment) {
            logsFragment.binding.appBar.setExpanded(false, true);
            var end = Math.max(adaptor.getItemCount() - 1, 0);
            if (adaptor.getItemCount() - layoutManager.findLastVisibleItemPosition() > SCROLL_THRESHOLD) {
                binding.recyclerView.scrollToPosition(end);
            } else {
                binding.recyclerView.smoothScrollToPosition(end);
            }
        }

        void attachListeners() {
            var parent = getParentFragment();
            if (parent instanceof LogsFragment logsFragment) {
                logsFragment.binding.appBar.setLifted(!binding.recyclerView.getBorderViewDelegate().isShowingTopBorder());
                binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> logsFragment.binding.appBar.setLifted(!top));
                logsFragment.setOptionsItemSelectListener(item -> {
                    int itemId = item.getItemId();
                    if (itemId == R.id.menu_scroll_top) {
                        scrollToTop(logsFragment);
                    } else if (itemId == R.id.menu_scroll_down) {
                        scrollToBottom(logsFragment);
                    } else if (itemId == R.id.menu_clear) {
                        if (ConfigManager.clearLogs(verbose)) {
                            logsFragment.showHint(R.string.logs_cleared, true);
                            adaptor.fullRefresh();
                        } else {
                            logsFragment.showHint(R.string.logs_clear_failed_2, true);
                        }
                        return true;
                    }
                    return false;
                });

                View.OnClickListener l = v -> scrollToTop(logsFragment);
                logsFragment.binding.clickView.setOnClickListener(l);
                logsFragment.binding.toolbar.setOnClickListener(l);
            }
        }

        void detachListeners() {
            binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener(null);
        }

        @Override
        public void onStart() {
            super.onStart();
            attachListeners();
        }

        @Override
        public void onResume() {
            super.onResume();
            attachListeners();
        }


        @Override
        public void onPause() {
            super.onPause();
            detachListeners();
        }

        @Override
        public void onStop() {
            super.onStop();
            detachListeners();
        }
    }

    public static class UnwrapLogFragment extends LogFragment {

        @Nullable
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            var root = super.onCreateView(inflater, container, savedInstanceState);
            binding.swipeRefreshLayout.removeView(binding.recyclerView);
            HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext());
            horizontalScrollView.setFillViewport(true);
            horizontalScrollView.setHorizontalScrollBarEnabled(false);
            horizontalScrollView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
            if (!AccessibilityUtils.isAnimationEnabled(requireContext().getContentResolver())) {
                horizontalScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
            }
            binding.swipeRefreshLayout.addView(horizontalScrollView);
            horizontalScrollView.addView(binding.recyclerView);
            binding.recyclerView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
            return root;
        }

        @Override
        protected LogAdaptor createAdaptor() {
            return new LogAdaptor() {
                @Override
                public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
                    super.onBindViewHolder(holder, position);
                    var view = holder.item;
                    view.measure(0, 0);
                    int desiredWidth = view.getMeasuredWidth();
                    ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
                    layoutParams.width = desiredWidth;
                    if (binding.recyclerView.getWidth() < desiredWidth) {
                        binding.recyclerView.requestLayout();
                    }
                }
            };
        }
    }

    class LogPageAdapter extends FragmentStateAdapter {

        public LogPageAdapter(@NonNull Fragment fragment) {
            super(fragment);
        }

        @NonNull
        @Override
        public Fragment createFragment(int position) {
            var bundle = new Bundle();
            bundle.putBoolean("verbose", verbose(position));
            var f = getItemViewType(position) == 0 ? new LogFragment() : new UnwrapLogFragment();
            f.setArguments(bundle);
            return f;
        }

        @Override
        public int getItemCount() {
            return 2;
        }

        @Override
        public long getItemId(int position) {
            return verbose(position) ? R.string.nav_item_logs_verbose : R.string.nav_item_logs_module;
        }

        @Override
        public boolean containsItem(long itemId) {
            return itemId == R.string.nav_item_logs_verbose || itemId == R.string.nav_item_logs_module;
        }

        public boolean verbose(int position) {
            return position != 0;
        }

        @Override
        public int getItemViewType(int position) {
            return wordWrap.isChecked() ? 0 : 1;
        }

        public void refresh() {
            runOnUiThread(this::notifyDataSetChanged);
        }
    }
}
