/*
 * This file is part of Bitsquare.
 *
 * Bitsquare is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at
 * your option) any later version.
 *
 * Bitsquare is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
 */
package io.bitsquare.trade;
import com.google.common.base.Throwables;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import io.bitsquare.app.Log;
import io.bitsquare.app.Version;
import io.bitsquare.arbitration.Arbitrator;
import io.bitsquare.arbitration.ArbitratorManager;
import io.bitsquare.btc.TradeWalletService;
import io.bitsquare.btc.WalletService;
import io.bitsquare.common.crypto.KeyRing;
import io.bitsquare.common.taskrunner.Model;
import io.bitsquare.crypto.DecryptedMsgWithPubKey;
import io.bitsquare.filter.FilterManager;
import io.bitsquare.p2p.NodeAddress;
import io.bitsquare.p2p.P2PService;
import io.bitsquare.storage.Storage;
import io.bitsquare.trade.offer.Offer;
import io.bitsquare.trade.offer.OpenOfferManager;
import io.bitsquare.trade.protocol.trade.ProcessModel;
import io.bitsquare.trade.protocol.trade.TradeProtocol;
import io.bitsquare.user.User;
import javafx.beans.property.*;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import static com.google.common.base.Preconditions.checkNotNull;
/**
 * Holds all data which are relevant to the trade, but not those which are only needed in the trade process as shared data between tasks. Those data are
 * stored in the task model.
 */
public abstract class Trade implements Tradable, Model {
    // That object is saved to disc. We need to take care of changes to not break deserialization.
    private static final long serialVersionUID = Version.LOCAL_DB_VERSION;
    private static final Logger log = LoggerFactory.getLogger(Trade.class);
    public enum State {
        PREPARATION(Phase.PREPARATION),
        TAKER_FEE_PAID(Phase.TAKER_FEE_PAID),
        OFFERER_SENT_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED),
        TAKER_PUBLISHED_DEPOSIT_TX(Phase.DEPOSIT_PAID),
        DEPOSIT_SEEN_IN_NETWORK(Phase.DEPOSIT_PAID), // triggered by balance update, used only in error cases
        TAKER_SENT_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PAID),
        OFFERER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PAID),
        DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN(Phase.DEPOSIT_PAID),
        BUYER_CONFIRMED_FIAT_PAYMENT_INITIATED(Phase.FIAT_SENT),
        BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT),
        SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT),
        SELLER_CONFIRMED_FIAT_PAYMENT_RECEIPT(Phase.FIAT_RECEIVED),
        SELLER_SENT_FIAT_PAYMENT_RECEIPT_MSG(Phase.FIAT_RECEIVED),
        BUYER_RECEIVED_FIAT_PAYMENT_RECEIPT_MSG(Phase.FIAT_RECEIVED),
        BUYER_COMMITTED_PAYOUT_TX(Phase.PAYOUT_PAID), //TODO needed?
        BUYER_STARTED_SEND_PAYOUT_TX(Phase.PAYOUT_PAID), // not from the success/arrived handler!
        SELLER_RECEIVED_AND_COMMITTED_PAYOUT_TX(Phase.PAYOUT_PAID),
        PAYOUT_BROAD_CASTED(Phase.PAYOUT_PAID),
        WITHDRAW_COMPLETED(Phase.WITHDRAWN);
        public Phase getPhase() {
            return phase;
        }
        private final Phase phase;
        State(Phase phase) {
            this.phase = phase;
        }
    }
    public enum Phase {
        PREPARATION,
        TAKER_FEE_PAID,
        DEPOSIT_REQUESTED,
        DEPOSIT_PAID,
        FIAT_SENT,
        FIAT_RECEIVED,
        PAYOUT_PAID,
        WITHDRAWN,
        DISPUTE
    }
    public enum DisputeState {
        NONE,
        DISPUTE_REQUESTED,
        DISPUTE_STARTED_BY_PEER,
        DISPUTE_CLOSED
    }
    public enum TradePeriodState {
        NORMAL,
        HALF_REACHED,
        TRADE_PERIOD_OVER
    }
    ///////////////////////////////////////////////////////////////////////////////////////////
    // Fields
    ///////////////////////////////////////////////////////////////////////////////////////////
    // Transient/Immutable
    transient private ObjectProperty<State> stateProperty;
    transient private ObjectProperty<DisputeState> disputeStateProperty;
    transient private ObjectProperty<TradePeriodState> tradePeriodStateProperty;
    // Trades are saved in the TradeList
    @Nullable
    transient private Storage<? extends TradableList> storage;
    transient protected TradeProtocol tradeProtocol;
    transient private Date maxTradePeriodDate, halfTradePeriodDate;
    // Immutable
    private final Offer offer;
    private final ProcessModel processModel;
    // Mutable
    private DecryptedMsgWithPubKey decryptedMsgWithPubKey;
    private Date takeOfferDate;
    private Coin tradeAmount;
    private long tradePrice;
    private NodeAddress tradingPeerNodeAddress;
    @Nullable
    private String takeOfferFeeTxId;
    protected State state;
    private DisputeState disputeState = DisputeState.NONE;
    private TradePeriodState tradePeriodState = TradePeriodState.NORMAL;
    private Transaction depositTx;
    private Contract contract;
    private String contractAsJson;
    private byte[] contractHash;
    private String takerContractSignature;
    private String offererContractSignature;
    private Transaction payoutTx;
    private long lockTimeAsBlockHeight;
    private NodeAddress arbitratorNodeAddress;
    private byte[] arbitratorBtcPubKey;
    private String takerPaymentAccountId;
    private String errorMessage;
    transient private StringProperty errorMessageProperty;
    transient private ObjectProperty<Coin> tradeAmountProperty;
    transient private ObjectProperty<Fiat> tradeVolumeProperty;
    transient private Set<DecryptedMsgWithPubKey> mailboxMessageSet = new HashSet<>();
    ///////////////////////////////////////////////////////////////////////////////////////////
    // Constructor, initialization
    ///////////////////////////////////////////////////////////////////////////////////////////
    // offerer
    protected Trade(Offer offer, Storage<? extends TradableList> storage) {
        this.offer = offer;
        this.storage = storage;
        this.takeOfferDate = new Date();
        processModel = new ProcessModel();
        tradeVolumeProperty = new SimpleObjectProperty<>();
        tradeAmountProperty = new SimpleObjectProperty<>();
        errorMessageProperty = new SimpleStringProperty();
        initStates();
        initStateProperties();
    }
    // taker
    protected Trade(Offer offer, Coin tradeAmount, long tradePrice, NodeAddress tradingPeerNodeAddress,
                    Storage<? extends TradableList> storage) {
        this(offer, storage);
        this.tradeAmount = tradeAmount;
        this.tradePrice = tradePrice;
        this.tradingPeerNodeAddress = tradingPeerNodeAddress;
        tradeAmountProperty.set(tradeAmount);
        tradeVolumeProperty.set(getTradeVolume());
        this.takeOfferDate = new Date();
    }
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        try {
            in.defaultReadObject();
            initStateProperties();
            initAmountProperty();
            errorMessageProperty = new SimpleStringProperty(errorMessage);
            mailboxMessageSet = new HashSet<>();
        } catch (Throwable t) {
            log.warn("Cannot be deserialized." + t.getMessage());
        }
    }
    public void init(P2PService p2PService,
                     WalletService walletService,
                     TradeWalletService tradeWalletService,
                     ArbitratorManager arbitratorManager,
                     TradeManager tradeManager,
                     OpenOfferManager openOfferManager,
                     User user,
                     FilterManager filterManager,
                     KeyRing keyRing,
                     boolean useSavingsWallet,
                     Coin fundsNeededForTrade) {
        Log.traceCall();
        processModel.onAllServicesInitialized(offer,
                tradeManager,
                openOfferManager,
                p2PService,
                walletService,
                tradeWalletService,
                arbitratorManager,
                user,
                filterManager,
                keyRing,
                useSavingsWallet,
                fundsNeededForTrade);
        createProtocol();
        log.trace("init: decryptedMsgWithPubKey = " + decryptedMsgWithPubKey);
        if (decryptedMsgWithPubKey != null && !mailboxMessageSet.contains(decryptedMsgWithPubKey)) {
            mailboxMessageSet.add(decryptedMsgWithPubKey);
            tradeProtocol.applyMailboxMessage(decryptedMsgWithPubKey, this);
        }
    }
    protected void initStateProperties() {
        stateProperty = new SimpleObjectProperty<>(state);
        disputeStateProperty = new SimpleObjectProperty<>(disputeState);
        tradePeriodStateProperty = new SimpleObjectProperty<>(tradePeriodState);
    }
    protected void initAmountProperty() {
        tradeAmountProperty = new SimpleObjectProperty<>();
        tradeVolumeProperty = new SimpleObjectProperty<>();
        if (tradeAmount != null) {
            tradeAmountProperty.set(tradeAmount);
            tradeVolumeProperty.set(getTradeVolume());
        }
    }
    ///////////////////////////////////////////////////////////////////////////////////////////
    // API
    ///////////////////////////////////////////////////////////////////////////////////////////
    // The deserialized tx has not actual confidence data, so we need to get the fresh one from the wallet.
    public void updateDepositTxFromWallet() {
        if (depositTx != null)
            setDepositTx(processModel.getTradeWalletService().getWalletTx(depositTx.getHash()));
    }
    public void setDepositTx(Transaction tx) {
        log.debug("setDepositTx " + tx);
        this.depositTx = tx;
        setupConfidenceListener();
        persist();
    }
    @Nullable
    public Transaction getDepositTx() {
        return depositTx;
    }
    public void setMailboxMessage(DecryptedMsgWithPubKey decryptedMsgWithPubKey) {
        log.trace("setMailboxMessage decryptedMsgWithPubKey=" + decryptedMsgWithPubKey);
        this.decryptedMsgWithPubKey = decryptedMsgWithPubKey;
        if (tradeProtocol != null && decryptedMsgWithPubKey != null && !mailboxMessageSet.contains(decryptedMsgWithPubKey)) {
            mailboxMessageSet.add(decryptedMsgWithPubKey);
            tradeProtocol.applyMailboxMessage(decryptedMsgWithPubKey, this);
        }
    }
    public DecryptedMsgWithPubKey getMailboxMessage() {
        return decryptedMsgWithPubKey;
    }
    public void setStorage(Storage<? extends TradableList> storage) {
        this.storage = storage;
    }
    ///////////////////////////////////////////////////////////////////////////////////////////
    // States
    ///////////////////////////////////////////////////////////////////////////////////////////
    public void setState(State state) {
        log.info("Trade.setState: " + state);
        boolean changed = this.state != state;
        this.state = state;
        stateProperty.set(state);
        if (changed)
            persist();
    }
    public void setDisputeState(DisputeState disputeState) {
        Log.traceCall("disputeState=" + disputeState + "\n\ttrade=" + this);
        boolean changed = this.disputeState != disputeState;
        this.disputeState = disputeState;
        disputeStateProperty.set(disputeState);
        if (changed)
            persist();
    }
    public DisputeState getDisputeState() {
        return disputeState;
    }
    public void setTradePeriodState(TradePeriodState tradePeriodState) {
        boolean changed = this.tradePeriodState != tradePeriodState;
        this.tradePeriodState = tradePeriodState;
        tradePeriodStateProperty.set(tradePeriodState);
        if (changed)
            persist();
    }
    public TradePeriodState getTradePeriodState() {
        return tradePeriodState;
    }
    public boolean isTakerFeePaid() {
        return state.getPhase() != null && state.getPhase().ordinal() >= Phase.TAKER_FEE_PAID.ordinal();
    }
    public boolean isDepositPaid() {
        return state.getPhase() != null && state.getPhase().ordinal() >= Phase.DEPOSIT_PAID.ordinal();
    }
    public State getState() {
        return state;
    }
    ///////////////////////////////////////////////////////////////////////////////////////////
    // Model implementation
    ///////////////////////////////////////////////////////////////////////////////////////////
    // Get called from taskRunner after each completed task
    @Override
    public void persist() {
        if (storage != null)
            storage.queueUpForSave();
    }
    @Override
    public void onComplete() {
        persist();
    }
    ///////////////////////////////////////////////////////////////////////////////////////////
    // Getter only
    ///////////////////////////////////////////////////////////////////////////////////////////
    public String getId() {
        return offer.getId();
    }
    public String getShortId() {
        return offer.getShortId();
    }
    public Offer getOffer() {
        return offer;
    }
    abstract public Coin getPayoutAmount();
    public ProcessModel getProcessModel() {
        return processModel;
    }
    @Nullable
    public Fiat getTradeVolume() {
        if (tradeAmount != null && getTradePrice() != null)
            return new ExchangeRate(getTradePrice()).coinToFiat(tradeAmount);
        else
            return null;
    }
    @Nullable
    public Date getMaxTradePeriodDate() {
        if (maxTradePeriodDate == null && takeOfferDate != null)
            maxTradePeriodDate = new Date(takeOfferDate.getTime() + getOffer().getPaymentMethod().getMaxTradePeriod());
        return maxTradePeriodDate;
    }
    @Nullable
    public Date getHalfTradePeriodDate() {


Answer:         if (halfTradePeriodDate == null && takeOfferDate != null)