/*
 *   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.bausystem.region;

import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.sql.SchematicNode;
import lombok.NonNull;
import yapion.hierarchy.types.YAPIONObject;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public abstract class RegionData {

    private final List<Property<?, ?>> properties = new ArrayList<>();

    protected final YAPIONObject data;
    protected final YAPIONObject flagData;
    protected final Runnable onChange;
    protected final Map<Flag<?>, Flag.Value<?>> flagMap = new HashMap<>();

    private final class Property<T, K> {
        private final String field;
        private final Function<K, T> loader;
        private final Function<T, K> writer;

        private T value;

        public Property(String field, Function<K, T> loader, Function<T, K> writer) {
            this.field = field;
            this.loader = loader;
            this.writer = writer;
            properties.add(this);
        }

        public void load() {
            if (flagData.containsKey(field)) {
                value = loader.apply(flagData.getPlainValue(field));
            } else {
                value = null;
            }
        }

        public T get() {
            return value;
        }

        public void set(T value) {
            this.value = value;
            if (value == null) {
                flagData.remove(field);
            } else {
                flagData.put(field, writer.apply(value));
            }
        }
    }

    private Property<SchematicNode, Integer> testblockSchematic = new Property<>("testblockSchematic", SchematicNode::byId, SchematicNode::getId);

    protected RegionData(YAPIONObject data, Runnable onChange) {
        this.data = data;
        this.flagData = data.getObjectOrSetDefault("flagStorage", new YAPIONObject());
        this.onChange = onChange;
        initialize();
        for (final Flag flag : Flag.getFlags()) {
            if (!has(flag).isWritable()) continue;
            try {
                String s = flagData.getPlainValue(flag.name());
                flagMap.put(flag, flag.valueOfValue(s));
            } catch (Exception e) {
                flagMap.put(flag, (Flag.Value<?>) flag.getDefaultValue());
            }
        }
        properties.forEach(Property::load);
    }

    protected void initialize() {
    }

    @NonNull
    public abstract <T extends Enum<T> & Flag.Value<T>> RegionFlagPolicy has(@NonNull Flag<T> flag);

    /**
     * Returns true if the flag was changed and did not already contain the provided value
     */
    public final <T extends Enum<T> & Flag.Value<T>> boolean set(@NonNull Flag<T> flag, @NonNull T value) {
        if (has(flag).isWritable()) {
            if (flagMap.put(flag, value) != value) {
                flagData.put(flag.name(), value.name());
                onChange.run();
                return true;
            }
        }
        return false;
    }

    @NonNull
    public final <T extends Enum<T> & Flag.Value<T>> FlagOptional<T> get(@NonNull Flag<T> flag) {
        return FlagOptional.of(flag, (T) flagMap.get(flag));
    }

    public final void clear() {
        for (Flag flag : Flag.getFlags()) {
            if (has(flag).isWritable()) {
                flagMap.remove(flag);
                flagData.remove(flag.name());
            }
        }
        properties.forEach(property -> property.set(null));
        onChange.run();
    }

    public final Map<Flag<?>, Flag.Value<?>> getBackedMap() {
        return flagMap;
    }

    public SchematicNode getTestblockSchematic() {
        return testblockSchematic.get();
    }

    public void setTestblockSchematic(SchematicNode schematic) {
        testblockSchematic.set(schematic);
        onChange.run();
    }

    @Override
    public final String toString() {
        return getClass().getSimpleName() + "{" +
                "flagMap=" + flagMap +
                '}';
    }
}
