/*
* Copyright 2014 http://Bither.net
*
* 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 net.bither.bitherj.core;

import net.bither.bitherj.db.AbstractDb;
import net.bither.bitherj.exception.ProtocolException;
import net.bither.bitherj.message.Message;
import net.bither.bitherj.script.Script;
import net.bither.bitherj.utils.Utils;
import net.bither.bitherj.utils.VarInt;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;

import javax.annotation.Nullable;


public class In extends Message {
    private static final Logger log = LoggerFactory.getLogger(In.class);

    public static final int OUTPOINT_MESSAGE_LENGTH = 36;
    public static final long NO_SEQUENCE = 0xFFFFFFFFL;

    private byte[] txHash;
    private int inSn;
    private byte[] prevTxHash;
    private int prevOutSn;
    private byte[] inSignature;
    private byte[] prevOutScript;
    private long inSequence;
    private Tx tx;
    private transient Out connectedOut;

    public byte[] getTxHash() {
        return txHash;
    }

    public void setTxHash(byte[] txHash) {
        this.txHash = txHash;
    }

    public int getInSn() {
        return inSn;
    }

    public void setInSn(int inSn) {
        this.inSn = inSn;
    }

    public byte[] getPrevTxHash() {
        return prevTxHash;
    }

    public void setPrevTxHash(byte[] prevTxHash) {
        this.prevTxHash = prevTxHash;
    }

    public int getPrevOutSn() {
        return prevOutSn;
    }

    public void setPrevOutSn(int prevOutSn) {
        this.prevOutSn = prevOutSn;
    }

    public byte[] getInSignature() {
        return inSignature;
    }

    public void setInSignature(byte[] inSignature) {
        this.inSignature = inSignature;
    }

    public byte[] getPrevOutScript() {
        return prevOutScript;
    }

    public void setPrevOutScript(byte[] prevOutScript) {
        this.prevOutScript = prevOutScript;
    }

    public long getInSequence() {
        return inSequence;
    }

    public void setInSequence(long inSequence) {
        this.inSequence = inSequence;
    }

    public OutPoint getOutpoint() {
        return new OutPoint(this.prevTxHash, this.prevOutSn);
    }

    public Tx getTx() {
        return tx;
    }

    public void setTx(Tx tx) {
        this.tx = tx;
        this.txHash = tx.getTxHash();
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof In) {
            In inItem = (In) o;
            return getInSn() == inItem.getInSn() &&
                    Arrays.equals(getPrevTxHash(), inItem.getPrevTxHash()) &&
                    getPrevOutSn() == inItem.getPrevOutSn() &&
                    Arrays.equals(getTxHash(), inItem.getTxHash()) &&
                    getInSequence() == inItem.getInSequence() &&
                    Arrays.equals(getInSignature(), inItem.getInSignature());
        } else {
            return false;
        }
    }

    public In() {
        this.inSequence = NO_SEQUENCE;
    }

    public In(Tx tx, byte[] msg, int offset) {
        super(msg, offset);
        this.tx = tx;
        this.txHash = tx.getTxHash();
    }

    public In(Tx tx, Out out) {
        this.tx = tx;
        this.txHash = tx.getTxHash();
        prevTxHash = out.getTxHash();
        prevOutSn = out.getOutSn();
        prevOutScript = out.getOutScript();
        this.inSequence = NO_SEQUENCE;
    }

    public In(@Nullable Tx parentTransaction, byte[] scriptBytes,
              Out outpoint) {
        super();
        this.inSignature = scriptBytes;
        this.prevTxHash = outpoint.getTxHash();
        this.prevOutSn = outpoint.getOutSn();
        this.prevOutScript = outpoint.getOutScript();
        this.inSequence = NO_SEQUENCE;
        this.tx = parentTransaction;
        this.txHash = this.tx.getTxHash();
        length = 40 + (scriptBytes == null ? 1 : VarInt.sizeOf(scriptBytes.length) + scriptBytes.length);
    }

    protected void parse() throws ProtocolException {
        int curs = cursor;
        int scriptLen = (int) readVarInt(36);
        length = cursor - offset + scriptLen + 4;
        cursor = curs;
        prevTxHash = readHash();
        prevOutSn = (int) readUint32();
        scriptLen = (int) readVarInt();
        inSignature = readBytes(scriptLen);
        inSequence = readUint32();
    }

    @Override
    protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
        stream.write(prevTxHash);
        Utils.uint32ToByteStreamLE(prevOutSn, stream);
        stream.write(new VarInt(inSignature.length).encode());
        stream.write(inSignature);
        Utils.uint32ToByteStreamLE(inSequence, stream);
    }

    /**
     * Coinbase transactions have special inputs with hashes of zero. If this is such an input, returns true.
     */
    public boolean isCoinBase() {
        return Arrays.equals(prevTxHash, new byte[32]) &&
                (prevOutSn & 0xFFFFFFFFL) == 0xFFFFFFFFL;  // -1 but all is serialized to the wire as unsigned int.
    }

    /**
     * @return true if this transaction's sequence number is set (ie it may be a part of a time-locked transaction)
     */
    public boolean hasSequence() {
        return inSequence != NO_SEQUENCE;
    }

    public Out getConnectedOut() {
        if (connectedOut == null) {
            Tx preTx = AbstractDb.txProvider.getTxDetailByTxHash(getPrevTxHash());
            if (preTx == null) {
                return null;
            }
            int prevOutSn = getPrevOutSn();
            for (Out out : preTx.getOuts()) {
                if (out.getOutSn() == prevOutSn) {
                    connectedOut = out;
                }
            }
        }
        return connectedOut;
    }

    public String getFromAddress() {
        if (getConnectedOut() != null) {
            return getConnectedOut().getOutAddress();
        } else if (this.getInSignature() != null && !this.isCoinBase()) {
            Script script = new Script(this.getInSignature());
            return script.getFromAddress();
        }
        return null;
    }

    public long getValue() {
        if (getConnectedOut() != null) {
            return getConnectedOut().getOutValue();
        }
        return 0;
    }

    public List<byte[]> getP2SHPubKeys() {
        Script script = new Script(this.getInSignature());
        return script.getP2SHPubKeys(this.tx, this.inSn);
    }
}