001/*
002 * PermissionsEx
003 * Copyright (C) zml and PermissionsEx contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package ca.stellardrift.permissionsex.context;
018
019import net.kyori.adventure.text.Component;
020import net.kyori.adventure.text.ComponentLike;
021import net.kyori.adventure.text.format.NamedTextColor;
022import net.kyori.adventure.text.format.TextColor;
023import net.kyori.adventure.text.format.TextDecoration;
024import org.checkerframework.checker.nullness.qual.NonNull;
025import org.checkerframework.checker.nullness.qual.Nullable;
026
027import static net.kyori.adventure.text.Component.text;
028import static net.kyori.adventure.text.format.Style.style;
029
030/**
031 * A (key, value) pair for one specific context entry.
032 *
033 * <p>This value holds both raw and parsed context values.</p>
034 *
035 * @param <V> value type
036 */
037public final class ContextValue<V> implements ComponentLike {
038    private static final Component JOINER = text("=");
039
040    private final String key;
041    private final String rawValue;
042    private @Nullable ContextDefinition<V> definition;
043    private @Nullable V parsedValue;
044
045    public ContextValue(final String key, final String rawValue) {
046        this.key = key;
047        this.rawValue = rawValue;
048    }
049
050    ContextValue(final ContextDefinition<V> def, final V value) {
051        this(def.name(), def.serialize(value));
052        this.definition = def;
053        this.parsedValue = value;
054    }
055
056    /**
057     * Get the key used to resolve a context value.
058     *
059     * @return value key
060     */
061    public String key() {
062        return this.key;
063    }
064
065    /**
066     * The raw value, before being deserialized by a context definition.
067     *
068     * @return raw value, as provided by the user
069     */
070    public String rawValue() {
071        return this.rawValue;
072    }
073
074    public @Nullable ContextDefinition<V> definition() {
075        return this.definition;
076    }
077
078    public @Nullable V parsedValue() {
079        return this.parsedValue;
080    }
081
082    @SuppressWarnings("unchecked")
083    public boolean tryResolve(final ContextDefinitionProvider provider) {
084        if (this.definition != null && !(this.definition instanceof SimpleContextDefinition.Fallback)) {
085            return this.parsedValue != null;
086        }
087        final @Nullable ContextDefinition<V> definition = (ContextDefinition<V>) provider.contextDefinition(this.key);
088        if (definition != null) {
089            this.definition = definition;
090            this.parsedValue = definition.deserialize(this.rawValue);
091            return this.parsedValue != null;
092        }
093        return false;
094    }
095
096    public V getParsedValue(final ContextDefinition<V> definition) {
097        if (this.definition != null && !this.definition.equals(definition)) {
098            throw new IllegalStateException("The provided context definition does not match the one this context object currently knows about");
099        }
100
101        this.definition = definition;
102        @Nullable V parsedValue = this.parsedValue;
103        if (parsedValue == null) {
104            parsedValue = definition.deserialize(this.rawValue);
105            this.parsedValue = parsedValue;
106        }
107        if (parsedValue == null) {
108            throw new IllegalArgumentException("Invalid value provided for context " + definition.name());
109        }
110        return parsedValue;
111    }
112
113    @SuppressWarnings("unchecked")
114    public V getParsedValue(final ContextDefinitionProvider provider) {
115        @Nullable V tempParsed = parsedValue;
116        if (tempParsed != null) {
117            return tempParsed;
118        }
119
120        final @Nullable ContextDefinition<?> def = provider.contextDefinition(this.key);
121        tempParsed = def == null ? null : (V) def.deserialize(this.rawValue);
122        if (tempParsed == null) {
123            throw new RuntimeException("No definition for context " + this.key);
124        }
125
126        parsedValue = tempParsed;
127        return tempParsed;
128    }
129
130    @Override
131    public boolean equals(final @Nullable Object other) {
132        if (this == other) return true;
133        if (!(other instanceof ContextValue<?>)) return false;
134
135        final ContextValue<?> that = (ContextValue<?>) other;
136        if (!this.key.equals(that.key)) return false;
137        if (!this.rawValue.equals(that.rawValue)) return false;
138
139        return true;
140    }
141
142    @Override
143    public int hashCode() {
144        int result = this.key.hashCode();
145        result = 31 * result + this.rawValue.hashCode();
146        return result;
147    }
148
149    @Override
150    public String toString() {
151        return this.key + ":" + this.parsedValue + " (raw: " + this.rawValue + ")";
152    }
153
154    @Override
155    public @NonNull Component asComponent() {
156        if (this.parsedValue != null) {
157            return text()
158                .content(this.key)
159                .append(JOINER)
160                .append(text(this.parsedValue.toString()))
161                .hoverEvent(text("(raw: " + this.rawValue + ")"))
162                .build();
163        } else {
164            return text()
165                .content(this.key)
166                .append(JOINER)
167                .append(text(this.rawValue))
168                .hoverEvent(text("(unresolved)", style(TextDecoration.BOLD, NamedTextColor.RED)))
169                .build();
170        }
171    }
172
173}