/*
 * Copyright 2011 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.js;

import com.google.gwt.dev.jjs.impl.OptimizerStats;
import com.google.gwt.dev.js.ast.JsBlock;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsModVisitor;
import com.google.gwt.dev.js.ast.JsProgram;
import com.google.gwt.dev.js.ast.JsStatement;
import com.google.gwt.dev.js.ast.JsSwitch;
import com.google.gwt.dev.js.ast.JsSwitchMember;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Combine case labels with identical bodies. Case bodies that may fall through
 * to the following case label and case bodies following a possible fallthrough
 * are left undisturbed.
 *
 * For example, consider the following input:
 *
 * <pre>
 * switch (x) {
 *   case 0: y = 17; break;
 *   case 1: if (z == 0) { y = 18; break; } else { y = 19 } // fallthrough else
 *   case 2: return 22;
 *   case 3: if (z == 0) { y = 18; break; } else { y = 19 } // fallthrough else
 *   case 4: y = 17; break;
 *   case 5: y = 17; break;
 *   case 6: return 22;
 * }
 * </pre>
 *
 * This will be transformed into:
 *
 * <pre>
 * switch (x) {
 *   case 0: y = 17; break;
 *   case 1: if (z == 0) { y = 18; break; } else { y = 19 }
 *   case 6: case 2: return 22;
 *   case 3: if (z == 0) { y = 18; break; } else { y = 19 }
 *   case 5: case 4: y = 17; break;
 * }
 *
 * <pre>
 *
 * Cases (2, 6) and (4, 5) have been coalesced.  Note that case 0 has not been
 * combined with cases 4 and 5 since case 4 cannot be moved due to the potential
 * fallthrough from case 3, and we currently only coalesce a given cases with a
 * preceding case and so cannot move case 0 downward.
 *
 * Although this pattern is unlikely to occur frequently in hand-written code,
 * it can account for a significant amount of space in generated code.
 */
public class JsDuplicateCaseFolder {
  private static final String NAME = JsDuplicateCaseFolder.class.getSimpleName();

  private class DuplicateCaseFolder extends JsModVisitor {

    public DuplicateCaseFolder() {
    }

    @Override
    public boolean visit(JsSwitch x, JsContext ctx) {
      int modified = 0;

      // A map from case body source code to the original case label
      // in which they appeared
      Map<String, JsSwitchMember> seen = new HashMap<String, JsSwitchMember>();

      // Original list of members
      List<JsSwitchMember> cases = x.getCases();
      // Coalesced list of members
      List<JsSwitchMember> newCases = new LinkedList<JsSwitchMember>();

      // Keep track of whether the previous case can fall through
      // to the current case
      boolean hasPreviousFallthrough = false;

      // Iterate over members and locate ones with bodies identical to
      // previous members
      for (JsSwitchMember member : cases) {
        List<JsStatement> stmts = member.getStmts();

        // Don't rewrite any cases that might fall through
        if (!unconditionalControlBreak(stmts)) {
          hasPreviousFallthrough = true;
          // copy the case into the output
          newCases.add(member);
          continue;
        }

        String body = toSource(stmts);
        JsSwitchMember previousCase = seen.get(body);
        if (previousCase == null || hasPreviousFallthrough) {
          // Don't coalesce a case that can be reached via fallthrough
          // from the previous case
          newCases.add(member);
          seen.put(body, member);
        } else {
          // Locate the position of the case that this case is to be
          // coalesced with. Note: linear search in output list
          int index = newCases.indexOf(previousCase);

          // Empty the case body and insert the case label into the output
          member.getStmts().clear();
          newCases.add(index, member);
          modified++;
        }

        hasPreviousFallthrough = false;
      }

      // Rewrite the AST if any cases have changed
      if (modified > 0) {
        numMods += modified;
        cases.clear();
        cases.addAll(newCases);
      }

      return true;
    }

    private String toSource(List<JsStatement> stmts) {
      StringBuilder sb = new StringBuilder();
      for (JsStatement stmt : stmts) {
        sb.append(stmt.toSource(true));
        sb.append("\n"); // separate statements
      }
      return sb.toString();
    }

    /**
     * See {@link JsStatement#unconditionalControlBreak()}.
     */
    private boolean unconditionalControlBreak(List<JsStatement> stmts) {
      for (JsStatement stmt : stmts) {
        if (stmt.unconditionalControlBreak()) {
          return true;
        }
      }
      return false;
    }
  }

  public static void exec(JsProgram program) {
    new JsDuplicateCaseFolder().execImpl(program.getFragmentBlock(0));
  }

  public JsDuplicateCaseFolder() {
  }

  private void execImpl(JsBlock fragment) {
    try (OptimizerStats stats = OptimizerStats.optimization(NAME)) {
      DuplicateCaseFolder dcf = new DuplicateCaseFolder();
      dcf.accept(fragment);
      stats.recordModified(dcf.getNumMods());
    }
  }
}
