/*
 * 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.generator.impl;

import jetbrains.mps.generator.IGeneratorLogger.ProblemDescription;
import jetbrains.mps.smodel.SNodeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import org.jetbrains.mps.openapi.language.SAbstractLink;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.model.SNode;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Role validation logic extracted out from AbstractTemplateGenerator
 * @author Artem Tikhomirov
 */
public class RoleValidation {
  private final boolean myShowBadChildWarning;
  private final RoleValidator mySuccessValidator = new RoleValidator();
  // the code might need refactoring to be more thread-friendly, e.g. validators per thread, not per single generator as it's now
  private final ConcurrentHashMap<SConcept, Map<SAbstractLink, RoleValidator>> validators = new ConcurrentHashMap<>();

  public RoleValidation(boolean showBadChildWarning) {
    myShowBadChildWarning = showBadChildWarning;
  }

  RoleValidator getValidator(SNode sourceNode, SReferenceLink role) {
    final SConcept srcConcept = sourceNode.getConcept();
    Map<SAbstractLink, RoleValidator> vmap = getCached(srcConcept);
    RoleValidator validator = vmap.get(role);
    if (validator != null) {
      return validator;
    }
    if (!srcConcept.isSubConceptOf(role.getOwner())) {
      String msg = String.format("Source node of %s concept could not have reference %s (from %s)", srcConcept.getQualifiedName(), role.getName(), role.getOwner().getQualifiedName());
      Status s = new Status(msg);
      validator = new RoleValidator(s);
    } else {
      validator = new AcceptableTargetValidator(role);
    }
    vmap.put(role, validator);
    return validator;
  }

  RoleValidator getValidator(SNode sourceNode, SContainmentLink role) {
    if (SNodeUtil.link_BaseConcept_smodelAttribute.equals(role)) {
      return mySuccessValidator;
    }
    final SConcept srcConcept = sourceNode.getConcept();
    Map<SAbstractLink, RoleValidator> vmap = getCached(srcConcept);
    RoleValidator validator = vmap.get(role);
    if (validator != null) {
      return validator;
    }
    if (!srcConcept.isSubConceptOf(role.getOwner())) {
      String msg = String.format("No child role %s (%s) known for source node's %s concept", role.getName(), role.getOwner().getQualifiedName(), srcConcept.getQualifiedName());
      Status s = new Status(msg);
      validator = new RoleValidator(s);
    } else {
      if (myShowBadChildWarning) {
        validator = new AcceptableTargetValidator(role);
      } else {
        validator = mySuccessValidator;
      }
    }
    vmap.put(role, validator);
    return validator;
  }


  @NotNull
  private Map<SAbstractLink, RoleValidator> getCached(SConcept concept) {
    Map<SAbstractLink, RoleValidator> vmap = validators.get(concept);
    if (vmap == null) {
      Map<SAbstractLink, RoleValidator> existing = validators.putIfAbsent(concept, vmap = new ConcurrentHashMap<>());
      if (existing != null) {
        vmap = existing;
      }
    }
    return vmap;
  }

  public static class RoleValidator {
    private final Status myStatus;

    protected RoleValidator() {
      myStatus = null;
    }

    protected RoleValidator(@NotNull Status status) {
      myStatus = status;
    }

    /**
     * @return null if validation succeed
     */
    public Status validate(SNode targetNode) {
      return myStatus;
    }
  }

  private static class AcceptableTargetValidator extends RoleValidator {
    private final SAbstractLink myLink;
    private final SAbstractConcept myLinkTarget;
    private final boolean myIsContainment;

    AcceptableTargetValidator(@NotNull SReferenceLink link) {
      myLink = link;
      myLinkTarget = link.getTargetConcept();
      myIsContainment = false;
    }

    AcceptableTargetValidator(@NotNull SContainmentLink link) {
      myLink = link;
      myLinkTarget = link.getTargetConcept();
      myIsContainment = true;
    }

    @Override
    public Status validate(SNode targetNode) {
      // XXX could keep set of concepts already checked to avoid isSubConceptOf (if expensive)
      if (targetNode.getConcept().isSubConceptOf(myLinkTarget)) {
        return null;
      }
      if (myIsContainment && DelayedChanges.isTempNode(targetNode)) {
        // temporary child node, ignore
        return null;
      }
      String expected = myLinkTarget.getQualifiedName();
      String was = targetNode.getConcept().getQualifiedName();
      String relationKind = myIsContainment ? "child" : "referent";
      String msg = String.format("%s '%s' is expected for role '%s' but was '%s'", relationKind, expected, myLink.getName(), was);
      return new Status(msg, GeneratorUtil.describe(targetNode, relationKind));
    }
  }

  public static class Status {
    private final String message;
    private final ProblemDescription[] descriptions;

    public Status(String message, ProblemDescription... descriptions) {
      this.message = message;
      this.descriptions = descriptions;
    }

    public String getMessage(String prefix) {
      return String.format("%s: %s", prefix, message);
    }

    public ProblemDescription[] describe(ProblemDescription... extras) {
      return GeneratorUtil.concat(this.descriptions, extras);
    }
  }
}
