/*
 * 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.j2cl.ast.visitors;

import com.google.j2cl.ast.AstUtils;
import com.google.j2cl.ast.CastExpression;
import com.google.j2cl.ast.CompilationUnit;
import com.google.j2cl.ast.Expression;
import com.google.j2cl.ast.MethodDescriptor.ParameterDescriptor;
import com.google.j2cl.ast.TypeDescriptor;
import com.google.j2cl.ast.TypeDescriptors;

/**
 * Inserts an unboxing operation (and optionally followed by a widening primitive conversion in some
 * contexts) when a boxed type is being put into a primitive type slot in casting, assignment,
 * method invocation, unary numeric promotion or binary numeric promotion conversion contexts.
 */
public class InsertUnboxingConversions extends NormalizationPass {
  @Override
  public void applyTo(CompilationUnit compilationUnit) {
    compilationUnit.accept(new ConversionContextVisitor(getContextRewriter()));
  }

  private ConversionContextVisitor.ContextRewriter getContextRewriter() {
    return new ConversionContextVisitor.ContextRewriter() {
      @Override
      public Expression rewriteAssignmentContext(
          TypeDescriptor toTypeDescriptor, Expression expression) {
        return maybeUnboxAndWiden(toTypeDescriptor, expression);
      }

      @Override
      public Expression rewriteBinaryNumericPromotionContext(
          Expression subjectOperandExpression, Expression otherOperandExpression) {
        if (TypeDescriptors.isBoxedType(subjectOperandExpression.getTypeDescriptor())) {
          return AstUtils.unbox(subjectOperandExpression);
        }
        return subjectOperandExpression;
      }

      @Override
      public Expression rewriteCastContext(CastExpression castExpression) {
        TypeDescriptor fromTypeDescriptor = castExpression.getExpression().getTypeDescriptor();
        TypeDescriptor toTypeDescriptor = castExpression.getCastTypeDescriptor();
        if (TypeDescriptors.isNonVoidPrimitiveType(toTypeDescriptor)
            && TypeDescriptors.isBoxedType(fromTypeDescriptor)) {

          // An unboxing conversion....
          Expression resultExpression = AstUtils.unbox(castExpression.getExpression());

          // ...optionally followed by a widening primitive conversion.
          fromTypeDescriptor = resultExpression.getTypeDescriptor();
          if (!fromTypeDescriptor.hasSameRawType(toTypeDescriptor)) {
            resultExpression =
                CastExpression.newBuilder()
                    .setExpression(resultExpression)
                    .setCastTypeDescriptor(toTypeDescriptor)
                    .build();
          }

          return resultExpression;
        }
        return castExpression;
      }

      @Override
      public Expression rewriteMethodInvocationContext(
          ParameterDescriptor parameterDescriptor, Expression argumentExpression) {
        return maybeUnboxAndWiden(parameterDescriptor.getTypeDescriptor(), argumentExpression);
      }

      @Override
      public Expression rewriteUnaryNumericPromotionContext(Expression operandExpression) {
        if (TypeDescriptors.isBoxedType(operandExpression.getTypeDescriptor())) {
          return AstUtils.unbox(operandExpression);
        }
        return operandExpression;
      }
    };
  }

  private static Expression maybeUnboxAndWiden(
      TypeDescriptor toTypeDescriptor, Expression expression) {
    TypeDescriptor fromTypeDescriptor = expression.getTypeDescriptor();

    if (TypeDescriptors.isNonVoidPrimitiveType(toTypeDescriptor)
        && TypeDescriptors.isBoxedType(fromTypeDescriptor)) {
      // An unboxing conversion....
      Expression resultExpression = AstUtils.unbox(expression);

      // ...optionally followed by a widening primitive conversion.
      fromTypeDescriptor = resultExpression.getTypeDescriptor();
      if (!fromTypeDescriptor.hasSameRawType(toTypeDescriptor)) {
        resultExpression =
            CastExpression.newBuilder()
                .setExpression(resultExpression)
                .setCastTypeDescriptor(toTypeDescriptor)
                .build();
      }

      return resultExpression;
    }
    return expression;
  }
}

@@ -937,16 +937,43 @@
 riptors;
+%0Aimport java.util.Optional;
 %0A%0A/**%0A *
@@ -1766,52 +1766,139 @@
     
-return maybeUnboxAndWiden(toTypeDescriptor, 
+Optional%3CExpression%3E unboxedExpression = maybeUnboxAndWiden(toTypeDescriptor, expression);%0A        return unboxedExpression.orElse(
 expr
@@ -2389,41 +2389,29 @@
     
-TypeDescriptor fromTypeDescriptor
+Expression expression
  = c
@@ -2439,36 +2439,16 @@
 ession()
-.getTypeDescriptor()
 ;%0A      
@@ -2527,776 +2527,148 @@
 ();%0A
-        if (TypeDescriptors.isNonVoidPrimitiveType(toTypeDescriptor)%0A            && TypeDescriptors.isBoxedType(fromTypeDescriptor)) %7B%0A%0A          // An unboxing conversion....%0A          Expression resultExpression = AstUtils.unbox(castExpression.getExpression());%0A%0A          // ...optionally followed by a widening primitive conversion.%0A          fromTypeDescriptor = resultExpression.getTypeDescriptor();%0A          if (!fromTypeDescriptor.hasSameRawType(toTypeDescriptor)) %7B%0A            resultExpression =%0A                CastExpression.newBuilder()%0A                    .setExpression(resultExpression)%0A                    .setCastTypeDescriptor(toTypeDescriptor)%0A                    .build();%0A          %7D%0A%0A          return resultExpression;%0A        %7D%0A        return 
+%0A        Optional%3CExpression%3E unboxedExpression = maybeUnboxAndWiden(toTypeDescriptor, expression);%0A        return unboxedExpression.orElse(
 cast
@@ -2669,32 +2669,33 @@
 e(castExpression
+)
 ;%0A      %7D%0A%0A     
@@ -2853,22 +2853,68 @@
         
-return
+Optional%3CExpression%3E unboxedExpression =%0A           
  maybeUn
@@ -2979,32 +2979,93 @@
 entExpression);%0A
+        return unboxedExpression.orElse(argumentExpression);%0A
       %7D%0A%0A      @
@@ -3380,16 +3380,25 @@
  static 
+Optional%3C
 Expressi
@@ -3399,16 +3399,17 @@
 pression
+%3E
  maybeUn
@@ -4206,16 +4206,28 @@
  return 
+Optional.of(
 resultEx
@@ -4230,24 +4230,25 @@
 ltExpression
+)
 ;%0A    %7D%0A    
@@ -4254,26 +4254,32 @@
  return 
-expression
+Optional.empty()
 ;%0A  %7D%0A%7D%0A

Refactor and remove duplicate code.