/*
 * Copyright 2009 The Closure Compiler Authors.
 *
 * 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.javascript.jscomp.instrumentation;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.primitives.UnsignedBytes;
import com.google.javascript.rhino.Node;
import java.util.BitSet;
import java.util.HashMap;
import java.util.Map;

/**
 * Holds instrumentation details related to a file, namely, the filename,
 * the array name used in instrumentation, and the lines which were
 * instrumented (in encoded form).
 */
@GwtIncompatible("com.google.common.primitives.UnsignedBytes")
class FileInstrumentationData {
  private final BitSet instrumentedBits; // Instrumented lines, a bit per line
  private final String arrayName;
  private final String fileName;

  //
  public static final class BranchIndexPair {
    private final int line;
    private final int branch;

    public int getLine() {
      return line;
    }

    public int getBranch() {
      return branch;
    }

    public BranchIndexPair(int line, int branch) {
      this.line = line;
      this.branch = branch;
    }

    public static BranchIndexPair of(int line, int branch) {
      return new BranchIndexPair(line, branch);
    }

    @Override
    public int hashCode() {
      return 31 * line + branch;
    }

    @Override
    public boolean equals(Object object) {
      if (object instanceof BranchIndexPair) {
        BranchIndexPair that = (BranchIndexPair) object;
        return this.getLine() == that.getLine()
            && this.getBranch() == that.getBranch();
      }
      return false;
    }
  }

  // branchPresent denotes the lines containing at least one branch
  // bit[i] set to 1 denotes the (i+1)-th line containing at least one branch
  // bit[i] bit set to 0 denotes the (i+1)-th line containing no branches
  private final BitSet branchPresent;
  // Number of branches in line, the index is zero-based.
  private final Map<Integer, Integer> branchesInLine;
  // Map of (line-no, branch-idx) to nodes of blocks of conditional branches.
  // The two indices are all zero-based.
  //
  // For example, if the source code has an 'if' statement on line 3 with an if and else branch,
  // the two branches will be indexed as (2, 0) and (2, 1).
  private final Map<BranchIndexPair, Node> branchNodes;

  FileInstrumentationData(String fileName, String arrayName) {
    this.fileName = fileName;
    this.arrayName = arrayName;

    instrumentedBits = new BitSet();

    branchPresent = new BitSet();
    branchesInLine = new HashMap<>();
    branchNodes = new HashMap<>();
  }

   String getArrayName() {
    return arrayName;
  }

  String getFileName() {
    return fileName;
  }

  int maxInstrumentedLine() {
    return instrumentedBits.length();
  }

  int maxBranchPresentLine() {
    return branchPresent.length();
  }

  /**
   * Store a node to be instrumented later for branch coverage.
   * @param lineNumber 1-based line number
   * @param branchNumber 1-based branch number
   * @param block the node of the conditional block.
   */
  void putBranchNode(int lineNumber, int branchNumber, Node block) {
    Preconditions.checkArgument(
        lineNumber > 0, "Expected non-zero positive integer as line number: %s", lineNumber);
    Preconditions.checkArgument(
        branchNumber > 0, "Expected non-zero positive integer as branch number: %s", branchNumber);

    branchNodes.put(BranchIndexPair.of(lineNumber - 1, branchNumber - 1), block);
  }

  /**
   * Get the block node to be instrumented for branch coverage.
   * @param lineNumber 1-based line number
   * @param branchNumber 1-based branch number
   * @return the node of the conditional block.
   */
  Node getBranchNode(int lineNumber, int branchNumber) {
    Preconditions.checkArgument(
        lineNumber > 0, "Expected non-zero positive integer as line number: %s", lineNumber);
    Preconditions.checkArgument(
        branchNumber > 0, "Expected non-zero positive integer as branch number: %s", branchNumber);

    return branchNodes.get(BranchIndexPair.of(lineNumber - 1, branchNumber - 1));
  }

  /**
   * Returns a byte-wise hex string representation of the BitField from
   * MSB (Most Significant Byte) to LSB (Least Significant Byte).
   * Eg. Single byte: a setting of "0001 1111", returns "1f"
   * Eg. Multiple bytes: a setting of "0000 0010 0001 1111", returns "1f02"
   *
   * @return string representation of bits set
   */
  private static String getHexString(BitSet bitSet) {
    StringBuilder builder = new StringBuilder();

    // Build the hex string.
    for (byte byteEntry : bitSet.toByteArray()) {
      // Java bytes are signed, but we want the value as if it were unsigned.
      int value = UnsignedBytes.toInt(byteEntry);
      String hexString = Integer.toHexString(value);

      // Pad string to be two characters (if it isn't already).
      hexString = Strings.padStart(hexString, 2, '0');

      builder.append(hexString);
    }

    return builder.toString();
  }

  /** Get a hex string representation of the instrumentedBits bit vector. */
  String getInstrumentedLinesAsHexString() {
    return getHexString(instrumentedBits);
  }

  /** Get a hex string representation of the branchPresent bit vector. */
  String getBranchPresentAsHexString() {
    return getHexString(branchPresent);
  }

  /**
   * Mark given 1-based line number as instrumented. Zero, Negative numbers
   * are not allowed.
   * @param lineNumber the line number which was instrumented
   */
  void setLineAsInstrumented(int lineNumber) {
    Preconditions.checkArgument(
        lineNumber > 0, "Expected non-zero positive integer as line number: %s", lineNumber);

    // Map the 1-based line number to 0-based bit position
    instrumentedBits.set(lineNumber - 1);
  }

  /**
   * Mark a given 1-based line number has branch presented.
   * @param lineNumber the line number which has conditional branches.
   */
  void setBranchPresent(int lineNumber) {
    Preconditions.checkArgument(
        lineNumber > 0, "Expected non-zero positive integer as line number: %s", lineNumber);

    // Map the 1-based line number to 0-based bit position
    branchPresent.set(lineNumber - 1);
  }

  /**
   * Add a number of branches to a line.
   * @param lineNumber the line number that contains the branch statement.
   * @param numberOfBranches the number of branches to add to the record.
   */
  void addBranches(int lineNumber, int numberOfBranches) {
    int lineIdx = lineNumber - 1;
    Integer currentValue = branchesInLine.get(Integer.valueOf(lineIdx));
    if (currentValue == null) {
      branchesInLine.put(lineIdx, numberOfBranches);
    } else {
      branchesInLine.put(lineIdx, currentValue + numberOfBranches);
    }
  }

  /**
   * Get the number of branches on a line
   * @param lineNumber - the 1-based line number
   * @return the number of branches on the line.
   */
  int getNumBranches(int lineNumber) {
    Integer numBranches = branchesInLine.get(lineNumber - 1);
    if (numBranches == null) {
      return 0;
    } else {
      return numBranches;
    }
  }

}
