/*
 * Copyright contributors to Hyperledger Besu..
 *
 * 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.api.jsonrpc.internal.methods;

import org.hyperledger.besu.datatypes.DataGas;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.TransactionTrace;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.debug.TraceFrame;
import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult;
import org.hyperledger.besu.ethereum.vm.BlockHashLookup;
import org.hyperledger.besu.ethereum.vm.CachingBlockHashLookup;
import org.hyperledger.besu.ethereum.vm.DebugOperationTracer;

import java.util.List;
import java.util.Optional;
import java.util.function.Function;

public class ExecuteTransactionStep implements Function<TransactionTrace, TransactionTrace> {

  private final TraceBlock.ChainUpdater chainUpdater;
  private final DebugOperationTracer tracer;
  private final MainnetTransactionProcessor transactionProcessor;
  private final Blockchain blockchain;
  private final ProtocolSpec protocolSpec;
  private final Block block;

  public ExecuteTransactionStep(
      final TraceBlock.ChainUpdater chainUpdater,
      final MainnetTransactionProcessor transactionProcessor,
      final Blockchain blockchain,
      final DebugOperationTracer tracer,
      final ProtocolSpec protocolSpec,
      final Block block) {
    this.chainUpdater = chainUpdater;
    this.transactionProcessor = transactionProcessor;
    this.blockchain = blockchain;
    this.tracer = tracer;
    this.protocolSpec = protocolSpec;
    this.block = block;
  }

  public ExecuteTransactionStep(
      final TraceBlock.ChainUpdater chainUpdater,
      final MainnetTransactionProcessor transactionProcessor,
      final Blockchain blockchain,
      final DebugOperationTracer tracer,
      final ProtocolSpec protocolSpec) {
    this(chainUpdater, transactionProcessor, blockchain, tracer, protocolSpec, null);
  }

  @Override
  public TransactionTrace apply(final TransactionTrace transactionTrace) {
    Block block = this.block;
    // case where transactionTrace is created only to trace a block reward
    if (block == null) {
      block =
          transactionTrace
              .getBlock()
              .orElseThrow(
                  () ->
                      new RuntimeException(
                          "Expecting reward block to be in transactionTrace but was empty"));
    }

    List<TraceFrame> traceFrames = null;
    TransactionProcessingResult result = null;
    // If it is not a reward Block trace
    if (transactionTrace.getTransaction() != null) {
      BlockHeader header = block.getHeader();
      final Optional<BlockHeader> maybeParentHeader =
          blockchain.getBlockHeader(header.getParentHash());
      final Wei dataGasPrice =
          protocolSpec
              .getFeeMarket()
              .dataPrice(
                  maybeParentHeader.flatMap(BlockHeader::getExcessDataGas).orElse(DataGas.ZERO));
      final BlockHashLookup blockHashLookup = new CachingBlockHashLookup(header, blockchain);
      result =
          transactionProcessor.processTransaction(
              blockchain,
              chainUpdater.getNextUpdater(),
              header,
              transactionTrace.getTransaction(),
              header.getCoinbase(),
              tracer,
              blockHashLookup,
              false,
              dataGasPrice);

      traceFrames = tracer.copyTraceFrames();
      tracer.reset();
    }
    return new TransactionTrace(
        transactionTrace.getTransaction(), result, traceFrames, transactionTrace.getBlock());
  }
}
