/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2025 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.*;
import lombok.Getter;

import java.sql.Timestamp;
import java.time.Instant;
import java.util.*;
import java.util.function.Predicate;

public class SchematicNode {

    static {
        SchematicType.Normal.name(); // Ensure SchematicType is loaded.
        new SqlTypeMapper<>(SchematicNode.class, null, (rs, identifier) -> {
            throw new SecurityException("SchematicNode cannot be used as type (recursive select)");
        }, (st, index, value) -> st.setInt(index, value.nodeId));
    }

    private static final Map<Integer, Map<String, List<String>>> TAB_CACHE = new HashMap<>();

    public static void clear() {
        TAB_CACHE.clear();
    }

    private static final String nodeSelector = "SELECT NodeId, NodeOwner, NodeOwner AS EffectiveOwner, NodeName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, Config FROM SchematicNode ";

    private static final Table<SchematicNode> table = new Table<>(SchematicNode.class);
    private static final Statement create = table.insertFields(true, "NodeOwner", "NodeName", "ParentNode", "NodeItem",
            "NodeType");
    private static final Statement update = table.update(Table.PRIMARY, "NodeName", "ParentNode", "NodeItem",
            "NodeType", "NodeRank", "Config");
    private static final Statement delete = table.delete(Table.PRIMARY);

    private static final SelectStatement<SchematicNode> byId = new SelectStatement<>(table,
            nodeSelector + "WHERE NodeId = ?");
    private static final SelectStatement<SchematicNode> byOwnerNameParent = new SelectStatement<>(table,
            nodeSelector + "WHERE NodeOwner = ? AND NodeName = ? AND ParentNode " + Statement.NULL_SAFE_EQUALS + "?");
    private static final SelectStatement<SchematicNode> byParent = new SelectStatement<>(table,
            nodeSelector + "WHERE ParentNode" + Statement.NULL_SAFE_EQUALS + "? ORDER BY NodeName");
    private static final SelectStatement<SchematicNode> dirsByParent = new SelectStatement<>(table, nodeSelector
            + "WHERE ParentNode" + Statement.NULL_SAFE_EQUALS + "? AND NodeType is NULL ORDER BY NodeName");
    private static final SelectStatement<SchematicNode> byOwnerType = new SelectStatement<>(table,
            nodeSelector + "WHERE NodeOwner = ? AND NodeType = ? ORDER BY NodeName");
    private static final SelectStatement<SchematicNode> byType = new SelectStatement<>(table,
            nodeSelector + "WHERE NodeType = ? ORDER BY NodeName");
    private static final SelectStatement<SchematicNode> all = new SelectStatement<>(table,
            "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");
    private static final SelectStatement<SchematicNode> list = new SelectStatement<>(table,
            "SELECT SchematicNode.NodeId, NodeOwner, ? AS EffectiveOwner, NodeName, NM.ParentId AS ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, Config FROM SchematicNode INNER JOIN NodeMember NM on SchematicNode.NodeId = NM.NodeId WHERE NM.ParentId "
                    + Statement.NULL_SAFE_EQUALS
                    + "? AND NM.UserId = ? UNION ALL SELECT SchematicNode.NodeId, NodeOwner, ? AS EffectiveOwner, 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");
    private static final SelectStatement<SchematicNode> byParentName = new SelectStatement<>(table,
            "SELECT SchematicNode.NodeId, NodeOwner, ? AS EffectiveOwner, NodeName, NM.ParentId AS ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, Config FROM SchematicNode INNER JOIN NodeMember NM on SchematicNode.NodeId = NM.NodeId WHERE NM.ParentId "
                    + Statement.NULL_SAFE_EQUALS
                    + "? AND NM.UserId = ? AND SchematicNode.NodeName = ? UNION ALL SELECT SchematicNode.NodeId, NodeOwner, ? AS EffectiveOwner, 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 = ?");
    private static final SelectStatement<SchematicNode> schematicAccessibleForUser = new SelectStatement<>(table,
            "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 NodeId = ?");
    private static final SelectStatement<SchematicNode> accessibleByUserTypeInParent = new SelectStatement<>(table,
            "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"
                    + Statement.NULL_SAFE_EQUALS + "? ORDER BY NodeName");
    private static final SelectStatement<SchematicNode> accessibleByUserType = new SelectStatement<>(table,
            "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 = ?");
    private static final SelectStatement<SchematicNode> byIdAndUser = new SelectStatement<>(table,
            "SELECT NodeId, NodeOwner, ? AS EffectiveOwner, NodeName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, Config FROM SchematicNode WHERE NodeId = ?");
    private static final SelectStatement<SchematicNode> allParentsOfNode = new SelectStatement<>(table,
            "WITH RECURSIVE R AS (SELECT NodeId, ParentNode FROM EffectiveSchematicNode WHERE NodeId = ? AND EffectiveOwner = ? UNION SELECT E.NodeId, E.ParentNode FROM R, EffectiveSchematicNode E WHERE R.ParentNode = E.NodeId AND E.EffectiveOwner = ?) SELECT SN.NodeId, SN.NodeOwner, ? AS EffectiveOwner, 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");

    static {
        NodeMember.init();
    }

    @Field(keys = { Table.PRIMARY }, autoincrement = true)
    private final int nodeId;
    @Field(keys = { "OwnerNameParent" })
    private final int nodeOwner;
    @Field(def = "0")
    @Getter
    private final int effectiveOwner;
    @Field(keys = { "OwnerNameParent" })
    private String nodeName;
    @Field(keys = { "OwnerNameParent" }, nullable = true)
    private Integer parentNode;
    @Field(def = "CURRENT_TIMESTAMP")
    private Timestamp lastUpdate;
    @Field(def = "''")
    private String nodeItem;
    @Field(def = "'normal'", nullable = true)
    private SchematicType nodeType;
    @Field(def = "0")
    private int nodeRank;
    @Field
    private int config;

    private String brCache;

    public SchematicNode(
            int nodeId,
            int nodeOwner,
            int effectiveOwner,
            String nodeName,
            Integer parentNode,
            Timestamp lastUpdate,
            String nodeItem,
            SchematicType nodeType,
            int nodeRank,
            int config) {
        this.nodeId = nodeId;
        this.nodeOwner = nodeOwner;
        this.effectiveOwner = effectiveOwner;
        this.nodeName = nodeName;
        this.parentNode = parentNode;
        this.nodeItem = nodeItem;
        this.nodeType = nodeType;
        this.lastUpdate = lastUpdate;
        this.nodeRank = nodeRank;
        this.config = config;
    }

    public static List<SchematicNode> getAll(SteamwarUser user) {
        return all.listSelect(user, user, user);
    }

    public static Map<Integer, List<SchematicNode>> getAllMap(SteamwarUser user) {
        return map(getAll(user));
    }

    public static List<SchematicNode> list(SteamwarUser user, Integer schematicId) {
        return list.listSelect(user, schematicId, user, user, schematicId, user, schematicId, schematicId);
    }

    public static SchematicNode byParentName(SteamwarUser user, Integer schematicId, String name) {
        return byParentName.select(user, schematicId, user, name, user, schematicId, user, schematicId, schematicId,
                name);
    }

    public static List<SchematicNode> accessibleByUserType(SteamwarUser user, SchematicType type) {
        return accessibleByUserType.listSelect(user, user, user, type);
    }

    public static Map<Integer, List<SchematicNode>> accessibleByUserTypeMap(SteamwarUser user, SchematicType type) {
        return map(accessibleByUserType(user, type));
    }

    public static boolean schematicAccessibleForUser(SteamwarUser user, Integer schematicId) {
        return schematicAccessibleForUser.select(user, user, user, schematicId) != null;
    }

    public static List<SchematicNode> accessibleByUserTypeParent(SteamwarUser user, SchematicType type,
            Integer parentId) {
        return accessibleByUserTypeInParent.listSelect(user, user, type, user, parentId);
    }

    public static SchematicNode byIdAndUser(SteamwarUser user, Integer id) {
        return byIdAndUser.select(user, id);
    }

    public static List<SchematicNode> parentsOfNode(SteamwarUser user, Integer id) {
        return allParentsOfNode.listSelect(id, user, user, user);
    }

    private static Map<Integer, List<SchematicNode>> map(List<SchematicNode> in) {
        Map<Integer, List<SchematicNode>> map = new HashMap<>();
        for (SchematicNode effectiveSchematicNode : in) {
            map.computeIfAbsent(effectiveSchematicNode.getOptionalParent().orElse(0), k -> new ArrayList<>())
                    .add(effectiveSchematicNode);
        }
        return map;
    }

    public static SchematicNode createSchematic(int owner, String name, Integer parent) {
        return createSchematicNode(owner, name, parent, SchematicType.Normal.toDB(), "");
    }

    public static SchematicNode createSchematicDirectory(int owner, String name, Integer parent) {
        return createSchematicNode(owner, name, parent, null, "");
    }

    public static SchematicNode createSchematicNode(int owner, String name, Integer parent, String type, String item) {
        if (parent != null && parent == 0)
            parent = null;
        int nodeId = create.insertGetKey(owner, name, parent, item, type);
        return getSchematicNode(nodeId);
    }

    public static SchematicNode getSchematicNode(int owner, String name, SchematicNode parent) {
        return getSchematicNode(owner, name, parent.getId());
    }

    public static SchematicNode getSchematicNode(int owner, String name, Integer parent) {
        return byOwnerNameParent.select(owner, name, parent);
    }

    public static List<SchematicNode> getSchematicNodeInNode(SchematicNode parent) {
        return getSchematicNodeInNode(parent.getId());
    }

    public static List<SchematicNode> getSchematicNodeInNode(Integer parent) {
        return byParent.listSelect(parent);
    }

    public static List<SchematicNode> getSchematicDirectoryInNode(Integer parent) {
        return dirsByParent.listSelect(parent);
    }

    @Deprecated
    public static SchematicNode getSchematicDirectory(String name, SchematicNode parent) {
        return getSchematicNode(name, parent.getId());
    }

    @Deprecated
    public static SchematicNode getSchematicDirectory(String name, Integer parent) {
        return getSchematicNode(name, parent);
    }

    public static SchematicNode getSchematicNode(String name, Integer parent) {
        return byParentName.select(name, parent);
    }

    public static SchematicNode getSchematicNode(int id) {
        return byId.select(id);
    }

    public static List<SchematicNode> getAccessibleSchematicsOfTypeInParent(int owner, String schemType,
            Integer parent) {
        return accessibleByUserTypeParent(SteamwarUser.get(owner), SchematicType.fromDB(schemType), parent);
    }

    public static List<SchematicNode> getAllAccessibleSchematicsOfType(int user, String schemType) {
        return accessibleByUserType(SteamwarUser.get(user), SchematicType.fromDB(schemType));
    }

    public static List<SchematicNode> getAllSchematicsOfType(int owner, String schemType) {
        return byOwnerType.listSelect(owner, schemType);
    }

    @Deprecated
    public static List<SchematicNode> getAllSchematicsOfType(String schemType) {
        return byType.listSelect(schemType);
    }

    public static List<SchematicNode> getAllSchematicsOfType(SchematicType schemType) {
        return byType.listSelect(schemType);
    }

    public static List<SchematicNode> deepGet(Integer parent, Predicate<SchematicNode> filter) {
        List<SchematicNode> finalList = new ArrayList<>();
        List<SchematicNode> nodes = SchematicNode.getSchematicNodeInNode(parent);
        nodes.forEach(node -> {
            if (node.isDir()) {
                finalList.addAll(deepGet(node.getId(), filter));
            } else {
                if (filter.test(node))
                    finalList.add(node);
            }
        });
        return finalList;
    }

    @Deprecated
    public static List<SchematicNode> getSchematicsAccessibleByUser(int user, Integer parent) {
        return list(SteamwarUser.get(user), parent);
    }

    @Deprecated
    public static List<SchematicNode> getAllSchematicsAccessibleByUser(int user) {
        return getAll(SteamwarUser.get(user));
    }

    public static List<SchematicNode> getAllParentsOfNode(SchematicNode node) {
        return getAllParentsOfNode(node.getId());
    }

    public static List<SchematicNode> getAllParentsOfNode(int node) {
        return allParentsOfNode.listSelect(node);
    }

    public static SchematicNode getNodeFromPath(SteamwarUser user, String s) {
        if (s.startsWith("/")) {
            s = s.substring(1);
        }
        if (s.endsWith("/")) {
            s = s.substring(0, s.length() - 1);
        }
        if (s.isEmpty()) {
            return null;
        }
        if (s.contains("/")) {
            String[] layers = s.split("/");
            Optional<SchematicNode> currentNode = Optional
                    .ofNullable(SchematicNode.byParentName(user, null, layers[0]));
            for (int i = 1; i < layers.length; i++) {
                int finalI = i;
                Optional<SchematicNode> node = currentNode.map(effectiveSchematicNode -> SchematicNode
                        .byParentName(user, effectiveSchematicNode.getId(), layers[finalI]));
                if (!node.isPresent()) {
                    return null;
                } else {
                    currentNode = node;
                    if (!currentNode.map(SchematicNode::isDir).orElse(false) && i != layers.length - 1) {
                        return null;
                    }
                }
            }
            return currentNode.orElse(null);
        } else {
            return SchematicNode.byParentName(user, null, s);
        }
    }

    public static List<SchematicNode> filterSchems(int user, Predicate<SchematicNode> filter) {
        List<SchematicNode> finalList = new ArrayList<>();
        List<SchematicNode> nodes = getSchematicsAccessibleByUser(user, null);
        nodes.forEach(node -> {
            if (node.isDir()) {
                finalList.addAll(deepGet(node.getId(), filter));
            } else {
                if (filter.test(node))
                    finalList.add(node);
            }
        });
        return finalList;
    }

    public int getId() {
        return nodeId;
    }

    public int getOwner() {
        return nodeOwner;
    }

    public String getName() {
        return nodeName;
    }

    public void setName(String name) {
        this.nodeName = name;
        updateDB();
    }

    public Integer getParent() {
        return parentNode;
    }

    public Optional<Integer> getOptionalParent() {
        return Optional.ofNullable(parentNode);
    }

    public void setParent(Integer parent) {
        this.parentNode = parent;
        updateDB();
    }

    public String getItem() {
        if (nodeItem.isEmpty()) {
            return isDir() ? "CHEST" : "CAULDRON_ITEM";
        }
        return nodeItem;
    }

    public void setItem(String item) {
        this.nodeItem = item;
        updateDB();
    }

    @Deprecated
    public String getType() {
        return nodeType.name();
    }

    @Deprecated
    public void setType(String type) {
        if (isDir())
            throw new SecurityException("Node is Directory");
        this.nodeType = SchematicType.fromDB(type);
        updateDB();
    }

    public boolean isDir() {
        return nodeType == null;
    }

    public String getFileEnding() {
        if (isDir())
            throw new SecurityException("Node is Directory");
        return NodeData.getLatest(this).getNodeFormat().getFileEnding();
    }

    public int getRank() {
        if (isDir())
            throw new SecurityException("Node is Directory");
        return nodeRank;
    }

    @Deprecated
    public int getRankUnsafe() {
        return nodeRank;
    }

    public void setRank(int rank) {
        if (isDir())
            throw new SecurityException("Node is Directory");
        this.nodeRank = rank;
    }

    public SchematicType getSchemtype() {
        if (isDir())
            throw new SecurityException("Is Directory");
        return nodeType;
    }

    public void setSchemtype(SchematicType type) {
        if (isDir())
            throw new SecurityException("Is Directory");
        this.nodeType = type;
        updateDB();
    }

    public boolean replaceColor() {
        return getConfig(ConfigFlags.REPLACE_COLOR);
    }

    public void setReplaceColor(boolean replaceColor) {
        if (isDir())
            throw new SecurityException("Is Directory");
        setConfig(ConfigFlags.REPLACE_COLOR, replaceColor);
    }

    public boolean allowReplay() {
        return getConfig(ConfigFlags.ALLOW_REPLAY);
    }

    public void setAllowReplay(boolean allowReplay) {
        if (isDir())
            throw new SecurityException("Is Directory");
        setConfig(ConfigFlags.ALLOW_REPLAY, allowReplay);
    }

    public boolean isPrepared() {
        return getConfig(ConfigFlags.IS_PREPARED);
    }

    public void setPrepared(boolean prepared) {
        if (isDir())
            throw new SecurityException("Is Directory");
        setConfig(ConfigFlags.IS_PREPARED, prepared);
    }

    public boolean getConfig(ConfigFlags flag) {
        return (config & (1 << flag.ordinal())) != 0;
    }

    public void setConfig(ConfigFlags flag, boolean value) {
        if (value) {
            config |= (1 << flag.ordinal());
        } else {
            config &= ~(1 << flag.ordinal());
        }
        updateDB();
    }

    public SchematicNode getParentNode() {
        if (parentNode == null)
            return null;
        return SchematicNode.getSchematicNode(parentNode);
    }

    public int getElo(int season) {
        return SchemElo.getElo(this, season);
    }

    public boolean accessibleByUser(SteamwarUser user) {
        return NodeMember.getNodeMember(nodeId, user) != null;
    }

    public Set<NodeMember> getMembers() {
        return NodeMember.getNodeMembers(nodeId);
    }

    public Timestamp getLastUpdate() {
        return lastUpdate;
    }

    private void updateDB() {
        this.lastUpdate = Timestamp.from(Instant.now());
        update.update(nodeName, parentNode, nodeItem, nodeType, nodeRank, config, nodeId);
        TAB_CACHE.clear();
    }

    public void delete() {
        delete.update(nodeId);
    }

    @Override
    public int hashCode() {
        return nodeId;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof SchematicNode))
            return false;

        return ((SchematicNode) obj).getId() == nodeId;
    }

    public String generateBreadcrumbs(SteamwarUser user) {
        return byIdAndUser(user, nodeId).generateBreadcrumbs();
    }

    public String generateBreadcrumbs(String split, SteamwarUser user) {
        return byIdAndUser(user, nodeId).generateBreadcrumbs(split);
    }

    public String generateBreadcrumbs() {
        if (brCache == null) {
            brCache = generateBreadcrumbs("/");
        }
        return brCache;
    }

    public String generateBreadcrumbs(String split) {
        StringBuilder builder = new StringBuilder(getName());
        Optional<SchematicNode> currentNode = Optional.of(this);
        if (currentNode.map(SchematicNode::isDir).orElse(false)) {
            builder.append(split);
        }
        while (currentNode.isPresent()) {
            currentNode = currentNode
                    .flatMap(schematicNode -> Optional
                            .ofNullable(NodeMember.getNodeMember(schematicNode.getId(), effectiveOwner))
                            .map(NodeMember::getParent).orElse(schematicNode.getOptionalParent()))
                    .map(SchematicNode::getSchematicNode);
            currentNode.ifPresent(node -> builder.insert(0, split).insert(0, node.getName()));
        }
        return builder.toString();
    }

    public List<Map.Entry<String, Integer>> generateBreadcrumbsMap(SteamwarUser user) {
        List<Map.Entry<String, Integer>> map = new ArrayList<>();
        Optional<SchematicNode> currentNode = Optional.of(this);
        if (currentNode.map(SchematicNode::isDir).orElse(false)) {
            map.add(new AbstractMap.SimpleEntry<>(getName(), getId()));
        }
        while (currentNode.isPresent()) {
            currentNode = currentNode
                    .flatMap(schematicNode -> Optional
                            .ofNullable(NodeMember.getNodeMember(schematicNode.getId(), effectiveOwner))
                            .map(NodeMember::getParent).orElse(schematicNode.getOptionalParent()))
                    .map(SchematicNode::getSchematicNode);
            currentNode.ifPresent(node -> map.add(0, new AbstractMap.SimpleEntry<>(node.getName(), node.getId())));
        }
        return map;
    }

    private static final List<String> FORBIDDEN_NAMES = Collections.unmodifiableList(Arrays.asList("public"));

    public static boolean invalidSchemName(String[] layers) {
        for (String layer : layers) {
            if (layer.isEmpty()) {
                return true;
            }
            if (layer.contains("/") ||
                    layer.contains("\\") ||
                    layer.contains("<") ||
                    layer.contains(">") ||
                    layer.contains("^") ||
                    layer.contains("°") ||
                    layer.contains("'") ||
                    layer.contains("\"") ||
                    layer.contains(" ")) {
                return true;
            }
            if (FORBIDDEN_NAMES.contains(layer.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    public static List<String> getNodeTabcomplete(SteamwarUser user, String s) {
        boolean sws = s.startsWith("/");
        if (sws) {
            s = s.substring(1);
        }
        int index = s.lastIndexOf("/");
        String cacheKey = index == -1 ? "" : s.substring(0, index);
        if (TAB_CACHE.containsKey(user.getId()) && TAB_CACHE.get(user.getId()).containsKey(cacheKey)) {
            return new ArrayList<>(TAB_CACHE.get(user.getId()).get(cacheKey));
        }
        List<String> list = new ArrayList<>();
        if (s.contains("/")) {
            String preTab = s.substring(0, s.lastIndexOf("/") + 1);
            SchematicNode pa = SchematicNode.getNodeFromPath(user, preTab);
            if (pa == null)
                return new ArrayList<>();
            List<SchematicNode> nodes = SchematicNode.list(user, pa.getId());
            String br = pa.generateBreadcrumbs();
            nodes.forEach(node -> list.add((sws ? "/" : "") + br + node.getName() + (node.isDir() ? "/" : "")));
        } else {
            List<SchematicNode> nodes = SchematicNode.list(user, null);
            nodes.forEach(node -> list.add((sws ? "/" : "") + node.getName() + (node.isDir() ? "/" : "")));
        }
        list.remove("//copy");
        TAB_CACHE.computeIfAbsent(user.getId(), integer -> new HashMap<>()).putIfAbsent(cacheKey, list);
        return list;
    }

    public static enum ConfigFlags {
        REPLACE_COLOR,
        ALLOW_REPLAY,
        IS_PREPARED
    }
}
