// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.view.WindowMetrics;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.BlendMode;
import android.graphics.BlendModeColorFilter;
import android.graphics.Insets;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.util.TypedValue;
import android.view.inputmethod.InputMethodManager;
import android.app.Activity;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.view.inputmethod.BaseInputConnection;
import android.os.Build;

class InputHandle extends ImageView {
    private PopupWindow mPopupWindow;
    private float mPressedX;
    private float mPressedY;
    private SlintInputView mRootView;
    private int cursorX;
    private int cursorY;
    private int attr;

    public InputHandle(SlintInputView rootView, int attr) {
        super(rootView.getContext());
        this.attr = attr;
        mRootView = rootView;
        Context ctx = rootView.getContext();
        mPopupWindow = new PopupWindow(ctx, null, android.R.attr.textSelectHandleWindowStyle);
        mPopupWindow.setSplitTouchEnabled(true);
        mPopupWindow.setClippingEnabled(false);
        int[] attrs = { attr };
        Drawable drawable = ctx.getTheme().obtainStyledAttributes(attrs).getDrawable(0);
        mPopupWindow.setWidth(drawable.getIntrinsicWidth());
        mPopupWindow.setHeight(drawable.getIntrinsicHeight());
        this.setImageDrawable(drawable);
        mPopupWindow.setContentView(this);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: {
                mPressedX = ev.getRawX() - cursorX;
                mPressedY = ev.getRawY() - cursorY;
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                mRootView.hideActionMenu(ActionMode.DEFAULT_HIDE_DURATION);
                int id = attr == android.R.attr.textSelectHandleLeft ? 1
                        : attr == android.R.attr.textSelectHandleRight ? 2 : 0;
                SlintAndroidJavaHelper.moveCursorHandle(id, Math.round(ev.getRawX() - mPressedX),
                        Math.round(ev.getRawY() - mPressedY));
                break;
            }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return true;
    }

    public void setPosition(int x, int y) {
        cursorX = x;
        cursorY = y;

        y += mPopupWindow.getHeight();
        if (attr == android.R.attr.textSelectHandleLeft) {
            x -= 3 * mPopupWindow.getWidth() / 4;
        } else if (attr == android.R.attr.textSelectHandleRight) {
            x -= mPopupWindow.getWidth() / 4;
        } else {
            x -= mPopupWindow.getWidth() / 2;
        }

        mPopupWindow.showAtLocation(mRootView, 0, x, y);
        mPopupWindow.update(x, y, -1, -1);
    }

    public void hide() {
        mPopupWindow.dismiss();
    }

    public void setHandleColor(int color) {
        Drawable drawable = getDrawable();
        if (drawable != null) {
            if (android.os.Build.VERSION.SDK_INT >= 29) {
                drawable.setColorFilter(new BlendModeColorFilter(color, BlendMode.SRC_IN));
            } else {
                drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
            }
            setImageDrawable(drawable);
        }
    }
}

class SlintInputView extends View {
    private String mText = "";
    private int mCursorPosition = 0;
    private int mAnchorPosition = 0;
    private int mPreeditStart = 0;
    private int mPreeditEnd = 0;
    private int mInputType = EditorInfo.TYPE_CLASS_TEXT;
    private int mInBatch = 0;
    private boolean mPending = false;
    private SlintEditable mEditable;

    public class SlintEditable extends SpannableStringBuilder {
        public SlintEditable() {
            super(mText);
        }

        @Override
        public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart, int tbend) {
            super.replace(start, end, tb, tbstart, tbend);
            setCursorPos(0, 0, 0, 0, 0, 0);
            if (mInBatch == 0) {
                update();
            } else {
                mPending = true;
            }
            return this;
        }

        public void update() {
            mPending = false;
            mText = toString();
            mCursorPosition = Selection.getSelectionStart(this);
            mAnchorPosition = Selection.getSelectionEnd(this);
            mPreeditStart = BaseInputConnection.getComposingSpanStart(this);
            mPreeditEnd = BaseInputConnection.getComposingSpanEnd(this);
            SlintAndroidJavaHelper.updateText(mText, mCursorPosition, mAnchorPosition, mPreeditStart, mPreeditEnd);
        }
    }

    public SlintInputView(Context context) {
        super(context);
        setFocusable(true);
        setFocusableInTouchMode(true);
        mEditable = new SlintEditable();
    }

    @Override
    public boolean onCheckIsTextEditor() {
        return true;
    }

    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        outAttrs.inputType = mInputType;
        outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI;
        outAttrs.initialSelStart = mCursorPosition;
        outAttrs.initialSelEnd = mAnchorPosition;
        return new BaseInputConnection(this, true) {
            @Override
            public Editable getEditable() {
                return mEditable;
            }

            @Override
            public boolean beginBatchEdit() {
                mInBatch += 1;
                return super.beginBatchEdit();
            }

            @Override
            public boolean endBatchEdit() {
                mInBatch -= 1;
                if (mInBatch == 0 && mPending) {
                    mEditable.update();
                }
                return super.endBatchEdit();
            }
        };
    }

    public void setText(String text, int cursorPosition, int anchorPosition, int preeditStart, int preeditEnd,
            int inputType) {
        boolean restart = mInputType != inputType || !mText.equals(text) || mCursorPosition != cursorPosition
                || mAnchorPosition != anchorPosition;
        mText = text;
        mCursorPosition = cursorPosition;
        mAnchorPosition = anchorPosition;
        mPreeditStart = preeditStart;
        mPreeditEnd = preeditEnd;
        mInputType = inputType;

        if (restart) {
            mEditable = new SlintEditable();
            Selection.setSelection(mEditable, cursorPosition, anchorPosition);
            InputMethodManager imm = (InputMethodManager) this.getContext()
                    .getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.restartInput(this);
        }
    }

    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
        SlintAndroidJavaHelper.setNightMode(currentNightMode);
    }

    private InputHandle mCursorHandle;
    private InputHandle mLeftHandle;
    private InputHandle mRightHandle;
    public Rect selectionRect = new Rect();

    // num_handles: 0=hidden, 1=cursor handle, 2=selection handles
    public void setCursorPos(int left_x, int left_y, int right_x, int right_y, int cursor_height, int num_handles) {
        int handleHeight = 0;
        if (num_handles == 1) {
            if (mLeftHandle != null) {
                mLeftHandle.hide();
            }
            if (mRightHandle != null) {
                mRightHandle.hide();
            }
            if (mCursorHandle == null) {
                mCursorHandle = new InputHandle(this, android.R.attr.textSelectHandle);
            }
            mCursorHandle.setPosition(left_x, left_y);
            handleHeight = mCursorHandle.getHeight();
        } else if (num_handles == 2) {
            if (left_x != -1) {
                if (mLeftHandle == null) {
                    mLeftHandle = new InputHandle(this, android.R.attr.textSelectHandleLeft);
                }
                mLeftHandle.setPosition(left_x, left_y);
                handleHeight = mLeftHandle.getHeight();
            } else {
                if (mLeftHandle != null) {
                    mLeftHandle.hide();
                }
            }
            if (right_x != -1) {
                if (mRightHandle == null) {
                    mRightHandle = new InputHandle(this, android.R.attr.textSelectHandleRight);
                }
                mRightHandle.setPosition(right_x, right_y);
                handleHeight = mRightHandle.getHeight();
            } else {
                if (mRightHandle != null) {
                    mRightHandle.hide();
                }
            }
            if (mCursorHandle != null) {
                mCursorHandle.hide();
            }
            showActionMenu();
        } else {
            if (mCursorHandle != null) {
                handleHeight = mCursorHandle.getHeight();
                mCursorHandle.hide();
            }
            if (mLeftHandle != null) {
                mLeftHandle.hide();
            }
            if (mRightHandle != null) {
                mRightHandle.hide();
            }
            hideActionMenu(-1);
        }

        selectionRect.set(Math.min(left_x, right_x), Math.min(left_y, right_y) - cursor_height,
                Math.max(left_x, right_x), Math.max(left_y, right_y) + handleHeight);
        if (mCurrentActionMode != null) {
            mCurrentActionMode.invalidateContentRect();
        }
    }

    public void setHandleColor(int color) {
        if (mCursorHandle != null) {
            mCursorHandle.setHandleColor(color);
        }
        if (mLeftHandle != null) {
            mLeftHandle.setHandleColor(color);
        }
        if (mRightHandle != null) {
            mRightHandle.setHandleColor(color);
        }
    }

    private ActionMode mCurrentActionMode;

    public void showActionMenu() {
        if (mCurrentActionMode != null) {
            mCurrentActionMode.hide(0);
            return;
        }
        ActionMode.Callback2 action = new ActionMode.Callback2() {
            @Override
            public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                mode.setTitle(null);
                mode.setSubtitle(null);
                mode.setTitleOptionalHint(true);
                if (android.os.Build.VERSION.SDK_INT >= 28) {
                    menu.setGroupDividerEnabled(true);
                }

                final TypedArray a = getContext().obtainStyledAttributes(new int[] {
                        android.R.attr.actionModeCutDrawable,
                        android.R.attr.actionModeCopyDrawable,
                        android.R.attr.actionModePasteDrawable,
                        android.R.attr.actionModeSelectAllDrawable,
                });

                // Note: the ids are used in Java_SlintAndroidJavaHelper_popupMenuAction
                menu.add(Menu.FIRST, 0, 0, android.R.string.cut)
                        .setAlphabeticShortcut('x')
                        .setIcon(a.getDrawable(0));
                menu.add(Menu.FIRST, 1, 1, android.R.string.copy)
                        .setAlphabeticShortcut('c')
                        .setIcon(a.getDrawable(1));
                menu.add(Menu.FIRST, 2, 2, android.R.string.paste)
                        .setAlphabeticShortcut('v')
                        .setIcon(a.getDrawable(2));
                menu.add(Menu.FIRST, 3, 3, android.R.string.selectAll)
                        .setAlphabeticShortcut('a')
                        .setIcon(a.getDrawable(3));

                a.recycle();

                return true;
            }

            @Override
            public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
                return true;
            }

            @Override
            public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
                SlintAndroidJavaHelper.popupMenuAction(item.getItemId());
                mode.finish();
                return true;
            }

            @Override
            public void onDestroyActionMode(ActionMode action) {
            }

            // Introduced in API level 23
            @Override
            public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
                outRect.set(selectionRect);
                int actionBarHeight = 0;
                TypedValue tv = new TypedValue();
                if (getContext().getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
                    actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data,
                            getContext().getResources().getDisplayMetrics());
                }
                outRect.top -= actionBarHeight;
                if (outRect.top < 0) {
                    // FIXME: I don't know why this is the case, but without that, the menu doesn't
                    // show at the right position when there is no room on top.
                    // Looks like the menu is always shown at outRect.top.
                    outRect.top = outRect.bottom;
                }
            }

        };
        mCurrentActionMode = startActionMode(action, ActionMode.TYPE_FLOATING);

    }

    public void hideActionMenu(int duration) {
        if (mCurrentActionMode != null) {
            if (duration < 0) {
                mCurrentActionMode.finish();
                mCurrentActionMode = null;
            } else {
                mCurrentActionMode.hide(duration);
            }
        }
    }
}

public class SlintAndroidJavaHelper {
    Activity mActivity;
    SlintInputView mInputView;

    public SlintAndroidJavaHelper(Activity activity) {
        this.mActivity = activity;
        this.mInputView = new SlintInputView(activity);
        this.mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
                        FrameLayout.LayoutParams.MATCH_PARENT);
                mActivity.addContentView(mInputView, params);
                mInputView.setVisibility(View.VISIBLE);
            }
        });
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            activity.getWindow().getDecorView().getRootView()
                    .setWindowInsetsAnimationCallback(
                            new WindowInsetsAnimation.Callback(
                                    WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
                                @Override
                                public WindowInsets onProgress(WindowInsets insets,
                                        java.util.List<WindowInsetsAnimation> runningAnimations) {
                                    mActivity.runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            Insets safeAreaInsets = insets.getInsets(WindowInsets.Type.systemBars());
                                            Insets keyboardAreaInsets = insets.getInsets(WindowInsets.Type.ime());
                                            Rect windowRect = get_view_rect();

                                            SlintAndroidJavaHelper.setInsets(
                                                    windowRect.top, windowRect.left,
                                                    windowRect.bottom, windowRect.right,
                                                    safeAreaInsets.top, safeAreaInsets.left,
                                                    safeAreaInsets.bottom, safeAreaInsets.right,
                                                    keyboardAreaInsets.top, keyboardAreaInsets.left,
                                                    keyboardAreaInsets.bottom, keyboardAreaInsets.right);
                                        }
                                    });
                                    return insets;
                                }
                            });
        } else {
            activity.getWindow().getDecorView().getRootView().getViewTreeObserver()
                    .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                        @Override
                        public void onGlobalLayout() {
                            mActivity.runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    Rect windowRect = get_view_rect();
                                    Rect safeAreaRect = get_safe_area();

                                    // This is only an approximation, because SDK level < 30 doesn't provide
                                    // a way to get the keyboard area directly.
                                    Rect visibleRect = new Rect();
                                    mActivity.getWindow().getDecorView().getRootView()
                                            .getWindowVisibleDisplayFrame(visibleRect);
                                    int keyboardBottom = windowRect.bottom - visibleRect.bottom;
                                    int keyboardLeft = windowRect.left - visibleRect.left;
                                    int keyboardTop = windowRect.top - visibleRect.top;
                                    int keyboardRight = windowRect.right - visibleRect.right;
                                    int max = Math.max(keyboardBottom, Math.max(keyboardLeft,
                                            Math.max(keyboardTop, keyboardRight)));

                                    // only take the largest value (it's probably always going to be bottom)
                                    if (max == keyboardBottom) {
                                        keyboardTop = 0;
                                        keyboardLeft = 0;
                                        keyboardRight = 0;
                                    } else if (max == keyboardLeft) {
                                        keyboardTop = 0;
                                        keyboardRight = 0;
                                        keyboardBottom = 0;
                                    } else if (max == keyboardTop) {
                                        keyboardLeft = 0;
                                        keyboardRight = 0;
                                        keyboardBottom = 0;
                                    } else {
                                        keyboardTop = 0;
                                        keyboardLeft = 0;
                                        keyboardBottom = 0;
                                    }

                                    SlintAndroidJavaHelper.setInsets(
                                            windowRect.top, windowRect.left,
                                            windowRect.bottom, windowRect.right,
                                            safeAreaRect.top, safeAreaRect.left,
                                            safeAreaRect.bottom, safeAreaRect.right,
                                            keyboardTop, keyboardLeft,
                                            keyboardBottom, keyboardRight);
                                }
                            });
                        }
                    });
        }
    }

    public void show_keyboard() {
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mInputView.requestFocus();
                InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
                imm.showSoftInput(mInputView, 0);
            }
        });
    }

    public void hide_keyboard() {
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
                imm.hideSoftInputFromWindow(mInputView.getWindowToken(), 0);
                mInputView.clearFocus();
                mInputView.setCursorPos(0, 0, 0, 0, 0, 0);
            }
        });
    }

    static public native void updateText(String text, int cursorPosition, int anchorPosition, int preeditStart,
            int preeditOffset);

    static public native void setNightMode(int nightMode);

    static public native void moveCursorHandle(int id, int pos_x, int pos_y);

    static public native void popupMenuAction(int id);

    static public native void setInsets(int window_top, int window_left, int window_bottom, int window_right,
            int safe_area_top, int safe_area_left, int safe_area_bottom, int safe_area_right,
            int keyboard_top, int keyboard_left, int keyboard_bottom, int keyboard_right);

    public void set_imm_data(String text, int cursor_position, int anchor_position, int preedit_start, int preedit_end,
            int cur_x, int cur_y, int anchor_x, int anchor_y, int cursor_height, int input_type,
            boolean show_cursor_handles) {

        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                int selStart = Math.min(cursor_position, anchor_position);
                int selEnd = Math.max(cursor_position, anchor_position);
                mInputView.setText(text, selStart, selEnd, preedit_start, preedit_end, input_type);
                int num_handles = 0;
                if (show_cursor_handles) {
                    num_handles = cursor_position == anchor_position ? 1 : 2;
                }
                if (cursor_position < anchor_position) {
                    mInputView.setCursorPos(cur_x, cur_y, anchor_x, anchor_y, cursor_height, num_handles);
                } else {
                    mInputView.setCursorPos(anchor_x, anchor_y, cur_x, cur_y, cursor_height, num_handles);
                }

            }
        });
    }

    public void set_handle_color(int color) {
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mInputView.setHandleColor(color);
            }
        });
    }

    public int color_scheme() {
        int nightModeFlags = mActivity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
        return nightModeFlags;
    }

    // Get the size of the window
    public Rect get_view_rect() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            // On Android 11 and above, we can get the window bounds directly
            WindowMetrics metrics = mActivity.getWindowManager().getCurrentWindowMetrics();
            return metrics.getBounds();
        } else {
            View rootView = mActivity.getWindow().getDecorView().getRootView();
            return new Rect(rootView.getLeft(), rootView.getTop(), rootView.getRight(), rootView.getBottom());
        }
    }

    // On SDK level < 30, returns the inset for the safe area and the keyboard.
    // On SDK level >= 30, returns the inset for the safe area only.
    public Rect get_safe_area() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            WindowMetrics metrics = mActivity.getWindowManager().getCurrentWindowMetrics();
            WindowInsets insets = metrics.getWindowInsets();
            Insets systemBars = insets.getInsets(WindowInsets.Type.systemBars());
            return new Rect(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
        } else {
            View decorView = mActivity.getWindow().getDecorView();
            // Note: `View.getRootWindowInsets` requires API level 23 or above
            WindowInsets insets = decorView.getRootView().getRootWindowInsets();
            if (insets != null) {
                return new Rect(
                        insets.getStableInsetLeft(),
                        insets.getStableInsetTop(),
                        insets.getStableInsetRight(),
                        insets.getStableInsetBottom());
            }
            return new Rect(0, 0, 0, 0);
        }
    }

    public void show_action_menu() {
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mInputView.showActionMenu();
            }
        });
    }

    public String get_clipboard() {
        FutureTask<String> future = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
                if (clipboard.hasPrimaryClip()) {
                    ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
                    return item.getText().toString();
                }
                return "";
            }
        });

        mActivity.runOnUiThread(future);
        try {
            return future.get(); // Wait for the result and return it
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }

    public void set_clipboard(String text) {
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
                ClipData clip = ClipData.newPlainText(null, text);
                clipboard.setPrimaryClip(clip);
            }
        });
    }
}
