@file:OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)

package com.jetpackduba.gitnuro.ui.log

import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.key.*
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.LocalWindowKeyEventKeysScope
import com.jetpackduba.gitnuro.extensions.*
import com.jetpackduba.gitnuro.git.graph.GraphCommitList
import com.jetpackduba.gitnuro.git.graph.GraphNode
import com.jetpackduba.gitnuro.git.workspace.StatusSummary
import com.jetpackduba.gitnuro.keybindings.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.theme.*
import com.jetpackduba.gitnuro.ui.SelectedItem
import com.jetpackduba.gitnuro.ui.components.AvatarImage
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.containCommit
import com.jetpackduba.gitnuro.ui.context_menu.*
import com.jetpackduba.gitnuro.ui.dialogs.NewBranchDialog
import com.jetpackduba.gitnuro.ui.dialogs.NewTagDialog
import com.jetpackduba.gitnuro.ui.dialogs.ResetBranchDialog
import com.jetpackduba.gitnuro.viewmodels.LogSearch
import com.jetpackduba.gitnuro.viewmodels.LogStatus
import com.jetpackduba.gitnuro.viewmodels.LogViewModel
import kotlinx.coroutines.launch
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.lib.RepositoryState
import org.eclipse.jgit.revwalk.RevCommit
import java.awt.Cursor

private val colors = listOf(
    Color(0xFF42a5f5),
    Color(0xFFef5350),
    Color(0xFFe78909c),
    Color(0xFFff7043),
    Color(0xFF66bb6a),
    Color(0xFFec407a),
)

private const val CANVAS_MIN_WIDTH = 100
private const val CANVAS_DEFAULT_WIDTH = 120
private const val MIN_GRAPH_LANES = 2

/**
 * Additional number of lanes to simulate to create a margin at the end of the graph.
 */
private const val MARGIN_GRAPH_LANES = 2
private const val LANE_WIDTH = 30f
private const val DIVIDER_WIDTH = 8

private const val LINE_HEIGHT = 40
private const val LOG_BOTTOM_PADDING = 80

// TODO Min size for message column
@OptIn(
    ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class
)
@Composable
fun Log(
    logViewModel: LogViewModel = gitnuroViewModel(),
    selectedItem: SelectedItem,
    repositoryState: RepositoryState,
) {
    val scope = rememberCoroutineScope()
    val logStatusState = logViewModel.logStatus.collectAsState()
    val logStatus = logStatusState.value
    val showLogDialog by logViewModel.logDialog.collectAsState()

    if (logStatus is LogStatus.Loaded) {
        val hasUncommitedChanges = logStatus.hasUncommitedChanges
        val commitList = logStatus.plotCommitList
        val verticalScrollState by logViewModel.verticalListState.collectAsState()
        val horizontalScrollState by logViewModel.horizontalListState.collectAsState()
        val searchFilter = logViewModel.logSearchFilterResults.collectAsState()
        val searchFilterValue = searchFilter.value
        // With this method, whenever the scroll changes, the log is recomposed and the graph list is updated with
        // the proper scroll position
        verticalScrollState.observeScrollChanges()

        LaunchedEffect(verticalScrollState, commitList) {
            launch {
                logViewModel.focusCommit.collect { commit ->
                    scrollToCommit(verticalScrollState, commitList, commit)
                }
            }
            launch {
                logViewModel.scrollToUncommitedChanges.collect {
                    scrollToUncommitedChanges(verticalScrollState, commitList)
                }
            }
        }

        LogDialogs(
            logViewModel,
            onResetShowLogDialog = { logViewModel.showDialog(LogDialog.None) },
            showLogDialog = showLogDialog,
        )

        Column(
            modifier = Modifier
                .background(MaterialTheme.colors.background)
                .fillMaxSize()
        ) {
            var graphPadding by remember(logViewModel) { mutableStateOf(logViewModel.graphPadding) }
            var graphWidth = (CANVAS_DEFAULT_WIDTH + graphPadding).dp

            if (graphWidth.value < CANVAS_MIN_WIDTH) graphWidth = CANVAS_MIN_WIDTH.dp

            if (searchFilterValue is LogSearch.SearchResults) {
                SearchFilter(logViewModel, searchFilterValue)
            }
            GraphHeader(
                graphWidth = graphWidth,
                onPaddingChange = {
                    graphPadding += it
                    logViewModel.graphPadding = graphPadding
                },
                onShowSearch = { scope.launch { logViewModel.onSearchValueChanged("") } }
            )

            Box {
                GraphList(
                    commitList = commitList,
                    selectedItem = selectedItem,
                    repositoryState = repositoryState,
                    horizontalScrollState = horizontalScrollState,
                    graphWidth = graphWidth,
                    verticalScrollState = verticalScrollState,
                    hasUncommitedChanges = hasUncommitedChanges,
                    commitsLimit = logStatus.commitsLimit,
                )

                // The commits' messages list overlaps with the graph list to catch all the click events but leaves
                // a padding, so it doesn't cover the graph
                MessagesList(
                    scrollState = verticalScrollState,
                    hasUncommitedChanges = hasUncommitedChanges,
                    searchFilter = if (searchFilterValue is LogSearch.SearchResults) searchFilterValue.commits else null,
                    logStatus = logStatus,
                    repositoryState = repositoryState,
                    selectedItem = selectedItem,
                    commitList = commitList,
                    logViewModel = logViewModel,
                    graphWidth = graphWidth,
                    commitsLimit = logStatus.commitsLimit,
                    onMerge = { ref ->
                        logViewModel.mergeBranch(ref)
                    },
                    onRebase = { ref ->
                        logViewModel.rebaseBranch(ref)
                    },
                    onShowLogDialog = { dialog ->
                        logViewModel.showDialog(dialog)
                    }
                )

                val density = LocalDensity.current.density
                DividerLog(
                    modifier = Modifier.draggable(
                        rememberDraggableState {
                            graphPadding += it / density
                            logViewModel.graphPadding = graphPadding
                        }, Orientation.Horizontal
                    ),
                    graphWidth = graphWidth,
                )

                // Scrollbar used to scroll horizontally the graph nodes
                // Added after every component to have the highest priority when clicking
                HorizontalScrollbar(
                    modifier = Modifier
                        .align(Alignment.BottomStart)
                        .width(graphWidth)
                        .padding(start = 4.dp, bottom = 4.dp), style = LocalScrollbarStyle.current.copy(
                        unhoverColor = MaterialTheme.colors.scrollbarNormal,
                        hoverColor = MaterialTheme.colors.scrollbarHover,
                    ),
                    adapter = rememberScrollbarAdapter(horizontalScrollState)
                )

                val isFirstItemVisible by remember {
                    derivedStateOf { verticalScrollState.firstVisibleItemIndex > 0 }
                }

                if (isFirstItemVisible) {
                    Box(
                        modifier = Modifier
                            .align(Alignment.TopCenter)
                            .padding(top = 16.dp)
                            .clip(RoundedCornerShape(50))
                            .handMouseClickable {
                                scope.launch {
                                    verticalScrollState.scrollToItem(0)
                                }
                            }
                            .background(MaterialTheme.colors.primary)
                            .padding(vertical = 4.dp, horizontal = 8.dp),
                    ) {
                        Row(verticalAlignment = Alignment.CenterVertically) {
                            Icon(
                                painterResource("align_top.svg"),
                                contentDescription = null,
                                tint = MaterialTheme.colors.onPrimary,
                                modifier = Modifier.size(20.dp),
                            )

                            Text(
                                text = "Scroll to top",
                                modifier = Modifier.padding(start = 8.dp),
                                color = MaterialTheme.colors.onPrimary,
                                style = MaterialTheme.typography.body2,
                            )
                        }
                    }
                }
            }
        }
    }
}

suspend fun scrollToCommit(
    verticalScrollState: LazyListState,
    commitList: GraphCommitList,
    commit: RevCommit?,
) {
    val index = commitList.indexOfFirst { it.name == commit?.name }
    // TODO Show a message informing the user why we aren't scrolling
    // Index can be -1 if the ref points to a commit that is not shown in the graph due to the limited
    // number of displayed commits.
    if (index >= 0) verticalScrollState.scrollToItem(index)
}

suspend fun scrollToUncommitedChanges(
    verticalScrollState: LazyListState,
    commitList: GraphCommitList,
) {
    if (commitList.isNotEmpty())
        verticalScrollState.scrollToItem(0)
}

@Composable
fun SearchFilter(
    logViewModel: LogViewModel,
    searchFilterResults: LogSearch.SearchResults
) {
    val scope = rememberCoroutineScope()
    var searchFilterText by remember { mutableStateOf(logViewModel.savedSearchFilter) }
    val textFieldFocusRequester = remember { FocusRequester() }

    LaunchedEffect(Unit) {
        textFieldFocusRequester.requestFocus()
    }

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .height(64.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        TextField(
            value = searchFilterText,
            onValueChange = {
                searchFilterText = it
                scope.launch {
                    logViewModel.onSearchValueChanged(it)
                }
            },
            maxLines = 1,
            modifier = Modifier
                .fillMaxSize()
                .focusRequester(textFieldFocusRequester)
                .onPreviewKeyEvent { keyEvent ->
                    when {
                        keyEvent.matchesBinding(KeybindingOption.SIMPLE_ACCEPT) && keyEvent.type == KeyEventType.KeyUp -> {
                            scope.launch {
                                logViewModel.selectNextFilterCommit()
                            }
                            true
                        }

                        keyEvent.matchesBinding(KeybindingOption.EXIT) && keyEvent.type == KeyEventType.KeyUp -> {
                            logViewModel.closeSearch()
                            true
                        }

                        else -> false
                    }
                },
            label = {
                Text("Search by message, author name or commit ID")
            },
            colors = textFieldColors(),
            textStyle = MaterialTheme.typography.body1,
            trailingIcon = {
                Row(
                    modifier = Modifier
                        .fillMaxHeight(),
                    verticalAlignment = Alignment.CenterVertically,
                ) {
                    if (searchFilterText.isNotEmpty()) {
                        Text(
                            "${searchFilterResults.index}/${searchFilterResults.totalCount}",
                            color = MaterialTheme.colors.onBackgroundSecondary,
                        )
                    }

                    IconButton(
                        modifier = Modifier
                            .handOnHover(),
                        onClick = {
                            scope.launch { logViewModel.selectPreviousFilterCommit() }
                        }
                    ) {
                        Icon(Icons.Default.KeyboardArrowUp, contentDescription = null)
                    }

                    IconButton(
                        modifier = Modifier
                            .handOnHover(),
                        onClick = {
                            scope.launch { logViewModel.selectNextFilterCommit() }
                        }
                    ) {
                        Icon(Icons.Default.KeyboardArrowDown, contentDescription = null)
                    }

                    IconButton(
                        modifier = Modifier
                            .handOnHover()
                            .padding(end = 4.dp),
                        onClick = { logViewModel.closeSearch() }
                    ) {
                        Icon(Icons.Default.Clear, contentDescription = null)
                    }
                }
            }
        )
    }
}

@Composable
fun MessagesList(
    scrollState: LazyListState,
    hasUncommitedChanges: Boolean,
    searchFilter: List<GraphNode>?,
    logStatus: LogStatus.Loaded,
    repositoryState: RepositoryState,
    selectedItem: SelectedItem,
    commitList: GraphCommitList,
    logViewModel: LogViewModel,
    commitsLimit: Int,
    onMerge: (Ref) -> Unit,
    onRebase: (Ref) -> Unit,
    onShowLogDialog: (LogDialog) -> Unit,
    graphWidth: Dp,
) {
    val keyScope = LocalWindowKeyEventKeysScope.current
    ScrollableLazyColumn(
        state = scrollState,
        modifier = Modifier.fillMaxSize(),
    ) {
        if (
            hasUncommitedChanges ||
            repositoryState.isMerging ||
            repositoryState.isRebasing ||
            repositoryState.isCherryPicking
        ) {
            item {
                UncommitedChangesLine(
                    graphWidth = graphWidth,
                    isSelected = selectedItem == SelectedItem.UncommitedChanges,
                    statusSummary = logStatus.statusSummary,
                    repositoryState = repositoryState,
                    onUncommitedChangesSelected = {
                        logViewModel.selectUncommitedChanges()
                    }
                )
            }
        }
        // Setting a key makes the graph preserve the scroll position when a new line has been added on top (uncommited changes)
        // Therefore, after popping a stash, the uncommited changes wouldn't be visible and requires the user scrolling.
        items(items = commitList) { graphNode ->
            CommitLine(
                graphWidth = graphWidth,
                logViewModel = logViewModel,
                graphNode = graphNode,
                isSelected = selectedItem.containCommit(graphNode),
                currentBranch = logStatus.currentBranch,
                matchesSearchFilter = searchFilter?.contains(graphNode),
                showCreateNewBranch = { onShowLogDialog(LogDialog.NewBranch(graphNode)) },
                showCreateNewTag = { onShowLogDialog(LogDialog.NewTag(graphNode)) },
                resetBranch = { onShowLogDialog(LogDialog.ResetBranch(graphNode)) },
                onMergeBranch = onMerge,
                onRebaseBranch = onRebase,
                onRebaseInteractive = { logViewModel.rebaseInteractive(graphNode) },
                onRevCommitSelected = {
                    logViewModel.selectLogLine(graphNode, keyScope.isCtrlPressed, keyScope.isShiftPressed)
                },
            )
        }

        if (commitsLimit >= 0 && commitsLimit <= commitList.count()) {
            item {
                Box(
                    modifier = Modifier
                        .padding(start = graphWidth + 24.dp)
                        .height(LINE_HEIGHT.dp),
                    contentAlignment = Alignment.CenterStart,
                ) {
                    Text(
                        text = "The commits list has been limited to $commitsLimit. Access the settings to change it.",
                        color = MaterialTheme.colors.onBackground,
                        fontStyle = FontStyle.Italic,
                        style = MaterialTheme.typography.body2,
                        maxLines = 1,
                    )
                }
            }
        }

        item {
            Box(modifier = Modifier.height(LOG_BOTTOM_PADDING.dp))
        }
    }
}

@Composable
fun GraphList(
    commitList: GraphCommitList,
    horizontalScrollState: ScrollState,
    verticalScrollState: LazyListState,
    graphWidth: Dp,
    hasUncommitedChanges: Boolean,
    selectedItem: SelectedItem,
    commitsLimit: Int,
    repositoryState: RepositoryState,
) {
    val maxLinePosition = if (commitList.isNotEmpty())
        commitList.maxLine
    else
        MIN_GRAPH_LANES

    var graphRealWidth = ((maxLinePosition + MARGIN_GRAPH_LANES) * LANE_WIDTH).dp

    // Using remember(graphRealWidth, graphWidth) makes the selected background color glitch when changing tabs
    if (graphRealWidth < graphWidth) {
        graphRealWidth = graphWidth
    }

    Box(
        Modifier
            .width(graphWidth)
            .fillMaxHeight()
    ) {

        Box(
            modifier = Modifier
                .fillMaxSize()
                .horizontalScroll(horizontalScrollState)
                .padding(bottom = 8.dp)
        ) {
            LazyColumn(
                state = verticalScrollState, modifier = Modifier.width(graphRealWidth)
            ) {
                if (
                    hasUncommitedChanges ||
                    repositoryState.isMerging ||
                    repositoryState.isRebasing ||
                    repositoryState.isCherryPicking
                ) {
                    item {
                        Row(
                            modifier = Modifier
                                .height(LINE_HEIGHT.dp)
                                .fillMaxWidth(),
                        ) {
                            UncommitedChangesGraphNode(
                                modifier = Modifier.fillMaxSize(),
                                hasPreviousCommits = commitList.isNotEmpty(),
                                isSelected = selectedItem is SelectedItem.UncommitedChanges,
                            )
                        }
                    }
                }

                items(items = commitList) { graphNode ->
                    val nodeColor = colors[graphNode.lane.position % colors.size]

                    Row(
                        modifier = Modifier
                            .height(LINE_HEIGHT.dp)
                            .fillMaxWidth(),
                    ) {
                        CommitsGraphLine(
                            modifier = Modifier.fillMaxSize(),
                            plotCommit = graphNode,
                            nodeColor = nodeColor,
                            isSelected = selectedItem.containCommit(graphNode),
                        )
                    }
                }

                // Spacing when the commits limit is present
                if (commitsLimit >= 0 && commitsLimit <= commitList.count()) {
                    item {
                        Box(
                            modifier = Modifier
                                .height(LINE_HEIGHT.dp),
                        )
                    }
                }

                item {
                    Box(modifier = Modifier.height(LOG_BOTTOM_PADDING.dp))
                }
            }
        }
    }
}

@Composable
fun LogDialogs(
    logViewModel: LogViewModel,
    onResetShowLogDialog: () -> Unit,
    showLogDialog: LogDialog,
) {
    when (showLogDialog) {
        is LogDialog.NewBranch -> {
            NewBranchDialog(onClose = onResetShowLogDialog, onAccept = { branchName ->
                logViewModel.createBranchOnCommit(branchName, showLogDialog.graphNode)
                onResetShowLogDialog()
            })
        }

        is LogDialog.NewTag -> {
            NewTagDialog(onReject = onResetShowLogDialog, onAccept = { tagName ->
                logViewModel.createTagOnCommit(tagName, showLogDialog.graphNode)
                onResetShowLogDialog()
            })
        }

        is LogDialog.ResetBranch -> ResetBranchDialog(onReject = onResetShowLogDialog, onAccept = { resetType ->
            logViewModel.resetToCommit(showLogDialog.graphNode, resetType)
            onResetShowLogDialog()
        })

        LogDialog.None -> {
        }
    }
}

@Composable
fun GraphHeader(
    graphWidth: Dp,
    onPaddingChange: (Float) -> Unit,
    onShowSearch: () -> Unit
) {
    Box(
        modifier = Modifier.fillMaxWidth()
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .height(34.dp)
                .background(MaterialTheme.colors.tertiarySurface),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Text(
                modifier = Modifier
                    .width(graphWidth)
                    .padding(start = 16.dp),
                text = "Graph",
                color = MaterialTheme.colors.onBackground,
                style = MaterialTheme.typography.body2,
                maxLines = 1,
            )

            val density = LocalDensity.current.density

            SimpleDividerLog(
                modifier = Modifier.draggable(
                    rememberDraggableState {
                        onPaddingChange(it * density) // Multiply by density for screens with scaling > 1
                    }, Orientation.Horizontal
                ),
            )

            Text(
                modifier = Modifier
                    .padding(start = 16.dp)
                    .weight(1f),
                text = "Message",
                color = MaterialTheme.colors.onBackground,
                style = MaterialTheme.typography.body2,
                maxLines = 1,
            )

            IconButton(
                modifier = Modifier
                    .padding(end = 8.dp)
                    .handOnHover(),
                onClick = onShowSearch
            ) {
                Icon(
                    Icons.Default.Search,
                    modifier = Modifier.size(18.dp),
                    contentDescription = null,
                    tint = MaterialTheme.colors.onBackground,
                )
            }
        }
    }
}

@Composable
fun UncommitedChangesLine(
    graphWidth: Dp,
    isSelected: Boolean,
    repositoryState: RepositoryState,
    statusSummary: StatusSummary,
    onUncommitedChangesSelected: () -> Unit,
) {
    Row(
        modifier = Modifier
            .height(40.dp)
            .fillMaxWidth()
            .handMouseClickable { onUncommitedChangesSelected() }
            .padding(start = graphWidth)
            .backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected)
            .padding(DIVIDER_WIDTH.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        val text = when {
            repositoryState.isRebasing -> "Pending changes to rebase"
            repositoryState.isMerging -> "Pending changes to merge"
            repositoryState.isCherryPicking -> "Pending changes to cherry-pick"
            repositoryState.isReverting -> "Pending changes to revert"
            else -> "Uncommited changes"
        }

        Text(
            text = text,
            fontStyle = FontStyle.Italic,
            modifier = Modifier.padding(start = 16.dp),
            style = MaterialTheme.typography.body2,
            maxLines = 1,
            color = MaterialTheme.colors.onBackground,
        )

        Spacer(modifier = Modifier.weight(1f))

        LogStatusSummary(
            statusSummary = statusSummary,
            modifier = Modifier.padding(horizontal = 8.dp),
        )
    }
}

@Composable
fun LogStatusSummary(statusSummary: StatusSummary, modifier: Modifier) {
    Row(
        modifier = modifier,
    ) {
        if (statusSummary.modifiedCount > 0) {
            SummaryEntry(
                count = statusSummary.modifiedCount,
                icon = Icons.Default.Edit,
                color = MaterialTheme.colors.modifyFile,
            )
        }

        if (statusSummary.addedCount > 0) {
            SummaryEntry(
                count = statusSummary.addedCount,
                icon = Icons.Default.Add,
                color = MaterialTheme.colors.addFile,
            )
        }

        if (statusSummary.deletedCount > 0) {
            SummaryEntry(
                count = statusSummary.deletedCount,
                icon = Icons.Default.Delete,
                color = MaterialTheme.colors.deleteFile,
            )
        }

        if (statusSummary.conflictingCount > 0) {
            SummaryEntry(
                count = statusSummary.conflictingCount,
                icon = Icons.Default.Warning,
                color = MaterialTheme.colors.conflictFile,
            )
        }
    }
}

@Composable
fun SummaryEntry(
    count: Int, icon: ImageVector, color: Color
) {
    Row(
        modifier = Modifier.padding(horizontal = 4.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Text(
            text = count.toString(),
            style = MaterialTheme.typography.body2,
            color = MaterialTheme.colors.onBackground,
        )

        Icon(
            imageVector = icon, tint = color, contentDescription = null, modifier = Modifier.size(14.dp)
        )
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CommitLine(
    graphWidth: Dp,
    logViewModel: LogViewModel,
    graphNode: GraphNode,
    isSelected: Boolean,
    currentBranch: Ref?,
    matchesSearchFilter: Boolean?,
    showCreateNewBranch: () -> Unit,
    showCreateNewTag: () -> Unit,
    resetBranch: () -> Unit,
    onMergeBranch: (Ref) -> Unit,
    onRebaseBranch: (Ref) -> Unit,
    onRevCommitSelected: () -> Unit,
    onRebaseInteractive: () -> Unit,
) {
    ContextMenu(
        items = {
            logContextMenu(
                onCheckoutCommit = { logViewModel.checkoutCommit(graphNode) },
                onCreateNewBranch = showCreateNewBranch,
                onCreateNewTag = showCreateNewTag,
                onRevertCommit = { logViewModel.revertCommit(graphNode) },
                onCherryPickCommit = { logViewModel.cherrypickCommit(graphNode) },
                onRebaseInteractive = onRebaseInteractive,
                onResetBranch = { resetBranch() },
            )
        },
    ) {
        Box(
            modifier = Modifier
                .clickable { onRevCommitSelected() }
                .padding(start = graphWidth)
                .height(LINE_HEIGHT.dp)
                .backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected)

        ) {

            if (matchesSearchFilter == true) {
                Box(
                    modifier = Modifier
                        .padding(start = DIVIDER_WIDTH.dp)
                        .background(MaterialTheme.colors.secondary)
                        .fillMaxHeight()
                        .width(4.dp)
                )
            }

            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(end = 4.dp),
            ) {
                val nodeColor = colors[graphNode.lane.position % colors.size]
                CommitMessage(
                    commit = graphNode,
                    refs = graphNode.refs,
                    nodeColor = nodeColor,
                    matchesSearchFilter = matchesSearchFilter,
                    currentBranch = currentBranch,
                    onCheckoutRef = { ref -> logViewModel.checkoutRef(ref) },
                    onMergeBranch = { ref -> onMergeBranch(ref) },
                    onDeleteBranch = { ref -> logViewModel.deleteBranch(ref) },
                    onDeleteRemoteBranch = { ref -> logViewModel.deleteRemoteBranch(ref) },
                    onDeleteTag = { ref -> logViewModel.deleteTag(ref) },
                    onRebaseBranch = { ref -> onRebaseBranch(ref) },
                    onPushRemoteBranch = { ref -> logViewModel.pushToRemoteBranch(ref) },
                    onPullRemoteBranch = { ref -> logViewModel.pullFromRemoteBranch(ref) },
                )
            }
        }
    }
}

@Composable
fun CommitMessage(
    commit: RevCommit,
    refs: List<Ref>,
    currentBranch: Ref?,
    nodeColor: Color,
    matchesSearchFilter: Boolean?,
    onCheckoutRef: (ref: Ref) -> Unit,
    onMergeBranch: (ref: Ref) -> Unit,
    onDeleteBranch: (ref: Ref) -> Unit,
    onDeleteRemoteBranch: (ref: Ref) -> Unit,
    onRebaseBranch: (ref: Ref) -> Unit,
    onDeleteTag: (ref: Ref) -> Unit,
    onPushRemoteBranch: (ref: Ref) -> Unit,
    onPullRemoteBranch: (ref: Ref) -> Unit,
) {
    Row(
        modifier = Modifier.fillMaxSize(),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Row(
            modifier = Modifier.padding(start = 16.dp)
        ) {
            refs.sortedWith { ref1, ref2 ->
                if (ref1.isSameBranch(currentBranch)) {
                    -1
                } else {
                    ref1.name.compareTo(ref2.name)
                }
            }.forEach { ref ->
                if (ref.isTag) {
                    TagChip(
                        ref = ref,
                        color = nodeColor,
                        onCheckoutTag = { onCheckoutRef(ref) },
                        onDeleteTag = { onDeleteTag(ref) },
                    )
                } else if (ref.isBranch) {
                    BranchChip(
                        ref = ref,
                        color = nodeColor,
                        currentBranch = currentBranch,
                        isCurrentBranch = ref.isSameBranch(currentBranch),
                        onCheckoutBranch = { onCheckoutRef(ref) },
                        onMergeBranch = { onMergeBranch(ref) },
                        onDeleteBranch = { onDeleteBranch(ref) },
                        onDeleteRemoteBranch = { onDeleteRemoteBranch(ref) },
                        onRebaseBranch = { onRebaseBranch(ref) },
                        onPullRemoteBranch = { onPullRemoteBranch(ref) },
                        onPushRemoteBranch = { onPushRemoteBranch(ref) },
                    )
                }
            }
        }
        Text(
            text = commit.shortMessage,
            modifier = Modifier
                .padding(start = 8.dp)
                .weight(1f),
            style = MaterialTheme.typography.body2,
            color = if (matchesSearchFilter == false) MaterialTheme.colors.onBackgroundSecondary else MaterialTheme.colors.onBackground,
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
        )

        Text(
            text = commit.authorIdent.`when`.toSmartSystemString(),
            modifier = Modifier.padding(horizontal = 16.dp),
            style = MaterialTheme.typography.caption,
            color = MaterialTheme.colors.onBackgroundSecondary,
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
        )
    }
}

@Composable
fun DividerLog(modifier: Modifier, graphWidth: Dp) {
    Box(
        modifier = Modifier
            .padding(start = graphWidth)
            .width(DIVIDER_WIDTH.dp)
            .then(modifier)
            .pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)))
    ) {
        Box(
            modifier = Modifier
                .fillMaxHeight()
                .width(1.dp)
                .background(color = MaterialTheme.colors.primaryVariant)
                .align(Alignment.Center)
        )
    }
}

@Composable
fun SimpleDividerLog(modifier: Modifier) {
    DividerLog(modifier, graphWidth = 0.dp)
}


@Composable
fun CommitsGraphLine(
    modifier: Modifier = Modifier,
    plotCommit: GraphNode,
    nodeColor: Color,
    isSelected: Boolean,
) {
    val passingLanes = plotCommit.passingLanes
    val forkingOffLanes = plotCommit.forkingOffLanes
    val mergingLanes = plotCommit.mergingLanes
    val density = LocalDensity.current.density
    val laneWidthWithDensity = remember(density) {
        LANE_WIDTH * density
    }

    Box(
        modifier = modifier
            .backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected)
    ) {
        val itemPosition = plotCommit.lane.position

        Canvas(
            modifier = Modifier.fillMaxSize()
        ) {
            clipRect {
                if (plotCommit.childCount > 0) {
                    drawLine(
                        color = colors[itemPosition % colors.size],
                        start = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y),
                        end = Offset(laneWidthWithDensity * (itemPosition + 1), 0f),
                        strokeWidth = 2f,
                    )
                }

                forkingOffLanes.forEach { plotLane ->
                    drawLine(
                        color = colors[plotLane.position % colors.size],
                        start = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y),
                        end = Offset(laneWidthWithDensity * (plotLane.position + 1), 0f),
                        strokeWidth = 2f,
                    )
                }

                mergingLanes.forEach { plotLane ->
                    drawLine(
                        color = colors[plotLane.position % colors.size],
                        start = Offset(laneWidthWithDensity * (plotLane.position + 1), this.size.height),
                        end = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y),
                        strokeWidth = 2f,
                    )
                }

                if (plotCommit.parentCount > 0) {
                    drawLine(
                        color = colors[itemPosition % colors.size],
                        start = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y),
                        end = Offset(laneWidthWithDensity * (itemPosition + 1), this.size.height),
                        strokeWidth = 2f,
                    )
                }

                passingLanes.forEach { plotLane ->
                    drawLine(
                        color = colors[plotLane.position % colors.size],
                        start = Offset(laneWidthWithDensity * (plotLane.position + 1), 0f),
                        end = Offset(laneWidthWithDensity * (plotLane.position + 1), this.size.height),
                        strokeWidth = 2f,
                    )
                }
            }
        }

        CommitNode(
            modifier = Modifier
                .align(Alignment.CenterStart)
                .padding(start = ((itemPosition + 1) * 30 - 15).dp),
            plotCommit = plotCommit,
            color = nodeColor,
        )
    }
}

@Composable
fun CommitNode(
    modifier: Modifier = Modifier,
    plotCommit: GraphNode,
    color: Color,
) {
    Box(
        modifier = modifier
            .size(30.dp)
            .border(2.dp, color, shape = CircleShape)
            .clip(CircleShape)
    ) {
        AvatarImage(
            modifier = Modifier.fillMaxSize(),
            personIdent = plotCommit.authorIdent,
            color = color,
        )
    }
}

@Composable
fun UncommitedChangesGraphNode(
    modifier: Modifier = Modifier,
    hasPreviousCommits: Boolean,
    isSelected: Boolean,
) {
    val density = LocalDensity.current.density

    val laneWidthWithDensity = remember(density) {
        LANE_WIDTH * density
    }
    Box(
        modifier = modifier
            .backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected)
    ) {
        Canvas(
            modifier = Modifier.fillMaxSize()
        ) {
            clipRect {
                if (hasPreviousCommits) drawLine(
                    color = colors[0],
                    start = Offset(laneWidthWithDensity, this.center.y),
                    end = Offset(laneWidthWithDensity, this.size.height),
                    strokeWidth = 2f,
                )

                drawCircle(
                    color = colors[0],
                    radius = 15f * density,
                    center = Offset(laneWidthWithDensity, this.center.y),
                )
            }
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun BranchChip(
    modifier: Modifier = Modifier,
    isCurrentBranch: Boolean = false,
    ref: Ref,
    currentBranch: Ref?,
    onCheckoutBranch: () -> Unit,
    onMergeBranch: () -> Unit,
    onDeleteBranch: () -> Unit,
    onDeleteRemoteBranch: () -> Unit,
    onRebaseBranch: () -> Unit,
    onPushRemoteBranch: () -> Unit,
    onPullRemoteBranch: () -> Unit,
    color: Color,
) {
    val contextMenuItemsList = {
        branchContextMenuItems(
            branch = ref,
            currentBranch = currentBranch,
            isCurrentBranch = isCurrentBranch,
            isLocal = ref.isLocal,
            onCheckoutBranch = onCheckoutBranch,
            onMergeBranch = onMergeBranch,
            onDeleteBranch = onDeleteBranch,
            onDeleteRemoteBranch = onDeleteRemoteBranch,
            onRebaseBranch = onRebaseBranch,
            onPushToRemoteBranch = onPushRemoteBranch,
            onPullFromRemoteBranch = onPullRemoteBranch,
        )
    }

    var endingContent: @Composable () -> Unit = {}
    if (isCurrentBranch) {
        endingContent = {
            Icon(
                painter = painterResource("location.svg"),
                contentDescription = null,
                modifier = Modifier.padding(end = 6.dp),
                tint = MaterialTheme.colors.primaryVariant,
            )
        }
    }

    RefChip(
        modifier = modifier,
        color = color,
        ref = ref,
        icon = "branch.svg",
        onCheckoutRef = onCheckoutBranch,
        contextMenuItemsList = contextMenuItemsList,
        endingContent = endingContent,
    )
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TagChip(
    modifier: Modifier = Modifier,
    ref: Ref,
    onCheckoutTag: () -> Unit,
    onDeleteTag: () -> Unit,
    color: Color,
) {
    val contextMenuItemsList = {
        tagContextMenuItems(
            onCheckoutTag = onCheckoutTag,
            onDeleteTag = onDeleteTag,
        )
    }

    RefChip(
        modifier,
        ref,
        "tag.svg",
        onCheckoutRef = onCheckoutTag,
        contextMenuItemsList = contextMenuItemsList,
        color = color,
    )
}

@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
@Composable
fun RefChip(
    modifier: Modifier = Modifier,
    ref: Ref,
    icon: String,
    color: Color,
    onCheckoutRef: () -> Unit,
    contextMenuItemsList: () -> List<ContextMenuElement>,
    endingContent: @Composable () -> Unit = {},
) {
    Box(
        modifier = Modifier
            .padding(horizontal = 4.dp)
            .clip(RoundedCornerShape(16.dp))
            .border(width = 2.dp, color = color, shape = RoundedCornerShape(16.dp))
            .combinedClickable(onDoubleClick = onCheckoutRef, onClick = {})
            .handOnHover()
    ) {
        ContextMenu(
            items = contextMenuItemsList
        ) {
            Row(
                modifier = modifier,
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Box(modifier = Modifier.background(color = color)) {
                    Icon(
                        modifier = Modifier
                            .padding(6.dp)
                            .size(14.dp),
                        painter = painterResource(icon),
                        contentDescription = null,
                        tint = MaterialTheme.colors.background,
                    )
                }
                Text(
                    text = ref.simpleLogName,
                    style = MaterialTheme.typography.body2,
                    color = MaterialTheme.colors.onBackground,
                    maxLines = 1,
                    modifier = Modifier.padding(horizontal = 6.dp)
                )

                endingContent()
            }
        }
    }
}