/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2026 SteamWar.de-Serverteam
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package de.steamwar.sql

import de.steamwar.sql.internal.fromSql
import de.steamwar.sql.internal.useDb
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.core.dao.id.EntityID
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
import org.jetbrains.exposed.v1.dao.IntEntity
import org.jetbrains.exposed.v1.dao.IntEntityClass
import org.jetbrains.exposed.v1.javatime.CurrentTimestamp
import org.jetbrains.exposed.v1.javatime.timestamp
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
import java.sql.Timestamp
import java.util.*
import java.util.function.Consumer

object SchematicNodeTable : IntIdTable("SchematicNode", "NodeId") {
    val owner = reference("NodeOwner", SteamwarUserTable).index()
    val name = varchar("NodeName", 64)
    val parent = optReference("ParentNode", SchematicNodeTable).index()
    val lastUpdate = timestamp("LastUpdate").defaultExpression(CurrentTimestamp)
    val item = text("NodeItem")
    val type = varchar("NodeType", 16).nullable().index()
    val config = integer("Config")

    init {
        uniqueIndex(parent, owner, name)
    }
}

class SchematicNode(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<SchematicNode>(SchematicNodeTable) {
        private val fieldIndex: Map<Expression<*>, Int> =
            SchematicNodeTable.columns.mapIndexed { index, column -> column to index }.toMap()

        private val FORBIDDEN_NAMES = listOf("public")
        private val FORBIDDEN_CHARS = listOf('/', '\\', '<', '>', '^', '°', '\'', '"', ' ')

        val tabCache = mutableMapOf<Int, MutableMap<String, List<String>>>()

        @JvmStatic
        fun clear() =
            tabCache.clear()

        private fun List<SchematicNode>.mapToIds(): Map<Int, SchematicNode> =
            this.associateBy { it.nodeId }

        @JvmStatic
        fun getAll(user: SteamwarUser) =
            fromSql(
                "WITH RECURSIVE Nodes AS (SELECT NodeId, ParentId as ParentNode FROM NodeMember WHERE UserId = ? UNION SELECT NodeId, ParentNode FROM SchematicNode WHERE NodeOwner = ?), RSN AS ( SELECT NodeId, ParentNode FROM Nodes UNION SELECT SN.NodeId, SN.ParentNode FROM SchematicNode SN, RSN WHERE SN.ParentNode = RSN.NodeId ) SELECT SN.* FROM RSN INNER JOIN SchematicNode SN ON RSN.NodeId = SN.NodeId",
                listOf(
                    IntegerColumnType() to user.getId(),
                    IntegerColumnType() to user.getId()
                ),
                fieldIndex
            )

        @JvmStatic
        fun getAllMap(user: SteamwarUser) =
            getAll(user).mapToIds()

        @JvmStatic
        fun list(user: SteamwarUser, schematicId: Int?) =
            fromSql(
                "SELECT SchematicNode.NodeId, NodeOwner, NodeName, NM.ParentId AS ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, Config FROM SchematicNode INNER JOIN NodeMember NM on SchematicNode.NodeId = NM.NodeId WHERE NM.ParentId <=> ? AND NM.UserId = ? UNION ALL SELECT SchematicNode.NodeId, NodeOwner, NodeName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, Config FROM SchematicNode WHERE (? IS NULL AND ParentNode IS NULL AND NodeOwner = ?) OR (? IS NOT NULL AND ParentNode = ?) ORDER BY NodeName",
                listOf(
                    IntegerColumnType() to schematicId,
                    IntegerColumnType() to user.getId(),
                    IntegerColumnType() to schematicId,
                    IntegerColumnType() to user.getId(),
                    IntegerColumnType() to schematicId,
                    IntegerColumnType() to schematicId,
                ), fieldIndex
            )

        @JvmStatic
        fun byParentName(user: SteamwarUser, schematicId: Int?, name: String) =
            fromSql(
                "SELECT SchematicNode.NodeId, NodeOwner, NodeName, NM.ParentId AS ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, Config FROM SchematicNode INNER JOIN NodeMember NM on SchematicNode.NodeId = NM.NodeId WHERE NM.ParentId <=> ? AND NM.UserId = ? AND SchematicNode.NodeName = ? UNION ALL SELECT SchematicNode.NodeId, NodeOwner, NodeName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, Config FROM SchematicNode WHERE ((? IS NULL AND ParentNode IS NULL AND NodeOwner = ?) OR (? IS NOT NULL AND ParentNode = ?)) AND NodeName = ?",
                listOf(
                    IntegerColumnType() to schematicId,
                    IntegerColumnType() to user.getId(),
                    VarCharColumnType() to name,
                    IntegerColumnType() to schematicId,
                    IntegerColumnType() to user.getId(),
                    IntegerColumnType() to schematicId,
                    IntegerColumnType() to schematicId,
                    VarCharColumnType() to name,
                ), fieldIndex
            ).firstOrNull()

        @JvmStatic
        fun accessibleByUserType(user: SteamwarUser, type: SchematicType) =
            fromSql(
                "WITH RECURSIVE Nodes AS (SELECT NodeId, ParentId as ParentNode FROM NodeMember WHERE UserId = ? UNION SELECT NodeId, ParentNode FROM SchematicNode WHERE NodeOwner = ?), RSN AS ( SELECT NodeId, ParentNode FROM Nodes UNION SELECT SN.NodeId, SN.ParentNode FROM SchematicNode SN, RSN WHERE SN.ParentNode = RSN.NodeId ) SELECT SN.*, ? AS EffectiveOwner FROM RSN INNER JOIN SchematicNode SN ON RSN.NodeId = SN.NodeId WHERE NodeType = ?",
                listOf(
                    IntegerColumnType() to user.getId(),
                    IntegerColumnType() to user.getId(),
                    IntegerColumnType() to user.getId(),
                    VarCharColumnType() to type.toDB(),
                ), fieldIndex
            )

        @JvmStatic
        fun accessibleByUserTypeMap(user: SteamwarUser, type: SchematicType) =
            accessibleByUserType(user, type).mapToIds()

        @JvmStatic
        fun schematicAccessibleForUser(user: SteamwarUser, schematicId: Int?) =
            fromSql(
                "WITH RECURSIVE Nodes AS (SELECT NodeId, ParentId as ParentNode FROM NodeMember WHERE UserId = ? UNION SELECT NodeId, ParentNode FROM SchematicNode WHERE NodeOwner = ?), RSN AS ( SELECT NodeId, ParentNode FROM Nodes UNION SELECT SN.NodeId, SN.ParentNode FROM SchematicNode SN, RSN WHERE SN.ParentNode = RSN.NodeId ) SELECT SN.*, ? AS EffectiveOwner FROM RSN INNER JOIN SchematicNode SN ON RSN.NodeId = SN.NodeId WHERE SN.NodeId = ?",
                listOf(
                    IntegerColumnType() to user.getId(),
                    IntegerColumnType() to user.getId(),
                    IntegerColumnType() to user.getId(),
                    IntegerColumnType() to schematicId,
                ),
                fieldIndex
            ).isNotEmpty()

        @JvmStatic
        fun accessibleByUserTypeParent(user: SteamwarUser, type: SchematicType, parentId: Int?) =
            fromSql(
                "WITH RECURSIVE RSASN AS(WITH RECURSIVE RSAN AS (WITH RSANH AS (WITH RECURSIVE RSA AS (SELECT SN.NodeId, NM.ParentId FROM SchematicNode SN LEFT JOIN NodeMember NM on SN.NodeId = NM.NodeId WHERE NM.UserId = ? UNION SELECT SN.NodeId, SN.ParentNode FROM SchematicNode SN INNER JOIN RSA ON RSA.NodeId = SN.ParentNode) SELECT * FROM RSA UNION SELECT NodeId, ParentNode FROM SchematicNode WHERE NodeOwner = ?) SELECT * FROM RSANH UNION SELECT SN.NodeId, SN.ParentNode FROM RSANH JOIN SchematicNode SN ON SN.ParentNode = RSANH.NodeId) SELECT RSAN.NodeId, RSAN.ParentId FROM RSAN JOIN SchematicNode SN ON SN.NodeId = RSAN.NodeId WHERE NodeType = ? UNION SELECT SN.NodeId, SN.ParentNode FROM SchematicNode SN JOIN RSASN ON SN.NodeId = RSASN.ParentId) SELECT SN.*, ? as EffectiveOwner, RSASN.ParentId AS ParentNode FROM RSASN JOIN SchematicNode SN ON SN.NodeId = RSASN.NodeId WHERE RSASN.ParentId <=> ? ORDER BY NodeName",
                listOf(
                    IntegerColumnType() to user.getId(),
                    IntegerColumnType() to user.getId(),
                    VarCharColumnType() to type.toDB(),
                    IntegerColumnType() to user.getId(),
                    IntegerColumnType() to parentId,
                ), fieldIndex
            )

        @JvmStatic
        @Deprecated("Use byId")
        fun byIdAndUser(ignored: SteamwarUser, id: Int) =
            useDb {
                findById(id)
            }

        @JvmStatic
        fun parentsOfNode(user: SteamwarUser, id: Int) =
            fromSql(
                """
            WITH RECURSIVE
                ESN_R AS (
                    SELECT SchematicNode.NodeId AS NodeId,
                           NM.UserId AS EffectiveOwner,
                           NM.ParentId AS ParentNode
                    FROM SchematicNode
                    INNER JOIN NodeMember NM ON NM.NodeId = SchematicNode.NodeId
                    UNION ALL
                    SELECT S.NodeId AS NodeId,
                           ESN_R.EffectiveOwner AS EffectiveOwner,
                           S.ParentNode AS ParentNode
                    FROM SchematicNode S
                    INNER JOIN ESN_R ON S.ParentNode = ESN_R.NodeId
                ),
                ESN_R2 AS (
                    SELECT SchematicNode.NodeId AS NodeId,
                           NM.UserId AS UserId,
                           SchematicNode.NodeOwner AS EffectiveOwner,
                           SchematicNode.ParentNode AS ParentNode
                    FROM SchematicNode
                    INNER JOIN NodeMember NM ON NM.NodeId = SchematicNode.NodeId
                    UNION ALL
                    SELECT S.NodeId AS NodeId,
                           ESN_R2.EffectiveOwner AS EffectiveOwner,
                           ESN_R2.UserId AS UserId,
                           S.ParentNode AS ParentNode
                    FROM SchematicNode S
                    INNER JOIN ESN_R2 ON S.ParentNode = ESN_R2.NodeId
                    WHERE S.NodeOwner <> ESN_R2.EffectiveOwner
                ),
                ESN_R3 AS (
                    SELECT SchematicNode.NodeId AS NodeId,
                           SchematicNode.NodeOwner AS NodeOwner,
                           SchematicNode.NodeOwner AS EffectiveOwner,
                           SchematicNode.ParentNode AS ParentNode
                    FROM SchematicNode
                    UNION ALL
                    SELECT S.NodeId AS NodeId,
                           S.NodeOwner AS NodeOwner,
                           ESN_R3.EffectiveOwner AS EffectiveOwner,
                           S.ParentNode AS ParentNode
                    FROM SchematicNode S
                    INNER JOIN ESN_R3 ON S.ParentNode = ESN_R3.NodeId
                    WHERE ESN_R3.NodeOwner <> S.NodeOwner
                ),
                ResolvedNodes AS (
                    SELECT ESN_R.NodeId AS NodeId,
                           ESN_R.EffectiveOwner AS EffectiveOwner,
                           ESN_R.ParentNode AS ParentNode
                    FROM ESN_R
                    UNION
                    SELECT ESN_R2.NodeId AS NodeId,
                           ESN_R2.UserId AS EffectiveOwner,
                           ESN_R2.ParentNode AS ParentNode
                    FROM ESN_R2
                    INNER JOIN SchematicNode SN2 ON ESN_R2.NodeId = SN2.NodeId
                    WHERE ESN_R2.ParentNode IS NOT NULL
                      AND SN2.NodeOwner <> ESN_R2.EffectiveOwner
                    UNION
                    SELECT ESN_R3.NodeId AS NodeId,
                           ESN_R3.EffectiveOwner AS EffectiveOwner,
                           ESN_R3.ParentNode AS ParentNode
                    FROM ESN_R3
                    WHERE ESN_R3.NodeOwner <> ESN_R3.EffectiveOwner
                    UNION
                    SELECT SchematicNode.NodeId AS NodeId,
                           SchematicNode.NodeOwner AS EffectiveOwner,
                           SchematicNode.ParentNode AS ParentNode
                    FROM SchematicNode
                ),
                R AS (
                    SELECT NodeId, ParentNode
                    FROM ResolvedNodes
                    WHERE NodeId = ?
                    UNION
                    SELECT E.NodeId, E.ParentNode
                    FROM R
                    INNER JOIN ResolvedNodes E ON R.ParentNode = E.NodeId
                    WHERE E.EffectiveOwner = ?
                )
            SELECT SN.NodeId, SN.NodeOwner, SN.NodeName, R.ParentNode, SN.LastUpdate, SN.NodeItem, SN.NodeType, SN.NodeRank, SN.Config
            FROM R
            INNER JOIN SchematicNode SN ON SN.NodeId = R.NodeId
            """.trimIndent(),
                listOf(
                    IntegerColumnType() to id,
                    IntegerColumnType() to user.getId()
                ), fieldIndex
            )

        @JvmStatic
        fun createSchematic(owner: Int, name: String, parent: Int?) =
            createSchematicNode(
                owner, name, parent,
                SchematicType.Normal.toDB(), ""
            )

        @JvmStatic
        fun createSchematicDirectory(owner: Int, name: String, parent: Int?) =
            createSchematicNode(owner, name, parent, null, "")

        @JvmStatic
        fun createSchematicNode(owner: Int, name: String, parent: Int?, type: String?, item: String) =
            useDb {
                val id = SchematicNodeTable.insertAndGetId {
                    it[this.owner] = EntityID(owner, SteamwarUserTable)
                    it[this.name] = name
                    it[this.parent] =
                        parent?.let { p -> if (p == 0) null else p }?.let { p -> EntityID(p, SchematicNodeTable) }
                    it[this.item] = item
                    it[this.type] = type
                }
                return@useDb findById(id)
                    ?: throw IllegalStateException("SchematicNode $id not found")
            }

        @JvmStatic
        fun byId(id: Int) =
            useDb { findById(id) }

        @JvmStatic
        fun getSchematicNode(owner: Int, name: String, parent: Int?) =
            useDb {
                find { (SchematicNodeTable.owner eq owner) and (SchematicNodeTable.name eq name) and (SchematicNodeTable.parent eq parent) }.firstOrNull()
            }

        @JvmStatic
        fun getSchematicNode(owner: Int, name: String, parent: SchematicNode) =
            getSchematicNode(owner, name, parent.nodeId)

        @JvmStatic
        fun getSchematicNodeInNode(parent: Int?) =
            useDb {
                find { (SchematicNodeTable.parent eq parent) }.orderBy(SchematicNodeTable.name to SortOrder.ASC).toList()
            }

        @JvmStatic
        fun getSchematicNodeInNode(parent: SchematicNode) =
            getSchematicNodeInNode(parent.nodeId)

        @JvmStatic
        fun getSchematicNode(name: String, parent: Int?) =
            useDb {
                find { (SchematicNodeTable.name eq name) and (SchematicNodeTable.parent eq parent) }.firstOrNull()
            }

        @JvmStatic
        fun getSchematicNode(id: Int) =
            byId(id)

        @JvmStatic
        fun getAllAccessibleSchematicsOfType(user: Int, type: String) =
            accessibleByUserType(SteamwarUser.byId(user)!!, SchematicType.fromDB(type)!!)

        @JvmStatic
        fun getAllSchematicsAccessibleByUser(user: Int) =
            getAll(SteamwarUser.byId(user)!!)

        @JvmStatic
        fun getAllSchematicsOfType(owner: Int, type: String) =
            useDb {
                find { (SchematicNodeTable.owner eq owner) and (SchematicNodeTable.type eq type) }.orderBy(
                    SchematicNodeTable.name to SortOrder.ASC
                ).toList()
            }

        @JvmStatic
        fun getAllSchematicsOfType(type: SchematicType) =
            useDb {
                find { (SchematicNodeTable.type eq type.toDB()) }.orderBy(SchematicNodeTable.name to SortOrder.ASC).toList()
            }

        @JvmStatic
        fun deepGet(parent: Int?, filter: (node: SchematicNode) -> Boolean): List<SchematicNode> =
            getSchematicNodeInNode(parent)
                .flatMap {
                    return@flatMap if (it.isDir()) {
                        deepGet(it.nodeId, filter)
                    } else {
                        if (filter(it)) listOf(it) else listOf()
                    }
                }

        @JvmStatic
        fun getNodeFromPath(user: SteamwarUser, path: String): SchematicNode? {
            var s = path
            if (s.startsWith("/")) {
                s = s.drop(1)
            }
            if (s.endsWith("/")) {
                s = s.dropLast(1)
            }
            if (s.isEmpty()) return null
            if (s.contains("/")) {
                val layers: Array<String?> = s.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
                var currentNode = byParentName(user, null, layers[0]!!)
                for (i in 1..<layers.size) {
                    val node = currentNode?.let { n -> byParentName(user, n.getId(), layers[i]!!) }
                    if (node == null) {
                        return null
                    } else {
                        currentNode = node
                        if (!currentNode.isDir() && i != layers.size - 1
                        ) {
                            return null
                        }
                    }
                }
                return currentNode
            } else {
                return byParentName(user, null, s)
            }
        }

        @JvmStatic
        fun invalidSchemName(layers: Array<String>) =
            layers.any {
                it.isEmpty() || FORBIDDEN_CHARS.any { c -> c in it } || FORBIDDEN_NAMES.any { n -> n == it.lowercase() }
            }

        @JvmStatic
        fun getNodeTabcomplete(user: SteamwarUser, s: String): List<String> {
            var s = s
            val sws = s.startsWith("/")
            if (sws) {
                s = s.substring(1)
            }
            val index = s.lastIndexOf("/")
            val cacheKey = if (index == -1) "" else s.take(index)
            tabCache[user.getId()]?.get(cacheKey)?.also { return it.toMutableList() }

            val list = mutableListOf<String>()
            if (s.contains("/")) {
                val preTab = s.take(s.lastIndexOf("/") + 1)
                val pa = getNodeFromPath(user, preTab)
                    ?: return mutableListOf()
                val nodes: List<SchematicNode> = list(user, pa.getId())
                val br = pa.generateBreadcrumbs(user)
                nodes.forEach(Consumer { node: SchematicNode? -> list.add((if (sws) "/" else "") + br + node!!.name + (if (node.isDir()) "/" else "")) })
            } else {
                val nodes: List<SchematicNode?> = list(user, null)
                nodes.forEach(Consumer { node: SchematicNode? -> list.add((if (sws) "/" else "") + node!!.name + (if (node.isDir()) "/" else "")) })
            }
            list.remove("//copy")
            tabCache.computeIfAbsent(user.getId()) { i -> mutableMapOf() }.putIfAbsent(cacheKey, list)
            return list
        }
    }

    val nodeId by SchematicNodeTable.id.transform({ EntityID(it, SchematicNodeTable) }, { it.value })
    val nodeOwner by SchematicNodeTable.owner
    val owner: Int get() = nodeOwner.value
    private var nodeName by SchematicNodeTable.name
    var name: String
        get() = nodeName
        set(value) = useDb {
            nodeName = value
        }
    private var parentNodeId by SchematicNodeTable.parent
    var parent: Int?
        get() = parentNodeId?.value
        set(value) = useDb {
            parentNodeId = value?.let { EntityID(it, SchematicNodeTable) }
        }
    val parentNode: SchematicNode?
        get() = useDb { parent?.let { findById(it) } }

    val optionalParent: Optional<Int> get() = Optional.ofNullable(parent)
    var lastUpdate by SchematicNodeTable.lastUpdate.transform({ it.toInstant() }, { Timestamp.from(it) })
    private var nodeItem by SchematicNodeTable.item
    var item: String
        get() = nodeItem.ifEmpty {
            if (isDir()) "CHEST" else "CAULDRON"
        }
        set(value) = useDb {
            nodeItem = value
        }
    private var nodeType by SchematicNodeTable.type
    var schemtype: SchematicType
        get() = checkDir {
            SchematicType.fromDB(nodeType!!)
                ?: SchematicType.Normal
        }
        set(value) = checkDir { useDb { nodeType = value.toDB() } }
    var config by SchematicNodeTable.config

    val members: Set<NodeMember> by lazy { NodeMember.getNodeMembers(nodeId) }
    private lateinit var breadcrumbs: String

    fun getFileEnding(): String =
        checkDir { NodeData.getLatest(this)!!.nodeFormat.fileEnding }

    fun getId() =
        nodeId

    fun isDir() =
        nodeType == null

    private fun <T> checkDir(block: () -> T): T {
        if (isDir()) {
            throw IllegalStateException("Node is a directory")
        }

        return block()
    }

    fun replaceColor() =
        getConfig(ConfigFlags.REPLACE_COLOR)

    fun setReplaceColor(value: Boolean) =
        setConfig(ConfigFlags.REPLACE_COLOR, value)

    fun allowReplay() =
        getConfig(ConfigFlags.ALLOW_REPLAY)

    fun setAllowReplay(value: Boolean) =
        setConfig(ConfigFlags.ALLOW_REPLAY, value)

    fun isPrepared() =
        getConfig(ConfigFlags.IS_PREPARED)

    fun setPrepared(value: Boolean) =
        setConfig(ConfigFlags.IS_PREPARED, value)

    fun getConfig(flag: ConfigFlags) =
        config and (1 shl flag.ordinal) != 0

    fun setConfig(flag: ConfigFlags, value: Boolean) =
        useDb {
            config = if (value) {
                config or (1 shl flag.ordinal)
            } else {
                config and (1 shl flag.ordinal).inv()
            }
        }

    override fun delete() =
        useDb {
            super.delete()
        }

    override fun hashCode() =
        nodeId

    override fun equals(other: Any?): Boolean {
        if (other !is SchematicNode) return false
        return nodeId == other.nodeId
    }

    fun generateBreadcrumbs(user: SteamwarUser): String {
        if (!this::breadcrumbs.isInitialized) {
            breadcrumbs = generateBreadcrumbs("/", user)
        }
        return breadcrumbs
    }

    fun generateBreadcrumbs(split: String, user: SteamwarUser): String =
        useDb {
            val builder: StringBuilder = StringBuilder(name)
            if (isDir()) {
                builder.append(split)
            }
            var currentNode: SchematicNode? = this@SchematicNode
            while (currentNode != null) {
                currentNode = currentNode
                    .let {
                        NodeMember.getNodeMember(it.nodeId, user.getId())
                            ?.parent?.orElse(null)
                            ?: it.parent
                    }
                    ?.let { findById(it) }
                    ?.also {
                        builder.insert(0, split).insert(0, it.name)
                    }
            }
            return@useDb builder.toString()
        }

    fun generateBreadcrumbsMap(user: SteamwarUser): List<Pair<String, Int>> =
        useDb {
            val map = mutableListOf<Pair<String, Int>>()
            var currentNode: SchematicNode? = this@SchematicNode
            while (currentNode != null) {
                currentNode = currentNode
                    .also {
                        map.add(0, Pair(it.name, it.nodeId))
                    }
                    .let {
                        NodeMember.getNodeMember(it.nodeId, user.getId())
                            ?.parent?.orElse(null)
                            ?: it.parent
                    }
                    ?.let { findById(it) }
            }
            return@useDb map
        }

    fun accessibleByUser(user: SteamwarUser) =
        schematicAccessibleForUser(user, nodeId)

    enum class ConfigFlags {
        REPLACE_COLOR,
        ALLOW_REPLAY,
        IS_PREPARED
    }

    @Deprecated("Removed")
    var rank = 0
}
