/*
 * Copyright ConsenSys AG.
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
package org.hyperledger.besu.evm.log;

import static com.google.common.base.Preconditions.checkArgument;
import static org.hyperledger.besu.crypto.Hash.keccak256;

import org.hyperledger.besu.ethereum.rlp.RLPException;
import org.hyperledger.besu.ethereum.rlp.RLPInput;

import java.util.Collection;

import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.DelegatingBytes;
import org.apache.tuweni.bytes.MutableBytes;

/**
 * Bloom filter implementation for storing persistent logs, describes a 2048-bit representation of
 * all log entries of a transaction, except data. Sets the bits of the 2048 byte array, where
 * indices are given by: The lower order 11-bits, of the first three double-bytes, of the KECCAK256,
 * of each value. For instance the address "0x0F572E5295C57F15886F9B263E2F6D2D6C7B5EC6" results in
 * the KECCAK256 hash "bd2b01afcd27800b54d2179edc49e2bffde5078bb6d0b204694169b1643fb108", of which
 * the corresponding double-bytes are: bd2b, 01af, cd27, corresponding to the following bits in the
 * bloom filter: 1323, 431, 1319
 */
public class LogsBloomFilter extends DelegatingBytes {

  /** The constant BYTE_SIZE. */
  public static final int BYTE_SIZE = 256;

  private static final int LEAST_SIGNIFICANT_BYTE = 0xFF;
  private static final int LEAST_SIGNIFICANT_THREE_BITS = 0x7;
  private static final int BITS_IN_BYTE = 8;

  /** Instantiates a new Logs bloom filter. */
  public LogsBloomFilter() {
    super(Bytes.wrap(new byte[BYTE_SIZE]));
  }

  /**
   * Instantiates a new Logs bloom filter.
   *
   * @param data the data
   */
  public LogsBloomFilter(final MutableBytes data) {
    this(data.copy());
  }

  /**
   * Instantiates a new Logs bloom filter.
   *
   * @param data the data
   */
  public LogsBloomFilter(final Bytes data) {
    super(data);
    checkArgument(
        data.size() == BYTE_SIZE,
        "Invalid size for bloom filter backing array: expected %s but got %s",
        BYTE_SIZE,
        data.size());
  }

  /**
   * Instantiates a new Logs bloom filter.
   *
   * @param logsBloomHexString the logs bloom hex string
   */
  public LogsBloomFilter(final String logsBloomHexString) {
    this(Bytes.fromHexString(logsBloomHexString));
  }

  /**
   * Instantiate logs bloom filter From hex string.
   *
   * @param hexString the hex string
   * @return the logs bloom filter
   */
  public static LogsBloomFilter fromHexString(final String hexString) {
    return new LogsBloomFilter(Bytes.fromHexString(hexString));
  }

  /**
   * Instantiates an empty logs bloom filter.
   *
   * @return the logs bloom filter
   */
  public static LogsBloomFilter empty() {
    return new LogsBloomFilter(Bytes.wrap(new byte[LogsBloomFilter.BYTE_SIZE]));
  }

  /**
   * Creates a bloom filter from the given RLP-encoded input.
   *
   * @param input The input to read from
   * @return the input's corresponding bloom filter
   */
  public static LogsBloomFilter readFrom(final RLPInput input) {
    final Bytes bytes = input.readBytes();
    if (bytes.size() != BYTE_SIZE) {
      throw new RLPException(
          String.format(
              "LogsBloomFilter unexpected size of %s (needs %s)", bytes.size(), BYTE_SIZE));
    }
    return new LogsBloomFilter(bytes);
  }

  /**
   * Checks of subset log bloom filter can be contained.
   *
   * @param subset the subset
   * @return the boolean
   */
  public boolean couldContain(final LogsBloomFilter subset) {
    if (subset == null) {
      return true;
    }
    if (subset.size() != size()) {
      return false;
    }
    for (int i = 0; i < size(); i++) {
      final byte subsetValue = subset.get(i);
      if ((get(i) & subsetValue) != subsetValue) {
        return false;
      }
    }
    return true;
  }

  /**
   * Instantiate Builder.
   *
   * @return the builder
   */
  public static Builder builder() {
    return new Builder();
  }

  /** The Builder. */
  public static final class Builder {
    /** The Data. */
    final MutableBytes data;

    private Builder() {
      data = MutableBytes.create(LogsBloomFilter.BYTE_SIZE);
    }

    /**
     * Insert filter.
     *
     * @param other the other
     * @return the builder
     */
    public Builder insertFilter(final LogsBloomFilter other) {
      for (int i = 0; i < data.size(); ++i) {
        data.set(i, (byte) ((data.get(i) | other.get(i)) & 0xff));
      }
      return this;
    }

    /**
     * Insert log.
     *
     * @param log the log
     * @return the builder
     */
    public Builder insertLog(final Log log) {
      insertBytes((Bytes) log.getLogger());

      for (final LogTopic topic : log.getTopics()) {
        insertBytes(topic);
      }
      return this;
    }

    /**
     * Insert logs.
     *
     * @param logs the logs
     * @return the builder
     */
    public Builder insertLogs(final Collection<Log> logs) {
      logs.forEach(this::insertLog);
      return this;
    }

    /**
     * Insert bytes.
     *
     * @param value the value
     * @return the builder
     */
    public Builder insertBytes(final Bytes value) {
      setBits(keccak256(value));
      return this;
    }

    /**
     * Build logs bloom filter.
     *
     * @return the logs bloom filter
     */
    public LogsBloomFilter build() {
      return new LogsBloomFilter(data);
    }

    /**
     * Discover the low order 11-bits, of the first three double-bytes, of the KECCAK256 hash, of
     * each value and update the bloom filter accordingly.
     *
     * @param hashValue The hash of the log item.
     */
    private void setBits(final Bytes hashValue) {
      for (int counter = 0; counter < 6; counter += 2) {
        final int setBloomBit =
            ((hashValue.get(counter) & LEAST_SIGNIFICANT_THREE_BITS) << BITS_IN_BYTE)
                + (hashValue.get(counter + 1) & LEAST_SIGNIFICANT_BYTE);
        setBit(setBloomBit);
      }
    }

    private void setBit(final int index) {
      final int byteIndex = BYTE_SIZE - 1 - index / 8;
      final int bitIndex = index % 8;
      data.set(byteIndex, (byte) (data.get(byteIndex) | (1 << bitIndex)));
    }
  }
}
