package compose.widgets

import androidx.compose.foundation.layout.*
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.isUnspecified
import compose.input.onMousePress
import compose.utils.SelectableBox


/**
 * A layout that arranges its content in rows and columns, with fixed width columns.
 */
@Composable
fun SimpleGrid(


    /**
     * The widths of the columns; this also implicitly specifies the number of columns.
     */
    columnWidths: List<Dp>,


    /**
     * A model for selecting rows.
     */
    rowSelectionModel: ListSelectionModel = NoSelectionModel,


    /**
     * The background color of selected rows.
     */
    selectedBackgroundColor: Color = MaterialTheme.colors.secondary,


    /**
     * The content color of selected rows.
     */
    selectedContentColor: Color = contentColorFor(selectedBackgroundColor),


    /**
     * The modifier to apply to the grid.
     */
    modifier: Modifier = Modifier,


    /**
     * The default modifier to apply to the rows.
     */
    defaultRowModifier: Modifier = Modifier.height(IntrinsicSize.Min),


    /**
     * The default (vertical) alignment of rows.
     */
    defaultRowAlignment: Alignment.Vertical = Alignment.Top,


    /**
     * The default modifier to apply to the cells.
     */
    defaultCellModifier: Modifier = Modifier,


    /**
     * The default content alignment of cells, by the index of the column.
     */
    defaultCellContentAlignment: (Int) -> Alignment = { Alignment.TopStart },


    /**
     * The content of the grid.
     */
    content: @SimpleGridBuilder @Composable GridScope.() -> Unit


) {
    val gridScope = remember(
        columnWidths,
        rowSelectionModel,
        selectedBackgroundColor,
        defaultRowModifier,
        defaultRowAlignment,
        defaultCellModifier,
        defaultCellContentAlignment
    ) {
        GridScope(
            columnWidths = ArrayList(columnWidths),
            rowSelectionModel = rowSelectionModel,
            selectedBackgroundColor = selectedBackgroundColor,
            selectedContentColor = selectedContentColor,
            defaultRowModifier = defaultRowModifier,
            rowVerticalAlignment = defaultRowAlignment,
            defaultCellModifier = defaultCellModifier,
            defaultCellContentAlignment = defaultCellContentAlignment
        )
    }

    Box(
        modifier = modifier
            .then(with(gridScope.dragHelper) { Modifier.dragToReorderContainer() })
    ) {
        Column {
            gridScope.content()
        }

        gridScope.dragHelper.DraggedContent()
    }
}


/**
 * Sums a list of [Dp] values that are [Dp.isSpecified].
 */
private fun List<Dp>.sumOfSpecified(): Dp {
    var result = 0.dp
    for (value in this) {
        if (value.isSpecified)
            result += value
    }
    return result
}


/**
 * The annotation for grid builder scopes.
 */
@DslMarker
@Target(AnnotationTarget.TYPE)
private annotation class SimpleGridBuilder


/**
 * The scope for building a simple grid.
 */
@Stable
class GridScope internal constructor(


    /**
     * The widths of the columns. This also implicitly specifies the number of the columns.
     */
    private val columnWidths: List<Dp>,


    /**
     * The selection model for rows.
     */
    private val rowSelectionModel: ListSelectionModel,


    /**
     * The background color selected rows.
     */
    private val selectedBackgroundColor: Color,


    /**
     * The content color selected rows.
     */
    private val selectedContentColor: Color,


    /**
     * The modifier applied to each row by default.
     */
    val defaultRowModifier: Modifier,


    /**
     * The (default) vertical alignment of rows.
     */
    private val rowVerticalAlignment: Alignment.Vertical,


    /**
     * The modifier applied to each cell by default.
     */
    val defaultCellModifier: Modifier,


    /**
     * The default content alignment of cells, by the index of the column.
     */
    private val defaultCellContentAlignment: (Int) -> Alignment = { Alignment.TopStart },


) {


    /**
     * Assists in implementing drag-to-reorder rows.
     */
    internal val dragHelper = DragRowsToReorderHelper()


    /**
     * Adds a row.
     */
    @Composable
    fun row(
        rowIndex: Int,
        modifier: Modifier = defaultRowModifier,
        verticalAlignment: Alignment.Vertical = rowVerticalAlignment,
        content: @SimpleGridBuilder @Composable GridRowScope.() -> Unit
    ) {
        val isSelected by remember(rowSelectionModel, rowIndex) {
            derivedStateOf { rowSelectionModel.isIndexSelected(rowIndex) }
        }

        rowImpl(
            rowIndex = rowIndex,
            modifier = Modifier
                .then(dragHelper.rowModifier(rowIndex))
                .then(modifier),
            verticalAlignment = verticalAlignment,
            isSelected = isSelected,
            content = content
        )
    }


    /**
     * The actual row composable.
     */
    @Composable
    private fun rowImpl(
        rowIndex: Int,
        modifier: Modifier,
        verticalAlignment: Alignment.Vertical,
        isSelected: Boolean,
        content: @SimpleGridBuilder @Composable GridRowScope.() -> Unit
    ) {
        SelectableBox(
            isSelected = isSelected,
            selectedBackgroundColor = selectedBackgroundColor,
            selectedContentColor = selectedContentColor,
            modifier = Modifier
                .onMousePress {
                    rowSelectionModel.selectIndex(rowIndex)
                }
                .then(modifier),
        ) {
            Row(verticalAlignment = verticalAlignment) {
                val gridRowScope = remember(this@GridScope, rowIndex, this) {
                    GridRowScope(rowIndex, this)
                }
                gridRowScope.content()
            }
        }
    }


    /**
     * Wraps content that is expected to emit a [row], specifying the row index earlier.
     * This allows us to avoid passing the row index down the hierarchy, causing unnecessary recompositions of
     * intermediate composable functions.
     */
    @Composable
    inline fun inRow(rowIndex: Int, crossinline content: @Composable GridScope.() -> Unit) {
        CompositionLocalProvider(LocalRowIndex provides rowIndex) {
            content()
        }
    }


    /**
     * Adds a row, with the row index expected to have already been specified by [inRow].
     */
    @Composable
    fun row(
        modifier: Modifier = defaultRowModifier,
        content: @SimpleGridBuilder @Composable GridRowScope.() -> Unit
    ) {
        val rowIndex = LocalRowIndex.current ?: error("row without rowIndex must be wrapped in `inRow`")
        row(
            rowIndex = rowIndex,
            modifier = modifier,
        ) {
            // Reset LocalRowIndex in case there's a SimpleGrid in the content
            CompositionLocalProvider(LocalRowIndex provides null) {
                content()
            }
        }
    }


    /**
     * Returns a modifier implementing drag-to-reorder for a row.
     *
     * This modifier should be attached to the [Modifier] passed to [row].
     *
     * See [DragRowsToReorderHelper.dragToReorder] for more information.
     */
    fun Modifier.dragRowToReorder(
        draggableContent: @Composable () -> Unit,
        canMoveToRow: (Int) -> Boolean,
        onDrop: (draggedRowIndex: Int, dropRowIndex: Int) -> Unit,
        onDragCancelled: (() -> Unit)? = null,
    ) = with(dragHelper) {
        dragToReorder(
            draggableContent = draggableContent,
            canMoveToRow = canMoveToRow,
            onDrop = onDrop,
            onDragCancelled = onDragCancelled
        )
    }


    /**
     * A composable that can be used to correctly lay out a dragged row in [Modifier.dragRowToReorder].
     */
    @Composable
    fun draggedRow(
        rowIndex: Int,
        modifier: Modifier = defaultRowModifier,
        verticalAlignment: Alignment.Vertical = rowVerticalAlignment,
        content: @SimpleGridBuilder @Composable GridRowScope.() -> Unit
    ) {
        rowImpl(
            rowIndex = rowIndex,
            modifier = modifier,
            verticalAlignment = verticalAlignment,
            isSelected = false,
            content = content
        )
    }


    /**
     * The scope for building a row.
     */
    inner class GridRowScope(


        /**
         * The index of this row.
         */
        val rowIndex: Int,


        /**
         * The underlying [RowScope].
         */
        rowScope: RowScope


    ) : RowScope by rowScope {


        /**
         * The default cell modifier.
         */
        val defaultCellModifier: Modifier
            get() = this@GridScope.defaultCellModifier


        /**
        * Whether this row is selected.
         */
        @Suppress("unused")
        val isRowSelected: Boolean
            get() = rowSelectionModel.isIndexSelected(rowIndex)


        /**
         * Adds a cell to this row.
         */
        @Composable
        fun cell(
            cellIndex: Int,
            colSpan: Int = 1,
            modifier: Modifier = defaultCellModifier,
            contentAlignment: Alignment = defaultCellContentAlignment(cellIndex),
            content: @SimpleGridBuilder @Composable GridCellScope.() -> Unit
        ) {
            if (cellIndex + colSpan > columnWidths.size)
                throw IllegalStateException("Too many cells in row $rowIndex")
            val cellColumnWidths = remember(columnWidths, cellIndex, colSpan) {
                columnWidths.subList(cellIndex, cellIndex + colSpan)
            }
            val width = remember(cellColumnWidths) { cellColumnWidths.sumOfSpecified() }
            val unspecifiedCount = remember(cellColumnWidths) { cellColumnWidths.count { it.isUnspecified } }
            val widthModifier = remember(width, unspecifiedCount) {
                if (unspecifiedCount == 0)
                    Modifier.width(width)
                else
                    Modifier.widthIn(min = width).weight(unspecifiedCount.toFloat())
            }

            Box(
                modifier = Modifier.then(widthModifier).fillMaxHeight().then(modifier),
                contentAlignment = contentAlignment
            ) {
                val gridCellScope = remember(this) { GridCellScope(this) }
                gridCellScope.content()
            }
        }


        /**
         * Adds an empty cell to this row.
         */
        @Composable
        fun emptyCell(
            cellIndex: Int,
            colSpan: Int = 1,
        ) {
            if (colSpan < 1)
                throw IllegalArgumentException("colSpan can't be less than 1; given $colSpan")
            cell(
                cellIndex = cellIndex,
                colSpan = colSpan
            ) {
                Spacer(
                    modifier = Modifier
                )
            }
        }


    }


    /**
     * The scope for building a cell.
     */
    @Immutable
    class GridCellScope(private val boxScope: BoxScope) {


        /**
         * Sets the alignment of an element inside the cell.
         */
        fun Modifier.align(alignment: Alignment): Modifier = with(boxScope) { this@align.align(alignment) }


    }


}


/**
 * A utility for showing a header for a [SimpleGrid].
 *
 * Note that this shouldn't be placed *inside* a [SimpleGrid].
 */
@Composable
fun SimpleGridHeaderRow(
    modifier: Modifier = Modifier,
    columnWidths: List<Dp>,
    defaultCellModifier: Modifier = Modifier,
    defaultCellContentAlignment: (Int) -> Alignment = { Alignment.TopStart },
    content: @Composable HeaderRowScope.() -> Unit
) {
    Row(
        modifier = modifier
    ) {
        val scope = object: HeaderRowScope, RowScope by this {

            override val defaultCellModifier: Modifier
                get() = defaultCellModifier

            override val defaultCellContentAlignment: (Int) -> Alignment
                get() = defaultCellContentAlignment

            override fun Modifier.cellWidthModifier(columnIndex: Int): Modifier {
                // Replicate the behavior of SimpleGrid.cell
                val width = columnWidths[columnIndex]
                return if (width.isSpecified)
                    width(width)
                else
                    weight(1f)
            }

            @Composable
            override fun EmptyCell(index: Int) {
                Spacer(Modifier.cellWidthModifier(index).then(defaultCellModifier))
            }

        }

        scope.content()
    }
}


/**
 * The content interface for providing the content of a [SimpleGridHeaderRow].
 */
interface HeaderRowScope: RowScope {


    /**
     * The default modifier for cells.
     */
    val defaultCellModifier: Modifier


    /**
     * The alignment of cells in each row.
     */
    val defaultCellContentAlignment: (Int) -> Alignment


    /**
     * Returns a modifier setting the width to the width of the column with the given index
     */
    fun Modifier.cellWidthModifier(columnIndex: Int): Modifier


    /**
     * Adds an empty cell.
     */
    @Composable
    fun EmptyCell(index: Int)


}


/**
 * Adds a cell with the given text.
 */
@Composable
fun HeaderRowScope.TextCell(
    index: Int,
    text: String,
    style: TextStyle = LocalTextStyle.current,
    modifier: Modifier = defaultCellModifier,
    contentAlignment: Alignment = defaultCellContentAlignment(index),
) {
    Box(
        modifier = Modifier.cellWidthModifier(index).then(modifier),
        contentAlignment = contentAlignment
    ) {
        SingleLineText(
            text = text,
            style = style,
        )
    }
}


/**
 * The composition local for passing the row index.
 */
val LocalRowIndex = compositionLocalOf<Int?> { null }