ConstraintSetParser.kt

/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.constraintlayout.compose

import androidx.compose.ui.unit.Dp
import androidx.constraintlayout.core.state.ConstraintReference
import androidx.constraintlayout.core.state.Dimension
import androidx.constraintlayout.core.state.Dimension.SPREAD_DIMENSION
import androidx.constraintlayout.core.state.State.Chain.*
import androidx.constraintlayout.core.state.Transition
import androidx.constraintlayout.core.state.helpers.GuidelineReference
import androidx.constraintlayout.core.widgets.ConstraintWidget
import org.json.JSONArray
import org.json.JSONObject

internal val PARSER_DEBUG = false

class LayoutVariables {
    val margins = HashMap<String, Int>()
    val generators = HashMap<String, GeneratedValue>()
    val arrayIds = HashMap<String, ArrayList<String>>()

    fun put(elementName: String, element: Int) {
        margins[elementName] = element
    }

    fun put(elementName: String, start: Float, incrementBy: Float) {
        if (generators.containsKey(elementName)) {
            if (generators[elementName] is OverrideValue) {
                return
            }
        }
        var generator = Generator(start, incrementBy)
        generators[elementName] = generator
    }

    fun putOverride(elementName: String, value: Float) {
        var generator = OverrideValue(value)
        generators[elementName] = generator
    }

    fun get(elementName: Any): Float {
        if (elementName is String) {
            if (generators.containsKey(elementName)) {
                val value = generators[elementName]!!.value()
                return value
            }
            if (margins.containsKey(elementName)) {
                return margins[elementName]!!.toFloat()
            }
        } else if (elementName is Int) {
            return elementName.toFloat()
        } else if (elementName is Double) {
            return elementName.toFloat()
        } else if (elementName is Float) {
            return elementName
        }
        return 0f
    }

    fun getList(elementName: String) : ArrayList<String>? {
        if (arrayIds.containsKey(elementName)) {
            return arrayIds[elementName]
        }
        return null
    }

    fun put(elementName: String, elements: ArrayList<String>) {
        arrayIds[elementName] = elements
    }

}
interface GeneratedValue {
    fun value() : Float
}

class Generator(start: Float, incrementBy: Float) : GeneratedValue {
    var start : Float = start
    var incrementBy: Float = incrementBy
    var current : Float = start
    var stop = false

    override fun value() : Float {
        if (!stop) {
            current += incrementBy
        }
        return current
    }
}

class OverrideValue(value: Float) : GeneratedValue {
    var value : Float = value
    override fun value() : Float {
        return value
    }
}

internal fun parseJSON(content: String, transition: Transition,
                       state: Int, layoutVariables: LayoutVariables) {
    val json = JSONObject(content)
    val elements = json.names() ?: return
    (0 until elements.length()).forEach { i ->
        val elementName = elements[i].toString()
        val element = json[elementName]
        if (element is JSONObject) {
            val customProperties = element.optJSONObject("custom")
            if (customProperties != null) {
                val properties = customProperties.names() ?: return
                (0 until properties.length()).forEach { i ->
                    val property = properties[i].toString()
                    val value = customProperties[property]
                    if (value is Int) {
                        transition.addCustomFloat(state, elementName, property, value.toFloat())
                    } else if (value is Float) {
                        transition.addCustomFloat(state, elementName, property, value)
                    } else if (value is String) {
                        if (value.startsWith('#')) {
                            var r = 0f
                            var g = 0f
                            var b = 0f
                            var a = 1f
                            if (value.length == 7 || value.length == 9) {
                                var hr = Integer.valueOf(value.substring(1, 3), 16)
                                var hg = Integer.valueOf(value.substring(3, 5), 16)
                                var hb = Integer.valueOf(value.substring(5, 7), 16)
                                r = hr.toFloat() / 255f
                                g = hg.toFloat() / 255f
                                b = hb.toFloat() / 255f
                            }
                            if (value.length == 9) {
                                var ha = Integer.valueOf(value.substring(5, 7), 16)
                                a = ha.toFloat() / 255f
                            }
                            transition.addCustomColor(state, elementName, property, r, g, b, a)
                        }
                    }
                }
            }
        }
    }
}

internal fun parseJSON(content: String, state: State, layoutVariables: LayoutVariables) {
    val json = JSONObject(content)
    val elements = json.names() ?: return
    (0 until elements.length()).forEach { i ->
        val elementName = elements[i].toString()
        val element = json[elementName]
        if (PARSER_DEBUG) {
            System.out.println("element <$elementName = $element> " + element.javaClass)
        }
        when (elementName) {
            "Variables" -> parseVariables(state, layoutVariables, element)
            "Helpers" -> parseHelpers(state, layoutVariables, element)
            "Generate" -> parseGenerate(state, layoutVariables, element)
            else -> {
                if (element is JSONObject) {
                    var type = lookForType(element)
                    if (type != null) {
                        when (type) {
                            "hGuideline" -> parseGuidelineParams(ConstraintWidget.HORIZONTAL, state, elementName, element)
                            "vGuideline" -> parseGuidelineParams(ConstraintWidget.VERTICAL, state, elementName, element)
                            "barrier" -> parseBarrier(state, elementName, element)
                        }
                    } else if (type == null) {
                        parseWidget(state, layoutVariables, elementName, element)
                    }
                }
            }
        }
    }
}

fun parseVariables(state: State, layoutVariables: LayoutVariables, json: Any) {
    if (!(json is JSONObject)) {
        return
    }
    val elements = json.names() ?: return
    (0 until elements.length()).forEach { i ->
        val elementName = elements[i].toString()
        val element = json[elementName]
        if (element is Int) {
            layoutVariables.put(elementName, element)
        } else if (element is JSONObject) {
            if (element.has("start") && element.has("increment")) {
                var start = layoutVariables.get(element["start"])
                var increment = layoutVariables.get(element["increment"])
                layoutVariables.put(elementName, start, increment)
            } else if (element.has("ids")) {
                var ids = element.getJSONArray("ids");
                var arrayIds = arrayListOf<String>()
                for (i in 0..ids.length()-1) {
                    arrayIds.add(ids.getString(i))
                }
                layoutVariables.put(elementName, arrayIds)
            } else if (element.has("tag")) {
                var arrayIds = state.getIdsForTag(element.getString("tag"))
                layoutVariables.put(elementName, arrayIds)
            }
        }
    }
}

fun parseHelpers(state: State, layoutVariables: LayoutVariables, element: Any) {
    if (!(element is JSONArray)) {
        return
    }
    (0 until element.length()).forEach { i ->
        val helper = element[i]
        if (helper is JSONArray && helper.length() > 1) {
            when (helper[0]) {
                "hChain" -> parseChain(ConstraintWidget.HORIZONTAL, state, layoutVariables, helper)
                "vChain" -> parseChain(ConstraintWidget.VERTICAL, state, layoutVariables, helper)
                "hGuideline" -> parseGuideline(ConstraintWidget.HORIZONTAL, state, layoutVariables, helper)
                "vGuideline" -> parseGuideline(ConstraintWidget.VERTICAL, state, layoutVariables, helper)
            }
        }
    }
}

fun parseGenerate(state: State, layoutVariables: LayoutVariables, json: Any) {
    if (!(json is JSONObject)) {
        return
    }
    val elements = json.names() ?: return
    (0 until elements.length()).forEach { i ->
        val elementName = elements[i].toString()
        val element = json[elementName]
        var arrayIds = layoutVariables.getList(elementName)
        if (arrayIds != null && element is JSONObject) {
            for (id in arrayIds) {
                parseWidget(state, layoutVariables, id, element)
            }
        }
    }
}

fun parseChain(orientation: Int, state: State, margins: LayoutVariables, helper: JSONArray) {
    var chain = if (orientation == ConstraintWidget.HORIZONTAL) state.horizontalChain() else state.verticalChain()
    var refs = helper[1]
    if (!(refs is JSONArray) || refs.length() < 1) {
        return
    }
    (0 until refs.length()).forEach { i ->
        chain.add(refs[i])
    }
    if (helper.length() > 2) { // we have additional parameters
        var params = helper[2]
        if (!(params is JSONObject)) {
            return
        }
        val constraints = params.names() ?: return
        (0 until constraints.length()).forEach{ i ->
            val constraintName = constraints[i].toString()
            when (constraintName) {
                "style" -> {
                    val styleObject = params[constraintName]
                    val styleValue : String
                    if (styleObject is JSONArray && styleObject.length() > 1) {
                        styleValue = styleObject[0].toString()
                        var biasValue = styleObject[1].toString().toFloat()
                        chain.bias(biasValue)
                    } else {
                        styleValue = styleObject.toString()
                    }
                    when (styleValue) {
                        "packed" -> chain.style(PACKED)
                        "spread_inside" -> chain.style(SPREAD_INSIDE)
                        else -> chain.style(SPREAD)
                    }
                }
                else -> {
                    parseConstraint(state, margins, params, chain as ConstraintReference, constraintName)
                }
            }
        }
    }
}

fun parseGuideline(orientation: Int, state: State, margins: LayoutVariables, helper: JSONArray) {
    var params = helper[1]
    if (!(params is JSONObject)) {
        return
    }
    val guidelineId = params.opt("id")
    if (guidelineId == null)  {
        return
    }
    parseGuidelineParams(orientation, state, guidelineId as String, params)
}

private fun parseGuidelineParams(
    orientation: Int,
    state: State,
    guidelineId: String,
    params: JSONObject
) {
    val constraints = params.names() ?: return
    var reference = state.constraints(guidelineId)
    if (orientation == ConstraintWidget.HORIZONTAL) {
        state.horizontalGuideline(guidelineId)
    } else {
        state.verticalGuideline(guidelineId)
    }
    var guidelineReference = reference.facade as GuidelineReference
    (0 until constraints.length()).forEach { i ->
        val constraintName = constraints[i].toString()
        when (constraintName) {
            "start" -> {
                val margin = state.convertDimension(
                    Dp(
                        params.getInt(constraintName).toFloat()
                    )
                )
                guidelineReference.start(margin)
            }
            "end" -> {
                val margin = state.convertDimension(
                    Dp(
                        params.getInt(constraintName).toFloat()
                    )
                )
                guidelineReference.end(margin)
            }
            "percent" -> {
                guidelineReference.percent(
                    params.getDouble(
                        constraintName
                    ).toFloat()
                )
            }
        }
    }
}

fun parseBarrier(
    state: State,
    elementName: String, element: JSONObject) {
    val reference = state.barrier(elementName, androidx.constraintlayout.core.state.State.Direction.END)
    val constraints = element.names() ?: return
    var barrierReference = reference
    (0 until constraints.length()).forEach { i ->
        val constraintName = constraints[i].toString()
        when (constraintName) {
            "direction" -> {
                var direction = element.getString(constraintName)
                when (direction) {
                    "start" -> barrierReference.setBarrierDirection(androidx.constraintlayout.core.state.State.Direction.START)
                    "end" -> barrierReference.setBarrierDirection(androidx.constraintlayout.core.state.State.Direction.END)
                    "left" -> barrierReference.setBarrierDirection(androidx.constraintlayout.core.state.State.Direction.LEFT)
                    "right" -> barrierReference.setBarrierDirection(androidx.constraintlayout.core.state.State.Direction.RIGHT)
                    "top" -> barrierReference.setBarrierDirection(androidx.constraintlayout.core.state.State.Direction.TOP)
                    "bottom" -> barrierReference.setBarrierDirection(androidx.constraintlayout.core.state.State.Direction.BOTTOM)
                }
            }
            "contains" -> {
                val list = element.optJSONArray(constraintName)
                if (list != null) {
                    for (i in 0..list.length() - 1) {
                        var elementName = list.get(i)
                        val reference = state.constraints(elementName)
                        System.out.println("Add REFERENCE ($elementName = $reference) TO BARRIER ")
                        barrierReference.add(reference)
                    }
                }
            }
        }
    }
}

fun parseWidget(
    state: State,
    layoutVariables: LayoutVariables,
    elementName: String,
    element: JSONObject
) {
    val reference = state.constraints(elementName)
    val constraints = element.names() ?: return
    reference.width = Dimension.Wrap()
    reference.height = Dimension.Wrap()
    (0 until constraints.length()).forEach { i ->
        val constraintName = constraints[i].toString()
        when (constraintName) {
            "width" -> {
                reference.width = parseDimension(element, constraintName, state)
            }
            "height" -> {
                reference.height = parseDimension(element, constraintName, state)
            }
            "center" -> {
                val target = element.getString(constraintName)
                val targetReference = if (target.toString().equals("parent")) {
                    state.constraints(SolverState.PARENT)
                } else {
                    state.constraints(target)
                }
                reference.startToStart(targetReference)
                reference.endToEnd(targetReference)
                reference.topToTop(targetReference)
                reference.bottomToBottom(targetReference)
            }
            "centerHorizontally" -> {
                val target = element.getString(constraintName)
                val targetReference = if (target.toString().equals("parent")) {
                    state.constraints(SolverState.PARENT)
                } else {
                    state.constraints(target)
                }
                reference.startToStart(targetReference)
                reference.endToEnd(targetReference)
            }
            "centerVertically" -> {
                val target = element.getString(constraintName)
                val targetReference = if (target.toString().equals("parent")) {
                    state.constraints(SolverState.PARENT)
                } else {
                    state.constraints(target)
                }
                reference.topToTop(targetReference)
                reference.bottomToBottom(targetReference)
            }
            "alpha" -> {
                val value = layoutVariables.get(element[constraintName])
                reference.alpha(value)
            }
            "scaleX" -> {
                val value = layoutVariables.get(element[constraintName])
                reference.scaleX(value)
            }
            "scaleY" -> {
                val value = layoutVariables.get(element[constraintName])
                reference.scaleY(value)
            }
            "translationX" -> {
                val value = layoutVariables.get(element[constraintName])
                reference.translationX(value)
            }
            "translationY" -> {
                val value = layoutVariables.get(element[constraintName])
                reference.translationY(value)
            }
            "pivotX" -> {
                val value = layoutVariables.get(element[constraintName])
                reference.pivotX(value)
            }
            "pivotY" -> {
                val value = layoutVariables.get(element[constraintName])
                reference.pivotY(value)
            }
            "rotationX" -> {
                val value = layoutVariables.get(element[constraintName])
                reference.rotationX(value)
            }
            "rotationY" -> {
                val value = layoutVariables.get(element[constraintName])
                reference.rotationY(value)
            }
            "rotationZ" -> {
                val value = layoutVariables.get(element[constraintName])
                reference.rotationZ(value)
            }
            "custom" -> {
                parseCustomProperties(element, reference, constraintName)
            }
            else -> {
                parseConstraint(state, layoutVariables, element, reference, constraintName)
            }
        }
    }
}

private fun parseCustomProperties(
    element: JSONObject,
    reference: ConstraintReference,
    constraintName: String
) {
    var json = element.optJSONObject(constraintName)
    if (json == null) {
        return
    }
    val properties = json.names() ?: return
    (0 until properties.length()).forEach { i ->
        val property = properties[i].toString()
        val value = json[property]
        if (value is Int) {
            reference.addCustomFloat(property, value.toFloat())
        } else if (value is Float) {
            reference.addCustomFloat(property, value)
        } else if (value is String) {
            if (value.startsWith('#')) {
                var r = 0f
                var g = 0f
                var b = 0f
                var a = 1f
                if (value.length == 7 || value.length == 9) {
                    var hr = Integer.valueOf(value.substring(1, 3), 16)
                    var hg = Integer.valueOf(value.substring(3, 5), 16)
                    var hb = Integer.valueOf(value.substring(5, 7), 16)
                    r = hr.toFloat() / 255f
                    g = hg.toFloat() / 255f
                    b = hb.toFloat() / 255f
                }
                if (value.length == 9) {
                    var ha = Integer.valueOf(value.substring(5, 7), 16)
                    a = ha.toFloat() / 255f
                }
                reference.addCustomColor(property, r, g, b, a)
            }
        }
    }
}

private fun parseConstraint(
    state: State,
    layoutVariables: LayoutVariables,
    element: JSONObject,
    reference: ConstraintReference,
    constraintName: String
) {
    val constraint = element.optJSONArray(constraintName)
    if (constraint != null && constraint.length() > 1) {
        val target = constraint[0]
        val anchor = constraint[1]
        var margin: Int = 0
        if (constraint.length() > 2) {
            margin = layoutVariables.get(constraint[2]).toInt()
        }
        margin = state.convertDimension(Dp(margin.toFloat()))

        val targetReference = if (target.toString().equals("parent")) {
            state.constraints(SolverState.PARENT)
        } else {
            state.constraints(target)
        }
        when (constraintName) {
            "circular" -> {
                var angle = layoutVariables.get(constraint[1])
                reference.circularConstraint(targetReference, angle, 0f)
            }
            "start" -> {
                when (anchor) {
                    "start" -> {
                        reference.startToStart(targetReference)
                    }
                    "end" -> reference.startToEnd(targetReference)
                }
            }
            "end" -> {
                when (anchor) {
                    "start" -> reference.endToStart(targetReference)
                    "end" -> reference.endToEnd(targetReference)
                }
            }
            "top" -> {
                when (anchor) {
                    "top" -> reference.topToTop(targetReference)
                    "bottom" -> reference.topToBottom(targetReference)
                }
            }
            "bottom" -> {
                when (anchor) {
                    "top" -> {
                        reference.bottomToTop(targetReference)
                    }
                    "bottom" -> {
                        reference.bottomToBottom(targetReference)
                    }
                }
            }
        }
        reference.margin(margin)
    } else {
        var target = element.optString(constraintName)
        if (target != null) {
            val targetReference = if (target.toString().equals("parent")) {
                state.constraints(SolverState.PARENT)
            } else {
                state.constraints(target)
            }
            when (constraintName) {
                "start" -> reference.startToStart(targetReference)
                "end" -> reference.endToEnd(targetReference)
                "top" -> reference.topToTop(targetReference)
                "bottom" -> reference.bottomToBottom(targetReference)
            }
        }
    }
}

private fun parseDimension(
    element: JSONObject,
    constraintName: String,
    state: State
): Dimension {
    var dimensionString = element.getString(constraintName)
    var dimension: Dimension
    when (dimensionString) {
        "wrap" -> dimension = Dimension.Wrap()
        "spread" -> dimension = Dimension.Suggested(SPREAD_DIMENSION)
        "parent" -> dimension = Dimension.Parent()
        else -> {
            if (dimensionString.endsWith('%')) {
                // parent percent
                var percentString = dimensionString.substringBefore('%')
                var percentValue = percentString.toFloat() / 100f
                dimension = Dimension.Percent(0, percentValue).suggested(0)
            } else if (dimensionString.contains(':')) {
                dimension = Dimension.Ratio(dimensionString).suggested(0)
            } else {
                dimension = Dimension.Fixed(
                    state.convertDimension(
                        Dp(
                            element.getInt(constraintName).toFloat()
                        )
                    )
                )
            }
        }
    }
    return dimension
}

fun lookForType(element: JSONObject): String? {
    val constraints = element.names() ?: return null
    (0 until constraints.length()).forEach { i ->
        val constraintName = constraints[i].toString()
        if (constraintName.equals("type")) {
            return element.getString("type")
        }
    }
    return null
}