/*
 * Copyright 2015 Google Inc.
 *
 * 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 com.google.template.soy.exprtree;

import com.google.common.base.Equivalence;
import com.google.common.primitives.Doubles;
import com.google.template.soy.exprtree.ExprNode.OperatorNode;
import com.google.template.soy.exprtree.ExprNode.ParentExprNode;

import java.util.Objects;

/**
 * An equivalence relation for expressions.
 * 
 * <p>This equivalence relation is meant to identify identical subexpressions and as such we ignore
 * a number of {@link ExprNode} properties.  For example:
 * <ul>
 *     <li>{@link ExprNode#getSourceLocation()}
 *     <li>{@link ExprNode#getType()}
 *     <li>{@link ExprNode#getParent()}
 * </ul>
 */
public final class ExprEquivalence extends Equivalence<ExprNode> {
  private static final ExprEquivalence INSTANCE = new ExprEquivalence();

  public static ExprEquivalence get() {
    return INSTANCE;
  }

  private final AbstractReturningExprNodeVisitor<Integer> hashCodeVisitor = 
      new AbstractReturningExprNodeVisitor<Integer>() {
    @Override protected Integer visitVarRefNode(VarRefNode node) {
      return Objects.hashCode(node.getDefnDecl());
    }

    @Override protected Integer visitFieldAccessNode(FieldAccessNode node) {
      return Objects.hash(
          wrap(node.getBaseExprChild()),
          node.getFieldName(),
          node.isNullSafe());
    }
    
    @Override protected Integer visitItemAccessNode(ItemAccessNode node) {
      return Objects.hash(
          wrap(node.getBaseExprChild()),
          wrap(node.getKeyExprChild()),
          node.isNullSafe());
    }

    @Override protected Integer visitFunctionNode(FunctionNode node) {
      return Objects.hash(pairwise().wrap(node.getChildren()), node.getFunctionName());
    }

    @Override protected Integer visitGlobalNode(GlobalNode node) {
      return node.getName().hashCode();
    }

    @Override protected Integer visitListLiteralNode(ListLiteralNode node) {
      return hashChildren(node);
    }

    @Override protected Integer visitMapLiteralNode(MapLiteralNode node) {
      return hashChildren(node);
    }

    // literals
    
    @Override protected Integer visitIntegerNode(IntegerNode node) {
      return node.getValue();
    }
    
    @Override protected Integer visitFloatNode(FloatNode node) {
      return Doubles.hashCode(node.getValue());
    }

    @Override protected Integer visitStringNode(StringNode node) {
      return node.getValue().hashCode();
    }

    @Override protected Integer visitExprRootNode(ExprRootNode node) {
      return hashChildren(node);
    }
    
    @Override protected Integer visitOperatorNode(OperatorNode node) {
      // operators are determined entirely by their children
      return hashChildren(node);
    }

    @Override protected Integer visitNullNode(NullNode node) {
      return 0;
    }

    @Override protected Integer visitExprNode(ExprNode node) {
      return 0;
    }
    
    private int hashChildren(ParentExprNode node) {
      return pairwise().hash(node.getChildren());
    }
  };

  final class EqualsVisitor extends AbstractReturningExprNodeVisitor<Boolean> {
    private final ExprNode other;

    EqualsVisitor(ExprNode other) {
      this.other = other;
    }

    @Override protected Boolean visitVarRefNode(VarRefNode node) {
      VarRefNode typedOther = (VarRefNode) other;
      // Varrefs are considered equivalent if they have identical vardefns
      return typedOther.getDefnDecl() == node.getDefnDecl();
    }

    @Override protected Boolean visitFieldAccessNode(FieldAccessNode node) {
      FieldAccessNode typedOther = (FieldAccessNode) other;
      return equivalent(node.getBaseExprChild(), typedOther.getBaseExprChild())
          && node.getFieldName().equals(typedOther.getFieldName())
          && node.isNullSafe() == typedOther.isNullSafe();
    }
    
    @Override protected Boolean visitItemAccessNode(ItemAccessNode node) {
      ItemAccessNode typedOther = (ItemAccessNode) other;
      return equivalent(node.getBaseExprChild(), typedOther.getBaseExprChild())
          && equivalent(node.getKeyExprChild(), typedOther.getKeyExprChild())
          && node.isNullSafe() == typedOther.isNullSafe();
    }

    @Override protected Boolean visitFunctionNode(FunctionNode node) {
      FunctionNode typedOther = (FunctionNode) other;
      return compareChildren(node) && node.getFunctionName().equals(typedOther.getFunctionName());
    }

    @Override protected Boolean visitGlobalNode(GlobalNode node) {
      return node.getName().equals(((GlobalNode) other).getName());
    }

    @Override protected Boolean visitListLiteralNode(ListLiteralNode node) {
      return compareChildren(node);
    }

    @Override protected Boolean visitMapLiteralNode(MapLiteralNode node) {
      return compareChildren(node);
    }

    // literals
    
    @Override protected Boolean visitBooleanNode(BooleanNode node) {
      return node.getValue() == ((BooleanNode) other).getValue();
    }
    
    @Override protected Boolean visitIntegerNode(IntegerNode node) {
      return node.getValue() == ((IntegerNode) other).getValue();
    }
    
    @Override protected Boolean visitFloatNode(FloatNode node) {
      return node.getValue() == ((FloatNode) other).getValue();
    }

    @Override protected Boolean visitStringNode(StringNode node) {
      return node.getValue().equals(((StringNode) other).getValue());
    }

    @Override protected Boolean visitExprRootNode(ExprRootNode node) {
      return compareChildren(node);
    }
    
    @Override protected Boolean visitOperatorNode(OperatorNode node) {
      // operators are determined entirely by their children
      return compareChildren(node);
    }

    @Override protected Boolean visitNullNode(NullNode node) {
      return true;
    }

    @Override protected Boolean visitExprNode(ExprNode node) {
      // default
      return false;
    }

    private boolean compareChildren(ParentExprNode node) {
      return pairwise().equivalent(node.getChildren(), ((ParentExprNode) other).getChildren());
    }
  }

  private ExprEquivalence() {}

  @Override protected boolean doEquivalent(ExprNode a, ExprNode b) {
    return a.getKind() == b.getKind() && new EqualsVisitor(a).exec(b);
  }

  @Override protected int doHash(ExprNode t) {
    return 31 * t.getKind().hashCode() + hashCodeVisitor.exec(t);
  }
}