/*
 * Copyright 2003-2017 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.interpreted;

import jetbrains.mps.generator.impl.GenerationFailureException;
import jetbrains.mps.generator.impl.TemplateQueryException;
import jetbrains.mps.generator.impl.query.CallArgumentQuery;
import jetbrains.mps.generator.impl.query.CreateRootCondition;
import jetbrains.mps.generator.impl.query.DropAttributeRuleCondition;
import jetbrains.mps.generator.impl.query.DropRuleCondition;
import jetbrains.mps.generator.impl.query.IfMacroCondition;
import jetbrains.mps.generator.impl.query.InlineSwitchCaseCondition;
import jetbrains.mps.generator.impl.query.InsertMacroQuery;
import jetbrains.mps.generator.impl.query.MapConfigurationCondition;
import jetbrains.mps.generator.impl.query.MapNodeQuery;
import jetbrains.mps.generator.impl.query.MapPostProcessor;
import jetbrains.mps.generator.impl.query.MapRootRuleCondition;
import jetbrains.mps.generator.impl.query.PatternRuleQuery;
import jetbrains.mps.generator.impl.query.PropertyValueQuery;
import jetbrains.mps.generator.impl.query.QueryKey;
import jetbrains.mps.generator.impl.query.QueryKeyImpl;
import jetbrains.mps.generator.impl.query.QueryProviderBase;
import jetbrains.mps.generator.impl.query.ReductionRuleCondition;
import jetbrains.mps.generator.impl.query.ReferenceTargetQuery;
import jetbrains.mps.generator.impl.query.ScriptCodeBlock;
import jetbrains.mps.generator.impl.query.SourceNodeQuery;
import jetbrains.mps.generator.impl.query.SourceNodesQuery;
import jetbrains.mps.generator.impl.query.VariableValueQuery;
import jetbrains.mps.generator.impl.query.WeaveAnchorQuery;
import jetbrains.mps.generator.impl.query.WeaveRuleCondition;
import jetbrains.mps.generator.impl.query.WeaveRuleQuery;
import jetbrains.mps.generator.template.CreateRootRuleContext;
import jetbrains.mps.generator.template.DropAttributeRuleContext;
import jetbrains.mps.generator.template.DropRootRuleContext;
import jetbrains.mps.generator.template.IfMacroContext;
import jetbrains.mps.generator.template.InlineSwitchCaseContext;
import jetbrains.mps.generator.template.InsertMacroContext;
import jetbrains.mps.generator.template.MapRootRuleContext;
import jetbrains.mps.generator.template.MapSrcMacroContext;
import jetbrains.mps.generator.template.MapSrcMacroPostProcContext;
import jetbrains.mps.generator.template.MappingScriptContext;
import jetbrains.mps.generator.template.PatternRuleContext;
import jetbrains.mps.generator.template.PropertyMacroContext;
import jetbrains.mps.generator.template.ReductionRuleQueryContext;
import jetbrains.mps.generator.template.ReferenceMacroContext;
import jetbrains.mps.generator.template.SourceSubstituteMacroNodeContext;
import jetbrains.mps.generator.template.SourceSubstituteMacroNodesContext;
import jetbrains.mps.generator.template.TemplateArgumentContext;
import jetbrains.mps.generator.template.TemplateFunctionMethodName;
import jetbrains.mps.generator.template.TemplateQueryContext;
import jetbrains.mps.generator.template.TemplateVarContext;
import jetbrains.mps.generator.template.WeavingAnchorContext;
import jetbrains.mps.generator.template.WeavingMappingRuleContext;
import jetbrains.mps.lang.pattern.GeneratedMatchingPattern;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.AttributeOperations;
import jetbrains.mps.util.IterableUtil;
import jetbrains.mps.util.QueryMethods;
import jetbrains.mps.util.QueryMethods.IllegalQueryMethodException;
import jetbrains.mps.util.QueryMethods.QueryMethod;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SProperty;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeAccessUtil;
import org.jetbrains.mps.openapi.model.SNodeReference;

import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.Collections;

/**
 * Access to QueriesGenerated methods via reflection
 *
 * @author Artem Tikhomirov
 */
public class ReflectiveQueryProvider extends QueryProviderBase {
  private final QueryMethods myQueryMethods;

  public ReflectiveQueryProvider(Class<?> generatedQueries) {
    super(1);
    myQueryMethods = new QueryMethods(generatedQueries);
  }

  @NotNull
  @Override
  public CreateRootCondition getCreateRootRuleCondition(@NotNull QueryKey identity) {
    String conditionMethod = TemplateFunctionMethodName.createRootRule_Condition(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl(myQueryMethods, conditionMethod);
  }

  @NotNull
  @Override
  public MapRootRuleCondition getMapRootRuleCondition(@NotNull QueryKey identity) {
    String conditionMethod = TemplateFunctionMethodName.baseMappingRule_Condition(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl(myQueryMethods, conditionMethod);
  }

  @NotNull
  @Override
  public ReductionRuleCondition getReductionRuleCondition(@NotNull QueryKey identity) {
    String conditionMethod = TemplateFunctionMethodName.baseMappingRule_Condition(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl(myQueryMethods, conditionMethod);
  }

  @NotNull
  @Override
  public PatternRuleQuery getPatternRuleCondition(@NotNull QueryKey identity) {
    String methodName = TemplateFunctionMethodName.patternRule_Condition(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl2<>(myQueryMethods, methodName, null);
  }

  @NotNull
  @Override
  public DropRuleCondition getDropRuleCondition(@NotNull QueryKey identity) {
    String conditionMethod = TemplateFunctionMethodName.dropRootRule_Condition(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl(myQueryMethods, conditionMethod, true);
  }

  @NotNull
  @Override
  public DropAttributeRuleCondition getDropAttributeRuleCondition(@NotNull QueryKey identity) {
    String conditionMethod = TemplateFunctionMethodName.dropAttributeRule_Condition(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl(myQueryMethods, conditionMethod, true);
  }

  @NotNull
  @Override
  public WeaveRuleCondition getWeaveRuleCondition(@NotNull QueryKey identity) {
    String conditionMethod = TemplateFunctionMethodName.baseMappingRule_Condition(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl(myQueryMethods, conditionMethod);
  }

  @NotNull
  @Override
  public WeaveRuleQuery getWeaveRuleQuery(@NotNull QueryKey identity) {
    String contentNodeMethod = TemplateFunctionMethodName.weaving_MappingRule_ContextNodeQuery(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl2<>(myQueryMethods, contentNodeMethod, null);
  }

  @NotNull
  @Override
  public WeaveAnchorQuery getWeaveAnchorQuery(@NotNull QueryKey identity) {
    String anchorQueryMethod = TemplateFunctionMethodName.weaving_AnchorQuery(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl2<>(myQueryMethods, anchorQueryMethod, null);
  }

  @NotNull
  @Override
  public ScriptCodeBlock getScriptCodeBlock(@NotNull QueryKey identity) {
    String codeBlockMethod = TemplateFunctionMethodName.mappingScript_CodeBlock(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl2<>(myQueryMethods, codeBlockMethod, null /*script doesn't return anything, this is just to tell we need default no-op behavior*/);
  }

  @NotNull
  @Override
  public MapConfigurationCondition getMapConfigurationCondition(@NotNull QueryKey identity) {
    String conditionMethod = TemplateFunctionMethodName.mappingConfiguration_Condition(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl(myQueryMethods, conditionMethod, true);
  }

  @NotNull
  @Override
  public SourceNodeQuery getSourceNodeQuery(@NotNull QueryKey identity) {
    String methodName = TemplateFunctionMethodName.sourceSubstituteMacro_SourceNodeQuery(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl2<SNode>(myQueryMethods, methodName, null);
  }

  @NotNull
  @Override
  public SourceNodesQuery getSourceNodesQuery(@NotNull QueryKey identity) {
    String methodName = TemplateFunctionMethodName.sourceSubstituteMacro_SourceNodesQuery(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl2<Iterable<SNode>>(myQueryMethods, methodName, Collections.emptyList());
  }

  @NotNull
  @Override
  public PropertyValueQuery getPropertyValueQuery(@NotNull QueryKey identity) {
    String methodName = TemplateFunctionMethodName.propertyMacro_GetPropertyValue(((QueryKeyImpl) identity).getQueryNodeId());
    // FIXME this is wrong use of getAPITransitionNode(). The problem here is that PVQ, unlike other queries, is expected to tell
    //       SProperty + default template value. Generated code could have these injected, interpreted needs to calculate these
    //       and can easily do so in TemplateNode, however both need to hide behind QueryKey api which doesn't suggest means to pass extra values
    //       Shall re-design this and decide whether I need Query to be more than just executable code (SProperty+default value look reasonable to
    //       get generated right away along with the query!) or it complicates matters way too much.
    SNode propertyMacro = ((QueryKeyImpl) identity).getAPITransitionNode();
    SNode templateNode = propertyMacro.getParent();
    final SProperty property = AttributeOperations.getProperty(propertyMacro);
    final Object templateValue = SNodeAccessUtil.getProperty(templateNode, property);
    return new PropMacro(myQueryMethods, propertyMacro.getReference(), methodName, property, templateValue);
  }

  @NotNull
  @Override
  public IfMacroCondition getIfMacroCondition(@NotNull QueryKey identity) {
    String methodName = TemplateFunctionMethodName.ifMacro_Condition(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl(myQueryMethods, methodName, false);
  }

  @NotNull
  @Override
  public InlineSwitchCaseCondition getInlineSwitchCaseCondition(@NotNull QueryKey identity) {
    final String methodName = TemplateFunctionMethodName.baseMappingRule_Condition(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl(myQueryMethods, methodName, false);
  }

  @NotNull
  @Override
  public ReferenceTargetQuery getReferenceTargetQuery(@NotNull QueryKey identity) {
    // XXX in fact, RQP may keep template model and find actual references based on identity's information
    // so that ReferenceTargetQuery can tell link + defaultResolveInfo (like PropertyValueQuery does)
    String methodName = TemplateFunctionMethodName.referenceMacro_GetReferent(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl2<>(myQueryMethods, methodName, null);
  }

  @NotNull
  @Override
  public CallArgumentQuery getTemplateCallArgumentQuery(@NotNull QueryKey identity) {
    String methodName = TemplateFunctionMethodName.templateArgumentQuery(((QueryKeyImpl) identity).getQueryNodeId());
    // DefaultQueryExecutionContext used to evaluate to null if no method was found.
    // We need that to support bootstrap for generator templates (e.g. if I add an argument to a CALL inside QueriesGenerated template)
    return new Impl2<>(myQueryMethods, methodName, null);
  }

  @NotNull
  @Override
  public VariableValueQuery getVariableValueQuery(@NotNull QueryKey identity) {
    String methodName = TemplateFunctionMethodName.varValue_Query(((QueryKeyImpl) identity).getQueryNodeId());
    // DefaultQueryExecutionContext used to evaluate to null if no method was found.
    // We need that to support bootstrap for generator templates (e.g. if I add a new VAR into QueriesGenerated template)
    return new Impl2<>(myQueryMethods, methodName, null);
  }

  @NotNull
  @Override
  public InsertMacroQuery getInsertMacroQuery(@NotNull QueryKey identity) {
    String methodName = TemplateFunctionMethodName.insertMacro_Query(((QueryKeyImpl) identity).getQueryNodeId());
    // DefaultQueryExecutionContext used to evaluate to null if no method was found.
    // We need that to support bootstrap for generator templates (e.g. if I add a new INSERT into QueriesGenerated template of any
    // language with bootstrap dependency to self (e.g. bl.collections))
    return new Impl2<>(myQueryMethods, methodName, null);
  }

  @NotNull
  @Override
  public MapNodeQuery getMapNodeQuery(@NotNull QueryKey identity) {
    String methodName = TemplateFunctionMethodName.mapSrcMacro_MapperFunction(((QueryKeyImpl) identity).getQueryNodeId());
    // it's not expected for mapper function to return null
    return new Impl2<>(myQueryMethods, methodName);
  }

  @NotNull
  @Override
  public MapPostProcessor getMapPostProcessor(@NotNull QueryKey identity) {
    String methodName = TemplateFunctionMethodName.mapSrcMacro_PostMapperFunction(((QueryKeyImpl) identity).getQueryNodeId());
    return new Impl2<>(myQueryMethods, methodName);
  }

  /*package*/ static GenerationFailureException fromQueryAccessCode(IllegalQueryMethodException ex) {
    return new GenerationFailureException(ex.getMessage(), ex.getCause());
  }

  /*package*/ static GenerationFailureException fromUserCode(String methodName, TemplateQueryContext ctx, InvocationTargetException ite) {
    SNodeReference templateLocation = ctx.getTemplateReference();
    String modelName = templateLocation == null || templateLocation.getModelReference() == null ? "<unknown>" : templateLocation.getModelReference().getModelName();
    String msg = String.format("Generated method %s (template model %s) failed", methodName, modelName);
    TemplateQueryException ex = new TemplateQueryException(msg, ite.getCause());
    ex.setQueryContext(ctx);
    return ex;
  }

  /*
   * NOTE Methods intentionally have try/catch not refactored into single invokeAndReturnValue to keep stack slim
   *      (2 extra arguments on the stack for each call).
   */


  // Boolean condition methods with default value
  private static final class Impl implements CreateRootCondition, MapRootRuleCondition, ReductionRuleCondition,
      DropRuleCondition, WeaveRuleCondition, MapConfigurationCondition, IfMacroCondition,
      InlineSwitchCaseCondition, DropAttributeRuleCondition {

    private final QueryMethods myQueryMethods;
    private final String myMethodName;
    private final boolean myDefValue;
    private volatile QueryMethod<Boolean> myMethod;

    Impl(QueryMethods queryMethods, @NotNull String methodName) {
      this(queryMethods, methodName, false);
    }

    Impl(QueryMethods queryMethods, @NotNull String methodName, boolean defValue) {
      myQueryMethods = queryMethods;
      myMethodName = methodName;
      myDefValue = defValue;
    }

    private boolean invokeBoolean(TemplateQueryContext ctx) throws GenerationFailureException {
      // I don't care to synchronize method evaluation as there's no difference for me which QM instance I use,
      // provided initialization of any is complete (that's why field is volatile)
      QueryMethod<Boolean> m = myMethod;
      if (m == null) {
        if (myQueryMethods.hasMethod(myMethodName)) {
          m = myQueryMethods.getMethod(myMethodName);
        } else {
          String fm = "cannot find condition method '%s' : evaluate to %s";
          ctx.showWarningMessage(null, String.format(fm, myMethodName, String.valueOf(myDefValue).toUpperCase()));
          m = contextObject -> myDefValue;
        }
        myMethod = m;
      }
      try {
        return m.invoke(ctx);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, ctx, e);
      }
    }

    @Override
    public boolean check(@NotNull CreateRootRuleContext ctx) throws GenerationFailureException {
      return invokeBoolean(ctx);
    }

    @Override
    public boolean check(@NotNull DropRootRuleContext ctx) throws GenerationFailureException {
      return invokeBoolean(ctx);
    }

    @Override
    public boolean check(@NotNull DropAttributeRuleContext ctx) throws GenerationFailureException {
      return invokeBoolean(ctx);
    }

    @Override
    public boolean check(@NotNull MapRootRuleContext ctx) throws GenerationFailureException {
      return invokeBoolean(ctx);
    }

    @Override
    public boolean check(@NotNull ReductionRuleQueryContext ctx) throws GenerationFailureException {
      return invokeBoolean(ctx);
    }

    @Override
    public boolean check(@NotNull WeavingMappingRuleContext ctx) throws GenerationFailureException {
      return invokeBoolean(ctx);
    }

    @Override
    public boolean check(@NotNull TemplateQueryContext ctx) throws GenerationFailureException {
      return invokeBoolean(ctx);
    }

    @Override
    public boolean check(@NotNull IfMacroContext ctx) throws GenerationFailureException {
      return invokeBoolean(ctx);
    }

    @Override
    public boolean check(@NotNull InlineSwitchCaseContext ctx) throws GenerationFailureException {
      return invokeBoolean(ctx);
    }
  }

  private static final class PropMacro extends PropertyValueQuery.Base {
    private final QueryMethods myQueryMethods;
    private final String myMethodName;
    private QueryMethod<Object> myMethod;

    public PropMacro(QueryMethods queryMethods, @NotNull SNodeReference macro, @NotNull String methodName, @NotNull SProperty property, Object templateValue) {
      super(macro, property, templateValue);
      myQueryMethods = queryMethods;
      myMethodName = methodName;
    }

    @Nullable
    @Override
    public Object evaluate(@NotNull PropertyMacroContext context) throws GenerationFailureException {
      if (myMethod == null) {
        myMethod = getMethod(context);
      }
      try {
        return myMethod.invoke(context);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, context, e);
      }
    }

    private QueryMethod<Object> getMethod(PropertyMacroContext context) throws GenerationFailureException {
      // We used to treat missing method for a property macro as GFE, while check or sourceNode queries
      // got default implementation, and now I've switched to null default value.
      if (!myQueryMethods.hasMethod(myMethodName)) {
        final String m = String.format("cannot find method '%s' for property macro", myMethodName);
        context.showWarningMessage(null, m);
        return contextObject -> null;
      }
      return myQueryMethods.getMethod(myMethodName);
    }
  }

  // queries that evaluate to <T>. Another difference from Impl is that
  // methods other than that with Boolean get cached in the field.
  static final class Impl2<T> implements VariableValueQuery, CallArgumentQuery, InsertMacroQuery, MapNodeQuery, MapPostProcessor,
      ReferenceTargetQuery, SourceNodeQuery, SourceNodesQuery, PatternRuleQuery, WeaveRuleQuery, ScriptCodeBlock, WeaveAnchorQuery {
    private final QueryMethods myQueryMethods; // not null
    private final String myMethodName; // not null
    private final T myMissingMethodValue;
    private final boolean myUseDefaultForMissing;
    private QueryMethod<T> myMethod;

    // arguments are not null
    public Impl2(QueryMethods queryMethods, String methodName) {
      myQueryMethods = queryMethods;
      myMethodName = methodName;
      myMissingMethodValue = null;
      myUseDefaultForMissing = false;
    }

    // arguments are not null, except for missingMethodValue
    public Impl2(QueryMethods queryMethods, String methodName, @Nullable T missingMethodValue) {
      myQueryMethods = queryMethods;
      myMethodName = methodName;
      myMissingMethodValue = missingMethodValue;
      myUseDefaultForMissing = true;
    }

    @Nullable
    @Override
    public Object evaluate(@NotNull TemplateArgumentContext context) throws GenerationFailureException {
      try {
        return getMethod(context, "cannot find method '%s' for template call argument").invoke(context);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, context, e);
      }
    }

    @Nullable
    @Override
    public Object evaluate(@NotNull TemplateVarContext context) throws GenerationFailureException {
      try {
        return getMethod(context, "cannot find method '%s' for VAR macro").invoke(context);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, context, e);
      }
    }

    @Nullable
    @Override
    public SNode evaluate(@NotNull InsertMacroContext context) throws GenerationFailureException {
      @SuppressWarnings("unchecked")
      QueryMethod<SNode> method = (QueryMethod<SNode>) getMethod(context, "cannot find method '%s' for INSERT macro");
      try {
        return method.invoke(context);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, context, e);
      }
    }

    @Nullable
    @Override
    public SNode evaluate(@NotNull MapSrcMacroContext context) throws GenerationFailureException {
      @SuppressWarnings("unchecked")
      QueryMethod<SNode> method = (QueryMethod<SNode>) getMethod(context, "cannot find method '%s' for MAP-SRC's mapping function");
      try {
        return method.invoke(context);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, context, e);
      }
    }

    @Override
    public void invoke(@NotNull MapSrcMacroPostProcContext context) throws GenerationFailureException {
      try {
        getMethod(context, "cannot find method '%s' for MAP-SRC's post-processor").invoke(context);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, context, e);
      }
    }

    @Nullable
    @Override
    public Object evaluate(@NotNull ReferenceMacroContext context) throws GenerationFailureException {
      try {
        return getMethod(context, "cannot find method '%s' for reference macro").invoke(context);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, context, e);
      }
    }

    @Nullable
    @Override
    public SNode evaluate(@NotNull SourceSubstituteMacroNodeContext context) throws GenerationFailureException {
      @SuppressWarnings("unchecked")
      QueryMethod<SNode> method = (QueryMethod<SNode>) getMethod(context, "cannot find node query '%s' : evaluate to null");
      try {
        return method.invoke(context);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, context, e);
      }
    }

    @NotNull
    @Override
    public Collection<SNode> evaluate(@NotNull SourceSubstituteMacroNodesContext context) throws GenerationFailureException {
      @SuppressWarnings("unchecked")
      QueryMethod<Iterable<SNode>> method = (QueryMethod<Iterable<SNode>>) getMethod(context, "cannot find nodes query '%s' : evaluate to empty list");
      try {
        Iterable<SNode> result = method.invoke(context);
        return IterableUtil.asCollection(result);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, context, e);
      }
    }

    @Override
    public GeneratedMatchingPattern pattern(@NotNull PatternRuleContext context) throws GenerationFailureException {
      @SuppressWarnings("unchecked")
      QueryMethod<GeneratedMatchingPattern> method = (QueryMethod<GeneratedMatchingPattern>) getMethod(context, "cannot find pattern condition method '%s' : not applied");
      try {
        return method.invoke(context);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, context, e);
      }
    }

    @Override
    public SNode contextNode(WeavingMappingRuleContext context) throws GenerationFailureException {
      @SuppressWarnings("unchecked")
      QueryMethod<SNode> method = (QueryMethod<SNode>) getMethod(context, "cannot find context node query '%s' : evaluate to null");
      try {
        return method.invoke(context);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, context, e);
      }
    }

    @Nullable
    @Override
    public SNode anchorNode(WeavingAnchorContext context) throws GenerationFailureException {
      @SuppressWarnings("unchecked")
      QueryMethod<SNode> method = (QueryMethod<SNode>) getMethod(context, "cannot find anchor node query '%s' : evaluate to null");
      try {
        return method.invoke(context);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, context, e);
      }
    }


    @Override
    public void invoke(MappingScriptContext context) throws GenerationFailureException {
      try {
        getMethod(context, "cannot run script '%s' : no generated code found").invoke(context);
      } catch (IllegalQueryMethodException e) {
        throw fromQueryAccessCode(e);
      } catch (InvocationTargetException e) {
        throw fromUserCode(myMethodName, context, e);
      }
    }

    private QueryMethod<T> getMethod(TemplateQueryContext ctx, String messageFormat) throws GenerationFailureException {
      // trick with local variable is to bring attention to the fact, that parallel evaluate() may
      // initialize myMethod twice and that invoke may work with an instance other than getMethod() had returned here.
      // Since QueryMethod is just a simple wrap around reflective call, I'm not too much concerned at the moment that
      // myMethod.invoke() might run with an instance different that came from getMethod(). Nor there's too much duplicated
      // effort when looking up a method (QueryMethodGenerated caches them). We've got extra QueryMethod wrappers and 'other this'
      // for invoke only.
      // Still, I don't want to bother with synchronization here, with stateless QM I'm unlikely to face any issues, and
      // though I don't like the code, I'd rather move forward towards non-reflective queries as the only option than
      // deal with synch of reflective calls.
      QueryMethod<T> m = myMethod;
      if (m == null) {
        if (!myQueryMethods.hasMethod(myMethodName)) {
          final String msg = String.format(messageFormat, myMethodName);
          if (myUseDefaultForMissing) {
            ctx.showWarningMessage(null, msg);
            return contextObject -> myMissingMethodValue;
          } else {
            ctx.showErrorMessage(null, msg);
            throw new GenerationFailureException(msg);
          }
        }
        m = myMethod = myQueryMethods.getMethod(myMethodName);
      }
      return m;
    }
  }
}