/*
 * 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.core.encoding;

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

import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.rlp.RLP;
import org.hyperledger.besu.ethereum.rlp.RLPInput;

import java.util.Optional;

import com.google.common.collect.ImmutableMap;
import org.apache.tuweni.bytes.Bytes;

public class TransactionDecoder {

  @FunctionalInterface
  interface Decoder {
    Transaction decode(RLPInput input);
  }

  private static final ImmutableMap<TransactionType, Decoder> TYPED_TRANSACTION_DECODERS =
      ImmutableMap.of(
          TransactionType.ACCESS_LIST,
          AccessListTransactionDecoder::decode,
          TransactionType.EIP1559,
          EIP1559TransactionDecoder::decode,
          TransactionType.BLOB,
          BlobTransactionDecoder::decode,
          TransactionType.DELEGATE_CODE,
          CodeDelegationTransactionDecoder::decode);

  private static final ImmutableMap<TransactionType, Decoder> POOLED_TRANSACTION_DECODERS =
      ImmutableMap.of(TransactionType.BLOB, BlobPooledTransactionDecoder::decode);

  /**
   * Decodes an RLP input into a transaction. If the input represents a typed transaction, it uses
   * the appropriate decoder for that type. Otherwise, it uses the frontier decoder.
   *
   * @param rlpInput the RLP input
   * @return the decoded transaction
   */
  public static Transaction decodeRLP(
      final RLPInput rlpInput, final EncodingContext encodingContext) {
    if (isTypedTransaction(rlpInput)) {
      return decodeTypedTransaction(rlpInput, encodingContext);
    } else {
      return FrontierTransactionDecoder.decode(rlpInput);
    }
  }

  /**
   * Decodes a typed transaction from an RLP input. It first reads the transaction type from the
   * input, then uses the appropriate decoder for that type.
   *
   * @param rlpInput the RLP input
   * @return the decoded transaction
   */
  private static Transaction decodeTypedTransaction(
      final RLPInput rlpInput, final EncodingContext context) {
    // Read the typed transaction bytes from the RLP input
    final Bytes typedTransactionBytes = rlpInput.readBytes();

    // Determine the transaction type from the typed transaction bytes
    TransactionType transactionType =
        getTransactionType(typedTransactionBytes)
            .orElseThrow((() -> new IllegalArgumentException("Unsupported transaction type")));
    return decodeTypedTransaction(typedTransactionBytes, transactionType, context);
  }

  /**
   * Decodes a typed transaction. The method first slices the transaction bytes to exclude the
   * transaction type, then uses the appropriate decoder for the transaction type to decode the
   * remaining bytes.
   *
   * @param transactionBytes the transaction bytes
   * @param transactionType the type of the transaction
   * @param context the encoding context
   * @return the decoded transaction
   */
  private static Transaction decodeTypedTransaction(
      final Bytes transactionBytes,
      final TransactionType transactionType,
      final EncodingContext context) {
    // Slice the transaction bytes to exclude the transaction type and prepare for decoding
    final RLPInput transactionInput = RLP.input(transactionBytes.slice(1));
    // Use the appropriate decoder for the transaction type to decode the remaining bytes
    return getDecoder(transactionType, context).decode(transactionInput);
  }

  /**
   * Decodes a transaction from opaque bytes. The method first determines the transaction type from
   * the bytes. If the type is present, it delegates the decoding process to the appropriate decoder
   * for that type. If the type is not present, it decodes the bytes as an RLP input.
   *
   * @param opaqueBytes the opaque bytes
   * @param context the encoding context
   * @return the decoded transaction
   */
  public static Transaction decodeOpaqueBytes(
      final Bytes opaqueBytes, final EncodingContext context) {
    var transactionType = getTransactionType(opaqueBytes);
    if (transactionType.isPresent()) {
      return decodeTypedTransaction(opaqueBytes, transactionType.get(), context);
    } else {
      // If the transaction type is not present, decode the opaque bytes as RLP
      return decodeRLP(RLP.input(opaqueBytes), context);
    }
  }

  /**
   * Retrieves the transaction type from the provided bytes. The method attempts to extract the
   * first byte from the input bytes and interpret it as a transaction type. If the byte does not
   * correspond to a valid transaction type, the method returns an empty Optional.
   *
   * @param opaqueBytes the bytes from which to extract the transaction type
   * @return an Optional containing the TransactionType if the first byte of the input corresponds
   *     to a valid transaction type, or an empty Optional if it does not
   */
  private static Optional<TransactionType> getTransactionType(final Bytes opaqueBytes) {
    try {
      byte transactionTypeByte = opaqueBytes.get(0);
      return Optional.of(TransactionType.of(transactionTypeByte));
    } catch (IllegalArgumentException ex) {
      return Optional.empty();
    }
  }

  /**
   * Checks if the given RLP input is a typed transaction.
   *
   * <p>See EIP-2718
   *
   * <p>If it starts with a value in the range [0, 0x7f] then it is a new transaction type
   *
   * <p>if it starts with a value in the range [0xc0, 0xfe] then it is a legacy transaction type
   *
   * @param rlpInput the RLP input
   * @return true if the RLP input is a typed transaction, false otherwise
   */
  private static boolean isTypedTransaction(final RLPInput rlpInput) {
    return !rlpInput.nextIsList();
  }

  /**
   * Gets the decoder for a given transaction type and encoding context. If the context is
   * POOLED_TRANSACTION, it uses the network decoder for the type. Otherwise, it uses the typed
   * decoder.
   *
   * @param transactionType the transaction type
   * @param encodingContext the encoding context
   * @return the decoder
   */
  private static Decoder getDecoder(
      final TransactionType transactionType, final EncodingContext encodingContext) {
    if (encodingContext.equals(EncodingContext.POOLED_TRANSACTION)) {
      if (POOLED_TRANSACTION_DECODERS.containsKey(transactionType)) {
        return POOLED_TRANSACTION_DECODERS.get(transactionType);
      }
    }
    return checkNotNull(
        TYPED_TRANSACTION_DECODERS.get(transactionType),
        "Developer Error. A supported transaction type %s has no associated decoding logic",
        transactionType);
  }
}
