/*
 * Copyright 2003-2016 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.cells;

import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.cells.SubstituteInfo;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.NodeReadAccessCasterInEditor;
import jetbrains.mps.smodel.NodeReadAccessInEditorListener;
import jetbrains.mps.util.Computable;
import jetbrains.mps.util.Pair;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.module.ModelAccess;

/**
 * Author: Sergey Dmitriev
 * Created Sep 14, 2003
 */
public class EditorCell_Property extends EditorCell_Label implements SynchronizeableEditorCell {
  private final ModelAccessor myModelAccessor;
  private boolean myCommitInProgress;
  private boolean myCommitInCommand = true;
  private String myLastModelText;

  public EditorCell_Property(EditorContext editorContext, ModelAccessor accessor, SNode node) {
    super(editorContext, node, accessor.getText());
    myModelAccessor = accessor;
    if (myModelAccessor instanceof TransactionalPropertyAccessor) {
      TransactionalPropertyAccessor propertyAccessor = (TransactionalPropertyAccessor) myModelAccessor;
      propertyAccessor.setCell(this);
    }
    synchronizeViewWithModel();
  }

  public static EditorCell_Property create(jetbrains.mps.openapi.editor.EditorContext editorContext, ModelAccessor modelAccessor, SNode node) {
    NodeReadAccessInEditorListener listener = NodeReadAccessCasterInEditor.getReadAccessListener();
    if (modelAccessor instanceof PropertyAccessor) {
      if (listener != null) {
        listener.clearCleanlyReadAccessProperties();
      }
    }
    EditorCell_Property result = new EditorCell_Property(editorContext, modelAccessor, node);
    if (listener != null) {
      // TODO: specify property name directly - we know it from PropertyAccessor
      addPropertyDependenciesToEditor(listener, result);
    }
    return result;
  }

  private static void addPropertyDependenciesToEditor(NodeReadAccessInEditorListener listener, EditorCell_Property result) {
    for (Pair<SNodeReference, String> pair : listener.popCleanlyReadAccessedProperties()) {
      result.getEditorComponent().getUpdater().getCurrentUpdateSession().registerCleanDependency(result, pair);
    }
  }

  @Override
  public void synchronizeViewWithModel() {
    String text = myModelAccessor.getText();
    myLastModelText = text;
    setErrorState(!isValidText(text));
    setText(text);
  }

  @Override
  public void setSelected(boolean selected) {
    boolean oldSelected = isSelected();
    super.setSelected(selected);
    if (oldSelected && !selected && isTransactional()) {
      final Runnable commitCommand = new Runnable() {
        @Override
        public void run() {
          commit();
        }
      };
      if (myCommitInCommand) {
        getModelAccess().executeCommandInEDT(commitCommand);
      } else {
        getModelAccess().runWriteInEDT(commitCommand);
      }
    }
  }

  public boolean hasUncommittedValue() {
    if (!isTransactional()) {
      return false;
    }

    TransactionalModelAccessor transactionalModelAccessor = (TransactionalModelAccessor) myModelAccessor;
    return transactionalModelAccessor.hasValueToCommit();
  }

  /**
   * should be executed inside write action
   *
   * @return true if new value was committed to model / false if nothing was changed
   */
  public boolean commit() {
    getModelAccess().checkWriteAccess();
    // a solution for MPS-13531
    // better solution is to redispatch all currently waiting EDT commands inside MPSProject.dispose() method
    // currently not available - not possible to redispatch all waiting commands from AWT Thread.
    if (!SNodeUtil.isAccessible(getSNode(), getContext().getRepository())) {
      return false;
    }
    if (myCommitInProgress) {
      return false;
    }
    myCommitInProgress = true;
    try {
      boolean result = false;
      if (isTransactional()) {
        TransactionalModelAccessor transactionalModelAccessor = (TransactionalModelAccessor) myModelAccessor;
        if (transactionalModelAccessor.hasValueToCommit()) {
          transactionalModelAccessor.commit();
          result = true;
        }
        getEditor().relayout();
        return result;
      }
    } finally {
      myCommitInProgress = false;
    }
    return false;
  }

  @Override
  public void changeText(String text) {
    super.changeText(text);

    if (isValidText(text) && isEditable()) {
      myModelAccessor.setText(text);
      synchronizeViewWithModel();
      return;
    }

    if (isTransactional()) {
      ((TransactionalModelAccessor) myModelAccessor).resetUncommittedValue();
    }
    setErrorState(!isValidText(text));
  }

  public String getLastModelText() {
    return myLastModelText;
  }

  @Override
  public boolean isValidText(final String text) {
    return new ModelAccessHelper(getModelAccess()).runReadAction(new Computable<Boolean>() {
      @Override
      public Boolean compute() {
        return myModelAccessor.isValidText(text);
      }
    });
  }

  @Override
  public SubstituteInfo getSubstituteInfo() {
    final SubstituteInfo substituteInfo = super.getSubstituteInfo();
    return new ModelAccessHelper(getModelAccess()).runReadAction(new Computable<SubstituteInfo>() {
      @Override
      public SubstituteInfo compute() {
        if (substituteInfo != null) {
          substituteInfo.setOriginalText(myModelAccessor.getText());
        }
        return substituteInfo;
      }
    });
  }

  public ModelAccessor getModelAccessor() {
    return myModelAccessor;
  }

  public void setCommitInCommand(boolean commit) {
    myCommitInCommand = commit;
  }

  @Override
  public void synchronize() {
    synchronizeViewWithModel();
  }

  @Override
  public boolean canBeSynchronized() {
    return false;
  }

  private ModelAccess getModelAccess() {
    return getContext().getRepository().getModelAccess();
  }

  private boolean isTransactional() {
    return myModelAccessor instanceof TransactionalModelAccessor;
  }

  @Override
  public void onAdd() {
    super.onAdd();
    if (isTransactional()) {
      getEditor().getCellTracker().addTransactionalCell(this);
    }
  }

  @Override
  public void onRemove() {
    if (isTransactional()) {
      getEditor().getCellTracker().removeTransactionalCell(this);
    }
    super.onRemove();
  }
}
