package compose.utils

import androidx.compose.foundation.lazy.LazyListState


/**
 * Scrolls the minimum distance to make the item at the given index fully visible.
 */
suspend fun LazyListState.scrollToMakeVisible(index: Int, animate: Boolean = true) {
    val viewportHeight = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
    val visibleItemsInfo = layoutInfo.visibleItemsInfo
    if (visibleItemsInfo.isEmpty())
        return

    suspend fun maybeAnimatedScrollToItem(itemIndex: Int, scrollOffset: Int = 0) {
        if (animate)
            animateScrollToItem(itemIndex, scrollOffset)
        else
            scrollToItem(itemIndex, scrollOffset)
    }
    
    val firstVisibleItem = visibleItemsInfo.first()
    val lastVisibleItem = visibleItemsInfo.last()

    if (index <= firstVisibleItem.index) {  // The item is before the first visible item
        maybeAnimatedScrollToItem(index, 0)
    } else if (index == lastVisibleItem.index) {  // The item is the last visible item (it may be partially visible)
        // The last visible item is already fully visible, such as when the viewport is taller than the contents
        if (lastVisibleItem.offset + lastVisibleItem.size <= viewportHeight)
            return

        // Walk up until we overshoot the viewport height.
        // The item we've reached is the item we need to scroll to, and the offset is the amount we overshoot
        var visibleItemIndex = visibleItemsInfo.lastIndex
        var distanceToItem = 0
        while (distanceToItem < viewportHeight){
            distanceToItem += visibleItemsInfo[visibleItemIndex].size
            visibleItemIndex -= 1
        }
        maybeAnimatedScrollToItem(visibleItemsInfo[visibleItemIndex+1].index, distanceToItem - viewportHeight)
    } else if (index == layoutInfo.totalItemsCount-1) {
        // In this special case we have an easy way, so we take it
        maybeAnimatedScrollToItem(index+1)
    } else if (index == lastVisibleItem.index+1) {  // The item is the first non-visible item below
        // We can't accurately align the bottom of the target item with the bottom of the list because we don't know
        // the height of its view. Best we can do is assume it's the same as the last visible item's view
        var visibleItemIndex = visibleItemsInfo.lastIndex
        var distanceToItem = lastVisibleItem.size  // <-- Assumption is here
        while (distanceToItem < viewportHeight){
            distanceToItem += visibleItemsInfo[visibleItemIndex].size
            visibleItemIndex -= 1
        }

        maybeAnimatedScrollToItem(visibleItemsInfo[visibleItemIndex+1].index, distanceToItem - viewportHeight)

        // This is jumpy, but without it we're risking ending up at the wrong position
        if (animate)
            scrollToItem(index+1, -viewportHeight)
    }  else if (index > lastVisibleItem.index) {
        // We assume all the items are of the same size, which is hopefully true.
        // There's nothing better we can do here because the views for these items don't actually exist, so we can't
        // know their sizes.
        val itemHeight = lastVisibleItem.size
        val scrollIndex = index - viewportHeight / itemHeight
        val scrollOffset = itemHeight - viewportHeight % itemHeight
        maybeAnimatedScrollToItem(scrollIndex, scrollOffset)

        // This is jumpy, but without it we're risking ending up at the wrong position
        if (animate)
            scrollToItem(index+1, -viewportHeight)
    }
}


/**
 * Returns the index of the item at the given offset (in pixels) from the start of the list.
 */
fun LazyListState.itemIndexAt(offset: Float): Int? {
    for (item in layoutInfo.visibleItemsInfo) {
        if ((offset >= item.offset) && (offset < item.offset + item.size))
            return item.index
    }

    return null
}
