package com.hubspot.jinjava.lib.fn;

import com.hubspot.jinjava.el.ext.AbstractCallableMethod;
import com.hubspot.jinjava.interpret.Context;
import com.hubspot.jinjava.interpret.DeferredValueException;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable;
import com.hubspot.jinjava.tree.Node;
import com.hubspot.jinjava.util.LengthLimitingStringBuilder;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * Function definition parsed from a jinjava template, stored in global macros registry in interpreter context.
 *
 * @author jstehler
 *
 */
public class MacroFunction extends AbstractCallableMethod {
  private final List<Node> content;

  private final boolean caller;

  private final Context localContextScope;

  private final int definitionLineNumber;

  private final int definitionStartPosition;

  private boolean deferred;

  public MacroFunction(
    List<Node> content,
    String name,
    LinkedHashMap<String, Object> argNamesWithDefaults,
    boolean caller,
    Context localContextScope,
    int lineNumber,
    int startPosition
  ) {
    super(name, argNamesWithDefaults);
    this.content = content;
    this.caller = caller;
    this.localContextScope = localContextScope;
    this.definitionLineNumber = lineNumber;
    this.definitionStartPosition = startPosition;
    this.deferred = false;
  }

  @Override
  public Object doEvaluate(
    Map<String, Object> argMap,
    Map<String, Object> kwargMap,
    List<Object> varArgs
  ) {
    JinjavaInterpreter interpreter = JinjavaInterpreter.getCurrent();
    Optional<String> importFile = Optional.ofNullable(
      (String) localContextScope.get(Context.IMPORT_RESOURCE_PATH_KEY)
    );

    // pushWithoutCycleCheck() is used to here so that macros calling macros from the same file will not throw a TagCycleException
    importFile.ifPresent(
      path ->
        interpreter
          .getContext()
          .getCurrentPathStack()
          .pushWithoutCycleCheck(
            path,
            interpreter.getLineNumber(),
            interpreter.getPosition()
          )
    );

    try (InterpreterScopeClosable c = interpreter.enterScope()) {
      interpreter.setLineNumber(definitionLineNumber);
      interpreter.setPosition(definitionStartPosition);

      for (Map.Entry<String, Object> scopeEntry : localContextScope
        .getScope()
        .entrySet()) {
        if (scopeEntry.getValue() instanceof MacroFunction) {
          interpreter.getContext().addGlobalMacro((MacroFunction) scopeEntry.getValue());
        } else {
          interpreter.getContext().put(scopeEntry.getKey(), scopeEntry.getValue());
        }
      }

      // named parameters
      for (Map.Entry<String, Object> argEntry : argMap.entrySet()) {
        interpreter.getContext().put(argEntry.getKey(), argEntry.getValue());
      }
      // parameter map
      interpreter.getContext().put("kwargs", kwargMap);
      // varargs list
      interpreter.getContext().put("varargs", varArgs);

      LengthLimitingStringBuilder result = new LengthLimitingStringBuilder(
        interpreter.getConfig().getMaxOutputSize()
      );

      for (Node node : content) {
        result.append(node.render(interpreter));
      }

      if (!interpreter.getContext().getDeferredNodes().isEmpty()) {
        throw new DeferredValueException(
          getName(),
          interpreter.getLineNumber(),
          interpreter.getPosition()
        );
      }

      return result.toString();
    } finally {
      importFile.ifPresent(path -> interpreter.getContext().getCurrentPathStack().pop());
    }
  }

  public void setDeferred(boolean deferred) {
    this.deferred = deferred;
  }

  public boolean isDeferred() {
    return deferred;
  }

  public boolean isCaller() {
    return caller;
  }

  public String reconstructImage() {
    if (content != null && !content.isEmpty()) {
      return content.get(0).getParent().reconstructImage();
    }
    return "";
  }
}