package com.github.ontio.smartcontract.neovm;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.github.ontio.OntSdk;
import com.github.ontio.account.Account;
import com.github.ontio.common.Address;
import com.github.ontio.common.ErrorCode;
import com.github.ontio.common.Helper;
import com.github.ontio.core.transaction.Transaction;
import com.github.ontio.sdk.exception.SDKException;
import com.github.ontio.smartcontract.neovm.abi.AbiFunction;
import com.github.ontio.smartcontract.neovm.abi.AbiInfo;
import com.github.ontio.smartcontract.neovm.abi.BuildParams;
import com.github.ontio.smartcontract.neovm.oep5.Oep5Param;

import java.util.ArrayList;
import java.util.List;

public class Oep5 {
    private OntSdk sdk;
    private String contractAddress = null;
    private String oep5abi = "{\"functions\":[{\"name\":\"Main\",\"parameters\":[{\"name\":\"operation\",\"type\":\"\"},{\"name\":\"args\",\"type\":\"\"}],\"returntype\":\"\"}," +
            "{\"name\":\"name\",\"parameters\":[],\"returntype\":\"\"}," +
            "{\"name\":\"symbol\",\"parameters\":[],\"returntype\":\"\"}," +
            "{\"name\":\"balanceOf\",\"parameters\":[{\"name\":\"owner\",\"type\":\"ByteArray\"}],\"returntype\":\"\"}," +
            "{\"name\":\"ownerOf\",\"parameters\":[{\"name\":\"tokenID\",\"type\":\"ByteArray\"}],\"returntype\":\"\"}," +
            "{\"name\":\"transfer\",\"parameters\":[{\"name\":\"toAcct\",\"type\":\"ByteArray\"},{\"name\":\"tokenID\",\"type\":\"ByteArray\"}],\"returntype\":\"\"}," +
            "{\"name\":\"transferMulti\",\"parameters\":[{\"name\":\"args\",\"type\":\"Array\"}],\"returntype\":\"\"}," +
            "{\"name\":\"approve\",\"parameters\":[{\"name\":\"toAcct\",\"type\":\"ByteArray\"},{\"name\":\"tokenID\",\"type\":\"ByteArray\"}],\"returntype\":\"\"}," +
            "{\"name\":\"takeOwnership\",\"parameters\":[{\"name\":\"toAcct\",\"type\":\"ByteArray\"},{\"name\":\"tokenID\",\"type\":\"ByteArray\"}],\"returntype\":\"\"}," +
            "{\"name\":\"concatkey\",\"parameters\":[{\"name\":\"str1\",\"type\":\"\"},{\"name\":\"str2\",\"type\":\"\"}],\"returntype\":\"\"}," +
            "{\"name\":\"init\",\"parameters\":[],\"returntype\":\"\"}," +
            "{\"name\":\"totalSupply\",\"parameters\":[],\"returntype\":\"\"}," +
            "{\"name\":\"queryTokenIDByIndex\",\"parameters\":[{\"name\":\"idx\",\"type\":\"Integer\"}],\"returntype\":\"\"}," +
            "{\"name\":\"queryTokenByID\",\"parameters\":[{\"name\":\"tokenID\",\"type\":\"ByteArray\"}],\"returntype\":\"\"}," +
            "{\"name\":\"getApproved\",\"parameters\":[{\"name\":\"tokenID\",\"type\":\"ByteArray\"}],\"returntype\":\"\"}," +
            "{\"name\":\"createMultiTokens\",\"parameters\":[],\"returntype\":\"\"}," +
            "{\"name\":\"createOneToken\",\"parameters\":[{\"name\":\"name\",\"type\":\"\"},{\"name\":\"url\",\"type\":\"\"},{\"name\":\"type\",\"type\":\"\"}],\"returntype\":\"\"}]}";

    public Oep5(OntSdk sdk){
        this.sdk = sdk;
    }
    public void setContractAddress(String codeHash) {
        this.contractAddress = codeHash.replace("0x", "");
    }

    public String getContractAddress() {
        return contractAddress;
    }

    public String sendInit(Account acct, Account payerAcct, long gaslimit, long gasprice) throws Exception {
        return (String)sendInit(acct,payerAcct,gaslimit,gasprice,false);
    }
    private Object sendInit(Account acct, Account payerAcct,long gaslimit,long gasprice,boolean preExec) throws Exception {
        if (contractAddress == null) {
            throw new SDKException(ErrorCode.NullCodeHash);
        }
        AbiInfo abiinfo = JSON.parseObject(oep5abi, AbiInfo.class);
        AbiFunction func = abiinfo.getFunction("init");
        if(preExec) {
            byte[] params = BuildParams.serializeAbiFunction(func);
            Transaction tx = sdk.vm().makeInvokeCodeTransaction(Helper.reverse(contractAddress), null, params,null,gaslimit, gasprice);
            if (acct != null) {
                sdk.signTx(tx, new Account[][]{{acct}});
            }
            Object obj = sdk.getConnect().sendRawTransactionPreExec(tx.toHexString());
            if (Integer.parseInt(((JSONObject) obj).getString("Result")) != 1){
                throw new SDKException(ErrorCode.OtherError("sendRawTransaction PreExec error: "+ obj));
            }
            return ((JSONObject) obj).getLong("Gas");
        }
        if(acct == null || payerAcct == null){
            throw new SDKException(ErrorCode.ParamError);
        }
        Object obj = sdk.neovm().sendTransaction(Helper.reverse(contractAddress),acct,payerAcct,gaslimit,gasprice,func,preExec);
        return obj;
    }

    public String ownerOf(byte[] tokenID) throws Exception {
        if (contractAddress == null) {
            throw new SDKException(ErrorCode.NullCodeHash);
        }
        AbiInfo abiinfo = JSON.parseObject(oep5abi, AbiInfo.class);
        AbiFunction func = abiinfo.getFunction("ownerOf");
        func.name = "ownerOf";
        func.setParamsValue(tokenID);
        Object obj =   sdk.neovm().sendTransaction(Helper.reverse(contractAddress),null,null,0,0,func, true);
        return ((JSONObject) obj).getString("Result");
    }

    public String transfer(Account owner, Oep5Param oep5Transfer, Account payer, long gaslimit, long gasprice) throws Exception {
        if (contractAddress == null) {
            throw new SDKException(ErrorCode.NullCodeHash);
        }
        AbiInfo abiinfo = JSON.parseObject(oep5abi, AbiInfo.class);
        AbiFunction func = abiinfo.getFunction("transfer");
        func.name = "transfer";
        func.setParamsValue(oep5Transfer.toAcct, oep5Transfer.tokenId);
        Object obj =   sdk.neovm().sendTransaction(Helper.reverse(contractAddress),owner,payer,gaslimit,gasprice,func, false);
        return (String) obj;
    }
    public String transferMulti(Account[] owners, Oep5Param[] oep5Transfers, Account payer, long gaslimit, long gasprice) throws Exception {
        if (contractAddress == null) {
            throw new SDKException(ErrorCode.NullCodeHash);
        }
        Transaction tx = makeTransferMulti(oep5Transfers, payer, gaslimit, gasprice);
        boolean haspayer = false;
        for(Account account : owners){
            if(account.equals(payer)){
                haspayer = true;
            }
            sdk.addSign(tx, account);
        }
        if(!haspayer){
            sdk.addSign(tx, payer);
        }
        boolean b = sdk.getConnect().sendRawTransaction(tx.toHexString());
        if (!b) {
            throw new SDKException(ErrorCode.SendRawTxError);
        }
        return tx.hash().toHexString();
    }

    public Transaction makeTransferMulti(Oep5Param[] oep5Transfers, Account payerAcct, long gaslimit, long gasprice) throws SDKException {
        if(oep5Transfers == null || payerAcct == null){
            throw new SDKException("params should not be null");
        }
        if(gaslimit < 0 || gasprice< 0){
            throw new SDKException("gaslimit or gasprice should not be less than 0");
        }
        List paramList = new ArrayList<>();
        paramList.add("transferMulti".getBytes());
        List tempList = new ArrayList();
        for(Oep5Param oep5Transfer : oep5Transfers){
            List list = new ArrayList();
            list.add(oep5Transfer.toAcct);
            list.add(oep5Transfer.tokenId);
            tempList.add(list);
        }
        paramList.add(tempList);
        byte[] params = BuildParams.createCodeParamsScript(paramList);
        Transaction tx = sdk.vm().makeInvokeCodeTransaction(Helper.reverse(contractAddress),null,params,payerAcct.getAddressU160().toBase58(),gaslimit,gasprice);
        return tx;
    }

    public String approve(Account owner, Oep5Param oep5Param, Account payer, long gaslimit, long gasprice) throws Exception {
        if (owner == null || oep5Param == null || payer == null) {
            throw new SDKException("params should not be null");
        }
        if(gaslimit < 0 || gasprice< 0){
            throw new SDKException("gaslimit or gasprice should not be less than 0");
        }
        AbiInfo abiinfo = JSON.parseObject(oep5abi, AbiInfo.class);
        AbiFunction func = abiinfo.getFunction("approve");
        func.setParamsValue(oep5Param.toAcct, oep5Param.tokenId);
        Object obj =   sdk.neovm().sendTransaction(Helper.reverse(contractAddress),owner,payer,gaslimit,gasprice,func, false);
        return (String) obj;
    }
    public String takeOwnership(Account owner, Oep5Param oep5Param, Account payer, long gaslimit, long gasprice) throws Exception {
        if (owner == null || oep5Param == null || payer == null) {
            throw new SDKException("params should not be null");
        }
        if(gaslimit < 0 || gasprice< 0){
            throw new SDKException("gaslimit or gasprice should not be less than 0");
        }
        AbiInfo abiinfo = JSON.parseObject(oep5abi, AbiInfo.class);
        AbiFunction func = abiinfo.getFunction("takeOwnership");
        func.setParamsValue(oep5Param.toAcct, oep5Param.tokenId);
        Object obj =   sdk.neovm().sendTransaction(Helper.reverse(contractAddress),owner,payer,gaslimit,gasprice,func, false);
        return (String) obj;
    }

    public String queryTotalSupply() throws Exception {
        if (contractAddress == null) {
            throw new SDKException(ErrorCode.NullCodeHash);
        }
        AbiInfo abiinfo = JSON.parseObject(oep5abi, AbiInfo.class);
        AbiFunction func = abiinfo.getFunction("totalSupply");
        func.setParamsValue();
        Object obj =   sdk.neovm().sendTransaction(Helper.reverse(contractAddress),null,null,0,0,func, true);
        return Helper.BigIntFromNeoBytes(Helper.hexToBytes(((JSONObject) obj).getString("Result"))).toString();
    }
    public String queryTokenIDByIndex(String idx) throws Exception {
        if (contractAddress == null) {
            throw new SDKException(ErrorCode.NullCodeHash);
        }
        AbiInfo abiinfo = JSON.parseObject(oep5abi, AbiInfo.class);
        AbiFunction func = abiinfo.getFunction("queryTokenIDByIndex");
        func.name = "queryTokenIDByIndex";
        func.setParamsValue(idx);
        Object obj =   sdk.neovm().sendTransaction(Helper.reverse(contractAddress),null,null,0,0,func, true);
        return ((JSONObject) obj).getString("Result");
    }
    public String queryTokenByID(String tokenID) throws Exception {
        if (contractAddress == null) {
            throw new SDKException(ErrorCode.NullCodeHash);
        }
        AbiInfo abiinfo = JSON.parseObject(oep5abi, AbiInfo.class);
        AbiFunction func = abiinfo.getFunction("queryTokenByID");
        func.setParamsValue(tokenID);
        Object obj =   sdk.neovm().sendTransaction(Helper.reverse(contractAddress),null,null,0,0,func, true);
        return ((JSONObject) obj).getString("Result");
    }
    public String getApproved(String tokenID) throws Exception {
        if (contractAddress == null) {
            throw new SDKException(ErrorCode.NullCodeHash);
        }
        AbiInfo abiinfo = JSON.parseObject(oep5abi, AbiInfo.class);
        AbiFunction func = abiinfo.getFunction("getApproved");
        func.name = "getApproved";
        func.setParamsValue(Helper.hexToBytes(tokenID));
        Object obj =   sdk.neovm().sendTransaction(Helper.reverse(contractAddress),null,null,0,0,func, true);
        return ((JSONObject) obj).getString("Result");
    }
    public String queryName() throws Exception {
        if (contractAddress == null) {
            throw new SDKException(ErrorCode.NullCodeHash);
        }
        AbiInfo abiinfo = JSON.parseObject(oep5abi, AbiInfo.class);
        AbiFunction func = abiinfo.getFunction("name");
        func.name = "name";
        func.setParamsValue();
        Object obj =   sdk.neovm().sendTransaction(Helper.reverse(contractAddress),null,null,0,0,func, true);
        return new String(Helper.hexToBytes(((JSONObject) obj).getString("Result")));
    }

    public String querySymbol() throws Exception {
        if (contractAddress == null) {
            throw new SDKException(ErrorCode.NullCodeHash);
        }
        AbiInfo abiinfo = JSON.parseObject(oep5abi, AbiInfo.class);
        AbiFunction func = abiinfo.getFunction("symbol");
        func.name = "symbol";
        func.setParamsValue();
        Object obj =   sdk.neovm().sendTransaction(Helper.reverse(contractAddress),null,null,0,0,func, true);
        return new String(Helper.hexToBytes(((JSONObject) obj).getString("Result")));
    }

    public String queryBalanceOf(String addr) throws Exception {
        if (contractAddress == null) {
            throw new SDKException(ErrorCode.NullCodeHash);
        }
        if(addr == null||addr.equals("")){
            throw new SDKException(ErrorCode.ParamError);
        }
        AbiInfo abiinfo = JSON.parseObject(oep5abi, AbiInfo.class);
        AbiFunction func = abiinfo.getFunction("balanceOf");
        func.name = "balanceOf";
        func.setParamsValue(Address.decodeBase58(addr).toArray());
        Object obj =  sdk.neovm().sendTransaction(Helper.reverse(contractAddress),null,null,0,0,func, true);
        String balance = ((JSONObject) obj).getString("Result");
        if(balance.equals("")){
            balance = "00";
        }
        return Helper.BigIntFromNeoBytes(Helper.hexToBytes(balance)).toString();
    }
    public String queryTokenIDByIndex(int index) throws Exception {
        if (contractAddress == null) {
            throw new SDKException(ErrorCode.NullCodeHash);
        }
        if(index < 0){
            throw new SDKException(ErrorCode.ParamError);
        }
        AbiInfo abiinfo = JSON.parseObject(oep5abi, AbiInfo.class);
        AbiFunction func = abiinfo.getFunction("queryTokenIDByIndex");
        func.setParamsValue((long)index);
        Object obj =  sdk.neovm().sendTransaction(Helper.reverse(contractAddress),null,null,0,0,func, true);
        String tokenId = ((JSONObject) obj).getString("Result");
        if(tokenId.equals("")){
            tokenId = "00";
        }
        return tokenId;
    }
}