# MeasurementsLineChart.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.chart

    import android.content.Context
    import android.util.AttributeSet
    import com.github.mikephil.charting.charts.LineChart
    import com.github.mikephil.charting.components.Description
    import com.github.mikephil.charting.components.XAxis
    import com.github.mikephil.charting.components.YAxis
    import com.github.mikephil.charting.data.Entry
    import com.github.mikephil.charting.data.LineData
    import com.github.mikephil.charting.data.LineDataSet
    import com.github.mikephil.charting.utils.Transformer
    import dk.nsp.mingraviditet.R
    import dk.nsp.mingraviditet.ui.main.journal.sections.measurements.chart.models.LineChartSetupData
    import dk.nsp.mingraviditet.ui.main.journal.sections.measurements.chart.renderers.MeasurementsLineChartRenderer
    import dk.nsp.mingraviditet.ui.main.journal.sections.measurements.chart.renderers.MeasurementsLineChartXAxisRenderer
    import dk.nsp.mingraviditet.ui.main.journal.sections.measurements.chart.renderers.MeasurementsLineChartYAxisRenderer
    import dk.nsp.mingraviditet.util.extensions.getColorFromAttr
    import dk.nsp.mingraviditet.util.extensions.getDimenFromAttr
    import kotlinx.coroutines.CoroutineScope
    import kotlinx.coroutines.Dispatchers
    import kotlinx.coroutines.launch

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

        // X Axis default max value in weeks
        private val xDefaultMax = 45f
        private val xDefaultMin = 0f

        private val yDefaultMax = 45f
        private val yDefaultMin = 0f

        // Extra offset for when using datas max as axis max values
        private val axisDataMaxOffset = 5f

        private val xValueFormatter = MeasurementsLineChartXAxisLabelFormatter()
        private val yValueFormatter = MeasurementsLineChartYAxisLabelFormatter()

        private val axisLabelTextSize by lazy {
            resources.getDimension(context.getDimenFromAttr(R.attr.measurementsChartTextSize)) / resources.displayMetrics.density
        }
        private val axisLabelTypeface by lazy { resources.getFont(R.font.ptsans_bold) }
        private val axisLabelTextColor by lazy { context.getColorFromAttr(R.attr.measurementsChartTextColor) }

        init {
            setup()
        }

        private fun setup() {
            val axisTransformer = getTransformer(YAxis.AxisDependency.LEFT)
            setupXAxisBasics(axisTransformer)
            setupYAxisBasic(axisTransformer)

            renderer = MeasurementsLineChartRenderer(
                this,
                animator,
                viewPortHandler,
                shadowColor = context.getColorFromAttr(R.attr.measurementsChartDotShadowColor)
            )

            // Adds extra spacing below the xAxis
            extraBottomOffset = resources.getDimension(R.dimen.default_spacing)

            // Hide descriptive views
            setNoDataText("")
            legend.isEnabled = false
            description = Description().apply {
                isEnabled = false
            }

            // Disable all user interaction
            setTouchEnabled(false)
        }

        private fun setupXAxisBasics(axisTransformer: Transformer) {
            setXAxisRenderer(
                MeasurementsLineChartXAxisRenderer(
                    viewPortHandler,
                    xAxis,
                    axisTransformer
                )
            )

            xAxis.apply {
                valueFormatter = xValueFormatter

                position = XAxis.XAxisPosition.BOTTOM

                setDrawAxisLine(true)
                setDrawGridLines(false)

                // This offsets the labels from the y Axis
                yOffset = 8f
                axisMinimum = xDefaultMin

                axisLineColor = context.getColorFromAttr(R.attr.measurementsChartAxisLinesColor)

                // Text setup
                typeface = axisLabelTypeface
                textColor = axisLabelTextColor
                xAxis.textSize = axisLabelTextSize
            }
        }

        private fun setupYAxisBasic(axisTransformer: Transformer) {
            // Disable the right y Axis
            axisRight.isEnabled = false

            rendererLeftYAxis = MeasurementsLineChartYAxisRenderer(
                viewPortHandler,
                axisLeft,
                axisTransformer,
                context.getColorFromAttr(R.attr.measurementsChartAxisLinesColor)
            )

            axisLeft.apply {
                valueFormatter = yValueFormatter

                setDrawAxisLine(false)
                setDrawGridLines(true)

                axisMinimum = yDefaultMin

                // Text setup
                typeface = resources.getFont(R.font.ptsans_bold)
                textColor = context.getColorFromAttr(R.attr.measurementsChartTextColor)
                textSize =
                    resources.getDimension(R.dimen.info_text_size) / resources.displayMetrics.density
            }
        }

        private fun getStyledLineData(entries: List<Entry>): LineDataSet =
            LineDataSet(entries, "").apply {
                // Removes the value label for each point
                setDrawValues(false)

                setDrawCircleHole(true)
                setDrawCircles(true)

                circleRadius = 8f
                circleHoleRadius = 6f

                val fillColor = context.getColorFromAttr(R.attr.measurementsChartDotInnerColor)

                setCircleColor(context.getColorFromAttr(R.attr.measurementsChartDotOuterColor))
                circleHoleColor = fillColor

                // Setup line properties
                lineWidth =
                    context.resources.getDimensionPixelSize(R.dimen.journal_chart_line_width).toFloat()
                color = fillColor
            }

        /**
         * Sets the data to the chart and ensures chart X/Y axis values are up to date
         */
        fun setChartData(data: LineChartSetupData) {
            CoroutineScope(Dispatchers.Default).launch {

                xValueFormatter.unit = data.config.xUnit.getString(resources)
                yValueFormatter.unit = data.config.yUnit.getString(resources)

                val entries = data.values.map { Entry(it.x, it.y) }
                val dataSet = getStyledLineData(entries)

                // Set the max to either default or update from data set if it's larger
                xAxis.axisMaximum = maxOf(dataSet.xMax + axisDataMaxOffset, xDefaultMax)
                axisLeft.axisMaximum = maxOf(dataSet.yMax + axisDataMaxOffset, yDefaultMax)

                this.launch(Dispatchers.Main) {
                    setData(LineData(dataSet))
                    invalidate()
                }
            }
        }

        companion object {
            /**
             * Denotes whether a given index on the yAxis should be a solid line or not
             */
            fun isYAxisPositionSolidLine(index: Int) =
                index % 2 == 0
        }
    }