/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2022 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

package com.rmsl.juce;

import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.text.InputType;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public final class ComponentPeerView extends ViewGroup
        implements View.OnFocusChangeListener, Application.ActivityLifecycleCallbacks
{
    public ComponentPeerView (Context context, boolean opaque_, long host)
    {
        super (context);

        if (Application.class.isInstance (context))
        {
            ((Application) context).registerActivityLifecycleCallbacks (this);
        } else
        {
            ((Application) context.getApplicationContext ()).registerActivityLifecycleCallbacks (this);
        }

        this.host = host;
        setWillNotDraw (false);
        opaque = opaque_;

        setFocusable (true);
        setFocusableInTouchMode (true);
        setOnFocusChangeListener (this);

        // swap red and blue colours to match internal opengl texture format
        ColorMatrix colorMatrix = new ColorMatrix ();

        float[] colorTransform = {0, 0, 1.0f, 0, 0,
                0, 1.0f, 0, 0, 0,
                1.0f, 0, 0, 0, 0,
                0, 0, 0, 1.0f, 0};

        colorMatrix.set (colorTransform);
        paint.setColorFilter (new ColorMatrixColorFilter (colorMatrix));

        java.lang.reflect.Method method = null;

        try
        {
            method = getClass ().getMethod ("setLayerType", int.class, Paint.class);
        } catch (SecurityException e)
        {
        } catch (NoSuchMethodException e)
        {
        }

        if (method != null)
        {
            try
            {
                int layerTypeNone = 0;
                method.invoke (this, layerTypeNone, null);
            } catch (java.lang.IllegalArgumentException e)
            {
            } catch (java.lang.IllegalAccessException e)
            {
            } catch (java.lang.reflect.InvocationTargetException e)
            {
            }
        }
    }

    public void clear ()
    {
        host = 0;
    }

    //==============================================================================
    private native void handlePaint (long host, Canvas canvas, Paint paint);

    @Override
    public void onDraw (Canvas canvas)
    {
        if (host == 0)
            return;

        handlePaint (host, canvas, paint);
    }

    @Override
    public boolean isOpaque ()
    {
        return opaque;
    }

    private final boolean opaque;
    private long host;
    private final Paint paint = new Paint ();

    //==============================================================================
    private native void handleMouseDown (long host, int index, float x, float y, long time);
    private native void handleMouseDrag (long host, int index, float x, float y, long time);
    private native void handleMouseUp (long host, int index, float x, float y, long time);
    private native void handleAccessibilityHover (long host, int action, float x, float y, long time);

    public static float getRawX(MotionEvent event, int pointerIndex)
    {
        float offset = event.getRawX() - event.getX();
        if (pointerIndex < event.getPointerCount()) {
            return event.getX(pointerIndex) + offset;
        }
        return 0.0f;
    }

    public static float getRawY(MotionEvent event, int pointerIndex)
    {
        float offset = event.getRawY() - event.getY();
        if (pointerIndex < event.getPointerCount()) {
            return event.getY(pointerIndex) + offset;
        }
        return 0.0f;
    }

    @Override
    public boolean onTouchEvent (MotionEvent event)
    {
        if (host == 0)
            return false;

        int action = event.getAction ();
        long time = event.getEventTime ();

        switch (action & MotionEvent.ACTION_MASK)
        {
            case MotionEvent.ACTION_DOWN:
                handleMouseDown (host, event.getPointerId (0), event.getRawX (), event.getRawY (), time);
                return true;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                handleMouseUp (host, event.getPointerId (0), event.getRawX (), event.getRawY (), time);
                return true;

            case MotionEvent.ACTION_MOVE:
            {
                for (int i = 0; i < event.getPointerCount (); ++i)
                    handleMouseDrag (host, event.getPointerId (i), getRawX (event, i), getRawY (event, i), time);

                return true;
            }

            case MotionEvent.ACTION_POINTER_UP:
            {
                int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                handleMouseUp (host, event.getPointerId (i), getRawX (event, i), getRawY (event, i), time);
                return true;
            }

            case MotionEvent.ACTION_POINTER_DOWN:
            {
                // this is a nasty hack to avoid multi-touch issues on some platforms like Realme UI,
                // where secondary pointer down events are just broken, but drag events are not;
                // in C++ code, handleMouseDownCallback is basically a wrapper around handleMouseDragCallback,
                // so skipping those broken events should do the trick, and it shouldn't be noticeable,
                // since secondary touches are only used for zooming/panning anyway:
                //int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                //handleMouseDown (host, event.getPointerId (i), getRawX (event, i), getRawY (event, i), time);
                return true;
            }

            default:
                break;
        }

        return false;
    }

    @Override
    public boolean onHoverEvent (MotionEvent event)
    {
        if (accessibilityManager.isTouchExplorationEnabled())
        {
            handleAccessibilityHover (host, event.getActionMasked(), event.getRawX(), event.getRawY(), event.getEventTime());
            return true;
        }

        return false;
    }

    //==============================================================================
    private native void handleKeyDown (long host, int keycode, int textchar, int kbFlags);
    private native void handleKeyUp (long host, int keycode, int textchar);
    private native void handleBackButton (long host);
    private native void handleKeyboardHidden (long host);

    public void showKeyboard (String type)
    {
        InputMethodManager imm = (InputMethodManager) getContext ().getSystemService (Context.INPUT_METHOD_SERVICE);

        if (imm != null)
        {
            if (type.length () > 0)
            {
                imm.showSoftInput (this, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT);
                imm.setInputMethod (getWindowToken (), type);
                keyboardDismissListener.startListening ();
            } else
            {
                imm.hideSoftInputFromWindow (getWindowToken (), 0);
                keyboardDismissListener.stopListening ();
            }
        }
    }

    public void backButtonPressed ()
    {
        if (host == 0)
            return;

        handleBackButton (host);
    }

    @Override
    public boolean onKeyDown (int keyCode, KeyEvent event)
    {
        if (host == 0)
            return false;

        switch (keyCode)
        {
            case KeyEvent.KEYCODE_VOLUME_UP:
            case KeyEvent.KEYCODE_VOLUME_DOWN:
                return super.onKeyDown (keyCode, event);
            case KeyEvent.KEYCODE_BACK:
            {
                backButtonPressed ();
                return true;
            }

            default:
                break;
        }

        handleKeyDown (host,
                       keyCode,
                       event.getUnicodeChar (),
                       event.getMetaState ());
        return true;
    }

    @Override
    public boolean onKeyUp (int keyCode, KeyEvent event)
    {
        if (host == 0)
            return false;

        handleKeyUp (host, keyCode, event.getUnicodeChar ());
        return true;
    }

    @Override
    public boolean onKeyMultiple (int keyCode, int count, KeyEvent event)
    {
        if (host == 0)
            return false;

        if (keyCode != KeyEvent.KEYCODE_UNKNOWN || event.getAction () != KeyEvent.ACTION_MULTIPLE)
            return super.onKeyMultiple (keyCode, count, event);

        if (event.getCharacters () != null)
        {
            int utf8Char = event.getCharacters ().codePointAt (0);

            handleKeyDown (host,
                           keyCode,
                           utf8Char,
                           event.getMetaState ());
            return true;
        }

        return false;
    }

    //==============================================================================
    private final class KeyboardDismissListener
    {
        public KeyboardDismissListener (ComponentPeerView viewToUse)
        {
            view = viewToUse;
        }

        private void startListening ()
        {
            view.getViewTreeObserver ().addOnGlobalLayoutListener (viewTreeObserver);
        }

        private void stopListening ()
        {
            view.getViewTreeObserver ().removeGlobalOnLayoutListener (viewTreeObserver);
        }

        private class TreeObserver implements ViewTreeObserver.OnGlobalLayoutListener
        {
            TreeObserver ()
            {
                keyboardShown = false;
            }

            @Override
            public void onGlobalLayout ()
            {
                Rect r = new Rect ();

                View parentView = getRootView ();
                int diff;

                if (parentView == null)
                {
                    getWindowVisibleDisplayFrame (r);
                    diff = getHeight () - (r.bottom - r.top);
                } else
                {
                    parentView.getWindowVisibleDisplayFrame (r);
                    diff = parentView.getHeight () - (r.bottom - r.top);
                }

                // Arbitrary threshold, surely keyboard would take more than 20 pix.
                if (diff < 20 && keyboardShown)
                {
                    keyboardShown = false;
                    handleKeyboardHidden (view.host);
                }

                if (!keyboardShown && diff > 20)
                    keyboardShown = true;
            }

            private boolean keyboardShown;
        }

        private final ComponentPeerView view;
        private final TreeObserver viewTreeObserver = new TreeObserver ();
    }

    private final KeyboardDismissListener keyboardDismissListener = new KeyboardDismissListener (this);

    // this is here to make keyboard entry work on a Galaxy Tab2 10.1
    @Override
    public InputConnection onCreateInputConnection (EditorInfo outAttrs)
    {
        outAttrs.actionLabel = "";
        outAttrs.hintText = "";
        outAttrs.initialCapsMode = 0;
        outAttrs.initialSelEnd = outAttrs.initialSelStart = -1;
        outAttrs.label = "";
        outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_EXTRACT_UI;
        outAttrs.inputType = InputType.TYPE_NULL;

        return new BaseInputConnection (this, false);
    }

    //==============================================================================
    @Override
    protected void onSizeChanged (int w, int h, int oldw, int oldh)
    {
        super.onSizeChanged (w, h, oldw, oldh);

        if (host != 0)
            viewSizeChanged (host);
    }

    @Override
    protected void onLayout (boolean changed, int left, int top, int right, int bottom)
    {
    }

    private native void viewSizeChanged (long host);

    @Override
    public void onFocusChange (View v, boolean hasFocus)
    {
        if (host == 0)
            return;

        if (v == this)
            focusChanged (host, hasFocus);
    }

    private native void focusChanged (long host, boolean hasFocus);

    public void setViewName (String newName)
    {
    }

    public void setSystemUiVisibilityCompat (int visibility)
    {
        Method systemUIVisibilityMethod = null;
        try
        {
            systemUIVisibilityMethod = this.getClass ().getMethod ("setSystemUiVisibility", int.class);
        } catch (SecurityException e)
        {
            return;
        } catch (NoSuchMethodException e)
        {
            return;
        }
        if (systemUIVisibilityMethod == null) return;

        try
        {
            systemUIVisibilityMethod.invoke (this, visibility);
        } catch (java.lang.IllegalArgumentException e)
        {
        } catch (java.lang.IllegalAccessException e)
        {
        } catch (java.lang.reflect.InvocationTargetException e)
        {
        }
    }

    public boolean isVisible ()
    {
        return getVisibility () == VISIBLE;
    }

    public void setVisible (boolean b)
    {
        setVisibility (b ? VISIBLE : INVISIBLE);
    }

    public boolean containsPoint (int x, int y)
    {
        return true; //xxx needs to check overlapping views
    }

    //==============================================================================
    private native void handleAppPaused (long host);
    private native void handleAppResumed (long host);

    @Override
    public void onActivityPaused (Activity activity)
    {
        if (host == 0)
            return;

        handleAppPaused (host);
    }

    @Override
    public void onActivityStopped (Activity activity)
    {

    }

    @Override
    public void onActivitySaveInstanceState (Activity activity, Bundle bundle)
    {

    }

    @Override
    public void onActivityDestroyed (Activity activity)
    {

    }

    @Override
    public void onActivityCreated (Activity activity, Bundle bundle)
    {

    }

    @Override
    public void onActivityStarted (Activity activity)
    {

    }

    @Override
    public void onActivityResumed (Activity activity)
    {
        if (host == 0)
            return;

        // Ensure that navigation/status bar visibility is correctly restored.
        handleAppResumed (host);
    }

    //==============================================================================
    private native boolean populateAccessibilityNodeInfo (long host, int virtualViewId, AccessibilityNodeInfo info);
    private native boolean handlePerformAction (long host, int virtualViewId, int action, Bundle arguments);
    private native Integer getInputFocusViewId (long host);
    private native Integer getAccessibilityFocusViewId (long host);

    private final class JuceAccessibilityNodeProvider extends AccessibilityNodeProvider
    {
        public JuceAccessibilityNodeProvider (ComponentPeerView viewToUse)
        {
            view = viewToUse;
        }

        @Override
        public AccessibilityNodeInfo createAccessibilityNodeInfo (int virtualViewId)
        {
            if (host == 0)
                return null;

            final AccessibilityNodeInfo nodeInfo = AccessibilityNodeInfo.obtain (view, virtualViewId);

            if (! populateAccessibilityNodeInfo (host, virtualViewId, nodeInfo))
            {
                nodeInfo.recycle();
                return null;
            }

            return nodeInfo;
        }

        @Override
        public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText (String text, int virtualViewId)
        {
            return new ArrayList<>();
        }

        @Override
        public AccessibilityNodeInfo findFocus (int focus)
        {
            if (host == 0)
                return null;

            Integer focusViewId = (focus == AccessibilityNodeInfo.FOCUS_INPUT ? getInputFocusViewId (host)
                                                                              : getAccessibilityFocusViewId (host));

            if (focusViewId != null)
                return createAccessibilityNodeInfo (focusViewId);

            return null;
        }

        @Override
        public boolean performAction (int virtualViewId, int action, Bundle arguments)
        {
            if (host == 0)
                return false;

            return handlePerformAction (host, virtualViewId, action, arguments);
        }

        private final ComponentPeerView view;
    }

    private final JuceAccessibilityNodeProvider nodeProvider = new JuceAccessibilityNodeProvider (this);
    private final AccessibilityManager accessibilityManager = (AccessibilityManager) getContext ().getSystemService (Context.ACCESSIBILITY_SERVICE);

    @Override
    public AccessibilityNodeProvider getAccessibilityNodeProvider ()
    {
        return nodeProvider;
    }
}
