// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

package org.qtproject.qt.android;

import android.util.Log;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * The QtQuickViewContent represents a QML component that can be loaded by a QtQuickView instance.
 *
 * This abstract class should be extended to be used by a QtQuickView. It provides QtQuickView with
 * essential information to load the QML component it represents.
 * It also offers convenient methods for seamless interaction with the QtQuickView that loads it.
 * @since 6.8
 **/
public abstract class QtQuickViewContent
{
    private final static String TAG = "QtQuickViewContent";
    private static AtomicInteger m_nextSignalId = new AtomicInteger();

    private WeakReference<QtQuickView> m_viewReference;
    private QtQmlStatusChangeListener m_statusChangeListener = null;
    private HashSet<Integer> m_signalListenerIds = new HashSet<>();
    private QtSignalQueue m_signalQueue = new QtSignalQueue();

    /**
     * Implement this to return the library name that this component belongs to.
     **/
    public abstract String getLibraryName();
    /**
     * Implement this to return the module name that this component belongs to.
     **/
    public abstract String getModuleName();
    /**
     * Implement this to return the qrc (Qt Resource) path of this QML component.
     **/
    public abstract String getFilePath();

    /**
     * Sets a StatusChangeListener to listen to status changes.
     *
     * @param listener an instance of a StatusChangeListener interface
     **/
    public void setStatusChangeListener(QtQmlStatusChangeListener listener)
    {
        m_statusChangeListener = listener;
        QtQuickView view = getQuickView();
        if (view != null)
            view.setStatusChangeListener(listener);
    }

    /**
     * Gets the QtQuickView instance that has loaded this component.
     *
     * @return Returns an instance of QtQuickView or null if this component is not loaded by any
     * QtQuickView.
     **/
    protected QtQuickView getQuickView()
    {
        if (m_viewReference != null)
            return m_viewReference.get();
        return null;
    }

    /**
     * Checks if this is currently attached to a QtQuickView instance
     *
     * @return Returns true if this is attached to a QtQuickView instance, otherwise, returns false.
     **/
    protected boolean isViewAttached() { return getQuickView() != null; }

    /**
     * Attaches this to a QtQuickView instance.
     **/
    protected void attachView(QtQuickView view)
    {
        m_viewReference = new WeakReference<>(view);
        if (view != null) {
            view.setStatusChangeListener(m_statusChangeListener);
            m_signalQueue.connectQueuedSignalListeners(view);
        }
    }

    /**
     * Detaches this from the QtQuickView to which it has previously been attached. A call to this
     * method will disconnect all signal listeners that have been connected before.
     **/
    protected void detachView()
    {
        QtQuickView view = getQuickView();
        if (view != null) {
            for (int signalListenerId : m_signalListenerIds)
                view.disconnectSignalListener(signalListenerId);

            view.setStatusChangeListener(null);
            m_viewReference.clear();
            if (m_statusChangeListener != null)
                m_statusChangeListener.onStatusChanged(QtQmlStatus.NULL);
        }
    }

    /**
     * Implement this to return more information about the QML Component.
     * Default implementation returns an empty HashMap.
     **/
    protected HashMap<String, Object> attributes() { return new HashMap<>(); }

    /**
     * Sets the value of an existing property on the QML component if it has already been attached
     * and loaded by a QtQuickView instance. The supported types are
     * {@link java.lang.Integer}, {@link java.lang.Double}, {@link java.lang.Float},
     * {@link java.lang.Boolean} and {@link java.lang.String}. These types get converted to their
     * corresponding QML types int, double/float, bool, and string. This function does not add
     * properties to the QML root object if they do not exist but prints a warning.
     *
     * @param propertyName the name of the existing QML property to set the value of
     * @param value        the value to set the property to QML's int, double/float,
                           bool or string
     * @see <a href="https://doc.qt.io/qt-6/qml-int.html">QML int</a>
     * @see <a href="https://doc.qt.io/qt-6/qml-double.html">QML double/float</a>
     * @see <a href="https://doc.qt.io/qt-6/qml-bool.html">QML bool</a>
     * @see <a href="https://doc.qt.io/qt-6/qml-string.html">QML string</a>
     **/
    protected void setProperty(String propertyName, Object value)
    {
        QtQuickView view = getQuickView();
        if (view == null) {
            Log.w(TAG, "Cannot set property as the QQmlComponent is not loaded in a QtQuickView.");
            return;
        }
        view.setProperty(propertyName, value);
    }

    /**
     * Gets the value of an existing property of the QML component if it has already been attached
     * and loaded by a QtQuickView instance. The supported types are
     * {@link java.lang.Integer}, {@link java.lang.Double}, {@link java.lang.Float},
     * {@link java.lang.Boolean} and {@link java.lang.String}. These types get converted to their
     * corresponding QML types int, double/float, bool and string. If the property does not
     * exist or the status of the QML component is anything other than
     * {@link QtQmlStatus#READY}, this function will return null.
     *
     * @param propertyName the name of the existing root object property
     * @throws ClassCastException if the returned type cannot be cast to the requested type.
     * @see <a href="https://doc.qt.io/qt-6/qml-int.html">QML int</a>
     * @see <a href="https://doc.qt.io/qt-6/qml-double.html">QML double/float</a>
     * @see <a href="https://doc.qt.io/qt-6/qml-bool.html">QML bool</a>
     * @see <a href="https://doc.qt.io/qt-6/qml-string.html">QML string</a>
     **/
    protected <T> T getProperty(String propertyName)
    {
        QtQuickView view = getQuickView();
        if (view == null) {
            Log.w(TAG, "Cannot get property as the QQmlComponent is not loaded in a QtQuickView.");
            return null;
        }
        return view.<T>getProperty(propertyName);
    }

    /**
     * Connects a SignalListener to a signal of the QML component if it has already been attached
     * and loaded by a QtQuickView instance. Otherwise, the connection is executed once the
     * component is attached to a view.
     *
     * @param signalName the name of the root object signal
     * @param argType    the Class type of the signal argument
     * @param listener   an instance of the QtSignalListener interface
     * @return a connection ID between signal and listener or the existing connection ID if there is
     *         an existing connection between the same signal and listener. Return a negative value
     *         if the signal does not exist on the QML root object. Always returns
     *         <code>true</code> if the component is not yet attached to a QtQuickView instance.
     **/
    protected <T> int connectSignalListener(String signalName, Class<T> argType,
                                            QtSignalListener<T> listener)
    {
        return connectSignalListener(signalName, new Class<?>[] { argType }, listener);
    }

    /**
     * Connects a SignalListener to a signal of the QML component if it has already been attached
     * and loaded by a QtQuickView instance. Otherwise, the connection is executed once the
     * component is attached to a view.
     *
     * @param signalName the name of the root object signal
     * @param argTypes   the Class types of the signal arguments
     * @param listener   an instance of the QtSignalListener interface
     * @return a connection ID between signal and listener or the existing connection ID if there is
     *         an existing connection between the same signal and listener. Return a negative value
     *         if the signal does not exist on the QML root object. Always returns
     *         <code>true</code> if the component is not yet attached to a QtQuickView instance.
     **/
    protected int connectSignalListener(String signalName, Class<?>[] argTypes, Object listener)
    {
        final int id = QtQuickViewContent.generateSignalId();
        if (isViewAttached()) {
            QtQuickView view = getQuickView();
            view.connectSignalListener(signalName, argTypes, listener, id);
            m_signalListenerIds.add(id);
        } else {
            m_signalQueue.add(signalName, argTypes, listener, id);
        }
        return id;
    }

    /**
     * Disconnects a SignalListener with a given id obtained from
     * {@link QtQuickView#connectSignalListener(String, Class, QtSignalListener)} or
     * {@link QtQuickView#connectSignalListener(String, Class[], Object)} call,
     * from listening to a signal.
     *
     * @param signalListenerId the connection id
     * @return Returns true if the connection id is valid and has been successfully removed,
     *         otherwise returns false.
     **/
    public boolean disconnectSignalListener(int signalListenerId)
    {
        if (isViewAttached()) {
            QtQuickView view = getQuickView();
            m_signalListenerIds.remove(signalListenerId);
            return view.disconnectSignalListener(signalListenerId);
        } else {
            return m_signalQueue.remove(signalListenerId);
        }
    }

    static int generateSignalId()
    {
        return m_nextSignalId.getAndIncrement();
    }

    /**
     * Invokes a QML method of the root object.
     *
     * Supported parameter types are {@link java.lang.Integer},{@link java.lang.Double},
     * {@link java.lang.Float}, {@link java.lang.Boolean} and {@link java.lang.String}.
     * These types get converted to their corresponding types: <code>int</code>,
     * <code>double</code>, <code>real</code>, <code>bool</code>, and <code>string</code>,
     * respectively.
     *
     * @param name name of the method
     * @param params array of parameters that are passed to the method
     *
     * @see <a href="https://doc.qt.io/qt-6/qml-int.html">QML int</a>
     * @see <a href="https://doc.qt.io/qt-6/qml-double.html">QML double</a>
     * @see <a href="https://doc.qt.io/qt-6/qml-real.html">QML real</a>
     * @see <a href="https://doc.qt.io/qt-6/qml-bool.html">QML bool</a>
     * @see <a href="https://doc.qt.io/qt-6/qml-string.html">QML string</a>
     **/
    public void invokeMethod(String name, Object[] params)
    {
        QtQuickView view = getQuickView();
        if (view != null) {
            view.invokeMethod(name, params);
        } else {
            Log.w(TAG,
                  "Cannot call method " + name +
                  " as the QQmlComponent is not loaded in a QtQuickView.");
        }
    }

    /**
     * Invokes a QML method of the root object.
     *
     * @param name name of the method
     *
     * @see QtQuickViewContent#invokeMethod(String, Object[])
     **/
    public void invokeMethod(String name)
    {
        QtQuickView view = getQuickView();
        if (view != null) {
            view.invokeMethod(name);
        } else {
            Log.w(TAG,
                  "Cannot call method " + name +
                  " as the QQmlComponent is not loaded in a QtQuickView.");
        }
    }
}
