package com.capgemini.reports.bomlineholder;

import com.capgemini.framework.teamcenter.authent.TcSession;
import com.capgemini.reports.bomlineholder.processor.IBOMLineHolderProcessor;
import com.capgemini.reports.bomlineholder.visitor.IBOMLineHolderExtractor;
import com.capgemini.reports.snecmaobjects.ISnecmaObject;
import com.capgemini.reports.snecmaobjects.part.AbstractAltOrDetRevision;
import com.capgemini.reports.teamcenter.ObjectInfoHelper;
import com.teamcenter.soa.client.model.ModelObject;
import com.teamcenter.soa.client.model.strong.BOMLine;
import com.teamcenter.soa.client.model.strong.Form;
import com.teamcenter.soa.client.model.strong.Item;
import com.teamcenter.soa.client.model.strong.ItemRevision;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * Container for BOM line and their BOM line children.<br/>
 * The BOM line holder also contains the corresponding ModelObject and/or the corresponding ISnecmaObject.
 * <p>
 * <p>
 * <b>Note:</b>To save memory space, the BOMLineHolder will go through th following life phases:<br>
 * <ol>
 * <li><b>{@link BOMLineHolderState#RAW}</b> mode<br>
 * The BOMLineHolder only contains raw Teamcenter data, namely:
 * <ul>
 * <li>A {@link BOMLine} called {@code BOMLineObject}</li>
 * <li>A {@link List} of {@link ModelObject}s containing all {@link Item}s found in an eventual pseudo-folder.</li>
 * <li>A {@link List} of children {@link BOMLineHolder}s called {@code bomChilds}, all of which are in the same state ({@link BOMLineHolderState#RAW})</li>
 * </ul>
 * <p>
 * <li><b>{@link BOMLineHolderState#PROCESSING}</b> mode<br>
 * The BOMLineHolder contains both the RAW Teamcenter data, and their {@link ISnecmaObject} representation.<br>
 * In this state, some processing can be performed to extract data from the TC object and dump it into an ISnecmaObject as appropriate:
 * <ul>
 * <li>A {@link BOMLine} called {@code BOMLineObject}</li>
 * <li>A {@link List} of {@link ModelObject}s containing all {@link Item}s found in an eventua pseudo-folder.</li>
 * <li>A {@link List} of children {@link BOMLineHolder}s called {@code bomChilds}, all of which are in the same state ({@link BOMLineHolderState#PROCESSING})</li>
 * <li>A {@link ISnecmaObject} containing a representation of data from the ItemRevision held by the BomLine</li>
 * </ul>
 * <p>
 * <p>
 * <li><b>{@link BOMLineHolderState#FROZEN}</b><br>
 * The BOMLineHolder contains no more Teamcenter objects (they are <code>null</code>) but instead contains some strongly-typed {@link ISnecmaObject}s holding all the relevant data:
 * <ul>
 * <li>A {@link ISnecmaObject} containing a representation of data from the ItemRevision held by the BomLine</li>
 * <li>A {@link List} of children {@link BOMLineHolder}s called {@code bomChilds}, all of which are in the same state ({@link BOMLineHolderState#FROZEN})</li>
 * </ul>
 * </ol>
 * <p>
 * <p>
 * To findAll into PROCESSING mode, use the {@link BOMLineHolderFactory}
 */
@Slf4j
@EqualsAndHashCode(exclude = {"parentBomLine"})
public class BOMLineHolder implements IBOMTree, Comparable<BOMLineHolder> {
  private static final long serialVersionUID = 5317661727930642676L;

  private TcSession session;
  /**
   * Current line in the BOM.
   **/
  private transient BOMLine bomLineObject;
  /**
   * Item Revision of the current {@link BOMLine}.
   */
  private transient ItemRevision itemRevision;
  /**
   * Wrapper for Model object (set as null by default).
   **/
  private ISnecmaObject snecmaObject;
  /**
   * Map of BOMLine IDs to the children BOM lines for the current BOM line.
   **/
  private List<BOMLineHolder> bomChilds = new ArrayList<>();
  private IBOMTree parentBomLine;

  /**
   * State of the BOMLIneHolder.
   */
  @Setter(value = AccessLevel.NONE)
  private BOMLineHolderState state = BOMLineHolderState.RAW;

  /**
   * Constructor using a simple BOM line. <br>
   * If you also have the ItemRevision already, use the other constructor: {@link BOMLineHolder#BOMLineHolder(BOMLine, ItemRevision)}. This should ONLY be used for the top BomLine.
   *
   * @param bomLine the BOMLine represented by this {@link BOMLineHolder}
   */
  public BOMLineHolder(final BOMLine bomLine) {
    this.bomLineObject = bomLine;

    // Get the TC ModelObject
    ModelObject theTopMo = bomLine.get_bl_line_object();
    // Build the embedded SnecmaObject
    if (theTopMo != null) {
      if (theTopMo instanceof ItemRevision) {
        this.itemRevision = (ItemRevision) theTopMo;
        return;
      }
      else {
        log.error("No ItemRevision found in the ModelObject");
      }
    }
    else {
      log.debug("BOMlineHolder built from a BomLine which does not have a ModelObjec: BomLine UID=" + bomLine.getUid());
    }

    // If not available, try to use the BomLine's UID (wich may change from one execution to the next
    this.itemRevision = null;
  }

  /**
   * Constructor using a simple BOM line.
   *
   * @param bomLine the BOMLine represented by this {@link BOMLineHolder}
   * @param itemRev the ItemRevision managed by this holder
   */
  public BOMLineHolder(final BOMLine bomLine, final ItemRevision itemRev) {
    this.bomLineObject = bomLine;
    this.itemRevision = itemRev;
  }


  /**
   * Constructor building a BOMLineHolder from another BOMLineHolder. Exact same copy, same UID.
   *
   * @param aBom BOMLineHolder
   */
  public BOMLineHolder(final IBOMTree aBom) {
    this.bomLineObject = aBom.getBomLineObject();
    this.itemRevision = aBom.getItemRevision();
    this.snecmaObject = aBom.getSnecmaObject();
    this.parentBomLine = aBom.getParentBomLine();
    this.bomChilds = aBom.getBomChilds();
  }

  /**
   * Constructor for {@link ItemRevision}s obtained without a {@link BOMLine} like extracted from a pseudo-folder.
   **/
  protected BOMLineHolder(final ItemRevision itemRevision) {
    this.bomLineObject = null;
    this.itemRevision = itemRevision;
  }

  public BOMLineHolder(final TcSession session, final BOMLine bomLine, final ItemRevision itemRevision, final List<BOMLineHolder> bomChilds) {
    this.session = session;
    this.bomLineObject = bomLine;
    this.itemRevision = itemRevision;
    this.bomChilds = bomChilds;
  }

  /**
   * This methods "freezes" the BOMLineholder by removing all its references to Teamcenter data.<br>
   * Should be called after it was fully built.
   */
  public void freeze() {
    this.state = BOMLineHolderState.FROZEN;
  }

  @Override
  public void process(final IBOMLineHolderProcessor aBOMLineHolderProcessor) {
    switch (this.state) {
      case RAW:
        log.warn("Skipping processing of a RAW BomLineHolder");
        break;
      case PROCESSING:
        aBOMLineHolderProcessor.process(this); // Perform the unit work
        for (BOMLineHolder child : bomChilds) {
          child.process(aBOMLineHolderProcessor); // Browse children recursively
        }
        break;
      case FROZEN:
        throw new IllegalStateException("Processing a BOMLineHolder which has been FROZEN is not allowed.");
      default:
        break;
    }
  }

  @Override
  public <T> T accept(final IBOMLineHolderExtractor<T> aBOMLineHolderExtractor) {
    if (this.state != BOMLineHolderState.FROZEN) {
      throw new IllegalStateException("Visiting a BOMLineHolder which has not been FROZEN is not allowed. Use the BOMLineHolderFactory on it first.");
    }
    return aBOMLineHolderExtractor.visitTopReceptorBom(this);
  }

  @Override
  public IBOMTree getParentBomLine() {
    return parentBomLine;
  }

  @Override
  public List<BOMLineHolder> getBomChilds() {
    return bomChilds;
  }

  @Override
  public void setParent(final BOMLineHolder aParentBomLine) {
    this.parentBomLine = aParentBomLine;
  }

  @Override
  public BOMLine getBomLineObject() {
    return bomLineObject;
  }

  /**
   * Assign a child BOM line to the current line and create a reference to its parent. A <code>null</code> child will not be added.
   */
  @Override
  public void addChild(final BOMLineHolder aChildBomLine) {
    if (aChildBomLine != null) {
      this.bomChilds.add(aChildBomLine);
      aChildBomLine.setParent(this);
    }
  }

  /**
   * Return the type of the object carried.
   *
   * @return a String containing the Type.
   */
  @Override
  public String getObjectType() {
    String type;
    switch (state) {
      case RAW:
        type = getRawType();
        break;
      case PROCESSING:
      case FROZEN:
        type = snecmaObject.getType();
        break;
      default:
        type = "Unknown State: " + state.toString();
        break;
    }
    return type;
  }

  private String getRawType() {
    return itemRevision.getTypeObject().getName();
  }

  /**
   * Generate unique key for BOM line using item_id + item_revision_id.<br>
   * This is used by the child tree map for sorting purpose.
   *
   * @return Unique key
   */
  @Override
  public String getKey() {
    return getUid();
  }

  @Override
  public ISnecmaObject getSnecmaObject() {
    return snecmaObject;
  }

  /**
   * This methods sets the SnecmaObject of this BomLineHolder. It also makes it processable if it was {@link BOMLineHolderState#RAW}.
   *
   * @param obj the {@link ISnecmaObject}
   */
  void setSnecmaObject(final ISnecmaObject obj) {
    this.snecmaObject = obj;
    if (state == BOMLineHolderState.RAW) {
      this.state = BOMLineHolderState.PROCESSING;
    }
  }

  @Override
  public String getUid() {
    return bomLineObject.getUid();
  }

  @Override
  public int compareTo(final BOMLineHolder other) {
    return getUid().compareTo(other.getUid());
  }

  private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
    stream.defaultReadObject(); // Deserialize the non-transient data
  }

  @Override
  public String toString() {
    switch (state) {
      case RAW:
        return toStringRaw();
      case PROCESSING:
        return toStringProcessing();
      case FROZEN:
        return toStringFrozen();
      default:
        return toStringDefault();
    }
  }

  private String toStringProcessing() {
    return String.format("BOMLineHolder %s (PROCESSING) with a %s whose ID=\"%s\"", getUid(), snecmaObject.getType(), snecmaObject.getItemId());
  }

  private String toStringDefault() {
    return String.format("BOMLineHolder %s in unknown state: \"%s\"", getUid(), state.toString());
  }

  private String toStringFrozen() {
    return String.format("BOMLineHolder %s (FROZEN) with a %s whose ID=\"%s\"", getUid(), snecmaObject.getType(), snecmaObject.getItemId());
  }

  private String toStringRaw() {
    StringBuilder builder = new StringBuilder("BOMLineHolder ");
    builder.append(getUid()).append(" (RAW) with a ");
    if (itemRevision == null) {
      builder.append("null object inside its BOMLine");
    }
    else {
      String itemId = itemRevision.get_item_id();
      String itemType = itemRevision.getTypeObject().getName();
      builder.append(itemType).append(" whose ID=\"").append(itemId).append("\"");
    }
    return builder.toString();
  }

  public ItemRevision getItemRevision() {
    return itemRevision;
  }

  public BOMLineHolderState getState() {
    return state;
  }

  public String getBomAlt() {
    return bomLineObject.getPropertyObject("ALT").getStringValue();
  }

  public String getBomSb() {
    return bomLineObject.getPropertyObject("SB").getStringValue();
  }

  public String getBomQty() {
    return bomLineObject.getPropertyObject("bl_quantity").getStringValue();
  }

  public String getBomQtyCumul() {
    return bomLineObject.getPropertyObject("Qte cumulee").getStringValue();
  }

  public String getBomPanache() {
    return bomLineObject.getPropertyObject("Panachage").getStringValue();
  }

  public String getUin() {
    return bomLineObject.getPropertyObject("UIN").getStringValue();
  }

  public String getBomConfig() {
    return bomLineObject.get_bl_config_string();
  }

  public List<Item> getPseudoFolder() {
    // Now this BomLineHolder may be a Alt or Det, which may require to have its pseudo-Folder setup.
    log.debug(" - Alt or DET found: investigating its pseudo-folder...");
    return ObjectInfoHelper.getPartsInPseudoFolderOfAltOrDetRevision(session, itemRevision);

  }

  public List<Form> getAllItemRevMasters() {
    List<Form> bomObjects = new ArrayList<>();
    bomObjects.add(itemRevision.get_item_master_tag());
    for (BOMLineHolder bomChild : bomChilds) {
      bomObjects.addAll(bomChild.getAllItemRevMasters());
    }
    return bomObjects;
  }


  /**
   * Represents the lifecycle of a {@link BOMLineHolder}.<br>
   * This cycle is the following sequence: {@link BOMLineHolderState #RAW RAW} --> {@link BOMLineHolderState #PROCESSING PROCESSED} --> {@link BOMLineHolderState #FROZEN FROZEN}
   */
  public enum BOMLineHolderState {
    /**
     * In this state the BOMLineHolder contains raw Teamcenter data.
     * <ul>
     * <li>A {@link BOMLine} called {@code BOMLineObject}</li>
     * <li>A {@link List} of children {@link BOMLineHolder}s called {@code bomChilds}, all of which are in the same state (RAW)</li>
     * <li>A pseudo-folder (a {@link List} of {@link ModelObject}s) containing some {@link Item}s</li>
     * </ul>
     */
    RAW,

    /**
     * In this state the BOMLineHolder contains processed Teamcenter data.
     * <ul>
     * <li>A {@link BOMLine} called {@code BOMLineObject}</li>
     * <li>A {@link List} of children {@link BOMLineHolder}s called {@code bomChilds}, all of which are in the same state (PROCESSED)</li>
     * <li>A pseudo-folder (a {@link List} of {@link ModelObject}s) containing all the correct {@link ItemRevision}s, obtained from their {@link Item}s earlier</li>
     * </ul>
     */
    PROCESSING,

    /**
     * In this state the BOMLineHolder contains no more Teamcenter objects (they are <code>null</code>) but instead contains some strongly-typed {@link ISnecmaObject}s.
     * <ul>
     * <li>A {@link List} of children {@link BOMLineHolder}s called {@code bomChilds}, all of which are in the same state (FROZEN)</li>
     * </ul>
     * Note : any {@link ISnecmaObject} of type {@link AbstractAltOrDetRevision} will have a pseudo-folder loaded with children {@link ISnecmaObject}s.
     */
    FROZEN
  }
}
