/*
 * Copyright 2003-2015 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.smodel;

import jetbrains.mps.RuntimeFlags;
import jetbrains.mps.logging.Logger;
import jetbrains.mps.smodel.adapter.structure.ref.SReferenceLinkAdapterById;
import jetbrains.mps.smodel.language.ConceptRegistryUtil;
import jetbrains.mps.smodel.legacy.ConceptMetaInfoConverter;
import jetbrains.mps.smodel.runtime.ConstraintsDescriptor;
import jetbrains.mps.smodel.runtime.PropertyConstraintsDescriptor;
import jetbrains.mps.smodel.runtime.ReferenceConstraintsDescriptor;
import jetbrains.mps.util.Pair;
import org.apache.log4j.LogManager;
import org.jetbrains.annotations.Nullable;
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.SNodeAccessUtil;

import java.util.HashSet;
import java.util.Set;

public class SNodeAccessUtilImpl extends SNodeAccessUtil {
  private static Logger LOG = Logger.wrap(LogManager.getLogger(SNodeAccessUtil.class));

  //SNodeAccessUtilImpl has only one instance, so we can omit remove() here though the field is not static
  private final ThreadLocal<Set<Pair<org.jetbrains.mps.openapi.model.SNode, SProperty>>> ourPropertySettersInProgress = new InProgressThreadLocal<SProperty>();
  private final ThreadLocal<Set<Pair<org.jetbrains.mps.openapi.model.SNode, SProperty>>> ourPropertyGettersInProgress = new InProgressThreadLocal<SProperty>();
  private final ThreadLocal<Set<Pair<org.jetbrains.mps.openapi.model.SNode, SReferenceLink>>> ourSetReferentEventHandlersInProgress =
      new InProgressThreadLocal<SReferenceLink>();

  @Override
  protected boolean hasPropertyImpl(org.jetbrains.mps.openapi.model.SNode node, SProperty property) {
    node.hasProperty(property); //todo this is to invoke corresponding read access. try to remove it by merging 2 types of access
    String property_internal = node.getProperty(property);
    return !SModelUtil_new.isEmptyPropertyValue(property_internal);
  }

  @Override
  protected boolean hasPropertyImpl(org.jetbrains.mps.openapi.model.SNode node, String name) {
    return hasPropertyImpl(node, ((ConceptMetaInfoConverter) node.getConcept()).convertProperty(name));
  }

  @Override
  public String getPropertyImpl(org.jetbrains.mps.openapi.model.SNode node, SProperty property) {
    if (RuntimeFlags.isMergeDriverMode()) return node.getProperty(property);

    Set<Pair<SNode, SProperty>> getters = ourPropertyGettersInProgress.get();
    Pair<SNode, SProperty> current = new Pair<SNode, SProperty>(node, property);
    if (getters.contains(current)) return node.getProperty(property);

    getters.add(current);
    try {
      PropertyConstraintsDescriptor descriptor = PropertySupport.getPropertyConstraintsDescriptor(node, property);
      Object getterValue = descriptor != null ? descriptor.getValue(node) : node.getProperty(property);
      return getterValue == null ? null : String.valueOf(getterValue);
    } catch (Throwable t) {
      LOG.error(t);
      return null;
    } finally {
      getters.remove(current);
    }
  }

  private static ReferenceConstraintsDescriptor getReferenceConstraintsDescriptor(SNode node, SReferenceLink referenceLink) {
    ConstraintsDescriptor constraintsDescriptor = ConceptRegistryUtil.getConstraintsDescriptor(node.getConcept());
    ReferenceConstraintsDescriptor descriptor;
    descriptor = constraintsDescriptor.getReference(referenceLink);
    return descriptor;
  }

  @Override
  public String getPropertyImpl(org.jetbrains.mps.openapi.model.SNode node, String name) {
    return getPropertyImpl(node, ((ConceptMetaInfoConverter) node.getConcept()).convertProperty(name));
  }

  @Override
  public void setPropertyImpl(org.jetbrains.mps.openapi.model.SNode node, SProperty property, String propertyValue) {
    Set<Pair<SNode, SProperty>> threadSet = ourPropertySettersInProgress.get();
    Pair<SNode, SProperty> pair = new Pair<SNode, SProperty>(node, property);

    //todo try to remove
    if (threadSet.contains(pair)) {
      node.setProperty(property, propertyValue);
      return;
    }

    PropertyConstraintsDescriptor descriptor = PropertySupport.getPropertyConstraintsDescriptor(node, property);
    threadSet.add(pair);
    try {
      if (descriptor != null) {
        descriptor.setValue(node, propertyValue);
      } else {
        LOG.error(
            "Can't find property constraints for property `" + property.getName() + "`. Setting directly. Value: `" + propertyValue + "`.", node);
        node.setProperty(property, propertyValue);
      }
    } catch (Exception t) {
      LOG.error(t);
    } finally {
      threadSet.remove(pair);
    }
  }

  @Override
  public void setPropertyImpl(org.jetbrains.mps.openapi.model.SNode node, String propertyName, String propertyValue) {
    setPropertyImpl(node, ((ConceptMetaInfoConverter) node.getConcept()).convertProperty(propertyName), propertyValue);
  }


  @Override
  public void setReferenceTargetImpl(org.jetbrains.mps.openapi.model.SNode node, SReferenceLink referenceLink,
      @Nullable org.jetbrains.mps.openapi.model.SNode target) {
    // invoke custom referent set event handler
    Set<Pair<SNode, SReferenceLink>> threadSet = ourSetReferentEventHandlersInProgress.get();
    Pair<SNode, SReferenceLink> pair = new Pair<SNode, SReferenceLink>(node, referenceLink);
    if (threadSet.contains(pair)) {
      node.setReferenceTarget(referenceLink, target);
      return;
    }

    ReferenceConstraintsDescriptor descriptor = getReferenceConstraintsDescriptor(node, referenceLink);

    if (descriptor == null) {
      LOG.error(
          "Can't find reference constraints for reference `" + referenceLink.getRoleName() + "`. Setting directly.", node);
      node.setReferenceTarget(referenceLink, target);
      return;
    }

    threadSet.add(pair);

    try {
      org.jetbrains.mps.openapi.model.SReference r = node.getReference(referenceLink);
      org.jetbrains.mps.openapi.model.SNode oldReferent = r == null ? null : SReference.getTargetNodeSilently(r);
      if (descriptor.validate(node, oldReferent, target)) {
        node.setReferenceTarget(referenceLink, target);
        descriptor.onReferenceSet(node, oldReferent, target);
      }
    } finally {
      threadSet.remove(pair);
    }
  }

  @Override
  public void setReferenceTargetImpl(org.jetbrains.mps.openapi.model.SNode node, String role, @Nullable org.jetbrains.mps.openapi.model.SNode target) {
    setReferenceTargetImpl(node, ((ConceptMetaInfoConverter) node.getConcept()).convertAssociation(role), target);
  }

  @Override
  public void setReferenceImpl(org.jetbrains.mps.openapi.model.SNode node, SReferenceLink referenceLink,
      @Nullable org.jetbrains.mps.openapi.model.SReference reference) {
    //todo for symmetry.Not yet used
    node.setReference(referenceLink, reference);
  }

  public void setReferenceImpl(org.jetbrains.mps.openapi.model.SNode node, String role, @Nullable org.jetbrains.mps.openapi.model.SReference reference) {
    setReferenceImpl(node, ((ConceptMetaInfoConverter) node.getConcept()).convertAssociation(role), reference);
  }

  private class InProgressThreadLocal<T> extends ThreadLocal<Set<Pair<org.jetbrains.mps.openapi.model.SNode, T>>> {
    @Override
    protected Set<Pair<org.jetbrains.mps.openapi.model.SNode, T>> initialValue() {
      return new HashSet<Pair<SNode, T>>();
    }
  }
}
