/*
 * Copyright 2013 Google Inc.
 * Copyright 2014 Andreas Schildbach
 *
 * 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 org.bitcoinj.wallet;

import org.bitcoinj.base.BitcoinNetwork;
import org.bitcoinj.crypto.ECKey;
import org.bitcoinj.crypto.ECKey.ECDSASignature;
import org.bitcoinj.crypto.SignatureDecodeException;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.ScriptChunk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.time.Instant;
import java.util.List;
import java.util.Optional;

import static org.bitcoinj.base.internal.Preconditions.checkState;

/**
 * <p>The default risk analysis. Currently, it only is concerned with whether a tx/dependency is non-final or not, and
 * whether a tx/dependency violates the dust rules. Outside of specialised protocols you should not encounter non-final
 * transactions.</p>
 */
public class DefaultRiskAnalysis implements RiskAnalysis {
    private static final Logger log = LoggerFactory.getLogger(DefaultRiskAnalysis.class);

    protected final Transaction tx;
    protected final List<Transaction> dependencies;
    @Nullable protected final Wallet wallet;

    private Transaction nonStandard;
    protected Transaction nonFinal;
    protected boolean analyzed;

    private DefaultRiskAnalysis(Wallet wallet, Transaction tx, List<Transaction> dependencies) {
        this.tx = tx;
        this.dependencies = dependencies;
        this.wallet = wallet;
    }

    @Override
    public Result analyze() {
        checkState(!analyzed);
        analyzed = true;

        Result result = analyzeIsFinal();
        if (result != null && result != Result.OK)
            return result;

        return analyzeIsStandard();
    }

    @Nullable
    private Result analyzeIsFinal() {
        // Transactions we create ourselves are, by definition, not at risk of double spending against us.
        if (tx.getConfidence().getSource() == TransactionConfidence.Source.SELF)
            return Result.OK;

        // We consider transactions that opt into replace-by-fee at risk of double spending.
        if (tx.isOptInFullRBF()) {
            nonFinal = tx;
            return Result.NON_FINAL;
        }

        // Relative time-locked transactions are risky too. We can't check the locks because usually we don't know the
        // spent outputs (to know when they were created).
        if (tx.hasRelativeLockTime()) {
            nonFinal = tx;
            return Result.NON_FINAL;
        }

        if (wallet == null)
            return null;


        final int height = wallet.getLastBlockSeenHeight();
        final Optional<Instant> time = wallet.lastBlockSeenTime();
        if (!time.isPresent())
            return null;

        // If the transaction has a lock time specified in blocks, we consider that if the tx would become final in the
        // next block it is not risky (as it would confirm normally).
        final int adjustedHeight = height + 1;
        final long timeSecs = time.get().getEpochSecond();

        if (!tx.isFinal(adjustedHeight, timeSecs)) {
            nonFinal = tx;
            return Result.NON_FINAL;
        }
        for (Transaction dep : dependencies) {
            if (!dep.isFinal(adjustedHeight, timeSecs)) {
                nonFinal = dep;
                return Result.NON_FINAL;
            }
        }

        return Result.OK;
    }

    /**
     * The reason a transaction is considered non-standard, returned by
     * {@link #isStandard(Transaction)}.
     */
    public enum RuleViolation {
        NONE,
        VERSION,
        DUST,
        SHORTEST_POSSIBLE_PUSHDATA,
        NONEMPTY_STACK, // Not yet implemented (for post 0.12)
        SIGNATURE_CANONICAL_ENCODING
    }

    /**
     * <p>Checks if a transaction is considered "standard" by Bitcoin Core's IsStandardTx and AreInputsStandard
     * functions.</p>
     *
     * <p>Note that this method currently only implements a minimum of checks. More to be added later.</p>
     */
    public static RuleViolation isStandard(Transaction tx) {
        // TODO: Finish this function off.
        if (tx.getVersion() > 2 || tx.getVersion() < 1) {
            log.warn("TX considered non-standard due to unknown version number {}", tx.getVersion());
            return RuleViolation.VERSION;
        }

        final List<TransactionOutput> outputs = tx.getOutputs();
        for (int i = 0; i < outputs.size(); i++) {
            TransactionOutput output = outputs.get(i);
            RuleViolation violation = isOutputStandard(output);
            if (violation != RuleViolation.NONE) {
                log.warn("TX considered non-standard due to output {} violating rule {}", i, violation);
                return violation;
            }
        }

        final List<TransactionInput> inputs = tx.getInputs();
        for (int i = 0; i < inputs.size(); i++) {
            TransactionInput input = inputs.get(i);
            RuleViolation violation = isInputStandard(input);
            if (violation != RuleViolation.NONE) {
                log.warn("TX considered non-standard due to input {} violating rule {}", i, violation);
                return violation;
            }
        }

        return RuleViolation.NONE;
    }

    /**
     * Checks the output to see if the script violates a standardness rule. Not complete.
     */
    public static RuleViolation isOutputStandard(TransactionOutput output) {
        if (output.isDust())
            return RuleViolation.DUST;
        for (ScriptChunk chunk : output.getScriptPubKey().chunks()) {
            if (chunk.isPushData() && !chunk.isShortestPossiblePushData())
                return RuleViolation.SHORTEST_POSSIBLE_PUSHDATA;
        }
        return RuleViolation.NONE;
    }

    /** Checks if the given input passes some of the AreInputsStandard checks. Not complete. */
    public static RuleViolation isInputStandard(TransactionInput input) {
        for (ScriptChunk chunk : input.getScriptSig().chunks()) {
            if (chunk.data != null && !chunk.isShortestPossiblePushData())
                return RuleViolation.SHORTEST_POSSIBLE_PUSHDATA;
            if (chunk.isPushData()) {
                ECDSASignature signature;
                try {
                    signature = ECKey.ECDSASignature.decodeFromDER(chunk.data);
                } catch (SignatureDecodeException x) {
                    // Doesn't look like a signature.
                    signature = null;
                }
                if (signature != null) {
                    if (!TransactionSignature.isEncodingCanonical(chunk.data))
                        return RuleViolation.SIGNATURE_CANONICAL_ENCODING;
                    if (!signature.isCanonical())
                        return RuleViolation.SIGNATURE_CANONICAL_ENCODING;
                }
            }
        }
        return RuleViolation.NONE;
    }

    private Result analyzeIsStandard() {
        // The IsStandard rules don't apply on testnet, because they're just a safety mechanism and we don't want to
        // crush innovation with valueless test coins.
        if (wallet != null && wallet.network() != BitcoinNetwork.MAINNET)
            return Result.OK;

        RuleViolation ruleViolation = isStandard(tx);
        if (ruleViolation != RuleViolation.NONE) {
            nonStandard = tx;
            return Result.NON_STANDARD;
        }

        for (Transaction dep : dependencies) {
            ruleViolation = isStandard(dep);
            if (ruleViolation != RuleViolation.NONE) {
                nonStandard = dep;
                return Result.NON_STANDARD;
            }
        }

        return Result.OK;
    }

    /** Returns the transaction that was found to be non-standard, or null. */
    @Nullable
    public Transaction getNonStandard() {
        return nonStandard;
    }

    /** Returns the transaction that was found to be non-final, or null. */
    @Nullable
    public Transaction getNonFinal() {
        return nonFinal;
    }

    @Override
    public String toString() {
        if (!analyzed)
            return "Pending risk analysis for " + tx.getTxId();
        else if (nonFinal != null)
            return "Risky due to non-finality of " + nonFinal.getTxId();
        else if (nonStandard != null)
            return "Risky due to non-standard tx " + nonStandard.getTxId();
        else
            return "Non-risky";
    }

    public static class Analyzer implements RiskAnalysis.Analyzer {
        @Override
        public DefaultRiskAnalysis create(Wallet wallet, Transaction tx, List<Transaction> dependencies) {
            return new DefaultRiskAnalysis(wallet, tx, dependencies);
        }
    }

    public static Analyzer FACTORY = new Analyzer();
}
