package im.xiaoyao.presto.ethereum.udfs;

import com.facebook.presto.spi.function.Description;
import com.facebook.presto.spi.function.ScalarFunction;
import com.facebook.presto.spi.function.SqlType;
import com.facebook.presto.spi.type.StandardTypes;
import im.xiaoyao.presto.ethereum.EthereumConnectorConfig;
import io.airlift.configuration.ConfigurationLoader;
import io.airlift.log.Logger;
import io.airlift.slice.Slice;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameter;
import org.web3j.protocol.http.HttpService;
import org.web3j.protocol.infura.InfuraHttpService;
import org.web3j.protocol.ipc.UnixIpcService;

import java.io.IOException;
import java.math.BigInteger;
import java.util.Map;

public class EthereumUDFs {
    private static final Logger log = Logger.get(EthereumUDFs.class);
    private static final String CONFIG_PATH = "etc/catalog/ethereum.properties";
    private static final String JSONRPC_KEY = "ethereum.jsonrpc";
    private static final String IPC_KEY = "ethereum.ipc";
    private static final String INFURA_KEY = "ethereum.infura";
    private static final String LATEST = "latest";
    private static final String ZERO_X = "0x";
    private static final Web3j web3j;

    // A hack, which I don't like
    static {
        log.info("Initializing Web3j in UDF...");
        ConfigurationLoader configLoader = new ConfigurationLoader();
        try {
            Map<String, String> config = configLoader.loadPropertiesFrom(CONFIG_PATH);
            if (config.get(JSONRPC_KEY) == null
                    && config.get(IPC_KEY) == null
                    && config.get(INFURA_KEY) == null) {
                web3j = Web3j.build(new HttpService(EthereumConnectorConfig.DEFAULT_JSON_RPC));
            } else if (config.get(JSONRPC_KEY) != null
                    && config.get(IPC_KEY) == null
                    && config.get(INFURA_KEY) == null) {
                web3j = Web3j.build(new HttpService(config.get(JSONRPC_KEY)));
            } else if (config.get(JSONRPC_KEY) == null
                    && config.get(IPC_KEY) != null
                    && config.get(INFURA_KEY) == null) {
                web3j = Web3j.build(new UnixIpcService(config.get(IPC_KEY)));
            } else if (config.get(JSONRPC_KEY) == null
                    && config.get(IPC_KEY) == null
                    && config.get(INFURA_KEY) != null) {
                web3j = Web3j.build(new InfuraHttpService(config.get(INFURA_KEY)));
            } else {
                throw new IllegalArgumentException("More than 1 Ethereum service providers found");
            }
        } catch (IOException e) {
            throw new IllegalStateException("Cannot load config from " + CONFIG_PATH);
        }
    }

    @ScalarFunction("eth_gasPrice")
    @Description("Returns current gas price")
    @SqlType(StandardTypes.DOUBLE)
    public static double ethGasPrice() throws IOException {
        return web3j.ethGasPrice().send().getGasPrice().doubleValue();
    }

    @ScalarFunction("eth_blockNumber")
    @Description("Returns current block number")
    @SqlType(StandardTypes.BIGINT)
    public static long ethBlockNumber() throws IOException {
        return web3j.ethBlockNumber().send().getBlockNumber().longValue();
    }

    @ScalarFunction("eth_getBalance")
    @Description("Returns the balance of an address")
    @SqlType(StandardTypes.DOUBLE)
    public static double ethGetBalance(@SqlType(StandardTypes.VARCHAR) Slice address) throws IOException {
        return web3j.ethGetBalance(address.toStringUtf8(), DefaultBlockParameter.valueOf(LATEST)).send().getBalance().doubleValue();
    }

    @ScalarFunction("eth_getBalance")
    @Description("Returns the balance of an address")
    @SqlType(StandardTypes.DOUBLE)
    public static double ethGetBalance(@SqlType(StandardTypes.VARCHAR) Slice address, @SqlType(StandardTypes.BIGINT) long blockNumber) throws IOException {
        return web3j.ethGetBalance(address.toStringUtf8(), DefaultBlockParameter.valueOf(BigInteger.valueOf(blockNumber))).send().getBalance().doubleValue();
    }

    @ScalarFunction("eth_getBalance")
    @Description("Returns the balance of an address")
    @SqlType(StandardTypes.DOUBLE)
    public static double ethGetBalance(@SqlType(StandardTypes.VARCHAR) Slice address, @SqlType(StandardTypes.VARCHAR) Slice blockName) throws IOException {
        return web3j.ethGetBalance(address.toStringUtf8(), DefaultBlockParameter.valueOf(blockName.toStringUtf8())).send().getBalance().doubleValue();
    }

    @ScalarFunction("eth_getTransactionCount")
    @Description("Returns the number of transactions from this address")
    @SqlType(StandardTypes.BIGINT)
    public static long ethGetTransactionCount(@SqlType(StandardTypes.VARCHAR) Slice address) throws IOException {
        return web3j.ethGetTransactionCount(address.toStringUtf8(), DefaultBlockParameter.valueOf(LATEST)).send().getTransactionCount().longValue();
    }

    @ScalarFunction("eth_getTransactionCount")
    @Description("Returns the number of transactions from this address")
    @SqlType(StandardTypes.BIGINT)
    public static long ethGetTransactionCount(@SqlType(StandardTypes.VARCHAR) Slice address, @SqlType(StandardTypes.BIGINT) long blockNumber) throws IOException {
        return web3j.ethGetTransactionCount(address.toStringUtf8(), DefaultBlockParameter.valueOf(BigInteger.valueOf(blockNumber))).send().getTransactionCount().longValue();
    }

    @ScalarFunction("eth_getTransactionCount")
    @Description("Returns the number of transactions from this address")
    @SqlType(StandardTypes.BIGINT)
    public static long ethGetTransactionCount(@SqlType(StandardTypes.VARCHAR) Slice address, @SqlType(StandardTypes.VARCHAR) Slice blockName) throws IOException {
        return web3j.ethGetTransactionCount(address.toStringUtf8(), DefaultBlockParameter.valueOf(blockName.toStringUtf8())).send().getTransactionCount().longValue();
    }

    @ScalarFunction("fromWei")
    @Description("fromWei")
    @SqlType(StandardTypes.DOUBLE)
    public static double fromWei(@SqlType(StandardTypes.DOUBLE) double num, @SqlType(StandardTypes.VARCHAR) Slice unit) {
        String unitStr = unit.toStringUtf8().toUpperCase();
        EthereumUnit u = EthereumUnit.valueOf(unitStr);
        return u.fromWei(num);
    }

    @ScalarFunction("toWei")
    @Description("toWei")
    @SqlType(StandardTypes.DOUBLE)
    public static double toWei(@SqlType(StandardTypes.DOUBLE) double num, @SqlType(StandardTypes.VARCHAR) Slice unit) {
        String unitStr = unit.toStringUtf8().toUpperCase();
        EthereumUnit u = EthereumUnit.valueOf(unitStr);
        return u.toWei(num);
    }

    @ScalarFunction("isContract")
    @Description("isContract")
    @SqlType(StandardTypes.BOOLEAN)
    public static boolean isContract(@SqlType(StandardTypes.VARCHAR) Slice address) throws IOException {
        return !web3j.ethGetCode(address.toStringUtf8(), DefaultBlockParameter.valueOf(LATEST)).send().getCode().equals(ZERO_X);
    }
}