/*
 * 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;

import jetbrains.mps.editor.runtime.descriptor.EditorBuilderEnvironment;
import jetbrains.mps.editor.runtime.style.StyleAttributes;
import jetbrains.mps.editor.runtime.style.StyleImpl;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.AttributeOperations;
import jetbrains.mps.nodeEditor.cells.EditorCellFactoryImpl;
import jetbrains.mps.nodeEditor.cells.EditorCell_Basic;
import jetbrains.mps.nodeEditor.cells.EditorCell_Collection;
import jetbrains.mps.nodeEditor.cells.EditorCell_Constant;
import jetbrains.mps.nodeEditor.cells.EditorCell_Property;
import jetbrains.mps.nodeEditor.cells.ModelAccessor;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.EditorCellFactory;
import jetbrains.mps.openapi.editor.style.Style;
import jetbrains.mps.openapi.editor.style.StyleAttribute;
import jetbrains.mps.openapi.editor.update.UpdateSession;
import jetbrains.mps.openapi.editor.update.AttributeKind;
import jetbrains.mps.smodel.SNodeUtil;
import jetbrains.mps.util.EqualUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SDataType;
import org.jetbrains.mps.openapi.language.SPrimitiveDataType;
import org.jetbrains.mps.openapi.language.SProperty;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SReference;

import java.awt.Color;
import java.util.Collection;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.LinkedList;

public abstract class AbstractDefaultEditor extends DefaultNodeEditor implements EditorBuilderEnvironment {
  private static final String NAME_NAME = "name";
  private static final int NAME_PRIORITY = 10000;
  private static final String IDENTIFIER_NAME = "identifier";
  private static final int IDENTIFIER_PRIORITY = 1700;
  private static final int NAME_ADD_PRIORITY = 1000;
  private static final String QUALIFIED_NAME = "qualified";
  private static final int QUALIFIED_PRIORITY = 200;
  private static final Color FIRST_LABEL_BACKGROUND_COLOR = new Color(107, 142, 20, 100);

  private SNode mySNode;
  private SConcept myConcept;
  private EditorContext myEditorContext;

  private Deque<EditorCell_Collection> collectionStack = new LinkedList<EditorCell_Collection>();
  private int currentCollectionIdNumber = 0;
  private int currentConstantIdNumber = 0;

  private Collection<SProperty> myProperties = new LinkedHashSet<SProperty>();
  private Collection<SReferenceLink> myReferenceLinks = new LinkedHashSet<SReferenceLink>();
  private Collection<SContainmentLink> myContainmentLinks = new LinkedHashSet<SContainmentLink>();


  public AbstractDefaultEditor(@NotNull SConcept concept) {
    myConcept = concept;
  }

  @Override
  public EditorCell createEditorCell(EditorContext editorContext, SNode node) {
    myEditorContext = editorContext;
    mySNode = node;
    assert myConcept.equals(mySNode.getConcept());
    init();
    SProperty nameProperty = getNameProperty();
    EditorCell_Collection mainCellCollection = pushCollection();
    mainCellCollection.setBig(true);
    mainCellCollection.setCellContext(getCellFactory().getCellContext());
    addLabel(camelToLabel(myConcept.getName()));
    addStyle(StyleAttributes.TEXT_BACKGROUND_COLOR, FIRST_LABEL_BACKGROUND_COLOR);
    if (nameProperty != null) {
      getProperties().remove(nameProperty);
      addPropertyCell(nameProperty);
    }
    addReferences();
    if (!getContainmentLinks().isEmpty() || !getProperties().isEmpty() || isAttribute()) {
      addPropertiesAndChildren();
    }
    popCollection();
    return mainCellCollection;
  }

  protected void init() {
    assert mySNode != null && myConcept != null;
    for (SProperty sProperty : mySNode.getProperties()) {
      if (!sProperty.getOwner().equals(SNodeUtil.concept_BaseConcept)) {
        myProperties.add(sProperty);
      }
    }

    for (SReference sReference : mySNode.getReferences()) {
      SReferenceLink link = sReference.getLink();
      assert link != null : "Null meta-link from node: " + this.mySNode + ", role: " + sReference.getRole();
      if (!link.getOwner().equals(SNodeUtil.concept_BaseConcept)) {
        myReferenceLinks.add(link);
      }
    }

    for (SNode child : this.mySNode.getChildren()) {
      SContainmentLink containmentLink = child.getContainmentLink();
      assert containmentLink != null : "Null meta-containmentLink returned for the child of node: " + this.mySNode + ", child: " + child;
      if (!containmentLink.getOwner().equals(SNodeUtil.concept_BaseConcept)) {
        myContainmentLinks.add(containmentLink);
      }
    }
  }

  protected void addProperty(SProperty property) {
    myProperties.add(property);
  }

  protected void addContainmentLink(SContainmentLink containmentLink) {
    myContainmentLinks.add(containmentLink);
  }

  protected void addReferenceLink(SReferenceLink link) {
    myReferenceLinks.add(link);
  }

  private SProperty getNameProperty() {
    SProperty nameProperty = null;
    int maxPriority = 0;
    for (SProperty property : getProperties()) {
      SDataType dataType = property.getType();
      if (!(dataType instanceof SPrimitiveDataType && ((SPrimitiveDataType) dataType).getType() == SPrimitiveDataType.STRING)) {
        continue;
      }
      String propertyName = property.getName();
      int propertyPriority = getPropertyPriority(propertyName);
      if (maxPriority < propertyPriority) {
        maxPriority = propertyPriority;
        nameProperty = property;
      }
    }
    return nameProperty;
  }

  private int getPropertyPriority(@NotNull String propertyName) {
    if (NAME_NAME.equals(propertyName)) {
      return NAME_PRIORITY;
    }
    int priority = 0;
    if (propertyName.toLowerCase().contains(IDENTIFIER_NAME)) {
      priority += IDENTIFIER_PRIORITY;
    }
    if (propertyName.toLowerCase().contains(NAME_NAME)) {
      priority += NAME_ADD_PRIORITY;
    }
    if (propertyName.toLowerCase().contains(QUALIFIED_NAME)) {
      priority += QUALIFIED_PRIORITY;
    }
    return priority;
  }

  private void addPropertiesAndChildren() {
    addLabel("{");
    addStyle(StyleAttributes.MATCHING_LABEL, "body-brace");
    addNewLine();
    pushCollection();
    setIndent(collectionStack.peek());
    addProperties();
    addLabel("");
    addNewLine();
    getCellFactory().pushCellContext();
    getCellFactory().removeCellContextHints(EditorCellFactoryImpl.BASE_REFLECTIVE_EDITOR_HINT);
    try {
      addChildren();
    } finally {
      getCellFactory().popCellContext();
    }
    addAttributedEntity();
    popCollection();
    addLabel("}");
    addStyle(StyleAttributes.MATCHING_LABEL, "body-brace");
  }

  protected Collection<SProperty> getProperties() {
    return myProperties;
  }

  protected Collection<SReferenceLink> getReferenceLinks() {
    return myReferenceLinks;
  }

  protected Collection<SContainmentLink> getContainmentLinks() {
    return myContainmentLinks;
  }

  protected abstract void addPropertyCell(final SProperty property);

  protected abstract void addChildCell(final SContainmentLink referenceLink);

  protected abstract void addReferenceCell(final SReferenceLink referenceLink);

  private void addProperties() {
    for (SProperty property : getProperties()) {
      addRoleLabel(property.getName(), "property");
      addPropertyCell(property);
      addNewLine();
    }
  }

  private void addReferences() {
    for (SReferenceLink reference : getReferenceLinks()) {
      addRoleLabel(reference.getName(), "reference");
      addReferenceCell(reference);
    }
  }

  private void addChildren() {
    for (SContainmentLink link : getContainmentLinks()) {
      addRoleLabel(link.getName(), "link");
      addNewLine();
      addChildCell(link);
      addNewLine();
    }
  }

  private void addAttributedEntity() {
    if (AttributeOperations.isNodeAttribute(mySNode)) {
      addAttributedCell("attributed node:", AttributeKind.NODE);
    } else if (AttributeOperations.isPropertyAttribute(mySNode)) {
      addAttributedCell("attributed property:", AttributeKind.PROPERTY);
    } else if (AttributeOperations.isLinkAttribute(mySNode)) {
      addAttributedCell("attributed reference:", AttributeKind.REFERENCE);
    }
  }

  private void addAttributedCell(String label, AttributeKind attributeKind) {
    addLabel(label);
    addNewLine();
    EditorCell editorCell = myEditorContext.getEditorComponent().getUpdater().getCurrentUpdateSession().getAttributedCell(attributeKind, mySNode);
    addCell(editorCell);
    addNewLine();
  }

  private boolean isAttribute() {
    return AttributeOperations.isNodeAttribute(mySNode) || AttributeOperations.isPropertyAttribute(mySNode) || AttributeOperations.isLinkAttribute(mySNode);
  }

  private void addRoleLabel(String role, String type) {
    if (role == null) {
      role = "<no " + type + ">";
    }
    addLabel(camelToLabel(role));
    addLabel(":");
  }

  protected void addLabel(String label) {
    addLabel(label, false);
  }

  protected void addLabel(String label, boolean editable) {
    EditorCell_Collection cellCollection = collectionStack.peek();
    EditorCell_Constant childLabel = new EditorCell_Constant(getEditorContext(), mySNode, label, editable);
    childLabel.setCellId("constant_" + currentConstantIdNumber);
    cellCollection.addEditorCell(childLabel);
    currentConstantIdNumber++;
  }

  protected void addCell(EditorCell cell) {
    collectionStack.peek().addEditorCell(cell);
  }

  private String camelToLabel(String text) {
    StringBuilder sb = new StringBuilder();
    char[] cs = text.toCharArray();
    for (int i = 0; i < cs.length; i++) {
      if (Character.isUpperCase(cs[i])) {
        if (sb.length() > 0) {
          sb.append(' ');
        }
        if (i + 1 < cs.length && Character.isLowerCase(cs[i + 1])) {
          sb.append(Character.toLowerCase(cs[i]));
          continue;
        }
        while (i + 1 < cs.length && !(Character.isLowerCase(cs[i + 1]))) {
          sb.append(cs[i]);
          i++;
        }
        if (i + 1 < cs.length) {
          i--;
          continue;
        }
      }
      sb.append(cs[i]);
    }
    return sb.toString();
  }

  protected EditorCell createReferentEditorCell(EditorContext editorContext, SReferenceLink link, final SNode targetNode) {
    EditorCell_Property result = new EditorCell_Property(editorContext, new ModelAccessor() {
      public String getText() {
        String name = targetNode.getName();
        if (name != null) {
          return name;
        }
        return targetNode.getPresentation();
      }

      public void setText(String s) {
      }

      public boolean isValidText(String s) {
        return EqualUtil.equals(s, getText());
      }
    }, targetNode);
    result.setRole(link.getName());
    result.setReferenceCell(true);
    return result;
  }

  protected void setSemanticNodeToCells(jetbrains.mps.openapi.editor.cells.EditorCell rootCell, SNode semanticNode) {
    if (!(rootCell instanceof EditorCell_Basic) || semanticNode == null) {
      return;
    }
    ((EditorCell_Basic) rootCell).setSNode(semanticNode);
    if (rootCell instanceof jetbrains.mps.openapi.editor.cells.EditorCell_Collection) {
      for (EditorCell child : ((jetbrains.mps.openapi.editor.cells.EditorCell_Collection) rootCell)) {
        setSemanticNodeToCells(child, semanticNode);
      }
    }
  }


  private EditorCell_Collection pushCollection() {
    EditorCell_Collection newCollection = EditorCell_Collection.createIndent2(getEditorContext(), mySNode);
    collectionStack.push(newCollection);
    return newCollection;
  }

  private EditorCell_Collection popCollection() {
    if (collectionStack.isEmpty()) {
      return null;
    }
    EditorCell_Collection result = collectionStack.pop();
    result.setCellId("collection_" + currentCollectionIdNumber);
    currentCollectionIdNumber++;
    if (!collectionStack.isEmpty()) {
      collectionStack.peek().addEditorCell(result);
    }
    return result;
  }

  protected void addNewLine() {
    addStyle(getLastCell(), StyleAttributes.INDENT_LAYOUT_NEW_LINE);
  }

  private EditorCell getLastCell() {
    EditorCell_Collection collection = collectionStack.peek();
    EditorCell lastCell = collection;
    if (!collection.isEmpty()) {
      lastCell = collection.lastCell();
    }
    return lastCell;
  }

  protected void setIndent(EditorCell cell) {
    addStyle(cell, StyleAttributes.INDENT_LAYOUT_INDENT);
  }

  protected <T> void addStyle(StyleAttribute<T> attribute, T value) {
    addStyle(getLastCell(), attribute, value);
  }

  protected <T> void addStyle(EditorCell cell, StyleAttribute<T> attribute, T value) {
    Style style = new StyleImpl();
    style.set(attribute, value);
    cell.getStyle().putAll(style);
  }

  protected void addStyle(EditorCell cell, StyleAttribute<Boolean> attribute) {
    addStyle(cell, attribute, true);
  }

  public static AbstractDefaultEditor createEditor(SNode node) {
    SConcept concept = node.getConcept();
    return concept.isValid() ? new DefaultEditor(concept) : new ReadOnlyDefaultEditor(concept);
  }

  /**
   * @deprecated since MPS 3.5 use {{@link #getNode()}
   */
  @Deprecated
  protected SNode getSNode() {
    return mySNode;
  }

  protected SConcept getConcept() {
    return myConcept;
  }

  public EditorContext getEditorContext() {
    return myEditorContext;
  }

  @Override
  public SNode getNode() {
    return mySNode;
  }

  @Override
  public EditorCellFactory getCellFactory() {
    return getUpdateSession().getCellFactory();
  }

  @Override
  public UpdateSession getUpdateSession() {
    return getEditorContext().getEditorComponent().getUpdater().getCurrentUpdateSession();
  }
}
