package org.web3j.codegen;

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.web3j.protocol.ObjectMapperFactory;
import org.web3j.protocol.core.methods.response.AbiDefinition;
import org.web3j.tx.ChainId;
import org.web3j.utils.Strings;

import static org.web3j.codegen.Console.exitError;
import static org.web3j.utils.Collection.tail;

/**
 * Java wrapper source code generator for Truffle JSON format. Truffle embeds the Solidity ABI
 * formatted JSON in its own format. That format also gives access to the binary code. It also
 * contains information about deployment addresses. This should make integration with Truffle
 * easier.
 */
public class TruffleJsonFunctionWrapperGenerator extends FunctionWrapperGenerator {

    private static final String USAGE = "truffle generate "
            + "[--javaTypes|--solidityTypes] "
            + "<input truffle json file>.json "
            + "-p|--package <base package name> "
            + "-o|--output <destination base directory>";


    private String jsonFileLocation;

    private TruffleJsonFunctionWrapperGenerator(
            String jsonFileLocation,
            String destinationDirLocation,
            String basePackageName,
            boolean useJavaNativeTypes) {

        super(destinationDirLocation, basePackageName, useJavaNativeTypes);
        this.jsonFileLocation = jsonFileLocation;
    }

    public static void run(String[] args) throws Exception {
        if (args.length < 1 || !"generate".equals(args[0])) {
            exitError(USAGE);
        } else {
            main(tail(args));
        }
    }

    public static void main(String[] args) throws Exception {

        String[] fullArgs;
        if (args.length == 5) {
            fullArgs = new String[args.length + 1];
            fullArgs[0] = JAVA_TYPES_ARG;
            System.arraycopy(args, 0, fullArgs, 1, args.length);
        } else {
            fullArgs = args;
        }

        if (fullArgs.length != 6) {
            exitError(USAGE);
        }

        boolean useJavaNativeTypes = useJavaNativeTypes(fullArgs[0], USAGE);

        String jsonFileLocation = parsePositionalArg(fullArgs, 1);
        String destinationDirLocation = parseParameterArgument(fullArgs, "-o", "--outputDir");
        String basePackageName = parseParameterArgument(fullArgs, "-p", "--package");

        if (Strings.isEmpty(jsonFileLocation)
                || Strings.isEmpty(destinationDirLocation)
                || Strings.isEmpty(basePackageName)) {
            exitError(USAGE);
        }

        new TruffleJsonFunctionWrapperGenerator(
                jsonFileLocation,
                destinationDirLocation,
                basePackageName,
                useJavaNativeTypes)
                .generate();
    }

    static Contract loadContractDefinition(File jsonFile)
            throws IOException {
        ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper();
        return objectMapper.readValue(jsonFile, Contract.class);
    }

    @SuppressWarnings("unchecked")
    private void generate() throws IOException, ClassNotFoundException {

        File truffleJsonFile = new File(jsonFileLocation);
        if (!truffleJsonFile.exists() || !truffleJsonFile.canRead()) {
            exitError("Invalid input json file specified: " + jsonFileLocation);
        }

        String fileName = truffleJsonFile.getName();
        String contractName = getFileNameNoExtension(fileName);

        Contract c = loadContractDefinition(truffleJsonFile);
        if (c == null) {
            exitError("Unable to parse input json file");
        } else {
            String className = Strings.capitaliseFirstLetter(contractName);
            System.out.printf("Generating " + basePackageName + "." + className + " ... ");
            Map<String, String> addresses;
            if (c.networks != null && !c.networks.isEmpty()) {
                addresses = c.networks.entrySet().stream()
                        .filter(e -> (e.getValue() != null && e.getValue().getAddress() != null))
                        .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getAddress()
                        ));
            } else {
                addresses = Collections.EMPTY_MAP;
            }
            new SolidityFunctionWrapper(useJavaNativeTypes)
                    .generateJavaFiles(contractName,
                            c.getBytecode(),
                            c.getAbi(),
                            destinationDirLocation.toString(),
                            basePackageName,
                            addresses);
            System.out.println("File written to " + destinationDirLocation.toString() + "\n");
        }
    }

    /**
     * Truffle Contract <p> Describes a contract exported by and consumable by Truffle, which may
     * include information about deployed instances on networks. </p>
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    @JsonPropertyOrder({
            "contractName",
            "abi",
            "bytecode",
            "deployedBytecode",
            "sourceMap",
            "deployedSourceMap",
            "source",
            "sourcePath",
            "ast",
            "compiler",
            "networks",
            "schemaVersion",
            "updatedAt"
    })
    public static class Contract {

        @JsonProperty("contractName")
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "^[a-zA-Z_][a-zA-Z0-9_]*$")
        public String contractName;
        @JsonProperty(value = "abi", required = true)
        public List<AbiDefinition> abi;
        @JsonProperty("bytecode")
        @JsonFormat(shape = JsonFormat.Shape.STRING,
                pattern = "^0x0$|^0x([a-fA-F0-9]{2}|__.{38})+$")
        public String bytecode;
        @JsonProperty("deployedBytecode")
        @JsonFormat(shape = JsonFormat.Shape.STRING,
                pattern = "^0x0$|^0x([a-fA-F0-9]{2}|__.{38})+$")
        public String deployedBytecode;
        @JsonProperty("sourceMap")
        public String sourceMap;
        @JsonProperty("deployedSourceMap")
        public String deployedSourceMap;
        @JsonProperty("source")
        public String source;
        @JsonProperty("sourcePath")
        public String sourcePath;
        @JsonProperty("ast")
        public JsonNode ast;
        @JsonProperty("compiler")
        public Compiler compiler;
        @JsonProperty("networks")
        public Map<String, NetworkInfo> networks;
        @JsonProperty("schemaVersion")
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "[0-9]+\\.[0-9]+\\.[0-9]+")
        public String schemaVersion;
        @JsonProperty("updatedAt")
        @JsonFormat(shape = JsonFormat.Shape.STRING,
                pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "GMT")
        public Date updatedAt;

        public Contract() {
        }

        public Contract(String contractName, List<AbiDefinition> abi, String bytecode,
                String deployedBytecode,
                String sourceMap, String deployedSourceMap, String source, String sourcePath,
                JsonNode ast,
                Compiler compiler, Map<String, NetworkInfo> networks, String schemaVersion,
                Date updatedAt) {
            super();
            this.contractName = contractName;
            this.abi = abi;
            this.bytecode = bytecode;
            this.deployedBytecode = deployedBytecode;
            this.sourceMap = sourceMap;
            this.deployedSourceMap = deployedSourceMap;
            this.source = source;
            this.sourcePath = sourcePath;
            this.ast = ast;
            this.compiler = compiler;
            this.networks = networks;
            this.schemaVersion = schemaVersion;
            this.updatedAt = updatedAt;
        }
        
        public String getContractName() {
            return contractName;
        }

        public List<AbiDefinition> getAbi() {
            return abi;
        }

        public String getBytecode() {
            return bytecode;
        }
        
        public NetworkInfo getNetwork(String networkId) {
            return networks == null ? null : networks.get(networkId);
        }

        public String getAddress(String networkId) {
            NetworkInfo network = getNetwork(networkId);
            return network == null ? null : network.getAddress();
        }

        /**
         * Convenience method to get the deployed address of the contract.
         *
         * @param network the contract's address on this Ethereum network
         * @return the contract's address or <code>null</code> if there isn't one known.
         */
        public String getAddress(Network network) {
            return getAddress(Long.toString(network.id));
        }

        /*
         * c.f., org.web3j.tx.ChainId
         *
         * This should be updated with https://github.com/web3j/web3j/issues/234
         */
        enum Network {
            olympic(0),
            mainnet(ChainId.MAINNET),
            morden(ChainId.EXPANSE_MAINNET),
            ropsten(ChainId.ROPSTEN),
            rinkeby(ChainId.RINKEBY),
            kovan(ChainId.KOVAN);

            public final long id;

            Network(long id) {
                this.id = id;
            }
        }

    }

    @JsonInclude(JsonInclude.Include.NON_NULL)
    @JsonPropertyOrder({
            "name",
            "version"
    })
    public static class Compiler {

        @JsonProperty("name")
        public String name;
        @JsonProperty("version")
        public String version;
        @JsonIgnore
        private Map<String, JsonNode> additionalProperties = new HashMap<String, JsonNode>();

        public Compiler() {
        }

        public Compiler(String name, String version) {
            super();
            this.name = name;
            this.version = version;
        }

        @JsonAnyGetter
        public Map<String, JsonNode> getAdditionalProperties() {
            return this.additionalProperties;
        }

        @JsonAnySetter
        public void setAdditionalProperty(String name, JsonNode value) {
            this.additionalProperties.put(name, value);
        }

        public Compiler withAdditionalProperty(String name, JsonNode value) {
            this.additionalProperties.put(name, value);
            return this;
        }
    }


    // For now we just ignore "events"
    @JsonIgnoreProperties(ignoreUnknown = true)
    @JsonInclude(JsonInclude.Include.NON_NULL)
    @JsonPropertyOrder({
            "events",
            "links",
            "address"
    })
    public static class NetworkInfo {

        @JsonProperty("events")
        public Map<String, JsonNode> events;
        @JsonProperty("links")
        public Map<String, JsonNode> links;
        @JsonProperty("address")
        public String address;

        public NetworkInfo() {
        }

        public NetworkInfo(Map<String, JsonNode> events, Map<String, JsonNode> links,
                String address) {
            super();
            this.events = events;
            this.links = links;
            this.address = address;
        }
        
        public String getAddress() {
            return address;
        }

        public void setAddress(String address) {
            this.address = address;
        }
    }

}