# JournalTableLayout.kt

    /*
     * The MIT License
     *
     * Original work sponsored and donated by The Danish Health Data Authority (http://www.sundhedsdatastyrelsen.dk)
     *
     * Copyright (C) 2021 The Danish Health Data Authority (http://www.sundhedsdatastyrelsen.dk)
     *
     *
     * Permission is hereby granted, free of charge, to any person obtaining a copy of
     * this software and associated documentation files (the "Software"), to deal in
     * the Software without restriction, including without limitation the rights to
     * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
     * of the Software, and to permit persons to whom the Software is furnished to do
     * so, subject to the following conditions:
     *
     *
     * The above copyright notice and this permission notice shall be included in all
     * copies or substantial portions of the Software.
     *
     *
     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     * SOFTWARE.
     */

    package dk.nsp.mingraviditet.ui.main.journal.sections.measurements.table.ui

    import android.content.Context
    import android.util.AttributeSet
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.widget.LinearLayout
    import android.widget.TextView
    import androidx.constraintlayout.widget.ConstraintSet
    import androidx.core.content.ContextCompat
    import com.commonsense.android.kotlin.base.extensions.isNull
    import com.commonsense.android.kotlin.system.extensions.getTextSafe
    import com.commonsense.android.kotlin.views.extensions.children
    import com.google.android.material.snackbar.Snackbar
    import dk.nsp.mingraviditet.R
    import dk.nsp.mingraviditet.databinding.JournalTableLayoutBinding
    import dk.nsp.mingraviditet.ui.main.journal.sections.measurements.table.models.common.JournalTableCellData
    import dk.nsp.mingraviditet.ui.main.journal.sections.measurements.table.models.common.TableColumnSet
    import dk.nsp.mingraviditet.util.extensions.getColorFromAttr
    import dk.nsp.shared.util.extensions.map

    class JournalTableLayout @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
    ) : LinearLayout(context, attrs, defStyleAttr) {

        private val initialProgressIndicatorMax = 100
        private val startProgressIndicatorOffsetPercentage = 2

        private val binding =
            JournalTableLayoutBinding.inflate(LayoutInflater.from(context), this, true)

        private val dividerColor by lazy {
            context.getColorFromAttr(R.attr.journalTableDividerColor)
        }

        private val dividerWidth by lazy {
            resources.getDimensionPixelSize(R.dimen.journal_table_divider_width)
        }

        private val titleColumnViewId = "TableTitleColumnViewId".hashCode()
        private val titleColumnDividerViewId = "titleColumnDividerViewId".hashCode()
        private val gradientViewId = "TableGradientViewId".hashCode()

        private val dividerViewTag = "TableDivider"

        private var tableHeaderTitleString: String = ""

        init {
            setTableHeaderTitleFromAttrs(attrs, defStyleAttr)
            setupProgressBar()
            setupRotateButton()
        }

        private val cellIndexToHeightsMap = mutableMapOf<Int, Int>()
        private val cellsMap = mutableMapOf<Int, MutableList<JournalTableCellView>>()

        private var valueColumns: MutableList<JournalTableColumnView> = mutableListOf()
        private var numberOfValueColumnsMeasured = 0
        private var totalValueColumnsWidth = 0

        private fun cellCallback(cellIndex: Int, cell: JournalTableCellView) {
            cellsMap.getOrPut(cellIndex, { mutableListOf() }).add(cell)

            val currentLargest = cellIndexToHeightsMap[cellIndex]
            when {
                currentLargest == null -> cellIndexToHeightsMap[cellIndex] = cell.height
                cell.height > currentLargest -> {
                    cellIndexToHeightsMap[cellIndex] = cell.height
                    cellsMap[cellIndex]?.forEach { view -> view.updateHeight(cell.height) }
                }
            }

            cellIndexToHeightsMap[cellIndex]?.let { updatedLargest ->
                cellsMap[cellIndex]?.forEach { cell ->
                    cell.updateHeight(updatedLargest)
                }
            }
        }

        //region Column width handling
        private fun valueColumnWidthCallback(width: Int) {
            numberOfValueColumnsMeasured += 1
            // Add this columns width to the total
            totalValueColumnsWidth += width

            if (numberOfValueColumnsMeasured == valueColumns.size) {
                val dividersTotalWidth = getNumberOfDividers() * dividerWidth
                val availableWidth = binding.tableScrollView.width
                // Check if we need to resize to fill the scrollview
                val contentFullSize = totalValueColumnsWidth + dividersTotalWidth

                if (contentFullSize < availableWidth) {
                    val newColumnWidth = (availableWidth - dividersTotalWidth) / valueColumns.size
                    valueColumns.forEach { it.updateWidth(newColumnWidth) }
                }
            }
        }

        private fun getNumberOfDividers() = binding.tableDataContainer.children.fold(0) { acc, view ->
            if (view.tag == dividerViewTag) {
                acc + 1
            } else {
                acc
            }
        }
        //endregion

        fun updateWithData(dataSet: TableColumnSet?) {
            // Clear local size data
            valueColumns.clear()
            numberOfValueColumnsMeasured = 0
            totalValueColumnsWidth = 0
            cellIndexToHeightsMap.clear()
            cellsMap.clear()

            // Ensure old data views are cleared
            removeTableChildren()

            visibility = dataSet.isNull.map(
                ifTrue = GONE,
                ifFalse = VISIBLE
            )

            if(dataSet == null) {
                return
            }

            setupTitleColumn(dataSet.titleColumn)

            dataSet.valueColumns.forEachIndexed { index, columnData ->
                val columnView = JournalTableColumnView(context).also {
                    it.setData(columnData, ::valueColumnWidthCallback, ::cellCallback)
                }

                if (index > 0) {
                    addVerticalDivider(binding.tableDataContainer)
                }

                binding.tableDataContainer.addView(
                    columnView, LayoutParams(
                        resources.getDimensionPixelSize(R.dimen.journal_table_column_base_width),
                        LayoutParams.WRAP_CONTENT
                    )
                )

                valueColumns.add(columnView)
            }
        }

        private fun setupTitleColumn(titleColumnData: List<JournalTableCellData>) {
            val titleColumnView = JournalTableColumnView(context).also {
                it.setData(titleColumnData, registerForSizeChanges = ::cellCallback)
                it.id = titleColumnViewId
            }

            val dividerView = createVerticalDividerView()
            val gradientView = createGradientView()

            binding.tableContainer.addView(titleColumnView)
            binding.tableContainer.addView(
                dividerView,
                ViewGroup.LayoutParams(
                    dividerWidth,
                    ViewGroup.LayoutParams.MATCH_PARENT
                )
            )
            binding.tableContainer.addView(gradientView)

            val tableContainerConstraints = ConstraintSet()
            tableContainerConstraints.clone(binding.tableContainer)

            // Setup title column
            tableContainerConstraints.connect(
                titleColumnView.id,
                ConstraintSet.START,
                binding.tableContainer.id,
                ConstraintSet.START
            )

            // Setup divider constraints
            tableContainerConstraints.connect(
                dividerView.id,
                ConstraintSet.START,
                titleColumnView.id,
                ConstraintSet.END
            )

            // Update scroll view to align with divider
            tableContainerConstraints.connect(
                binding.tableScrollView.id,
                ConstraintSet.START,
                dividerView.id,
                ConstraintSet.END
            )

            // Setup gradient constraints
            tableContainerConstraints.connect(
                gradientView.id,
                ConstraintSet.START,
                dividerView.id,
                ConstraintSet.END
            )

            tableContainerConstraints.applyTo(binding.tableContainer)
        }

        private fun createGradientView() = View(context).also {
            it.background =
                ContextCompat.getDrawable(context, R.drawable.table_title_column_gradient)
            it.id = gradientViewId
        }

        private fun removeTableChildren() {
            // Remove all children from the scrollviews content view
            binding.tableDataContainer.removeAllViews()

            // Remove all children that are not the scrollview
            (0..binding.tableContainer.childCount).forEach { childNumber ->
                binding.tableContainer.getChildAt(childNumber)?.let { child ->
                    if(child.id != binding.tableScrollView.id) {
                        binding.tableContainer.removeView(child)
                    }
                }
            }
        }

        private fun addVerticalDivider(parent: LinearLayout, index: Int? = null) {
            val divider = createVerticalDividerView()

            if (index != null) {
                parent.addView(
                    divider,
                    index,
                    LayoutParams(dividerWidth, LayoutParams.MATCH_PARENT)
                )
            } else {
                parent.addView(
                    divider,
                    LayoutParams(dividerWidth, LayoutParams.MATCH_PARENT)
                )
            }
        }

        private fun createVerticalDividerView() = View(context).also {
            it.setBackgroundColor(dividerColor)
            it.id = titleColumnDividerViewId
            it.tag = dividerViewTag
        }

        private fun setupRotateButton() {
            binding.rotateTableButton.setOnClickListener {
                val snackbar = Snackbar.make(
                    this,
                    R.string.android_enable_screen_rotation,
                    Snackbar.LENGTH_INDEFINITE
                )
                // The only way to actually access the snackbar text view
                snackbar.view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)?.isSingleLine = false
                snackbar.setAction(R.string.common_ok) { snackbar.dismiss() }
                snackbar.show()
            }
        }

        private fun setTableHeaderTitleFromAttrs(attrs: AttributeSet?, defStyleAttr: Int) {
            context.obtainStyledAttributes(attrs, R.styleable.JournalTableLayout, defStyleAttr, 0).apply {
                tableHeaderTitleString = getTextSafe(R.styleable.JournalTableLayout_jtl_title).toString()

                binding.tableHeader.text = tableHeaderTitleString
                recycle()
            }
        }

        //region Progress bar
        private fun setupProgressBar() {
            // Set defaults until scroll happens
            binding.tableProgressBar.apply {
                max = initialProgressIndicatorMax
                progress = startProgressIndicatorOffsetPercentage
            }

            binding.tableScrollView.setOnScrollChangeListener { _, scrollX, _, _, _ ->
                // Get the full width of the root view in the scroll view and subtract the visible part of the scrollview to get total possible scroll length
                val totalScrollLength = binding.tableDataContainer.width - binding.tableScrollView.width
                // Ensure scroll is inside range, due to scrollview actually overscrolling a tiny bit
                val scrollXCoerced = scrollX.coerceIn(0..totalScrollLength)

                // We always want a tiny bit of the progress bar to show, so we offset everything by [startProgressOffsetPercentage]
                val calculatedOffset =
                    (totalScrollLength * (startProgressIndicatorOffsetPercentage / 100.0)).toInt()
                val maxWithInitialOffset = totalScrollLength + calculatedOffset
                val progressWithInitialOffset = scrollXCoerced + calculatedOffset

                binding.tableProgressBar.apply {
                    max = maxWithInitialOffset
                    progress = progressWithInitialOffset
                }
            }

            updateProgressBarState()
        }

        override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
            super.onSizeChanged(w, h, oldw, oldh)
            updateProgressBarState()
        }

        private fun updateProgressBarState() {
            post {
                binding.tableProgressBar.visibility = canContentScroll().map(
                    ifTrue = VISIBLE,
                    ifFalse = GONE
                )

            }
        }

        private fun canContentScroll(): Boolean =
            binding.tableScrollView.width < binding.tableDataContainer.width
        //endregion
    }