ToolingUtils.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.layout.Measurable
import androidx.compose.ui.layout.layoutId
import androidx.constraintlayout.core.widgets.ConstraintWidget
import androidx.constraintlayout.core.widgets.ConstraintWidgetContainer
import androidx.constraintlayout.core.widgets.HelperWidget
import org.json.JSONArray
import org.json.JSONObject

private const val CONSTRAINTS_JSON_VERSION = 1

internal fun parseConstraintsToJson(
    root: ConstraintWidgetContainer,
    state: State,
    startX: Int,
    startY: Int
): String {
    // The root id is not user defined, so we create one
    val rootId = Any().toString()
    val idToConstraintsJson = JSONObject()
    root.children.forEach { constraintWidget ->
        val constraintsInfoArray = JSONArray()
        val helperReferences = mutableListOf<String>()
        val isParent = root == constraintWidget
        val isHelper = constraintWidget is HelperWidget
        val widgetId = when {
            // The Id for helpers are set differently from Composables
            isParent -> rootId
            isHelper -> constraintWidget.getHelperId(state)
            else -> constraintWidget.getRefId()
        }

        if (isHelper) {
            addReferencesIds(constraintWidget as HelperWidget, helperReferences, root, rootId)
        }

        constraintWidget.anchors.forEach { anchor ->
            if (anchor.isConnected) {
                val targetWidget = anchor.target.owner
                val targetIsParent = root == targetWidget
                val targetIsHelper = targetWidget is HelperWidget
                val targetId = when {
                    targetIsParent -> rootId
                    targetIsHelper -> targetWidget.getHelperId(state)
                    else -> targetWidget.getRefId()
                }
                constraintsInfoArray.put(
                    JSONObject()
                        .put("originAnchor", anchor.type)
                        .put("targetAnchor", anchor.target!!.type)
                        .put("target", targetId)
                        .put("margin", anchor.margin)
                )
            }
        }

        idToConstraintsJson.putViewIdToBoundsAndConstraints(
            viewId = widgetId,
            boxJson = constraintWidget.boundsToJson(startX, startY),
            isHelper = constraintWidget is HelperWidget,
            isRoot = false,
            helperReferences = helperReferences,
            constraintsInfoArray = constraintsInfoArray
        )
    }
    idToConstraintsJson.putViewIdToBoundsAndConstraints(
        viewId = rootId,
        boxJson = root.boundsToJson(startX, startY),
        isHelper = false,
        isRoot = true,
        helperReferences = emptyList(),
        constraintsInfoArray = JSONArray()
    )
    return createDesignInfoJson(idToConstraintsJson)
}

private fun addReferencesIds(
    helperWidget: HelperWidget,
    helperReferences: MutableList<String>,
    root: ConstraintWidgetContainer,
    rootId: String
) {
    helperWidget.mWidgets.forEach { referencedWidget ->
        val referenceId = if (referencedWidget == root) rootId else referencedWidget.getRefId()
        helperReferences.add(referenceId)
    }
}

/**
 * Returns the Id used for HelperWidgets like barriers or guidelines. Blank if there's no Id.
 */
private fun ConstraintWidget.getHelperId(state: State): String =
    state.getKeyId(this as HelperWidget).toString()

/**
 * Returns the Id used for Composables within the layout. Blank if there's no Id.
 */
private fun ConstraintWidget?.getRefId(): String =
    (this?.companionWidget as? Measurable)?.layoutId.toString()

private fun createDesignInfoJson(content: JSONObject) = JSONObject()
    .put("type", "CONSTRAINTS")
    .put("version", CONSTRAINTS_JSON_VERSION)
    .put("content", content).toString()

private fun ConstraintWidget.boundsToJson(startX: Int, startY: Int) = JSONObject()
    .put("left", left + startX)
    .put("top", top + startY)
    .put("right", right + startX)
    .put("bottom", bottom + startY)

private fun JSONObject.putViewIdToBoundsAndConstraints(
    viewId: String,
    boxJson: JSONObject,
    isHelper: Boolean,
    isRoot: Boolean,
    helperReferences: List<String>,
    constraintsInfoArray: JSONArray
) {
    val viewWithBoundsAndConstraints = JSONObject()
    viewWithBoundsAndConstraints.put("viewId", viewId)
    viewWithBoundsAndConstraints.put("box", boxJson)
    viewWithBoundsAndConstraints.put("isHelper", isHelper)
    viewWithBoundsAndConstraints.put("isRoot", isRoot)

    val helperReferencesArray = JSONArray()
    helperReferences.forEach(helperReferencesArray::put)
    viewWithBoundsAndConstraints.put("helperReferences", helperReferencesArray)

    viewWithBoundsAndConstraints.put("constraints", constraintsInfoArray)
    put(viewId, viewWithBoundsAndConstraints)
}