package com.oguerreiro.resilient.domain;

import java.util.ArrayList;
import java.util.List;

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

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

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

/**
 * Organization
 * Hierarchical organigram of the organization.
 */
@Entity
@Table(name = "organization")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@ResilientSecurityResourceConfig()
public class Organization extends AbstractResilientLongIdEntity<Organization> {

  private static final long serialVersionUID = 1L;

  /**
   * User defined code. Must be unique in the Organization
   */
  @NotNull
  @Size(min = 3)
  @Column(name = "code", nullable = false, unique = true)
  private String code;

  /**
   * The name of the Organization entry. Allows duplicate name's, but NOT duplicate code's.
   */
  @NotNull
  @Size(min = 3)
  @Column(name = "name", nullable = false)
  private String name;

  @Lob
  @Column(name = "image")
  private byte[] image;

  @Column(name = "image_content_type")
  private String imageContentType;

  @Column(name = "input_inventory")
  private Boolean inputInventory;

  @Column(name = "output_inventory")
  private Boolean outputInventory;

  @OneToOne(fetch = FetchType.LAZY)
  @JoinColumn(unique = true)
  private User user;

  // Virtual property to use in children @OrderBy statement
  @Formula("(CASE WHEN parent_id IS NULL THEN 0 ELSE 1 END)")
  private Integer virtualParentOrder;
  // Virtual property to use in children @OrderBy statement
  @Formula("(CASE WHEN sort IS NULL THEN 1 ELSE 0 END)")
  private Integer virtualNullSortOrder;

  @OneToMany(fetch = FetchType.LAZY, mappedBy = "parent")
  @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
  @OrderBy("virtualParentOrder ASC, virtualNullSortOrder ASC, sort ASC")
  @JsonIgnoreProperties(value = { "user", "children", "inputData", "inputDataUploads", "parent",
      "organizationType" }, allowSetters = true)
  private List<Organization> children = new ArrayList<>();

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

  @ManyToOne(fetch = FetchType.LAZY)
  @JsonIgnoreProperties(value = { "organizations", "metadataProperties" }, allowSetters = true)
  private OrganizationType organizationType;

  @Column(name = "sort")
  private Integer sort;

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

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

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

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

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

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

  public Integer getSort() {
    return this.sort;
  }

  public Organization sort(Integer sort) {
    this.setSort(sort);
    return this;
  }

  public void setSort(Integer sort) {
    this.sort = sort;
  }

  public byte[] getImage() {
    return this.image;
  }

  public Organization image(byte[] image) {
    this.setImage(image);
    return this;
  }

  public void setImage(byte[] image) {
    this.image = image;
  }

  public String getImageContentType() {
    return this.imageContentType;
  }

  public Organization imageContentType(String imageContentType) {
    this.imageContentType = imageContentType;
    return this;
  }

  public void setImageContentType(String imageContentType) {
    this.imageContentType = imageContentType;
  }

  public Boolean getInputInventory() {
    return this.inputInventory;
  }

  public Organization inputInventory(Boolean inputInventory) {
    this.setInputInventory(inputInventory);
    return this;
  }

  public void setInputInventory(Boolean inputInventory) {
    this.inputInventory = inputInventory;
  }

  public Boolean getOutputInventory() {
    return this.outputInventory;
  }

  public Organization outputInventory(Boolean outputInventory) {
    this.setOutputInventory(outputInventory);
    return this;
  }

  public void setOutputInventory(Boolean outputInventory) {
    this.outputInventory = outputInventory;
  }

  public User getUser() {
    return this.user;
  }

  public void setUser(User user) {
    this.user = user;
  }

  public Organization user(User user) {
    this.setUser(user);
    return this;
  }

  public List<Organization> getChildren() {
    return this.children;
  }

  public void setChildren(List<Organization> organizations) {
    if (this.children != null) {
      this.children.forEach(i -> i.setParent(null));
    }
    if (organizations != null) {
      organizations.forEach(i -> i.setParent(this));
    }
    this.children = organizations;
  }

  public Organization children(List<Organization> organizations) {
    this.setChildren(organizations);
    return this;
  }

  public Organization addChild(Organization organization) {
    this.children.add(organization);
    organization.setParent(this);
    return this;
  }

  public Organization removeChild(Organization organization) {
    this.children.remove(organization);
    organization.setParent(null);
    return this;
  }

  public Organization getParent() {
    return this.parent;
  }

  public void setParent(Organization organization) {
    this.parent = organization;
  }

  public Organization parent(Organization organization) {
    this.setParent(organization);
    return this;
  }

  public OrganizationType getOrganizationType() {
    return this.organizationType;
  }

  public void setOrganizationType(OrganizationType organizationType) {
    this.organizationType = organizationType;
  }

  public Organization organizationType(OrganizationType organizationType) {
    this.setOrganizationType(organizationType);
    return this;
  }

  public Boolean isChildOf(Organization organization) {
    Organization parent = null;
    Boolean found = false;

    do {
      parent = this.getParent();
      if (parent == null) {
        break;
      }
      if (parent.equals(organization)) {
        found = true;
        break;
      }
    } while (true);

    return found;
  }

//@formatter:off
  @Override
  public String toString() {
    return "Organization{" + 
              "id=" + getId() + 
              ", code='" + getCode() + "'" + 
              ", name='" + getName() + "'" +
              ", sort='" + getSort() + "'" + 
              ", image='" + getImage() + "'" + 
              ", imageContentType='" + getImageContentType() + "'" + 
              ", inputInventory='" + getInputInventory() + "'" + 
              ", outputInventory='" + getOutputInventory() + "'" + 
              ", version=" + getVersion()
        + "}";
  }
//@formatter:on
}
