package awais.instagrabber.dialogs;

import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.util.SparseBooleanArray;

import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;

import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.primitives.Booleans;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

public class MultiOptionDialogFragment<T extends Serializable> extends DialogFragment {
    private static final String TAG = MultiOptionDialogFragment.class.getSimpleName();

    public enum Type {
        MULTIPLE,
        SINGLE_CHECKED,
        SINGLE
    }

    private Context context;
    private Type type;
    private MultiOptionDialogCallback<T> callback;
    private MultiOptionDialogSingleCallback<T> singleCallback;
    private List<Option<?>> options;

    @NonNull
    public static <E extends Serializable> MultiOptionDialogFragment<E> newInstance(final int requestCode,
                                                                                    @StringRes final int title,
                                                                                    @NonNull final ArrayList<Option<E>> options) {
        return newInstance(requestCode, title, 0, 0, options, Type.SINGLE);
    }

    @NonNull
    public static <E extends Serializable> MultiOptionDialogFragment<E> newInstance(final int requestCode,
                                                                                    @StringRes final int title,
                                                                                    @StringRes final int positiveButtonText,
                                                                                    @StringRes final int negativeButtonText,
                                                                                    @NonNull final ArrayList<Option<E>> options,
                                                                                    @NonNull final Type type) {
        Bundle args = new Bundle();
        args.putInt("requestCode", requestCode);
        args.putInt("title", title);
        args.putInt("positiveButtonText", positiveButtonText);
        args.putInt("negativeButtonText", negativeButtonText);
        args.putSerializable("options", options);
        args.putSerializable("type", type);
        MultiOptionDialogFragment<E> fragment = new MultiOptionDialogFragment<>();
        fragment.setArguments(args);
        return fragment;
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    @Override
    public void onAttach(@NonNull final Context context) {
        super.onAttach(context);
        this.context = context;
        final Fragment parentFragment = getParentFragment();
        if (parentFragment != null) {
            if (parentFragment instanceof MultiOptionDialogCallback) {
                callback = (MultiOptionDialogCallback) parentFragment;
            }
            if (parentFragment instanceof MultiOptionDialogSingleCallback) {
                singleCallback = (MultiOptionDialogSingleCallback) parentFragment;
            }
            return;
        }
        final FragmentActivity fragmentActivity = getActivity();
        if (fragmentActivity instanceof MultiOptionDialogCallback) {
            callback = (MultiOptionDialogCallback) fragmentActivity;
        }
        if (fragmentActivity instanceof MultiOptionDialogSingleCallback) {
            singleCallback = (MultiOptionDialogSingleCallback) fragmentActivity;
        }
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        final Bundle arguments = getArguments();
        int title = 0;
        int rc = 0;
        if (arguments != null) {
            rc = arguments.getInt("requestCode");
            title = arguments.getInt("title");
            type = (Type) arguments.getSerializable("type");
        }
        final int requestCode = rc;
        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
        if (title != 0) {
            builder.setTitle(title);
        }
        try {
            //noinspection unchecked
            options = arguments != null ? (List<Option<?>>) arguments.getSerializable("options")
                                        : Collections.emptyList();
        } catch (Exception e) {
            Log.e(TAG, "onCreateDialog: ", e);
            options = Collections.emptyList();
        }
        final int negativeButtonText = arguments != null ? arguments.getInt("negativeButtonText", -1) : -1;
        if (negativeButtonText > 0) {
            builder.setNegativeButton(negativeButtonText, (dialog, which) -> {
                if (callback != null) {
                    callback.onCancel(requestCode);
                    return;
                }
                if (singleCallback != null) {
                    singleCallback.onCancel(requestCode);
                }
            });
        }
        if (type == Type.MULTIPLE || type == Type.SINGLE_CHECKED) {
            final int positiveButtonText = arguments != null ? arguments.getInt("positiveButtonText", -1) : -1;
            if (positiveButtonText > 0) {
                builder.setPositiveButton(positiveButtonText, (dialog, which) -> {
                    if (callback == null || options == null || options.isEmpty()) return;
                    try {
                        final List<T> selected = new ArrayList<>();
                        final SparseBooleanArray checkedItemPositions = ((AlertDialog) dialog).getListView().getCheckedItemPositions();
                        for (int i = 0; i < checkedItemPositions.size(); i++) {
                            final int position = checkedItemPositions.keyAt(i);
                            final boolean checked = checkedItemPositions.get(position);
                            if (!checked) continue;
                            //noinspection unchecked
                            final Option<T> option = (Option<T>) options.get(position);
                            selected.add(option.value);
                        }
                        callback.onMultipleSelect(requestCode, selected);
                    } catch (Exception e) {
                        Log.e(TAG, "onCreateDialog: ", e);
                    }
                });
            }
        }
        if (type == Type.MULTIPLE) {
            if (options != null && !options.isEmpty()) {
                final String[] items = options.stream()
                                              .map(option -> option.label)
                                              .toArray(String[]::new);
                final boolean[] checkedItems = Booleans.toArray(options.stream()
                                                                       .map(option -> option.checked)
                                                                       .collect(Collectors.toList()));
                builder.setMultiChoiceItems(items, checkedItems, (dialog, which, isChecked) -> {
                    if (callback == null) return;
                    try {
                        final Option<?> option = options.get(which);
                        //noinspection unchecked
                        callback.onCheckChange(requestCode, (T) option.value, isChecked);
                    } catch (Exception e) {
                        Log.e(TAG, "onCreateDialog: ", e);
                    }
                });
            }
        } else {
            if (options != null && !options.isEmpty()) {
                final String[] items = options.stream()
                                              .map(option -> option.label)
                                              .toArray(String[]::new);
                if (type == Type.SINGLE_CHECKED) {
                    int index = -1;
                    for (int i = 0; i < options.size(); i++) {
                        if (options.get(i).checked) {
                            index = i;
                            break;
                        }
                    }
                    builder.setSingleChoiceItems(items, index, (dialog, which) -> {
                        if (callback == null) return;
                        try {
                            final Option<?> option = options.get(which);
                            //noinspection unchecked
                            callback.onCheckChange(requestCode, (T) option.value, true);
                        } catch (Exception e) {
                            Log.e(TAG, "onCreateDialog: ", e);
                        }
                    });
                } else if (type == Type.SINGLE) {
                    builder.setItems(items, (dialog, which) -> {
                        if (singleCallback == null) return;
                        try {
                            final Option<?> option = options.get(which);
                            //noinspection unchecked
                            singleCallback.onSelect(requestCode, (T) option.value);
                        } catch (Exception e) {
                            Log.e(TAG, "onCreateDialog: ", e);
                        }
                    });
                }
            }
        }
        return builder.create();
    }

    public void setCallback(final MultiOptionDialogCallback<T> callback) {
        if (callback == null) return;
        this.callback = callback;
    }

    public void setSingleCallback(final MultiOptionDialogSingleCallback<T> callback) {
        if (callback == null) return;
        this.singleCallback = callback;
    }

    public interface MultiOptionDialogCallback<T> {
        void onSelect(int requestCode, T result);

        void onMultipleSelect(int requestCode, List<T> result);

        void onCheckChange(int requestCode, T item, boolean isChecked);

        void onCancel(int requestCode);
    }

    public interface MultiOptionDialogSingleCallback<T> {
        void onSelect(int requestCode, T result);

        void onCancel(int requestCode);
    }

    public static class Option<T extends Serializable> implements Serializable {
        private final String label;
        private final T value;
        private final boolean checked;

        public Option(final String label, final T value) {
            this.label = label;
            this.value = value;
            this.checked = false;
        }

        public Option(final String label, final T value, final boolean checked) {
            this.label = label;
            this.value = value;
            this.checked = checked;
        }

        public String getLabel() {
            return label;
        }

        public T getValue() {
            return value;
        }

        public boolean isChecked() {
            return checked;
        }
    }
}
