/*
 * 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.ethereum.eth.messages;

import static com.google.common.base.Preconditions.checkArgument;

import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.AbstractMessageData;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
import org.hyperledger.besu.ethereum.rlp.RLP;
import org.hyperledger.besu.ethereum.rlp.RLPInput;
import org.hyperledger.besu.ethereum.rlp.RLPOutput;

import java.util.Optional;
import java.util.OptionalLong;

import org.apache.tuweni.bytes.Bytes;

/** PV62 GetBlockHeaders Message. */
public final class GetBlockHeadersMessage extends AbstractMessageData {

  private GetBlockHeadersData getBlockHeadersData = null;

  public static GetBlockHeadersMessage readFrom(final MessageData message) {
    if (message instanceof GetBlockHeadersMessage) {
      return (GetBlockHeadersMessage) message;
    }
    final int code = message.getCode();
    if (code != EthPV62.GET_BLOCK_HEADERS) {
      throw new IllegalArgumentException(
          String.format("Message has code %d and thus is not a GetBlockHeadersMessage.", code));
    }
    return new GetBlockHeadersMessage(message.getData());
  }

  public static GetBlockHeadersMessage create(
      final long blockNum, final int maxHeaders, final int skip, final boolean reverse) {
    final GetBlockHeadersData getBlockHeadersData =
        GetBlockHeadersData.create(blockNum, maxHeaders, skip, reverse);
    final BytesValueRLPOutput tmp = new BytesValueRLPOutput();
    getBlockHeadersData.writeTo(tmp);
    return new GetBlockHeadersMessage(tmp.encoded());
  }

  public static GetBlockHeadersMessage create(
      final Hash hash, final int maxHeaders, final int skip, final boolean reverse) {
    final GetBlockHeadersData getBlockHeadersData =
        GetBlockHeadersData.create(hash, maxHeaders, skip, reverse);
    final BytesValueRLPOutput tmp = new BytesValueRLPOutput();
    getBlockHeadersData.writeTo(tmp);
    return new GetBlockHeadersMessage(tmp.encoded());
  }

  private GetBlockHeadersMessage(final Bytes data) {
    super(data);
  }

  @Override
  public int getCode() {
    return EthPV62.GET_BLOCK_HEADERS;
  }

  /**
   * Returns the block number that the message requests or {@link OptionalLong#empty()} if the
   * request specifies a block hash.
   *
   * @return Block Number Requested or {@link OptionalLong#empty()}
   */
  public OptionalLong blockNumber() {
    return getBlockHeadersData().blockNumber;
  }

  /**
   * Returns the block hash that the message requests or {@link Optional#empty()} if the request
   * specifies a block number.
   *
   * @return Block Hash Requested or {@link Optional#empty()}
   */
  public Optional<Hash> hash() {
    return getBlockHeadersData().blockHash;
  }

  public int maxHeaders() {
    return getBlockHeadersData().maxHeaders;
  }

  public int skip() {
    return getBlockHeadersData().skip;
  }

  public boolean reverse() {
    return getBlockHeadersData().reverse;
  }

  private GetBlockHeadersData getBlockHeadersData() {
    if (getBlockHeadersData == null) {
      getBlockHeadersData = GetBlockHeadersData.readFrom(RLP.input(data));
    }
    return getBlockHeadersData;
  }

  private static class GetBlockHeadersData {
    private final Optional<Hash> blockHash;
    private final OptionalLong blockNumber;
    private final int maxHeaders;
    private final int skip;
    private final boolean reverse;

    private GetBlockHeadersData(
        final Optional<Hash> blockHash,
        final OptionalLong blockNumber,
        final int maxHeaders,
        final int skip,
        final boolean reverse) {
      checkArgument(
          validateBlockHashAndNumber(blockHash, blockNumber),
          "Either blockHash or blockNumber should be non-empty");
      this.blockHash = blockHash;
      this.blockNumber = blockNumber;
      this.maxHeaders = maxHeaders;
      this.skip = skip;
      this.reverse = reverse;
    }

    private static boolean validateBlockHashAndNumber(
        final Optional<Hash> blockHash, final OptionalLong blockNumber) {
      return (blockHash.isPresent() || blockNumber.isPresent())
          && !(blockHash.isPresent() && blockNumber.isPresent());
    }

    public static GetBlockHeadersData readFrom(final RLPInput input) {
      input.enterList();

      final Optional<Hash> blockHash;
      final OptionalLong blockNumber;
      if (input.nextSize() == Hash.SIZE) {
        blockHash = Optional.of(Hash.wrap(input.readBytes32()));
        blockNumber = OptionalLong.empty();
      } else {
        blockHash = Optional.empty();
        blockNumber = OptionalLong.of(input.readLongScalar());
      }

      final int maxHeaders = input.readIntScalar();
      final int skip = input.readIntScalar();
      final boolean reverse = input.readIntScalar() != 0;

      input.leaveList();

      return new GetBlockHeadersData(blockHash, blockNumber, maxHeaders, skip, reverse);
    }

    public static GetBlockHeadersData create(
        final long blockNum, final int maxHeaders, final int skip, final boolean reverse) {
      return new GetBlockHeadersData(
          Optional.empty(), OptionalLong.of(blockNum), maxHeaders, skip, reverse);
    }

    public static GetBlockHeadersData create(
        final Hash hash, final int maxHeaders, final int skip, final boolean reverse) {
      return new GetBlockHeadersData(
          Optional.of(hash), OptionalLong.empty(), maxHeaders, skip, reverse);
    }

    /**
     * Write an RLP representation.
     *
     * @param out The RLP output to write to
     */
    public void writeTo(final RLPOutput out) {
      out.startList();

      if (blockHash.isPresent()) {
        out.writeBytes(blockHash.get());
      } else {
        out.writeLongScalar(blockNumber.getAsLong());
      }
      out.writeIntScalar(maxHeaders);
      out.writeIntScalar(skip);
      out.writeIntScalar(reverse ? 1 : 0);

      out.endList();
    }
  }
}
