/*
 * Copyright 2003-2011 JetBrains s.r.o.
 *
 * 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 jetbrains.mps.nodeEditor;

import jetbrains.mps.nodeEditor.cells.DefaultCellInfo;
import jetbrains.mps.nodeEditor.cells.EditorCell_Label;
import jetbrains.mps.nodeEditor.cells.EditorCell_Property;
import jetbrains.mps.nodeEditor.cells.TransactionalPropertyAccessor;
import jetbrains.mps.nodeEditor.selection.SelectionInfoImpl;
import jetbrains.mps.openapi.editor.EditorComponentState;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.cells.CellInfo;
import jetbrains.mps.openapi.editor.cells.CellTraversalUtil;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.EditorCell_Collection;
import jetbrains.mps.openapi.editor.cells.optional.WithCaret;
import jetbrains.mps.openapi.editor.selection.Selection;
import jetbrains.mps.openapi.editor.selection.SelectionInfo;
import jetbrains.mps.smodel.SNodePointer;
import jetbrains.mps.util.EqualUtil;
import jetbrains.mps.util.Pair;
import org.jdom.Attribute;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.model.SNodeUtil;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

class Memento implements EditorComponentState {
  private static final Comparator<Pair<EditorCell_Collection, Boolean>> COLLAPSED_STATES_COMPARATOR = new Comparator<Pair<EditorCell_Collection, Boolean>>() {
    @Override
    public int compare(Pair<EditorCell_Collection, Boolean> p1,
        Pair<EditorCell_Collection, Boolean> p2) {
      int depthDelta = getDepth(p2.o1) - getDepth(p1.o1);
      return depthDelta != 0 ? depthDelta : CELL_COMPARATOR.compare(p2.o1, p1.o1);
    }

    private int getDepth(EditorCell cell) {
      int depth = 0;
      while (cell.getParent() != null) {
        cell = cell.getParent();
        depth++;
      }
      return depth;
    }
  };

  private static final Comparator<EditorCell> CELL_COMPARATOR =
      (cell1, cell2) -> CellTraversalUtil.getCommonParent(cell1, cell2) == null ? 0 : CellTraversalUtil.compare(cell1, cell2);

  private List<SelectionInfo> mySelectionStack = new ArrayList<>();
  private List<CellInfo> myCollectionsWithEnabledBraces = new ArrayList<>();

  private List<Pair<CellInfo, Boolean>> myFoldableStates = new ArrayList<>();
  private List<Pair<CellInfo, Boolean>> myInitiallyCollapsedStates = new ArrayList<>();
  private List<Pair<CellInfo, Boolean>> myRestoreAlwaysStates = new ArrayList<>();

  private List<ErrorMarker> myErrors = new ArrayList<>();
  private List<TransactionalPropertyState> myTransactionalProperties = new ArrayList<>();
  private Point myViewPosition;
  private String[] myEnabledHints = null;
  private SNodeReference myEditedNodeReference;
  private boolean mySaveSessionState = true;

  private Memento() {
  }

  @Override
  public void clearSessionState() {
    mySaveSessionState = false;
  }

  Memento(EditorContext context, boolean saveEditedNode) {
    EditorComponent nodeEditor = (EditorComponent) context.getEditorComponent();
    SNode editedNode = nodeEditor.getEditedNode();
    if (editedNode == null || SNodeUtil.isAccessible(editedNode, context.getRepository())) {
      if (saveEditedNode && editedNode != null) {
        myEditedNodeReference = editedNode.getReference();
      }
      mySelectionStack = nodeEditor.getSelectionManager().getSelectionInfoStack();

      getFoldableStates(collectRestoreAlways(nodeEditor, mySelectionStack)).forEach(myRestoreAlwaysStates::add);
      getFoldableStates(collectFoldable(nodeEditor, false)).forEach(myFoldableStates::add);
      getFoldableStates(collectFoldable(nodeEditor, true)).forEach(myInitiallyCollapsedStates::add);

      nodeEditor.getBracesEnabledCells().stream().sorted(CELL_COMPARATOR).map(EditorCell::getCellInfo).forEach(myCollectionsWithEnabledBraces::add);

      // collect errors
      nodeEditor.getCellTracker().getErrorCells().stream().sorted(CELL_COMPARATOR).filter(
          cell -> cell instanceof EditorCell_Label && ((EditorCell_Label) cell).isEditable()).map(cell -> new ErrorMarker((EditorCell_Label) cell)).forEach(
          myErrors::add);

      // collect transactionals
      nodeEditor.getCellTracker().getTransactionalCells().stream().sorted(CELL_COMPARATOR).filter(
          cell -> ((TransactionalPropertyAccessor) cell.getModelAccessor()).hasValueToCommit()).map(TransactionalPropertyState::new).forEach(
          myTransactionalProperties::add);
    }

    myViewPosition = nodeEditor.getViewPosition();
    myEnabledHints = nodeEditor.getUpdater().getInitialEditorHints();
  }

  @NotNull
  private Stream<EditorCell_Collection> collectRestoreAlways(EditorComponent nodeEditor, List<SelectionInfo> selectionInfoStack) {
    Set<EditorCell_Collection> visitedCollections = new HashSet<>();
    List<EditorCell_Collection> result = new ArrayList<>();
    for (SelectionInfo selectionInfo : selectionInfoStack) {
      Selection selection = selectionInfo.createSelection(nodeEditor);
      if (selection == null) {
        continue;
      }
      for (EditorCell nextCell : selection.getSelectedCells()) {
        for (EditorCell_Collection parent = nextCell.getParent(); parent != null; parent = parent.getParent()) {
          if (visitedCollections.add(parent) && parent.isFoldable()) {
            result.add(parent);
          }
        }
      }
    }
    return result.stream();
  }

  private Stream<EditorCell_Collection> collectFoldable(EditorComponent nodeEditor, boolean initiallyCollapsed) {
    return nodeEditor.getCellTracker().getFoldableCells().stream().map(EditorCell_Collection.class::cast).filter(
        cell -> initiallyCollapsed == cell.isInitiallyCollapsed());
  }

  private Stream<Pair<CellInfo, Boolean>> getFoldableStates(Stream<EditorCell_Collection> cells) {
    return cells.map(cell -> new Pair<>(cell, cell.isCollapsed())).sorted(COLLAPSED_STATES_COMPARATOR).map(
        collapsedState -> new Pair<>(collapsedState.o1.getCellInfo(), collapsedState.o2));
  }

  void restore(EditorComponent editor) {
    boolean editorRebuildRequired = editor.getUpdater().setInitialEditorHints(myEnabledHints);

    if (myEditedNodeReference != null) {
      SNode newEditedNode = myEditedNodeReference.resolve(editor.getEditorContext().getRepository());
      if (newEditedNode != null && editor.getEditedNode() != newEditedNode) {
        editor.editNode(newEditedNode);
        editor.getUpdater().flushModelEvents();
        editorRebuildRequired = false;
      }
    }
    if (editorRebuildRequired) {
      editor.rebuildEditorContent();
      editor.getUpdater().flushModelEvents();
    }

    editor.clearBracesEnabledCells();
    editor.getUpdater().flushModelEvents();

    // TODO: remove this variable and simply mark editor as "needsRelayout" from the top editor cell + relayout it on .. next paint?
    boolean needsRelayout = restoreErrors(editor) | restoreTransactionals(editor);

    // Restore collapse states before restoring selection, otherwise selection inside initially collapsed cells disappears
    needsRelayout = restoreFoldingStates(myFoldableStates, editor) | needsRelayout;
    needsRelayout = restoreFoldingStates(mySaveSessionState ? myInitiallyCollapsedStates : myRestoreAlwaysStates, editor) | needsRelayout;

    editor.getSelectionManager().setSelectionInfoStack(mySelectionStack);
    EditorCell selectedCell = editor.getDeepestSelectedCell();
    if (selectedCell instanceof WithCaret) {
      ((WithCaret) selectedCell).setCaretVisible(false);
    }
    for (CellInfo collectionInfo : myCollectionsWithEnabledBraces) {
      EditorCell collection = collectionInfo.findCell(editor);
      if (!(collection instanceof jetbrains.mps.nodeEditor.cells.EditorCell_Collection)) {
        continue;
      }
      if (((jetbrains.mps.nodeEditor.cells.EditorCell_Collection) collection).usesBraces()) {
        ((jetbrains.mps.nodeEditor.cells.EditorCell_Collection) collection).enableBraces();
      }
    }

    if (needsRelayout) {
      editor.relayout();
    }
    if (myViewPosition != null) {
      editor.setViewPosition(myViewPosition);
    }
  }

  private boolean restoreFoldingStates(Iterable<Pair<CellInfo, Boolean>> foldingStates, EditorComponent editor) {
    boolean needsRelayout = false;
    for (Pair<CellInfo, Boolean> collapseState : foldingStates) {
      EditorCell cell = collapseState.o1.findCell(editor);
      if (!(cell instanceof EditorCell_Collection)) {
        continue;
      }
      EditorCell_Collection collection = (EditorCell_Collection) cell;
      needsRelayout = true;
      if (collapseState.o2) {
        collection.fold();
      } else {
        collection.unfold();
      }
    }
    return needsRelayout;
  }

  private boolean restoreErrors(EditorComponent editor) {
    boolean needsRelayout = false;
    for (EditorCell cell : new ArrayList<EditorCell>(editor.getCellTracker().getErrorCells())) {
      if (cell instanceof EditorCell_Label && ((EditorCell_Label) cell).isEditable()) {
        EditorCell_Label label = (EditorCell_Label) cell;
        label.synchronizeViewWithModel();
        needsRelayout = true;
      }
    }

    for (ErrorMarker error : myErrors) {
      needsRelayout = error.restore(editor) || needsRelayout;
    }
    return needsRelayout;
  }

  private boolean restoreTransactionals(EditorComponent editor) {
    boolean needsRelayout = false;
    for (EditorCell_Property transactionalProperty : editor.getCellTracker().getTransactionalCells()) {
      if (transactionalProperty.getModelAccessor() instanceof TransactionalPropertyAccessor) {
        TransactionalPropertyAccessor accessor = (TransactionalPropertyAccessor) transactionalProperty.getModelAccessor();
        if (accessor.hasValueToCommit()) {
          accessor.resetUncommittedValue();
          transactionalProperty.synchronize();
        }
      }
    }

    for (TransactionalPropertyState transactionalProperty : myTransactionalProperties) {
      needsRelayout = transactionalProperty.restore(editor) || needsRelayout;
    }
    return needsRelayout;
  }

  public boolean equals(Object object) {
    if (object == this) {
      return true;
    }
    if (object instanceof Memento) {
      Memento m = (Memento) object;
      if (EqualUtil.equals(mySelectionStack, m.mySelectionStack) && EqualUtil.equals(myCollectionsWithEnabledBraces, m.myCollectionsWithEnabledBraces) &&
          EqualUtil.equals(myFoldableStates, m.myFoldableStates) && EqualUtil.equals(myInitiallyCollapsedStates, m.myInitiallyCollapsedStates) &&
          EqualUtil.equals(myRestoreAlwaysStates, m.myRestoreAlwaysStates) && EqualUtil.equals(myErrors, m.myErrors) &&
          EqualUtil.equals(myTransactionalProperties, m.myTransactionalProperties) && EqualUtil.equals(myViewPosition, m.myViewPosition) &&
          Arrays.equals(myEnabledHints, m.myEnabledHints) && EqualUtil.equals(myEditedNodeReference, m.myEditedNodeReference)) {
        return true;
      }
    }
    return false;
  }

  public int hashCode() {
    return (mySelectionStack != null ? mySelectionStack.hashCode() : 0);
  }

  public String toString() {
    return "Editor Memento[\n" +
        "  selectedStack = " + mySelectionStack + "\n" +
        "  collectionsWithBraces = " + myCollectionsWithEnabledBraces + "\n" +
        "  foldableCells = " + myFoldableStates + "\n" +
        "  collapsedCells = " + myInitiallyCollapsedStates + "\n" +
        "  expandAlwaysCells = " + myRestoreAlwaysStates + "\n" +
        "  enabledHints = " + Arrays.toString(myEnabledHints) + "\n" +
        "  editedNodeReference = " + myEditedNodeReference + "\n" +
        "]\n";
  }

  private static final String SELECTION_STACK = "selectionStack";
  private static final String STACK_ELEMENT = "stackElement";
  private static final String FOLDABLE = "foldable";
  private static final String INITIALLY_COLLAPSED = "initiallyCollapsed";
  private static final String RESTORE_ALWAYS = "restoreAlways";
  private static final String COLLAPSED_ELEMENT = "collapsedElement";
  private static final String CELL_ID_ELEMENT = "cellIdElement";
  private static final String COLLAPSED_VALUE = "isCollapsed";
  private static final String VIEW_POSITION_X = "viewPositionX";
  private static final String VIEW_POSITION_Y = "viewPositionY";
  private static final String ENABLED_HINTS = "enabledHints";
  private static final String ENABLED_HINTS_ELEMENT = "enabledHintsElement";
  private static final String ENABLED_HINTS_ATTRIBUTE = "enabledHintsAttribute";
  private static final String ERROR_MARKERS = "errorMarkers";
  private static final String TRANSACTIONAL_PROPERTIES = "transactionalProperties";
  private static final String EDITED_NODE = "currentlyEditedNode";
  private static final String SAVE_SESSION_STATE = "saveSessionState";

  public void save(Element e) {
    if (myEditedNodeReference != null) {
      e.setAttribute(EDITED_NODE, SNodePointer.serialize(myEditedNodeReference));
    }
    e.setAttribute(SAVE_SESSION_STATE, Boolean.toString(mySaveSessionState));

    Element selectionStack = new Element(SELECTION_STACK);
    e.addContent(selectionStack);
    for (SelectionInfo selectionInfo : mySelectionStack) {
      Element stackElement = new Element(STACK_ELEMENT);
      ((SelectionInfoImpl) selectionInfo).persistToXML(stackElement);
      selectionStack.addContent(stackElement);
    }

    Element errorMarkers = new Element(ERROR_MARKERS);
    e.addContent(errorMarkers);
    for (ErrorMarker error : myErrors) {
      error.save(errorMarkers);
    }

    Element transactionalProperties = new Element(TRANSACTIONAL_PROPERTIES);
    e.addContent(transactionalProperties);
    for (TransactionalPropertyState transactionalProperty : myTransactionalProperties) {
      transactionalProperty.save(transactionalProperties);
    }

    saveFoldingStates(new Element(FOLDABLE), e, myFoldableStates);
    if (mySaveSessionState) {
      saveFoldingStates(new Element(INITIALLY_COLLAPSED), e, myInitiallyCollapsedStates);
    }
    saveFoldingStates(new Element(RESTORE_ALWAYS), e, myRestoreAlwaysStates);

    e.setAttribute(VIEW_POSITION_X, String.valueOf(myViewPosition.x));
    e.setAttribute(VIEW_POSITION_Y, String.valueOf(myViewPosition.y));
    if (myEnabledHints != null) {
      Element hintsElement = new Element(ENABLED_HINTS);
      for (String hint : myEnabledHints) {
        Element hintElement = new Element(ENABLED_HINTS_ELEMENT);
        hintElement.setAttribute(ENABLED_HINTS_ATTRIBUTE, hint);
        hintsElement.addContent(hintElement);
      }
      e.addContent(hintsElement);
    }
  }

  private static void saveFoldingStates(Element element, Element parentElement, Iterable<Pair<CellInfo, Boolean>> foldingStates) {
    for (Pair<CellInfo, Boolean> collapsedState : foldingStates) {
      if (collapsedState.o1 instanceof DefaultCellInfo) {
        Element collapsedElement = new Element(COLLAPSED_ELEMENT);
        collapsedElement.setAttribute(COLLAPSED_VALUE, collapsedState.o2.toString());
        Element cellId = new Element(CELL_ID_ELEMENT);
        ((DefaultCellInfo) collapsedState.o1).saveTo(cellId);
        collapsedElement.addContent(cellId);
        element.addContent(collapsedElement);
      } else {
        return;
      }
    }
    parentElement.addContent(element);
  }

  public static Memento load(Element e) {
    Memento memento = new Memento();
    Attribute editedNodeAttribute = e.getAttribute(EDITED_NODE);
    if (editedNodeAttribute != null) {
      memento.myEditedNodeReference = SNodePointer.deserialize(editedNodeAttribute.getValue());
    }

    memento.mySaveSessionState = Boolean.parseBoolean(e.getAttributeValue(SAVE_SESSION_STATE));

    Element selectionStack = e.getChild(SELECTION_STACK);
    if (selectionStack != null) {
      selectionStack.getChildren(STACK_ELEMENT).stream().map(SelectionInfoImpl::new).forEach(memento.mySelectionStack::add);
    }

    Element errorMarkers = e.getChild(ERROR_MARKERS);
    if (errorMarkers != null) {
      ErrorMarker.loadMarkers(errorMarkers).forEach(memento.myErrors::add);
    }

    Element transactionalProperties = e.getChild(TRANSACTIONAL_PROPERTIES);
    if (transactionalProperties != null) {
      TransactionalPropertyState.load(transactionalProperties).forEach(memento.myTransactionalProperties::add);
    }

    loadFoldingStates(e.getChild(FOLDABLE), memento.myFoldableStates);
    loadFoldingStates(e.getChild(INITIALLY_COLLAPSED), memento.myInitiallyCollapsedStates);
    loadFoldingStates(e.getChild(RESTORE_ALWAYS), memento.myRestoreAlwaysStates);

    try {
      int viewPositionX = Integer.valueOf(e.getAttributeValue(VIEW_POSITION_X));
      int viewPositionY = Integer.valueOf(e.getAttributeValue(VIEW_POSITION_Y));
      memento.myViewPosition = new Point(viewPositionX, viewPositionY);
    } catch (NumberFormatException nfe) {
    }

    Element hintsElement = e.getChild(ENABLED_HINTS);
    if (hintsElement != null) {
      List<String> enabledHints = new ArrayList<>();
      List children = hintsElement.getChildren(ENABLED_HINTS_ELEMENT);
      for (Object o : children) {
        enabledHints.add(((Element) o).getAttributeValue(ENABLED_HINTS_ATTRIBUTE));
      }
      memento.myEnabledHints = enabledHints.toArray(new String[enabledHints.size()]);
    }

    return memento;
  }

  private static void loadFoldingStates(Element element, List<Pair<CellInfo, Boolean>> result) {
    if (element == null) {
      return;
    }
    element.getChildren(COLLAPSED_ELEMENT).stream().map(el -> new Pair<CellInfo, Boolean>(DefaultCellInfo.loadFrom(el.getChild(CELL_ID_ELEMENT)),
        Boolean.valueOf(el.getAttributeValue(COLLAPSED_VALUE)))).forEach(result::add);
  }

  private static class ErrorMarker {
    private static final String ERROR_MARKER = "errorMarker";
    private static final String TEXT = "text";
    private static final String MODEL_TEXT = "modelText";
    private static final String PROPERTY_CELL = "propertyCell";

    private CellInfo myCellInfo;
    private String myText;
    private String myModelText = null;
    private boolean myPropertyCell = false;

    ErrorMarker(EditorCell_Label label) {
      myText = label.getText();
      myCellInfo = label.getCellInfo();
      if (label instanceof EditorCell_Property) {
        myModelText = ((EditorCell_Property) label).getLastModelText();
        myPropertyCell = true;
      }
    }

    private ErrorMarker(Element errorElement) {
      myText = errorElement.getAttributeValue(TEXT);
      myModelText = errorElement.getAttributeValue(MODEL_TEXT);
      myPropertyCell = Boolean.parseBoolean(errorElement.getAttributeValue(PROPERTY_CELL));
      myCellInfo = DefaultCellInfo.loadFrom(errorElement);
    }

    private boolean isPropertyCell() {
      return myPropertyCell;
    }

    public boolean restore(EditorComponent editor) {
      EditorCell cell = myCellInfo.findCell(editor);
      if (!(cell instanceof EditorCell_Label)) {
        return false;
      }
      EditorCell_Label cellLabel = (EditorCell_Label) cell;
      if (!cellLabel.isEditable()) {
        return false;
      }

      return isPropertyCell() ? restorePropertyCell(cellLabel) : restoreLabelCell(cellLabel);
    }

    private boolean restoreLabelCell(EditorCell_Label cellLabel) {
      if (canRestoreText(cellLabel)) {
        cellLabel.changeText(myText);
        return true;
      }
      return false;
    }

    private boolean restorePropertyCell(EditorCell_Label cell) {
      if (!(cell instanceof EditorCell_Property)) {
        return false;
      }
      EditorCell_Property cellProperty = (EditorCell_Property) cell;
      if (EqualUtil.equals(myModelText, cellProperty.getLastModelText()) && canRestoreText(cellProperty)) {
        cellProperty.changeText(myText);
        return true;
      }
      return false;
    }

    private boolean canRestoreText(EditorCell_Label cellLabel) {
      return !EqualUtil.equals(cellLabel.getText(), myText) && !cellLabel.isValidText(myText);
    }

    public void save(Element errorMarkers) {
      Element errorElement = new Element(ERROR_MARKER);
      errorElement.setAttribute(TEXT, myText);
      if (myModelText != null) {
        errorElement.setAttribute(MODEL_TEXT, myModelText);
      }
      errorElement.setAttribute(PROPERTY_CELL, Boolean.toString(myPropertyCell));
      ((DefaultCellInfo) myCellInfo).saveTo(errorElement);
      errorMarkers.addContent(errorElement);
    }

    static Stream<ErrorMarker> loadMarkers(Element errorMarkers) {
      return errorMarkers.getChildren(ERROR_MARKER).stream().map(ErrorMarker::new);
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }

      ErrorMarker that = (ErrorMarker) o;

      if (myPropertyCell != that.myPropertyCell) {
        return false;
      }
      if (!myCellInfo.equals(that.myCellInfo)) {
        return false;
      }
      if (!myText.equals(that.myText)) {
        return false;
      }
      return !(myModelText != null ? !myModelText.equals(that.myModelText) : that.myModelText != null);
    }

    @Override
    public int hashCode() {
      return myCellInfo.hashCode();
    }
  }

  private static class TransactionalPropertyState {
    private static final String UNCOMMITTED_VALUE = "uncommittedValue";
    private static final String TRANSACTIONAL_PROPERTY = "transactionalProperty";

    private final String myUncommittedValue;
    private final CellInfo myCellInfo;

    TransactionalPropertyState(EditorCell_Property propertyCell) {
      TransactionalPropertyAccessor accessor = (TransactionalPropertyAccessor) propertyCell.getModelAccessor();
      assert accessor.hasValueToCommit();
      myUncommittedValue = accessor.doGetValue();
      myCellInfo = propertyCell.getCellInfo();
    }

    private TransactionalPropertyState(Element transactionalElement) {
      myUncommittedValue = transactionalElement.getAttributeValue(UNCOMMITTED_VALUE);
      myCellInfo = DefaultCellInfo.loadFrom(transactionalElement);
    }

    public boolean restore(EditorComponent editor) {
      EditorCell cell = myCellInfo.findCell(editor);
      if (!(cell instanceof EditorCell_Property)) {
        return false;
      }
      EditorCell_Property propertyCell = (EditorCell_Property) cell;
      if (propertyCell.getModelAccessor() instanceof TransactionalPropertyAccessor) {
        TransactionalPropertyAccessor modelAccessor = (TransactionalPropertyAccessor) propertyCell.getModelAccessor();
        modelAccessor.doSetValue(myUncommittedValue);
        propertyCell.synchronize();
        return true;
      }
      return false;
    }

    public void save(Element transactionalProperties) {
      Element transactionalElement = new Element(TRANSACTIONAL_PROPERTY);
      transactionalElement.setAttribute(UNCOMMITTED_VALUE, myUncommittedValue);
      ((DefaultCellInfo) myCellInfo).saveTo(transactionalElement);
      transactionalProperties.addContent(transactionalElement);
    }

    public static Stream<TransactionalPropertyState> load(Element transactionalProperties) {
      return transactionalProperties.getChildren(TRANSACTIONAL_PROPERTY).stream().map(TransactionalPropertyState::new);
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }

      TransactionalPropertyState that = (TransactionalPropertyState) o;

      return myCellInfo.equals(that.myCellInfo) &&
          (myUncommittedValue == null ? that.myUncommittedValue == null : myUncommittedValue.equals(that.myUncommittedValue));
    }

    @Override
    public int hashCode() {
      return myCellInfo.hashCode();
    }
  }
}
