package com.oguerreiro.resilient.domain;

import java.util.HashSet;
import java.util.Set;

import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.oguerreiro.resilient.domain.enumeration.InputMode;
import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;

/**
 * Variable Definition (GEE variable classification)
 */
@Entity
@Table(name = "variable")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@ResilientSecurityResourceConfig()
public class Variable extends AbstractResilientLongIdEntity<Variable> {

  private static final long serialVersionUID = 1L;

  /**
   * Unique key, composed by: <br>
   * VariableScope.code + VariableCategory.code + <index>
   * NOTA: User will provide the index property, and the software will create the code
   * index and code are NOT changeable
   */
  @NotNull
  @Pattern(regexp = "^[0-9]*$")
  @Column(name = "code", nullable = false, unique = true)
  private String code;

  /**
   * Variable index
   */
  @Column(name = "variable_index", nullable = false)
  private Integer variableIndex;

  /**
   * The name or title for the variable
   */
  @NotNull
  @Column(name = "name", nullable = false)
  private String name;

  /**
   * The descriptions of the variable. Its meaning. Its usage.
   */
  @NotNull
  @Column(name = "description", nullable = false)
  private String description;

  /**
   * Defines this variable as input. Values will be inserted for this variable
   */
  @NotNull
  @Column(name = "input", nullable = false)
  private Boolean input;

  /**
   * Defines the way of input. See the enum for instructions.
   */
  @Enumerated(EnumType.STRING)
  @Column(name = "input_mode", nullable = false)
  private InputMode inputMode;

  /**
   * Defines this variable as output. This variable will be evaluated for output
   */
  @NotNull
  @Column(name = "output", nullable = false)
  private Boolean output;

  /**
   * Defines this variable as output. This variable will be evaluated for output
   */
  @NotNull
  @Column(name = "output_single", nullable = true)
  private Boolean outputSingle;

  @ManyToOne(fetch = FetchType.LAZY)
  @JsonIgnoreProperties(value = { "user", "children", "parent", "organizationType" }, allowSetters = true)
  private Organization outputOwner;

  /**
   * The formula to calculate the output valut of the variable. Mandatory when output==TRUE
   */
  @Column(name = "output_formula")
  private String outputFormula;

  /**
   * Record version for concurrency control.
   */
  @Column(name = "value_scale")
  private Integer valueScale;

  @OneToMany(fetch = FetchType.LAZY, mappedBy = "variable", cascade = CascadeType.ALL, orphanRemoval = true)
  @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
  @JsonIgnoreProperties(value = { "variable", "unit" }, allowSetters = true)
  private Set<VariableUnits> variableUnits = new HashSet<>();

  @NotNull
  @ManyToOne(fetch = FetchType.LAZY)
  @JsonIgnoreProperties(value = { "unitType" }, allowSetters = true)
  private Unit baseUnit;

  @ManyToOne(fetch = FetchType.LAZY)
  @JsonIgnoreProperties(value = { "variableCategories", "variables" }, allowSetters = true)
  private VariableScope variableScope;

  @ManyToOne(fetch = FetchType.LAZY)
  @JsonIgnoreProperties(value = { "variables", "variableScope" }, allowSetters = true)
  private VariableCategory variableCategory;

  @ManyToOne(fetch = FetchType.LAZY)
  private VariableClassType variableClassType;

  @Column(name = "variable_class_label")
  private String variableClassLabel;

  @Column(name = "variable_class_mandatory")
  private Boolean variableClassMandatory;

  @Column(name = "hidden_for_main")
  private Boolean hiddenForMain;

  public String getCode() {
    return this.code;
  }

  public Variable code(String code) {
    this.setCode(code);
    return this;
  }

  public void setCode(String code) {
    this.code = code;
  }

  public Integer getVariableIndex() {
    return this.variableIndex;
  }

  public Variable variableIndex(Integer variableIndex) {
    this.setVariableIndex(variableIndex);
    return this;
  }

  public void setVariableIndex(Integer variableIndex) {
    this.variableIndex = variableIndex;
  }

  public String getName() {
    return this.name;
  }

  public Variable name(String name) {
    this.setName(name);
    return this;
  }

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

  public String getDescription() {
    return this.description;
  }

  public Variable description(String description) {
    this.setDescription(description);
    return this;
  }

  public void setDescription(String description) {
    this.description = description;
  }

  public Boolean getInput() {
    return this.input;
  }

  public Variable input(Boolean input) {
    this.setInput(input);
    return this;
  }

  public void setInput(Boolean input) {
    this.input = input;
  }

  public InputMode getInputMode() {
    return this.inputMode;
  }

  public Variable inputMode(InputMode inputMode) {
    this.setInputMode(inputMode);
    return this;
  }

  public void setInputMode(InputMode inputMode) {
    this.inputMode = inputMode;
  }

  public Boolean getOutput() {
    return this.output;
  }

  public Variable output(Boolean output) {
    this.setOutput(output);
    return this;
  }

  public void setOutput(Boolean output) {
    this.output = output;
  }

  public Boolean getOutputSingle() {
    return this.outputSingle;
  }

  public Variable outputSingle(Boolean outputSingle) {
    this.setOutputSingle(outputSingle);
    return this;
  }

  public void setOutputSingle(Boolean outputSingle) {
    this.outputSingle = outputSingle;
  }

  public String getOutputFormula() {
    return this.outputFormula;
  }

  public Variable outputFormula(String outputFormula) {
    this.setOutputFormula(outputFormula);
    return this;
  }

  public void setOutputFormula(String outputFormula) {
    this.outputFormula = outputFormula;
  }

  public Integer getValueScale() {
    return this.valueScale;
  }

  public Variable valueScale(Integer valueScale) {
    this.setValueScale(valueScale);
    return this;
  }

  public void setValueScale(Integer valueScale) {
    this.valueScale = valueScale;
  }

  public boolean isUnitAllowed(Unit unit) {
    return this.getVariableUnits().stream().anyMatch(varUnit -> varUnit.getUnit().equals(unit))
        || this.getBaseUnit().getUnitType().equals(unit.getUnitType());
  }

  public Set<VariableUnits> getVariableUnits() {
    return this.variableUnits;
  }

  public void setVariableUnits(Set<VariableUnits> variableUnits) {
    if (this.variableUnits != null) {
      this.variableUnits.forEach(i -> i.setVariable(null));
    }
    if (variableUnits != null) {
      variableUnits.forEach(i -> i.setVariable(this));
    }
    this.variableUnits = variableUnits;
  }

  public Variable variableUnits(Set<VariableUnits> variableUnits) {
    this.setVariableUnits(variableUnits);
    return this;
  }

  public Variable addVariableUnits(VariableUnits variableUnits) {
    this.variableUnits.add(variableUnits);
    variableUnits.setVariable(this);
    return this;
  }

  public Variable removeVariableUnits(VariableUnits variableUnits) {
    this.variableUnits.remove(variableUnits);
    variableUnits.setVariable(null);
    return this;
  }

  public Organization getOutputOwner() {
    return this.outputOwner;
  }

  public void setOutputOwner(Organization outputOwner) {
    this.outputOwner = outputOwner;
  }

  public Variable outputOwner(Organization outputOwner) {
    this.setOutputOwner(outputOwner);
    return this;
  }

  public Unit getBaseUnit() {
    return this.baseUnit;
  }

  public void setBaseUnit(Unit unit) {
    this.baseUnit = unit;
  }

  public Variable baseUnit(Unit unit) {
    this.setBaseUnit(unit);
    return this;
  }

  public VariableScope getVariableScope() {
    return this.variableScope;
  }

  public void setVariableScope(VariableScope variableScope) {
    this.variableScope = variableScope;
  }

  public Variable variableScope(VariableScope variableScope) {
    this.setVariableScope(variableScope);
    return this;
  }

  public VariableCategory getVariableCategory() {
    return this.variableCategory;
  }

  public void setVariableCategory(VariableCategory variableCategory) {
    this.variableCategory = variableCategory;
  }

  public Variable variableCategory(VariableCategory variableCategory) {
    this.setVariableCategory(variableCategory);
    return this;
  }

  public VariableClassType getVariableClassType() {
    return this.variableClassType;
  }

  public void setVariableClassType(VariableClassType variableClassType) {
    this.variableClassType = variableClassType;
  }

  public Variable variableClassType(VariableClassType variableClassType) {
    this.setVariableClassType(variableClassType);
    return this;
  }

  public String getVariableClassLabel() {
    return variableClassLabel;
  }

  public void setVariableClassLabel(String variableClassLabel) {
    this.variableClassLabel = variableClassLabel;
  }

  public Variable variableClassLabel(String variableClassLabel) {
    this.setVariableClassLabel(variableClassLabel);
    return this;
  }

  public Boolean getVariableClassMandatory() {
    return variableClassMandatory;
  }

  public void setVariableClassMandatory(Boolean variableClassMandatory) {
    this.variableClassMandatory = variableClassMandatory;
  }

  public Variable variableClassMandatory(Boolean variableClassMandatory) {
    this.setVariableClassMandatory(variableClassMandatory);
    return this;
  }

  public Boolean getHiddenForMain() {
    return hiddenForMain;
  }

  public void setHiddenForMain(Boolean hiddenForMain) {
    this.hiddenForMain = hiddenForMain;
  }

  public Variable hiddenForMain(Boolean hiddenForMain) {
    this.setHiddenForMain(hiddenForMain);
    return this;
  }

//@formatter:off
  @Override
  public String toString() {
    return "Variable{" + 
              "id=" + getId() + 
              ", code='" + getCode() + "'" + 
              ", variableIndex=" + getVariableIndex() + 
              ", name='" + getName() + "'" + 
              ", description='" + getDescription() + "'" +
              ", input='" + getInput() + "'" +
              ", inputMode='" + getInputMode() + "'" + 
              ", output='" + getOutput() + "'" +
              ", outputSingle='" + getOutputSingle() + "'" + 
              ", outputFormula='" + getOutputFormula() + "'" + 
              ", valueScale=" + getValueScale() + 
              ", hiddenForMain=" + getHiddenForMain() + 
              " , version=" + getVersion() + 
           "}";
  }
//@formatter:on
}
