// Copyright 2016 The Bazel Authors. All Rights Reserved.
//
// 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.testing.coverage;

import java.util.Arrays;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

/**
 * Abstracts bit field operations.
 */
public class BitField {

  private byte[] bytes;

  private static final int[] BIT_COUNT_LOOKUP = new int[256];

  static {
    BIT_COUNT_LOOKUP[0] = 0;
    BIT_COUNT_LOOKUP[1] = 1;
    for (int i = 2; i < 256; i += 2) {
      int count = BIT_COUNT_LOOKUP[i / 2];
      BIT_COUNT_LOOKUP[i] = count;
      BIT_COUNT_LOOKUP[i + 1] = count + 1;
    }
  }

  public BitField() {
    this(new byte[0]);
  }

  public BitField(byte[] bytes) {
    this.bytes = bytes.clone();
  }

  /**
   * Returns a copy of the underlying byte array.
   *
   * @return byte array copy
   */
  public byte[] getBytes() {
    return bytes.clone();
  }

  /**
   * Sets or clears a bit at the given index.
   *
   * @param index bit index
   */
  public void setBit(int index) {
    setBit(index, true);
  }

  /**
   * Clears a bit at the given index
   *
   * @param index bit index
   */
  public void clearBit(int index) {
    setBit(index, false);
  }

  /**
   * Sets or clears a bit at the given index.
   *
   * @param index bit index
   */
  private void setBit(int index, boolean isSet) {
    int byteIndex = index / 8;
    int newByteSize = byteIndex + 1;
    if (bytes.length < newByteSize) {
      bytes = Arrays.copyOf(bytes, newByteSize);
    }

    int bitIndex = index % 8;
    int mask = 1 << bitIndex;

    if (isSet) {
      bytes[byteIndex] = (byte) (bytes[byteIndex] | mask);
    } else {
      bytes[byteIndex] = (byte) (bytes[byteIndex] & ~mask);
    }
  }

  /**
   * Checks whether a bit at the given index is set.
   *
   * @param index bit index
   * @return true if set, false otherwise
   */
  public boolean isBitSet(int index) {
    int byteIndex = index / 8;

    if (byteIndex >= bytes.length) {
      return false;
    }

    int bitIndex = index % 8;
    int mask = 1 << bitIndex;
    return (bytes[byteIndex] & mask) != 0;
  }

  /** Performs a non-destructive bit-wise "and" of this bit field with another one. */
  public BitField and(BitField other) {
    int size = Math.min(bytes.length, other.bytes.length);
    byte[] result = new byte[size];

    for (int i = 0; i < size; i++) {
      result[i] = (byte) (bytes[i] & other.bytes[i]);
    }

    return new BitField(result);
  }

  /**
   * Performs a non-destructive bit-wise merge of this bit field and another one.
   *
   * @param other the other bit field
   * @return this bit field
   */
  public BitField or(BitField other) {
    byte[] largerArray, smallerArray;
    if (bytes.length < other.bytes.length) {
      largerArray = other.bytes;
      smallerArray = bytes;
    } else {
      largerArray = bytes;
      smallerArray = other.bytes;
    }

    // Start out with a copy of the larger of the two arrays.
    byte[] result = Arrays.copyOf(largerArray, largerArray.length);

    for (int i = 0; i < smallerArray.length; i++) {
      result[i] |= smallerArray[i];
    }

    return new BitField(result);
  }

  /**
   * Compares two bit fields for equality.
   *
   * @param obj another object
   * @return true if the other object is a bit field with the same bits set
   */
  @Override
  public boolean equals(Object obj) {
    return EqualsBuilder.reflectionEquals(this, obj);
  }

  /**
   * Compare a BitField object with an array of bytes
   *
   * @param other a byte array to compare to
   * @return true if the underlying byte array is equal to the given byte array
   */
  public boolean equals(byte[] other) {
    return Arrays.equals(bytes, other);
  }

  @Override
  public int hashCode() {
    return HashCodeBuilder.reflectionHashCode(this);
  }

  public int countBitsSet() {
    int count = 0;
    for (byte b : bytes) {
      // JAVA doesn't have the concept of unsigned byte; need to & with 255
      // to avoid exception of IndexOutOfBoundException when b < 0.
      count += BIT_COUNT_LOOKUP[0xFF & b];
    }
    return count;
  }

  public BitField not() {
    byte[] invertedBytes = new byte[bytes.length];
    for (int i = 0; i < bytes.length; i++) {
      invertedBytes[i] = Integer.valueOf(~bytes[i]).byteValue();
    }
    return new BitField(invertedBytes);
  }

  public int sizeInBits() {
    return bytes.length * 8;
  }

  public boolean any() {
    for (int i = 0; i < bytes.length; i++) {
      if (bytes[i] != (byte) 0) {
        return true;
      }
    }
    return false;
  }
}
