/*
    SPDX-FileCopyrightText: 2020 Nate Graham <nate@kde.org>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

pragma ComponentBehavior: Bound

import QtQuick
import QtQuick.Layouts
import QtQuick.Templates as T
import org.kde.plasma.core as PlasmaCore
import org.kde.ksvg as KSvg
import org.kde.plasma.components as PlasmaComponents3
import org.kde.plasma.extras as PlasmaExtras
import org.kde.kirigami as Kirigami

/*!
  \qmltype ExpandableListItem
  \inqmlmodule org.kde.plasma.extras

  \brief A list item that expands when clicked to show additional actions and/or a
  custom view.

  The list item has a standardized appearance, with an icon on the left badged
  with an optional emblem, a title and optional subtitle to the right, an
  optional default action button, and a button to expand and collapse the list
  item.

  When expanded, the list item shows a list of contextually-appropriate actions
  if contextualActions has been defined.
  If customExpandedViewContent has been defined, it will show a custom view.
  If both have been defined, it shows both, with the actions above the custom
  view.

  It is not valid to define neither; define one or both.

  \note this component should only be used for lists where the maximum number
  of items is very low, ideally less than 10. For longer lists, consider using
  a different paradigm.


  Example usage:

  \qml
  import QtQuick
  import QtQuick.Controls as QQC2
  import org.kde.kirigami as Kirigami
  import org.kde.plasma.extras as PlasmaExtras
  import org.kde.plasma.components as PlasmaComponents

  PlasmaComponents.ScrollView {
      ListView {
          anchors.fill: parent
          focus: true
          currentIndex: -1
          clip: true
          model: myModel
          highlight: PlasmaExtras.Highlight {}
          highlightMoveDuration: Kirigami.Units.shortDuration
          highlightResizeDuration: Kirigami.Units.shortDuration
          delegate: PlasmaExtras.ExpandableListItem {
              icon: model.iconName
              iconEmblem: model.isPaused ? "emblem-pause" : ""
              title: model.name
              subtitle: model.subtitle
              isDefault: model.isDefault
              defaultActionButtonAction: QQC2.Action {
                  icon.name: model.isPaused ? "media-playback-start" : "media-playback-pause"
                  text: model.isPaused ? "Resume" : "Pause"
                  onTriggered: {
                      if (model.isPaused) {
                          model.resume(model.name);
                      } else {
                          model.pause(model.name);
                      }
                  }
              }
              contextualActions: [
                  QQC2.Action {
                      icon.name: "configure"
                      text: "Configure…"
                      onTriggered: model.configure(model.name);
                  }
              ]
          }
      }
  }
  \endqml
 */
Item {
    id: listItem

    /*!
      \qmlproperty icon ExpandableListItem::icon

      The name of the icon used in the list item.

      Required.

      \sa Kirigami.Icon::source

     */
    property alias icon: listItemIcon.source

    /*!
      \qmlproperty icon ExpandableListItem::iconEmblem

      The name of the emblem to badge the icon with.

      Optional, defaults to nothing, in which case there is no emblem.

      \sa Kirigami.Icon::source
     */
    property alias iconEmblem: iconEmblem.source

    /*!
      \qmlproperty string ExpandableListItem::title

      The name or title for this list item.

      Optional; if not defined, there will be no title and the subtitle will be
      vertically centered in the list item.
     */
    property alias title: listItemTitle.text

    /*!
      \qmlproperty string ExpandableListItem::subtitle

      The subtitle for this list item, displayed under the title.

      Optional; if not defined, there will be no subtitle and the title will be
      vertically centered in the list item.
     */
    property alias subtitle: listItemSubtitle.text

    /*!
      Whether to allow the subtitle to become a multi-line string instead of
      eliding when the text is very long.

      Optional, defaults to false.
     */
    property bool subtitleCanWrap: false

    /*!
      The maximum number of lines the subtitle can have when subtitleCanWrap is true.

      Optional, defaults to -1, which means no limit.
      \since 6.9
     */
    property int subtitleMaximumLineCount: -1

    /*!
      \qmlproperty color ExpandableListItem::subtitleColor

      subtitleColor: color
      The color of the subtitle text

      Optional; if not defined, the subtitle will use the default text color
     */
    property alias subtitleColor: listItemSubtitle.color

    /*!
      Whether to allow the title, subtitle, and tooltip to contain styled text.
      For performance and security reasons, keep this off unless needed.

      Optional, defaults to false.
     */
    property bool allowStyledText: false

    /*!
      \qmlproperty Action ExpandableListItem::defaultActionButtonAction

      The Action to execute when the default button is clicked.

      Optional; if not defined, no default action button will be displayed.
     */
    property alias defaultActionButtonAction: defaultActionButton.action

    /*!
      When/whether to show to default action button. Useful for making it
      conditionally appear or disappear.

      Optional; defaults to true
     */
    property bool defaultActionButtonVisible: true

    /*!
      Whether to continue showing the default action button while the busy
      indicator is visible. Useful for cancelable actions that could take a few
      seconds and show a busy indicator while processing.

      Optional; defaults to false
     */
    property bool showDefaultActionButtonWhenBusy: false

    /*!
      A list of standard QQC2.Action objects that describes additional actions
      that can be performed on this list item. For example:

      \qml
      contextualActions: [
          Action {
              text: "Do something"
              icon.name: "document-edit"
              onTriggered: doSomething()
          },
          Action {
              text: "Do something else"
              icon.name: "draw-polygon"
              onTriggered: doSomethingElse()
          },
          Action {
              text: "Do something completely different"
              icon.name: "games-highscores"
              onTriggered: doSomethingCompletelyDifferent()
          }
      ]
      \endqml

      Optional; if not defined, no contextual actions will be displayed and
      you should instead assign a custom view to customExpandedViewContent,
      which will be shown when the user expands the list item.
     */
    property list<T.Action> contextualActions

    readonly property list<T.Action> __enabledContextualActions: contextualActions.filter(action => action?.enabled ?? false)

    /*!
      A custom view to display when the user expands the list item.

      This component must define width and height properties. Width should be
      equal to the width of the list item itself, while height: will depend
      on the component itself.

      Optional; if not defined, no custom view actions will be displayed and
      you should instead define contextualActions, and then actions will
      be shown when the user expands the list item.
     */
    property Component customExpandedViewContent

    /*!
      \qmlproperty Item ExpandableListItem::customExpandedViewContentItem
      The actual instance of the custom view content, if loaded.
      \since 5.72
     */
    property alias customExpandedViewContentItem: customContentLoader.item

    /*!
      Whether or not to display a busy indicator on the list item. Set to true
      while the item should be non-interactive because things are processing.

      Optional; defaults to false.
     */
    property bool isBusy: false

    /*!
      Whether or not this list item should be considered the "default" or
      "Current" item in the list. When set to true, and the list itself has
      more than one item in it, the list item's title and subtitle will be
      drawn in a bold style.

      Optional; defaults to false.
     */
    property bool isDefault: false

    /*!
      \qmlproperty bool ExpandableListItem::expanded

      Whether the expanded view is visible.

      \since 5.98
     */
    readonly property alias expanded: expandedView.expanded

    /*!
      Whether or not this expandable list item is actually expandable. True if
      this item has either a custom view or else at least one enabled action.
      Otherwise false.
     */
    readonly property bool hasExpandableContent: customExpandedViewContent !== null || __enabledContextualActions.length > 0

    /*!
      Show the expanded view, growing the list item to its taller size.
     */
    function expand() {
        if (!listItem.hasExpandableContent) {
            return;
        }
        expandedView.expanded = true
        listItem.itemExpanded()
    }

    /*!
      Hide the expanded view and collapse the list item to its shorter size.
     */
    function collapse() {
        if (!listItem.hasExpandableContent) {
            return;
        }
        expandedView.expanded = false
        listItem.itemCollapsed()
    }

    /*!
      Expand or collapse the list item depending on its current state.
     */
    function toggleExpanded() {
        if (!listItem.hasExpandableContent) {
            return;
        }
        expandedView.expanded ? listItem.collapse() : listItem.expand()
    }

    /*!

     */
    signal itemExpanded()

    /*!

     */
    signal itemCollapsed()

    width: parent ? parent.width : undefined // Assume that we will be used as a delegate, not placed in a layout
    height: mainLayout.height

    Behavior on height {
        enabled: listItem.ListView.view.highlightResizeDuration > 0
        SmoothedAnimation { // to match the highlight
            id: heightAnimation
            duration: listItem.ListView.view.highlightResizeDuration || -1
            velocity: listItem.ListView.view.highlightResizeVelocity
            easing.type: Easing.InOutCubic
        }
    }
    clip: heightAnimation.running || expandedItemOpacityFade.running

    onEnabledChanged: if (!listItem.enabled) { collapse() }

    Keys.onPressed: event => {
        if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
            if (defaultActionButtonAction) {
                defaultActionButtonAction.trigger()
            } else {
                toggleExpanded();
            }
            event.accepted = true;
        } else if (event.key === Qt.Key_Escape) {
            if (expandedView.expanded) {
                collapse();
                event.accepted = true;
            }
            // if not active, we'll let the Escape event pass through, so it can close the applet, etc.
        } else if (event.key === Qt.Key_Space) {
            toggleExpanded();
            event.accepted = true;
        }
    }

    KeyNavigation.tab: defaultActionButtonVisible ? defaultActionButton : expandToggleButton
    KeyNavigation.right: defaultActionButtonVisible ? defaultActionButton : expandToggleButton
    KeyNavigation.down: expandToggleButton.KeyNavigation.down
    Keys.onDownPressed: event => {
        if (!actionsListLoader.item || ListView.view.currentIndex < 0) {
            ListView.view.incrementCurrentIndex();
            const item = ListView.view.currentItem;
            if (item) {
                item.forceActiveFocus(Qt.TabFocusReason);
            }
            event.accepted = true;
            return;
        }
        event.accepted = false; // Forward to KeyNavigation.down
    }
    Keys.onUpPressed: event => {
        if (ListView.view.currentIndex === 0) {
            event.accepted = false;
        } else {
            ListView.view.decrementCurrentIndex();
            const item = ListView.view.currentItem;
            if (item) {
                item.forceActiveFocus(Qt.BacktabFocusReason);
            }
            event.accepted = true;
        }
    }

    Accessible.role: Accessible.Button
    Accessible.name: title
    Accessible.description: subtitle

    PlasmaComponents3.ToolTip {
        text: listItem.title + (listItem.subtitle.length > 0 ? "\n" + listItem.subtitle : "")
        visible: mouseArea.containsMouse && (listItemTitle.truncated || listItemSubtitle.truncated)
        textFormat: listItem.allowStyledText ? Text.StyledText : Text.PlainText
    }

    // Handle left clicks and taps; don't accept stylus input or else it steals
    // events from the buttons on the list item
    TapHandler {
        enabled: listItem.hasExpandableContent

        acceptedPointerTypes: PointerDevice.Generic | PointerDevice.Finger

        onSingleTapped: {
            listItem.ListView.view.currentIndex = index
            listItem.toggleExpanded()
        }
    }

    MouseArea {
        id: mouseArea

        property PlasmaExtras.Menu contextMenu: null

        anchors.fill: parent

        acceptedButtons: !listItem.expanded && listItem.__enabledContextualActions.length > 0 ? Qt.RightButton : Qt.NoButton
        hoverEnabled: true

        // using onPositionChanged instead of onContainsMouseChanged so this doesn't trigger when the list reflows
        onPositionChanged: {
            // don't change currentIndex if it would make listview scroll
            // see https://bugs.kde.org/show_bug.cgi?id=387797
            // this is a workaround till https://bugreports.qt.io/browse/QTBUG-114574 gets fixed
            // which would allow a proper solution
            if (parent.y - listItem.ListView.view.contentY >= 0 && parent.y - listItem.ListView.view.contentY + parent.height  + 1 /* border */ < listItem.ListView.view.height) {
                listItem.ListView.view.currentIndex = (containsMouse ? index : -1)
            }
        }
        onPressed: (mouse) => {
            contextMenu = menuComponent.createObject(listItem);
            for (let action of listItem.__enabledContextualActions) {
                const menuItem = menuItemComponent.createObject(contextMenu, {
                    // Unfortunately cannot just assign action to menuItem.action.
                    text: action.text,
                    icon: action.icon.name,
                });
                menuItem.clicked.connect(action.trigger);
            }
            contextMenu.open(mouse.x, mouse.y);
        }
        onExited: if (listItem.ListView.view.currentIndex === index && contextMenu?.status !== PlasmaExtras.Menu.Open) {
            listItem.ListView.view.currentIndex = -1;
        }

        Component {
            id: menuComponent
            PlasmaExtras.Menu {
                onStatusChanged: {
                    if (status === PlasmaExtras.Menu.Closed) {
                        destroy();
                        mouseArea.contextMenu = null;
                    }
                }
            }
        }

        Component {
            id: menuItemComponent
            PlasmaExtras.MenuItem { }
        }

        ColumnLayout {
            id: mainLayout

            anchors.top: parent.top
            anchors.left: parent.left
            anchors.right: parent.right

            spacing: 0

            RowLayout {
                id: mainRowLayout

                Layout.fillWidth: true
                Layout.margins: Kirigami.Units.smallSpacing
                // Otherwise it becomes taller when the button appears
                Layout.minimumHeight: defaultActionButton.height

                // Icon and optional emblem
                Kirigami.Icon {
                    id: listItemIcon

                    implicitWidth: Kirigami.Units.iconSizes.medium
                    implicitHeight: Kirigami.Units.iconSizes.medium

                    Kirigami.Icon {
                        id: iconEmblem

                        visible: valid

                        anchors.right: parent.right
                        anchors.bottom: parent.bottom

                        implicitWidth: Kirigami.Units.iconSizes.small
                        implicitHeight: Kirigami.Units.iconSizes.small
                    }
                }

                // Title and subtitle
                ColumnLayout {
                    Layout.fillWidth: true
                    Layout.alignment: Qt.AlignVCenter

                    spacing: 0

                    Kirigami.Heading {
                        id: listItemTitle

                        visible: text.length > 0

                        Layout.fillWidth: true

                        level: 5

                        textFormat: listItem.allowStyledText ? Text.StyledText : Text.PlainText
                        elide: Text.ElideRight
                        maximumLineCount: 1

                        // Even if it's the default item, only make it bold when
                        // there's more than one item in the list, or else there's
                        // only one item and it's bold, which is a little bit weird
                        font.weight: listItem.isDefault && listItem.ListView.view.count > 1
                                            ? Font.Bold
                                            : Font.Normal
                    }

                    PlasmaComponents3.Label {
                        id: listItemSubtitle

                        visible: text.length > 0
                        font: Kirigami.Theme.smallFont

                        // Otherwise colored text can be hard to see
                        opacity: color === Kirigami.Theme.textColor ? 0.75 : 1.0

                        Layout.fillWidth: true

                        textFormat: listItem.allowStyledText ? Text.StyledText : Text.PlainText
                        elide: Text.ElideRight
                        maximumLineCount: subtitleCanWrap ? (subtitleMaximumLineCount === -1 ? undefined : subtitleMaximumLineCount) : 1
                        wrapMode: subtitleCanWrap ? Text.WordWrap : Text.NoWrap
                    }
                }

                // Busy indicator
                PlasmaComponents3.BusyIndicator {
                    id: busyIndicator

                    visible: listItem.isBusy

                    // Otherwise it makes the list item taller when it appears
                    Layout.maximumHeight: defaultActionButton.implicitHeight
                    Layout.maximumWidth: Layout.maximumHeight
                }

                // Default action button
                PlasmaComponents3.ToolButton {
                    id: defaultActionButton

                    visible: defaultActionButtonAction
                            && listItem.defaultActionButtonVisible
                            && (!busyIndicator.visible || listItem.showDefaultActionButtonWhenBusy)

                    KeyNavigation.tab: expandToggleButton
                    KeyNavigation.right: expandToggleButton
                    KeyNavigation.down: expandToggleButton.KeyNavigation.down
                    Keys.onUpPressed: event => listItem.Keys.upPressed(event)

                    Accessible.name: action !== null ? action.text : ""
                }

                // Expand/collapse button
                PlasmaComponents3.ToolButton {
                    id: expandToggleButton
                    visible: listItem.hasExpandableContent

                    display: PlasmaComponents3.AbstractButton.IconOnly
                    text: expandedView.expanded ? i18ndc("libplasma6", "@action:button", "Collapse") : i18ndc("libplasma6", "@action:button", "Expand")
                    icon.name: expandedView.expanded ? "collapse" : "expand"

                    Keys.onUpPressed: event => listItem.Keys.upPressed(event)

                    onClicked: listItem.toggleExpanded()

                    PlasmaComponents3.ToolTip {
                        text: parent.text
                    }
                }
            }


            // Expanded view with actions and/or custom content in it
            Item {
                id: expandedView
                property bool expanded: false

                Layout.preferredHeight: expanded ?
                    expandedViewLayout.implicitHeight + expandedViewLayout.anchors.topMargin + expandedViewLayout.anchors.bottomMargin : 0
                Layout.fillWidth: true

                opacity: expanded ? 1 : 0
                Behavior on opacity {
                    enabled: listItem.ListView.view.highlightResizeDuration > 0
                    SmoothedAnimation { // to match the highlight
                        id: expandedItemOpacityFade
                        duration: listItem.ListView.view.highlightResizeDuration || -1
                        // velocity is divided by the default speed, as we're in the range 0-1
                        velocity: listItem.ListView.view.highlightResizeVelocity / 200
                        easing.type: Easing.InOutCubic
                    }
                }
                visible: opacity > 0

                ColumnLayout {
                    id: expandedViewLayout
                    anchors.fill: parent
                    anchors.margins: Kirigami.Units.smallSpacing

                    spacing: Kirigami.Units.smallSpacing

                    // Actions list
                    Loader {
                        id: actionsListLoader

                        visible: status === Loader.Ready
                        active: expandedView.visible && listItem.__enabledContextualActions.length > 0

                        Layout.fillWidth: true

                        sourceComponent: Item {
                            height: childrenRect.height
                            width: actionsListLoader.width // basically, parent.width but null-proof

                            ColumnLayout {
                                anchors.top: parent.top
                                anchors.left: parent.left
                                anchors.right: parent.right
                                anchors.leftMargin: Kirigami.Units.gridUnit
                                anchors.rightMargin: Kirigami.Units.gridUnit

                                spacing: 0

                                Repeater {
                                    id: actionRepeater

                                    model: listItem.__enabledContextualActions

                                    delegate: PlasmaComponents3.ToolButton {
                                        required property int index
                                        required property T.Action modelData

                                        Layout.fillWidth: true

                                        text: modelData.text
                                        icon.name: modelData.icon.name

                                        KeyNavigation.up: index > 0 ? actionRepeater.itemAt(index - 1) : expandToggleButton
                                        Keys.onDownPressed: event => {
                                            if (index === actionRepeater.count - 1) {
                                                event.accepted = true;
                                                listItem.ListView.view.incrementCurrentIndex();
                                                listItem.ListView.view.currentItem.forceActiveFocus(Qt.TabFocusReason);
                                            } else {
                                                event.accepted = false; // Forward to KeyNavigation.down
                                            }
                                        }

                                        onClicked: {
                                            modelData.trigger()
                                            collapse()
                                        }
                                    }
                                }
                            }
                        }
                    }

                    // Separator between the two items when both are shown
                    KSvg.SvgItem {
                        Layout.fillWidth: true
                        imagePath: "widgets/line"
                        elementId: "horizontal-line"
                        visible: actionsListLoader.visible && customContentLoader.visible
                    }

                    // Custom content item, if any
                    Loader {
                        id: customContentLoader
                        visible: status === Loader.Ready

                        Layout.fillWidth: true

                        active: expandedView.visible
                        asynchronous: true
                        sourceComponent: listItem.customExpandedViewContent
                    }
                }
            }
        }
    }
}
