/*
 * Copyright 2008 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.gwt.dev.jjs.impl;

import com.google.gwt.dev.PropertyAndBindingInfo;
import com.google.gwt.dev.cfg.BindingProperty;
import com.google.gwt.dev.cfg.PermutationProperties;
import com.google.gwt.dev.jjs.InternalCompilerException;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.SourceOrigin;
import com.google.gwt.dev.jjs.ast.AccessModifier;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JBlock;
import com.google.gwt.dev.jjs.ast.JCaseStatement;
import com.google.gwt.dev.jjs.ast.JClassType;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodBody;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JModVisitor;
import com.google.gwt.dev.jjs.ast.JPermutationDependentValue;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JReferenceType;
import com.google.gwt.dev.jjs.ast.JReturnStatement;
import com.google.gwt.dev.jjs.ast.JSwitchStatement;
import com.google.gwt.dev.jjs.ast.RuntimeConstants;
import com.google.gwt.thirdparty.guava.common.base.Function;
import com.google.gwt.thirdparty.guava.common.base.Joiner;
import com.google.gwt.thirdparty.guava.common.collect.FluentIterable;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Multimap;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * Replaces any "GWT.create()" calls with a new expression for the actual result of the deferred
 * binding decision and System.getProperty() with the actual value for that property.
 * <p>
 * When properties are collapsed (e.g. in soft permutations) a method is create that resolves
 * the value at runtime.
 * </p>
 *
 */
public class ResolvePermutationDependentValues {

  private class ValueReplacer extends JModVisitor {
    @Override
    public void endVisit(JPermutationDependentValue x, Context ctx) {

      if (x.isTypeRebind()) {
        ctx.replaceMe(rebindClassExpression(x));
        return;
      }

      assert x.isProperty();
      ctx.replaceMe(propertyValueExpression(x));
    }

    private JExpression propertyValueExpression(JPermutationDependentValue x) {
      List<String> propertyValues = props.getConfigurationProperties()
          .getStringsOrNull(x.getRequestedValue());
      if (propertyValues != null) {
        // It is a configuration property.
        // If no values are set, propertyValues is either empty (multivalued properties)
        // or contains a single null (other properties).
        if (propertyValues.stream().anyMatch(Objects::nonNull)) {
          return program.getLiteral(x.getSourceInfo(),
              Joiner.on(",").skipNulls().join(propertyValues));
        }
        if (x.getDefaultValueExpression() != null) {
          return x.getDefaultValueExpression();
        }
        return program.getLiteralNull();
      }

      if (isSoftPermutationProperty(x.getRequestedValue())) {
        JMethod method = getOrCreateSoftPropertyMethod(x.getSourceInfo(), x.getRequestedValue());
        return new JMethodCall(x.getSourceInfo(), null, method);
      }

      String propertyValue = commonPropertyAndBindingInfo.getPropertyValue(x.getRequestedValue());

      if (propertyValue != null) {
        return program.getLiteral(x.getSourceInfo(), propertyValue);
      }

      return x.getDefaultValueExpression();
    }

    private JExpression rebindClassExpression(JPermutationDependentValue x) {
      if (isSoftTypeRebind(x.getRequestedValue())) {
        JMethod method = getOrCreateTypeRebindMethod(x.getSourceInfo(), x.getRequestedValue(),
                x.getResultValues(), x.getResultExpressions());
        return new JMethodCall(x.getSourceInfo(), null, method);
      }

      return computeInstantiationExpression(x);
    }
  }

  public static boolean exec(JProgram program, PermutationProperties props,
      List<PropertyAndBindingInfo> propertyAndBindingInfo) {
    return new ResolvePermutationDependentValues(program, props, propertyAndBindingInfo).execImpl();
  }

  private final JProgram program;
  private final PermutationProperties props;
  private final Set<String> bindingPropertyNames;
  private final List<PropertyAndBindingInfo> permutationPropertyAndBindingInfo;
  private final PropertyAndBindingInfo commonPropertyAndBindingInfo;
  private final JClassType holderType;
  private final JMethod permutationIdMethod;
  private final Map<String, JMethod> softPermutationMethods = Maps.newHashMap();

  private ResolvePermutationDependentValues(JProgram program, PermutationProperties props,
      List<PropertyAndBindingInfo> gwtCreateAnswers) {
    this.program = program;
    this.permutationPropertyAndBindingInfo = gwtCreateAnswers;
    this.props = props;
    this.bindingPropertyNames = FluentIterable.from(props.getBindingProperties()).transform(
        new Function<BindingProperty, String>() {
          @Override
          public String apply(BindingProperty bindingProperty) {
            return bindingProperty.getName();
          }
        }).toSet();
    this.commonPropertyAndBindingInfo = PropertyAndBindingInfo.getCommonAnswers(gwtCreateAnswers);
    this.holderType = (JClassType) program.getIndexedType("CollapsedPropertyHolder");
    this.permutationIdMethod = program.getIndexedMethod(
        RuntimeConstants.COLLAPSED_PROPERTY_HOLDER_GET_PERMUTATION_ID);
  }

  public JExpression computeInstantiationExpression(JPermutationDependentValue x) {
    String reqType = x.getRequestedValue();
    // Rebinds are always on a source type name.
    String reboundClassName = commonPropertyAndBindingInfo.getReboundType(reqType);
    if (reboundClassName == null) {
      // The fact that we already compute every rebind permutation before
      // compiling should prevent this case from ever happening in real life.
      //
      throw new InternalCompilerException("Unexpected failure to rebind '" + reqType + "'");
    }
    assert program.getFromTypeMap(reboundClassName) != null;
    int index = x.getResultValues().indexOf(reboundClassName);
    if (index == -1) {
      throw new InternalCompilerException("No matching rebind result in all rebind results!");
    }
    return x.getResultExpressions().get(index);
  }

  private boolean execImpl() {
    ValueReplacer valueReplacer = new ValueReplacer();
    valueReplacer.accept(program);
    return valueReplacer.didChange();
  }

  private boolean isSoftTypeRebind(String requestType) {
    return !commonPropertyAndBindingInfo.containsType(requestType);
  }

  private boolean isSoftPermutationProperty(String propertyName) {
    return bindingPropertyNames.contains(propertyName) &&
        !commonPropertyAndBindingInfo.containsProperty(propertyName);
  }

  private JMethod getOrCreateSoftPropertyMethod(final SourceInfo info, String propertyName) {
    JMethod toReturn = softPermutationMethods.get(propertyName);
    if (toReturn != null) {
      return toReturn;
    }
    Multimap<String, Integer> resultsToPermutations = PropertyAndBindingInfo
        .getPermutationIdsByPropertyName(permutationPropertyAndBindingInfo, propertyName);

    List<String> propertyValues = Lists.newArrayList(resultsToPermutations.keySet());
    return createReboundValueSelectorMethod(info, "property_", propertyName,
        program.getTypeJavaLangString(), propertyValues,
        FluentIterable.from(propertyValues).transform(
            new Function<String, JExpression>() {
              @Override
              public JExpression apply(String s) {
                return program.getLiteral(info, s);
              }
            }).toList(), resultsToPermutations);
  }

  private JMethod getOrCreateTypeRebindMethod(SourceInfo info, String requestType,
      List<String> resultTypes, List<JExpression> instantiationExpressions) {
    assert resultTypes.size() == instantiationExpressions.size();

    JMethod toReturn = softPermutationMethods.get(requestType);
    if (toReturn != null) {
      return toReturn;
    }
    Multimap<String, Integer> resultsToPermutations = PropertyAndBindingInfo
        .getPermutationIdsByRequestTypes(permutationPropertyAndBindingInfo, requestType);

    return createReboundValueSelectorMethod(info, "create_", requestType,
        program.getTypeJavaLangObject().strengthenToNonNull(), resultTypes,
        instantiationExpressions, resultsToPermutations);
  }

  private JMethod createReboundValueSelectorMethod(SourceInfo info, String prefix,
      String parameterType, JReferenceType returntype, List<String> results,
      List<JExpression> resultExpressions, Multimap<String, Integer> resultsToPermutations) {

    // Pick the most-used result type to emit less code
    String mostUsed = mostUsedValue(resultsToPermutations);
    assert mostUsed != null;
    JMethod toReturn;
    info = info.makeChild(SourceOrigin.UNKNOWN);
    // c_g_g_d_c_i_DOMImpl
    toReturn =
        new JMethod(info, prefix + parameterType.replace("_", "_1").replace('.', '_'), holderType,
            returntype, false, true, true, AccessModifier.PUBLIC);
    toReturn.setBody(new JMethodBody(info));
    holderType.addMethod(toReturn);
    toReturn.freezeParamTypes();
    info.addCorrelation(info.getCorrelator().by(toReturn));
    softPermutationMethods.put(parameterType, toReturn);

    // Used in the return statement at the end
    JExpression mostUsedExpression = null;

    JBlock switchBody = new JBlock(info);
    for (int i = 0, j = results.size(); i < j; i++) {
      String resultType = results.get(i);
      JExpression expression = resultExpressions.get(i);

      Collection<Integer> permutations = resultsToPermutations.get(resultType);
      if (permutations.isEmpty()) {
        // This rebind result is unused in this permutation
        continue;
      } else if (resultType.equals(mostUsed)) {
        // Save off the fallback expression and go onto the next type
        mostUsedExpression = expression;
        continue;
      }

      for (int permutationId : permutations) {
        // case 33:
        switchBody.addStmt(new JCaseStatement(info, program.getLiteralInt(permutationId)));
      }

      // return new FooImpl();
      switchBody.addStmt(expression.makeReturnStatement());
    }

    assert switchBody.getStatements().size() > 0 : "No case statement emitted "
        + "for supposedly soft-rebind " + parameterType;

    // switch (CollapsedPropertyHolder.getPermutationId()) { ... }
    JSwitchStatement sw =
        new JSwitchStatement(info, new JMethodCall(info, null, permutationIdMethod), switchBody);

    // return new FallbackImpl(); at the very end.
    assert mostUsedExpression != null : "No most-used expression";
    JReturnStatement fallbackReturn = mostUsedExpression.makeReturnStatement();

    JMethodBody body = (JMethodBody) toReturn.getBody();
    body.getBlock().addStmt(sw);
    body.getBlock().addStmt(fallbackReturn);

    return toReturn;
  }

  private String mostUsedValue(Multimap<String, Integer> resultsToPermutations) {
    String mostUsed = null;
    int max = 0;
    for (String key : resultsToPermutations.keySet()) {

      int size = resultsToPermutations.get(key).size();
      if (size > max) {
        max = size;
        mostUsed = key;
      }
    }
    return mostUsed;
  }
}
