/*
 * Copyright (c) [2016] [ <ether.camp> ]
 * This file is part of the ethereumJ library.
 *
 * The ethereumJ library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * The ethereumJ library 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
 */
package org.ethereum.net.rlpx;

import org.ethereum.crypto.ECKey;
import org.ethereum.util.FastByteComparisons;
import org.spongycastle.util.BigIntegers;

import java.security.SignatureException;

import static org.ethereum.crypto.HashUtil.sha3;
import static org.ethereum.util.ByteUtil.merge;
import static org.ethereum.util.ByteUtil.toHexString;

public abstract class Message {

    byte[] wire;

    byte[] mdc;
    byte[] signature;
    byte[] type;
    byte[] data;

    public static Message decode(byte[] wire) {

        if (wire.length < 98) throw new RuntimeException("Bad message");

        byte[] mdc = new byte[32];
        System.arraycopy(wire, 0, mdc, 0, 32);

        byte[] signature = new byte[65];
        System.arraycopy(wire, 32, signature, 0, 65);

        byte[] type = new byte[1];
        type[0] = wire[97];

        byte[] data = new byte[wire.length - 98];
        System.arraycopy(wire, 98, data, 0, data.length);

        byte[] mdcCheck = sha3(wire, 32, wire.length - 32);

        int check = FastByteComparisons.compareTo(mdc, 0, mdc.length, mdcCheck, 0, mdcCheck.length);

        if (check != 0) throw new RuntimeException("MDC check failed");

        Message msg;
        if (type[0] == 1) msg = new PingMessage();
        else if (type[0] == 2) msg = new PongMessage();
        else if (type[0] == 3) msg = new FindNodeMessage();
        else if (type[0] == 4) msg = new NeighborsMessage();
        else throw new RuntimeException("Unknown RLPx message: " + type[0]);

        msg.mdc = mdc;
        msg.signature = signature;
        msg.type = type;
        msg.data = data;
        msg.wire = wire;

        msg.parse(data);

        return msg;
    }


    public Message encode(byte[] type, byte[] data, ECKey privKey) {

        /* [1] Calc keccak - prepare for sig */
        byte[] payload = new byte[type.length + data.length];
        payload[0] = type[0];
        System.arraycopy(data, 0, payload, 1, data.length);
        byte[] forSig = sha3(payload);

        /* [2] Crate signature*/
        ECKey.ECDSASignature signature = privKey.sign(forSig);

        signature.v -= 27;

        byte[] sigBytes =
                merge(BigIntegers.asUnsignedByteArray(32, signature.r),
                        BigIntegers.asUnsignedByteArray(32, signature.s), new byte[]{signature.v});

        // [3] calculate MDC
        byte[] forSha = merge(sigBytes, type, data);
        byte[] mdc = sha3(forSha);

        // wrap all the data in to the packet
        this.mdc = mdc;
        this.signature = sigBytes;
        this.type = type;
        this.data = data;

        this.wire = merge(this.mdc, this.signature, this.type, this.data);

        return this;
    }

    public ECKey getKey() {

        byte[] r = new byte[32];
        byte[] s = new byte[32];
        byte v = signature[64];

        // todo: remove this when cpp conclude what they do here
        if (v == 1) v = 28;
        if (v == 0) v = 27;

        System.arraycopy(signature, 0, r, 0, 32);
        System.arraycopy(signature, 32, s, 0, 32);

        ECKey.ECDSASignature signature = ECKey.ECDSASignature.fromComponents(r, s, v);
        byte[] msgHash = sha3(wire, 97, wire.length - 97);

        ECKey outKey = null;
        try {
            outKey = ECKey.signatureToKey(msgHash, signature);
        } catch (SignatureException e) {
            e.printStackTrace();
        }

        return outKey;
    }

    public byte[] getNodeId() {
        return getKey().getNodeId();
    }

    public byte[] getPacket() {
        return wire;
    }

    public byte[] getMdc() {
        return mdc;
    }

    public byte[] getSignature() {
        return signature;
    }

    public byte[] getType() {
        return type;
    }

    public byte[] getData() {
        return data;
    }

    public abstract void parse(byte[] data);

    @Override
    public String toString() {
        return "{" +
                "mdc=" + toHexString(mdc) +
                ", signature=" + toHexString(signature) +
                ", type=" + toHexString(type) +
                ", data=" + toHexString(data) +
                '}';
    }
}
