package com.alphawallet.token.tools;

import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.ECPointUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.jcajce.provider.digest.Keccak;
import org.bouncycastle.util.encoders.Hex;

import java.math.BigInteger;
import java.security.*;
import java.security.interfaces.ECPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;

/***** WARNING *****
 *
 * TrustAddress can be generated without the TokenScript being
 * signed. It's digest is produced in the way "as if tokenscript is
 * signed", therefore please do not add logic like extracting
 * <SignedInfo> from the TokenScript assuming it's signed.
 * - Weiwu
 */
public class TrustAddressGenerator {
    private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
    private static final ECDomainParameters CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(),
            CURVE_PARAMS.getN(), CURVE_PARAMS.getH());

    public static final byte[] masterPubKey = Hex.decode("04f0985bd9dbb6f461adc994a0c12595716a7f4fb2879bfc5155dffec3770096201c13f8314b46db8d8177887f8d95af1f2dd217291ce6ffe9183681186696bbe5");

    public static String getTrustAddress(String contractAddress, String digest) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
        return preimageToAddress((contractAddress + "TRUST" + digest).getBytes());
    }

    public static String getRevokeAddress(String contractAddress, String digest) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
        return preimageToAddress((contractAddress + "REVOKE" + digest).getBytes());
    }

    // this won't make sense at all if you didn't read security.md
    // https://github.com/AlphaWallet/TokenScript/blob/master/doc/security.md
    public static String preimageToAddress(byte[] preimage) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
        Security.addProvider(new BouncyCastleProvider());

        // get the hash of the preimage text
        Keccak.Digest256 digest = new Keccak.Digest256();
        digest.update(preimage);
        byte[] hash = digest.digest();

        // use the hash to derive a new address
        BigInteger keyDerivationFactor = new BigInteger(Numeric.toHexStringNoPrefix(hash), 16);
        ECPoint donatePKPoint = extractPublicKey(decodeKey(masterPubKey));
        ECPoint digestPKPoint = donatePKPoint.multiply(keyDerivationFactor);
        return getAddress(digestPKPoint);
    }

    private static ECPoint extractPublicKey(ECPublicKey ecPublicKey) {
        java.security.spec.ECPoint publicPointW = ecPublicKey.getW();
        BigInteger xCoord = publicPointW.getAffineX();
        BigInteger yCoord = publicPointW.getAffineY();
        return CURVE.getCurve().createPoint(xCoord, yCoord);
    }

    private static ECPublicKey decodeKey(byte[] encoded)
            throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
        ECNamedCurveParameterSpec params = ECNamedCurveTable.getParameterSpec("secp256k1");
        KeyFactory fact = KeyFactory.getInstance("ECDSA", "BC");
        ECCurve curve = params.getCurve();
        java.security.spec.EllipticCurve ellipticCurve = EC5Util.convertCurve(curve, params.getSeed());
        java.security.spec.ECPoint point = ECPointUtil.decodePoint(ellipticCurve, encoded);
        java.security.spec.ECParameterSpec params2 = EC5Util.convertSpec(ellipticCurve, params);
        java.security.spec.ECPublicKeySpec keySpec = new java.security.spec.ECPublicKeySpec(point, params2);
        return (ECPublicKey) fact.generatePublic(keySpec);
    }

    private static String getAddress(ECPoint pub) {
        byte[] pubKeyHash = computeAddress(pub);
        return Numeric.toHexString(pubKeyHash);
    }

    private static byte[] computeAddress(byte[] pubBytes) {
        Keccak.Digest256 digest = new Keccak.Digest256();
        digest.update(Arrays.copyOfRange(pubBytes, 1, pubBytes.length));
        byte[] addressBytes = digest.digest();
        return Arrays.copyOfRange(addressBytes, 0, 20);
    }

    private static byte[] computeAddress(ECPoint pubPoint) {
        return computeAddress(pubPoint.getEncoded(false ));
    }

    /**********************************************************************************
     For use in Command Console
     **********************************************************************************/

    public static void main(String args[]) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
        if (args.length == 2) {
            System.out.println("Express of Trust Address derived using the following:");
            System.out.println("");
            System.out.println("\tContract Address: " + args[0]);
            System.out.println("\tXML Digest for Signature: " + args[1]);
            System.out.println("");
            System.out.println("Are:");
            System.out.println("");
            System.out.println("\tTrust Address:\t" + getTrustAddress(args[0], args[1]));
            System.out.println("\tRevoke Address:\t" + getRevokeAddress(args[0], args[1]));
        } else {
            System.out.println("This utility generates express-of-trust address and its revocation address\n for a given pair of token contract and TokenScript");
            System.out.println("");
            System.out.println("Expecting two arguments: contract address and XML digest.");
            System.out.println("");
            System.out.println("\tExample:");
            System.out.println("\tAssuming classpath is set properly,:");
            System.out.println("\te.g. if you built the lib project with `gradle shadowJar` and you've set");
            System.out.println("\tCLASSPATH=build/libs/lib-all.jar");
            System.out.println("\tRun the following:");
            System.out.println("");
            System.out.println("$ java " + TrustAddressGenerator.class.getCanonicalName() +
                    "0x63cCEF733a093E5Bd773b41C96D3eCE361464942 z+I6NxdALVtlc3TuUo2QEeV9rwyAmKB4UtQWkTLQhpE=");
        }
    }

    /**********************************************************************************
     For use in Amazon Lambda
     **********************************************************************************/

    public Response DeriveTrustAddress(Request req) throws Exception {
        String trust = getTrustAddress(req.contract, req.getDigest());
        String revoke = getRevokeAddress(req.contract, req.getDigest());
        return new Response(trust, revoke);
    }

    public static class Request {
        String contract;
        String digest;

        public String getContractAddress() {
            return contract;
        }

        public void setContractAddress(String contractAddress) {
            this.contract = contractAddress;
        }

        public String getDigest() {
            return digest;
        }

        public void setDigest(String digest) {
            this.digest = digest;
        }

        public Request(String contractAddress, String digest) {
            this.contract = contractAddress;
            this.digest = digest;
        }

        public Request() {
        }
    }

    public static class Response {
        String trustAddress;
        String revokeAddress;

        public String getTrustAddress() { return trustAddress; }

        public void setTrustAddress(String trustAddress) { this.trustAddress = trustAddress; }

        public String getRevokeAddress() { return revokeAddress; }

        public void setRevokeAddress(String revokeAddress) { this.revokeAddress = revokeAddress; }

        public Response(String trustAddress, String revokeAddress) {
            this.trustAddress = trustAddress;
            this.revokeAddress = revokeAddress;
        }

        public Response() {
        }
    }
}