/*
 * Copyright (c) 2013 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */
package com.ibm.wala.cast.ir.translator;

import com.ibm.wala.cast.tree.CAst;
import com.ibm.wala.cast.tree.CAstEntity;
import com.ibm.wala.cast.tree.CAstNode;
import com.ibm.wala.cast.tree.CAstSourcePositionMap.Position;
import com.ibm.wala.cast.tree.impl.CAstControlFlowRecorder;
import com.ibm.wala.cast.tree.impl.CAstNodeTypeMapRecorder;
import com.ibm.wala.cast.tree.impl.CAstSourcePositionRecorder;
import com.ibm.wala.cast.tree.rewrite.CAstCloner;
import com.ibm.wala.cast.tree.rewrite.CAstRewriter;
import com.ibm.wala.cast.tree.rewrite.CAstRewriter.CopyKey;
import com.ibm.wala.cast.tree.rewrite.CAstRewriter.RewriteContext;
import com.ibm.wala.cast.tree.rewrite.CAstRewriterFactory;
import com.ibm.wala.core.util.warnings.Warning;
import com.ibm.wala.util.WalaException;
import com.ibm.wala.util.collections.HashMapFactory;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public interface TranslatorToCAst {
  <C extends RewriteContext<K>, K extends CopyKey<K>> void addRewriter(
      CAstRewriterFactory<C, K> factory, boolean prepend);

  class Error extends WalaException {
    private static final long serialVersionUID = -8440950320425119751L;
    public final Set<Warning> warning;

    public Error(Set<Warning> message) {
      super(message.iterator().next().getMsg());
      warning = message;
    }
  }

  CAstEntity translateToCAst() throws Error, IOException;

  interface WalkContext<C extends WalkContext<C, T>, T> {

    WalkContext<C, T> getParent();

    /**
     * Add a name declaration to this context. For variables or constants, n should be a {@link
     * CAstNode#DECL_STMT}, and the initialization of the variable (if any) may occur in a separate
     * assignment. For functions, n should be a {@link CAstNode#FUNCTION_STMT}, including the
     * function body.
     */
    default void addNameDecl(CAstNode n) {
      getParent().addNameDecl(n);
    }

    default List<CAstNode> getNameDecls() {
      return getParent().getNameDecls();
    }

    /**
     * get a mapping from CAstNodes to the scoped entities (e.g. functions or local classes)
     * introduced by those nodes. Also maps {@code null} to those entities not corresponding to any
     * node (e.g nested classes)
     */
    default Map<CAstNode, Collection<CAstEntity>> getScopedEntities() {
      return getParent().getScopedEntities();
    }

    default CAstNode getCatchTarget() {
      return getParent().getCatchTarget();
    }

    default CAstNode getCatchTarget(String s) {
      return getParent().getCatchTarget(s);
    }

    default T top() {
      return getParent().top();
    }

    /** associate a child entity with a given CAstNode, e.g. for a function declaration */
    default void addScopedEntity(CAstNode newNode, CAstEntity visit) {
      getParent().addScopedEntity(newNode, visit);
    }

    /** for recording control-flow relationships among the CAst nodes */
    default CAstControlFlowRecorder cfg() {
      return getParent().cfg();
    }

    /** for recording source positions */
    default CAstSourcePositionRecorder pos() {
      return getParent().pos();
    }

    /** for recording types of nodes */
    default CAstNodeTypeMapRecorder getNodeTypeMap() {
      return getParent().getNodeTypeMap();
    }

    /** for a 'continue' style goto, return the control flow target */
    default T getContinueFor(String label) {
      return getParent().getContinueFor(label);
    }

    /** for a 'break' style goto, return the control flow target */
    default T getBreakFor(String label) {
      return getParent().getBreakFor(label);
    }
  }

  class RootContext<C extends WalkContext<C, T>, T> implements WalkContext<C, T> {
    @Override
    public Map<CAstNode, Collection<CAstEntity>> getScopedEntities() {
      assert false;
      return Collections.emptyMap();
    }

    @Override
    public List<CAstNode> getNameDecls() {
      return null;
    }

    @Override
    public void addScopedEntity(CAstNode newNode, CAstEntity visit) {
      assert false;
    }

    @Override
    public CAstControlFlowRecorder cfg() {
      assert false;
      return null;
    }

    @Override
    public CAstSourcePositionRecorder pos() {
      assert false;
      return null;
    }

    @Override
    public CAstNodeTypeMapRecorder getNodeTypeMap() {
      assert false;
      return null;
    }

    @Override
    public T getContinueFor(String label) {
      assert false;
      return null;
    }

    @Override
    public T getBreakFor(String label) {
      assert false;
      return null;
    }

    @Override
    public T top() {
      assert false;
      return null;
    }

    @Override
    public WalkContext<C, T> getParent() {
      assert false;
      return null;
    }
  }

  class DelegatingContext<C extends WalkContext<C, T>, T> implements WalkContext<C, T> {
    protected final C parent;

    protected DelegatingContext(C parent) {
      this.parent = parent;
    }

    @Override
    public T top() {
      return parent.top();
    }

    @Override
    public WalkContext<C, T> getParent() {
      return parent;
    }
  }

  class BreakContext<C extends WalkContext<C, T>, T> extends DelegatingContext<C, T> {
    private final T breakTarget;
    protected final String label;

    protected BreakContext(C parent, T breakTarget, String label) {
      super(parent);
      this.breakTarget = breakTarget;
      this.label = label;
    }

    @Override
    public T getBreakFor(String l) {
      return (l == null || l.equals(label)) ? breakTarget : super.getBreakFor(l);
    }
  }

  class LoopContext<C extends WalkContext<C, T>, T> extends BreakContext<C, T> {
    private final T continueTo;

    protected LoopContext(C parent, T breakTo, T continueTo, String label) {
      super(parent, breakTo, label);
      this.continueTo = continueTo;
    }

    @Override
    public T getContinueFor(String l) {
      return (l == null || l.equals(label)) ? continueTo : super.getContinueFor(l);
    }
  }

  class TryCatchContext<C extends WalkContext<C, T>, T> implements WalkContext<C, T> {
    private final Map<String, CAstNode> catchNode;
    private final WalkContext<C, T> parent;

    protected TryCatchContext(C parent, CAstNode catchNode) {
      this(parent, Collections.singletonMap(null, catchNode));
    }

    protected TryCatchContext(C parent, Map<String, CAstNode> catchNode) {
      this.parent = parent;
      this.catchNode = catchNode;
    }

    @Override
    public CAstNode getCatchTarget() {
      return getCatchTarget(null);
    }

    @Override
    public CAstNode getCatchTarget(String s) {
      return catchNode.get(s);
    }

    @Override
    public WalkContext<C, T> getParent() {
      return parent;
    }
  }

  class FunctionContext<C extends WalkContext<C, T>, T> extends DelegatingContext<C, T> {
    private final T topNode;
    private final CAstSourcePositionRecorder pos = new CAstSourcePositionRecorder();
    private final CAstControlFlowRecorder cfg = new CAstControlFlowRecorder(pos);
    private final Map<CAstNode, Collection<CAstEntity>> scopedEntities = HashMapFactory.make();

    protected FunctionContext(C parent, T s) {
      super(parent);
      this.topNode = s;
    }

    @Override
    public T top() {
      return topNode;
    }

    @Override
    public void addScopedEntity(CAstNode construct, CAstEntity e) {
      if (!scopedEntities.containsKey(construct)) {
        scopedEntities.put(construct, new HashSet<>(1));
      }
      scopedEntities.get(construct).add(e);
    }

    @Override
    public Map<CAstNode, Collection<CAstEntity>> getScopedEntities() {
      return scopedEntities;
    }

    @Override
    public CAstControlFlowRecorder cfg() {
      return cfg;
    }

    @Override
    public CAstSourcePositionRecorder pos() {
      return pos;
    }
  }

  class DoLoopTranslator {
    private final boolean replicateForDoLoops;

    private final CAst Ast;

    public DoLoopTranslator(boolean replicateForDoLoops, CAst ast) {
      this.replicateForDoLoops = replicateForDoLoops;
      Ast = ast;
    }

    public CAstNode translateDoLoop(
        CAstNode loopTest,
        CAstNode loopBody,
        CAstNode continueNode,
        CAstNode breakNode,
        WalkContext<?, ?> wc) {
      if (replicateForDoLoops) {
        loopBody = Ast.makeNode(CAstNode.BLOCK_STMT, loopBody, continueNode);

        CAstRewriter.Rewrite x =
            new CAstCloner(Ast, false)
                .copy(loopBody, wc.cfg(), wc.pos(), wc.getNodeTypeMap(), null, null);
        CAstNode otherBody = x.newRoot();

        wc.cfg().addAll(x.newCfg());
        wc.pos().addAll(x.newPos());
        wc.getNodeTypeMap().addAll(x.newTypes());

        return Ast.makeNode(
            CAstNode.BLOCK_STMT,
            loopBody,
            Ast.makeNode(CAstNode.LOOP, loopTest, otherBody),
            breakNode);

      } else {
        CAstNode header =
            Ast.makeNode(
                CAstNode.LABEL_STMT, Ast.makeConstant("_do_label"), Ast.makeNode(CAstNode.EMPTY));
        CAstNode loopGoto = Ast.makeNode(CAstNode.IFGOTO, loopTest);

        wc.cfg().map(header, header);
        wc.cfg().map(loopGoto, loopGoto);
        wc.cfg().add(loopGoto, header, Boolean.TRUE);

        return Ast.makeNode(
            CAstNode.BLOCK_STMT,
            header,
            Ast.makeNode(CAstNode.BLOCK_STMT, loopBody, continueNode),
            loopGoto,
            breakNode);
      }
    }
  }

  default <X extends WalkContext<X, Y>, Y> void pushSourcePosition(
      WalkContext<X, Y> context, CAstNode n, Position p) {
    assert context.pos() != null;
    if (n != null) {
      if (context.pos().getPosition(n) == null
          && !(n.getKind() == CAstNode.FUNCTION_EXPR || n.getKind() == CAstNode.FUNCTION_STMT)) {
        context.pos().setPosition(n, p);
        for (CAstNode child : n.getChildren()) {
          pushSourcePosition(context, child, p);
        }
      }
    }
  }
}
