Asset.java

/*
 * Copyright 2020-2022 Foreseeti AB <https://foreseeti.com>
 *
 * 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 org.mal_lang.langspec;

import static java.util.Objects.requireNonNull;

import jakarta.json.Json;
import jakarta.json.JsonObject;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.mal_lang.langspec.builders.AssetBuilder;

/**
 * Immutable class representing an asset in a MAL language.
 *
 * @since 1.0.0
 */
public final class Asset {
  private final String name;
  private final Meta meta;
  private final Category category;
  private final boolean isAbstract;
  private Asset superAsset;
  private final Map<String, Field> fields = new LinkedHashMap<>();
  private final Map<String, Variable> variables = new LinkedHashMap<>();
  private final Map<String, AttackStep> attackSteps = new LinkedHashMap<>();
  private final byte[] svgIcon;
  private final byte[] pngIcon;

  private Asset(
      String name,
      Meta meta,
      Category category,
      boolean isAbstract,
      byte[] svgIcon,
      byte[] pngIcon) {
    this.name = requireNonNull(name);
    this.meta = requireNonNull(meta);
    this.category = requireNonNull(category);
    this.isAbstract = isAbstract;
    this.svgIcon = svgIcon == null ? null : svgIcon.clone();
    this.pngIcon = pngIcon == null ? null : pngIcon.clone();
    category.addAsset(this);
  }

  /**
   * Returns the name of this {@code Asset} object.
   *
   * @return the name of this {@code Asset} object
   * @since 1.0.0
   */
  public String getName() {
    return this.name;
  }

  /**
   * Returns the meta info of this {@code Asset} object.
   *
   * @return the meta info of this {@code Asset} object
   * @since 1.0.0
   */
  public Meta getMeta() {
    return this.meta;
  }

  /**
   * Returns the category of this {@code Asset} object.
   *
   * @return the category of this {@code Asset} object
   * @since 1.0.0
   */
  public Category getCategory() {
    return this.category;
  }

  /**
   * Returns whether this {@code Asset} object is abstract.
   *
   * @return whether this {@code Asset} object is abstract
   * @since 1.0.0
   */
  public boolean isAbstract() {
    return this.isAbstract;
  }

  /**
   * Returns whether this {@code Asset} object has a super asset.
   *
   * @return whether this {@code Asset} object has a super asset
   * @since 1.0.0
   */
  public boolean hasSuperAsset() {
    return this.superAsset != null;
  }

  /**
   * Returns the super asset of this {@code Asset} object.
   *
   * @return the super asset of this {@code Asset} object
   * @throws java.lang.UnsupportedOperationException if this {@code Asset} object does not have a
   *     super asset
   * @since 1.0.0
   */
  public Asset getSuperAsset() {
    if (!this.hasSuperAsset()) {
      throw new UnsupportedOperationException("Super asset not found");
    }
    return this.superAsset;
  }

  void setSuperAsset(Asset superAsset) {
    this.superAsset = requireNonNull(superAsset);
  }

  /**
   * Returns whether {@code name} is the name of a local field in this {@code Asset} object.
   *
   * @param name the name of the local field
   * @return whether {@code name} is the name of a local field in this {@code Asset} object
   * @throws java.lang.NullPointerException if {@code name} is {@code null}
   * @since 1.0.0
   */
  public boolean hasLocalField(String name) {
    return this.fields.containsKey(requireNonNull(name));
  }

  /**
   * Returns the local field with the name {@code name} in this {@code Asset} object.
   *
   * @param name the name of the local field
   * @return the local field with the name {@code name} in this {@code Asset} object
   * @throws java.lang.NullPointerException if {@code name} is {@code null}
   * @throws java.lang.IllegalArgumentException if {@code name} is not the name of a local field in
   *     this {@code Asset} object
   * @since 1.0.0
   */
  public Field getLocalField(String name) {
    if (!this.hasLocalField(name)) {
      throw new IllegalArgumentException(String.format("Local field \"%s\" not found", name));
    }
    return this.fields.get(name);
  }

  /**
   * Returns a list of all local fields in this {@code Asset} object.
   *
   * @return a list of all local fields in this {@code Asset} object
   * @since 1.0.0
   */
  public List<Field> getLocalFields() {
    return List.copyOf(this.fields.values());
  }

  /**
   * Returns whether {@code name} is the name of a field in this {@code Asset} object.
   *
   * @param name the name of the field
   * @return whether {@code name} is the name of a field in this {@code Asset} object
   * @throws java.lang.NullPointerException if {@code name} is {@code null}
   * @since 1.0.0
   */
  public boolean hasField(String name) {
    return this.hasLocalField(name) || this.hasSuperAsset() && this.getSuperAsset().hasField(name);
  }

  /**
   * Returns the field with the name {@code name} in this {@code Asset} object.
   *
   * @param name the name of the field
   * @return the field with the name {@code name} in this {@code Asset} object
   * @throws java.lang.NullPointerException if {@code name} is {@code null}
   * @throws java.lang.IllegalArgumentException if {@code name} is not the name of a field in this
   *     {@code Asset} object
   * @since 1.0.0
   */
  public Field getField(String name) {
    if (!this.hasField(name)) {
      throw new IllegalArgumentException(String.format("Field \"%s\" not found", name));
    }
    return this.hasLocalField(name)
        ? this.getLocalField(name)
        : this.getSuperAsset().getField(name);
  }

  /**
   * Returns a list of all fields in this {@code Asset} object.
   *
   * @return a list of all fields in this {@code Asset} object
   * @since 1.0.0
   */
  public List<Field> getFields() {
    return List.copyOf(this.getFieldsMap().values());
  }

  void addField(Field field) {
    requireNonNull(field);
    this.fields.put(field.getName(), field);
  }

  private Map<String, Field> getFieldsMap() {
    var fieldsMap =
        this.hasSuperAsset()
            ? this.getSuperAsset().getFieldsMap()
            : new LinkedHashMap<String, Field>();
    fieldsMap.putAll(this.fields);
    return fieldsMap;
  }

  /**
   * Returns whether {@code name} is the name of a local variable in this {@code Asset} object.
   *
   * @param name the name of the local variable
   * @return whether {@code name} is the name of a local variable in this {@code Asset} object
   * @throws java.lang.NullPointerException if {@code name} is {@code null}
   * @since 1.0.0
   */
  public boolean hasLocalVariable(String name) {
    return this.variables.containsKey(requireNonNull(name));
  }

  /**
   * Returns the local variable with the name {@code name} in this {@code Asset} object.
   *
   * @param name the name of the local variable
   * @return the local variable with the name {@code name} in this {@code Asset} object
   * @throws java.lang.NullPointerException if {@code name} is {@code null}
   * @throws java.lang.IllegalArgumentException if {@code name} is not the name of a local variable
   *     in this {@code Asset} object
   * @since 1.0.0
   */
  public Variable getLocalVariable(String name) {
    if (!this.hasLocalVariable(name)) {
      throw new IllegalArgumentException(String.format("Local variable \"%s\" not found", name));
    }
    return this.variables.get(name);
  }

  /**
   * Returns a list of all local variables in this {@code Asset} object.
   *
   * @return a list of all local variables in this {@code Asset} object
   * @since 1.0.0
   */
  public List<Variable> getLocalVariables() {
    return List.copyOf(this.variables.values());
  }

  /**
   * Returns whether {@code name} is the name of a variable in this {@code Asset} object.
   *
   * @param name the name of the variable
   * @return whether {@code name} is the name of a variable in this {@code Asset} object
   * @throws java.lang.NullPointerException if {@code name} is {@code null}
   * @since 1.0.0
   */
  public boolean hasVariable(String name) {
    return this.hasLocalVariable(name)
        || this.hasSuperAsset() && this.getSuperAsset().hasVariable(name);
  }

  /**
   * Returns the variable with the name {@code name} in this {@code Asset} object.
   *
   * @param name the name of the variable
   * @return the variable with the name {@code name} in this {@code Asset} object
   * @throws java.lang.NullPointerException if {@code name} is {@code null}
   * @throws java.lang.IllegalArgumentException if {@code name} is not the name of a variable in
   *     this {@code Asset} object
   * @since 1.0.0
   */
  public Variable getVariable(String name) {
    if (!this.hasVariable(name)) {
      throw new IllegalArgumentException(String.format("Variable \"%s\" not found", name));
    }
    return this.hasLocalVariable(name)
        ? this.getLocalVariable(name)
        : this.getSuperAsset().getVariable(name);
  }

  /**
   * Returns a list of all variables in this {@code Asset} object.
   *
   * @return a list of all variables in this {@code Asset} object
   * @since 1.0.0
   */
  public List<Variable> getVariables() {
    return List.copyOf(this.getVariablesMap().values());
  }

  private void addVariable(Variable variable) {
    requireNonNull(variable);
    this.variables.put(variable.getName(), variable);
  }

  private Map<String, Variable> getVariablesMap() {
    var variablesMap =
        this.hasSuperAsset()
            ? this.getSuperAsset().getVariablesMap()
            : new LinkedHashMap<String, Variable>();
    variablesMap.putAll(this.variables);
    return variablesMap;
  }

  /**
   * Returns whether {@code name} is the name of a local attack step in this {@code Asset} object.
   *
   * @param name the name of the local attack step
   * @return whether {@code name} is the name of a local attack step in this {@code Asset} object
   * @throws java.lang.NullPointerException if {@code name} is {@code null}
   * @since 1.0.0
   */
  public boolean hasLocalAttackStep(String name) {
    return this.attackSteps.containsKey(requireNonNull(name));
  }

  /**
   * Returns the local attack step with the name {@code name} in this {@code Asset} object.
   *
   * @param name the name of the local attack step
   * @return the local attack step with the name {@code name} in this {@code Asset} object
   * @throws java.lang.NullPointerException if {@code name} is {@code null}
   * @throws java.lang.IllegalArgumentException if {@code name} is not the name of a local attack
   *     step in this {@code Asset} object
   * @since 1.0.0
   */
  public AttackStep getLocalAttackStep(String name) {
    if (!this.hasLocalAttackStep(name)) {
      throw new IllegalArgumentException(String.format("Local attack step \"%s\" not found", name));
    }
    return this.attackSteps.get(name);
  }

  /**
   * Returns a list of all local attack steps in this {@code Asset} object.
   *
   * @return a list of all local attack steps in this {@code Asset} object
   * @since 1.0.0
   */
  public List<AttackStep> getLocalAttackSteps() {
    return List.copyOf(this.attackSteps.values());
  }

  /**
   * Returns whether {@code name} is the name of an attack step in this {@code Asset} object.
   *
   * @param name the name of the attack step
   * @return whether {@code name} is the name of an attack step in this {@code Asset} object
   * @throws java.lang.NullPointerException if {@code name} is {@code null}
   * @since 1.0.0
   */
  public boolean hasAttackStep(String name) {
    return this.hasLocalAttackStep(name)
        || this.hasSuperAsset() && this.getSuperAsset().hasAttackStep(name);
  }

  /**
   * Returns the attack step with the name {@code name} in this {@code Asset} object.
   *
   * @param name the name of the attack step
   * @return the attack step with the name {@code name} in this {@code Asset} object
   * @throws java.lang.NullPointerException if {@code name} is {@code null}
   * @throws java.lang.IllegalArgumentException if {@code name} is not the name of an attack step in
   *     this {@code Asset} object
   * @since 1.0.0
   */
  public AttackStep getAttackStep(String name) {
    if (!this.hasAttackStep(name)) {
      throw new IllegalArgumentException(String.format("Attack step \"%s\" not found", name));
    }
    return this.hasLocalAttackStep(name)
        ? this.getLocalAttackStep(name)
        : this.getSuperAsset().getAttackStep(name);
  }

  /**
   * Returns a list of all attack steps in this {@code Asset} object.
   *
   * @return a list of all attack steps in this {@code Asset} object
   * @since 1.0.0
   */
  public List<AttackStep> getAttackSteps() {
    return List.copyOf(this.getAttackStepsMap().values());
  }

  private void addAttackStep(AttackStep attackStep) {
    requireNonNull(attackStep);
    this.attackSteps.put(attackStep.getName(), attackStep);
  }

  private Map<String, AttackStep> getAttackStepsMap() {
    var attackStepsMap =
        this.hasSuperAsset()
            ? this.getSuperAsset().getAttackStepsMap()
            : new LinkedHashMap<String, AttackStep>();
    attackStepsMap.putAll(this.attackSteps);
    return attackStepsMap;
  }

  /**
   * Returns whether this {@code Asset} object has a local SVG icon.
   *
   * @return whether this {@code Asset} object has a local SVG icon
   * @since 1.0.0
   */
  public boolean hasLocalSvgIcon() {
    return this.svgIcon != null;
  }

  /**
   * Returns the local SVG icon of this {@code Asset} object.
   *
   * @return the local SVG icon of this {@code Asset} object
   * @throws java.lang.UnsupportedOperationException if this {@code Asset} object does not have a
   *     local SVG icon
   * @since 1.0.0
   */
  public byte[] getLocalSvgIcon() {
    if (!this.hasLocalSvgIcon()) {
      throw new UnsupportedOperationException("Local SVG icon not found");
    }
    return this.svgIcon.clone();
  }

  /**
   * Returns whether this {@code Asset} object has an SVG icon.
   *
   * @return whether this {@code Asset} object has an SVG icon
   * @since 1.0.0
   */
  public boolean hasSvgIcon() {
    return this.hasLocalSvgIcon() || this.hasSuperAsset() && this.getSuperAsset().hasSvgIcon();
  }

  /**
   * Returns the SVG icon of this {@code Asset} object.
   *
   * @return the SVG icon of this {@code Asset} object
   * @throws java.lang.UnsupportedOperationException if this {@code Asset} object does not have an
   *     SVG icon
   * @since 1.0.0
   */
  public byte[] getSvgIcon() {
    if (!this.hasSvgIcon()) {
      throw new UnsupportedOperationException("SVG icon not found");
    }
    return this.hasLocalSvgIcon() ? this.getLocalSvgIcon() : this.getSuperAsset().getSvgIcon();
  }

  /**
   * Returns whether this {@code Asset} object has a local PNG icon.
   *
   * @return whether this {@code Asset} object has a local PNG icon
   * @since 1.0.0
   */
  public boolean hasLocalPngIcon() {
    return this.pngIcon != null;
  }

  /**
   * Returns the local PNG icon of this {@code Asset} object.
   *
   * @return the local PNG icon of this {@code Asset} object
   * @throws java.lang.UnsupportedOperationException if this {@code Asset} object does not have a
   *     local PNG icon
   * @since 1.0.0
   */
  public byte[] getLocalPngIcon() {
    if (!this.hasLocalPngIcon()) {
      throw new UnsupportedOperationException("Local PNG icon not found");
    }
    return this.pngIcon.clone();
  }

  /**
   * Returns whether this {@code Asset} object has an PNG icon.
   *
   * @return whether this {@code Asset} object has an PNG icon
   * @since 1.0.0
   */
  public boolean hasPngIcon() {
    return this.hasLocalPngIcon() || this.hasSuperAsset() && this.getSuperAsset().hasPngIcon();
  }

  /**
   * Returns the PNG icon of this {@code Asset} object.
   *
   * @return the PNG icon of this {@code Asset} object
   * @throws java.lang.UnsupportedOperationException if this {@code Asset} object does not have an
   *     PNG icon
   * @since 1.0.0
   */
  public byte[] getPngIcon() {
    if (!this.hasPngIcon()) {
      throw new UnsupportedOperationException("PNG icon not found");
    }
    return this.hasLocalPngIcon() ? this.getLocalPngIcon() : this.getSuperAsset().getPngIcon();
  }

  /**
   * Returns whether this {@code Asset} object is a sub type of {@code other}.
   *
   * @param other another {@code Asset} object
   * @return whether this {@code Asset} object is a sub type of {@code other}
   * @throws java.lang.NullPointerException if {@code other} is {@code null}
   * @since 1.0.0
   */
  public boolean isSubTypeOf(Asset other) {
    requireNonNull(other);
    if (this == other) {
      return true;
    }
    if (!this.hasSuperAsset()) {
      return false;
    }
    return this.getSuperAsset().isSubTypeOf(other);
  }

  /**
   * Returns the least upper bound of {@code asset1} and {@code asset2}, or {@code null} if {@code
   * asset1} and {@code asset2} have no upper bound.
   *
   * @param asset1 an {@code Asset} object
   * @param asset2 an {@code Asset} object
   * @return the least upper bound of {@code asset1} and {@code asset2}, or {@code null} if {@code
   *     asset1} and {@code asset2} have no upper bound
   * @throws java.lang.NullPointerException if {@code asset1} or {@code asset2} is {@code null}
   * @since 1.0.0
   */
  public static Asset leastUpperBound(Asset asset1, Asset asset2) {
    requireNonNull(asset1);
    requireNonNull(asset2);
    if (asset1.isSubTypeOf(asset2)) {
      return asset2;
    }
    if (asset2.isSubTypeOf(asset1)) {
      return asset1;
    }
    if (!asset1.hasSuperAsset() || !asset2.hasSuperAsset()) {
      return null;
    }
    return Asset.leastUpperBound(asset1.getSuperAsset(), asset2.getSuperAsset());
  }

  JsonObject toJson() {
    var jsonVariables = Json.createArrayBuilder();
    for (var variable : this.variables.values()) {
      jsonVariables.add(variable.toJson());
    }

    var jsonAttackSteps = Json.createArrayBuilder();
    for (var attackStep : this.attackSteps.values()) {
      jsonAttackSteps.add(attackStep.toJson());
    }

    var jsonAsset =
        Json.createObjectBuilder()
            .add("name", this.name)
            .add("meta", this.meta.toJson())
            .add("category", this.category.getName())
            .add("isAbstract", this.isAbstract);
    if (this.superAsset == null) {
      jsonAsset.addNull("superAsset");
    } else {
      jsonAsset.add("superAsset", this.superAsset.getName());
    }
    return jsonAsset.add("variables", jsonVariables).add("attackSteps", jsonAttackSteps).build();
  }

  static Asset fromBuilder(AssetBuilder builder, Map<String, Category> categories) {
    requireNonNull(builder);
    requireNonNull(categories);
    if (!categories.containsKey(builder.getCategory())) {
      throw new IllegalArgumentException(
          String.format("Category \"%s\" not found", builder.getCategory()));
    }
    var asset =
        new Asset(
            builder.getName(),
            Meta.fromBuilder(builder.getMeta()),
            categories.get(builder.getCategory()),
            builder.isAbstract(),
            builder.getSvgIcon(),
            builder.getPngIcon());
    for (var variableBuilder : builder.getVariables()) {
      asset.addVariable(Variable.fromBuilder(variableBuilder, asset));
    }
    for (var attackStepBuilder : builder.getAttackSteps()) {
      asset.addAttackStep(AttackStep.fromBuilder(attackStepBuilder, asset));
    }
    return asset;
  }
}