package org.arquillian.cube.docker.impl.docker.compose;

import org.arquillian.cube.docker.impl.client.config.BuildImage;
import org.arquillian.cube.docker.impl.client.config.CubeContainer;
import org.arquillian.cube.docker.impl.client.config.Device;
import org.arquillian.cube.docker.impl.client.config.DockerCompositions;
import org.arquillian.cube.docker.impl.client.config.ExposedPort;
import org.arquillian.cube.docker.impl.client.config.Image;
import org.arquillian.cube.docker.impl.client.config.Link;
import org.arquillian.cube.docker.impl.client.config.PortBinding;
import org.arquillian.cube.docker.impl.client.config.RestartPolicy;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static org.arquillian.cube.docker.impl.util.YamlUtil.asBoolean;
import static org.arquillian.cube.docker.impl.util.YamlUtil.asInt;
import static org.arquillian.cube.docker.impl.util.YamlUtil.asListOfString;
import static org.arquillian.cube.docker.impl.util.YamlUtil.asLong;
import static org.arquillian.cube.docker.impl.util.YamlUtil.asMap;
import static org.arquillian.cube.docker.impl.util.YamlUtil.asMapOfStrings;
import static org.arquillian.cube.docker.impl.util.YamlUtil.asString;


public class ContainerBuilder {

    private static final String IMAGE = "image";
    private static final String BUILD = "build";
    private static final String COMMAND = "command";
    private static final String LINKS = "links";
    private static final String EXTERNAL_LINKS = "external_links";
    private static final String LABELS = "labels";
    private static final String LOG_DRIVER = "log_driver";
    private static final String SECURITY_OPT = "security_opt";
    private static final String DOCKERFILE = "dockerfile";
    private static final String READ_ONLY = "read_only";
    private static final String EXTENDS = "extends";
    private static final String PORTS = "ports";
    private static final String EXPOSE = "expose";
    private static final String VOLUMES = "volumes";
    private static final String VOLUMES_FROM = "volumes_from";
    private static final String ENVIRONMENT = "environment";
    private static final String ENV_FILE = "env_file";
    private static final String NET = "net";
    private static final String DNS = "dns";
    private static final String CAP_ADD = "cap_add";
    private static final String CAP_DROP = "cap_drop";
    private static final String DNS_SEARCH = "dns_search";
    private static final String WORKING_DIR = "working_dir";
    private static final String ENTRYPOINT = "entrypoint";
    private static final String PID = "pid";
    private static final String USER = "user";
    private static final String HOSTNAME = "hostname";
    private static final String DOMAINNAME = "domainname";
    private static final String MEM_LIMIT = "mem_limit";
    private static final String MEM_SWAP_LIMIT = "memswap_limit";
    private static final String SHM_SIZE = "shm_size";
    private static final String PRIVILEGED = "privileged";
    private static final String RESTART = "restart";
    private static final String STDIN_OPEN = "stdin_open";
    private static final String TTY = "tty";
    private static final String CPU_SHARES = "cpu_shares";
    private static final String CPU_SET = "cpuset";
    private static final String CPU_QUOTA = "cpu_quota";
    private static final String EXTRA_HOSTS = "extra_hosts";
    private static final String DEVICES = "devices";
    private static final String CONTAINERNAME = "container_name";
    private static final String DEPENDS_ON = "depends_on";
    private static final String CONTEXT = "context";
    private static final String NETWORKS = "networks";

    private static List<String> AVAILABLE_COMMANDS = Arrays.asList(IMAGE, BUILD, COMMAND, LINKS, EXTERNAL_LINKS, DOCKERFILE,
            EXTENDS, PORTS, EXPOSE, VOLUMES, VOLUMES_FROM, ENVIRONMENT, ENV_FILE, NET, DNS, CAP_ADD, CAP_DROP,
            DNS_SEARCH, WORKING_DIR, ENTRYPOINT, USER, HOSTNAME, MEM_LIMIT, PRIVILEGED, RESTART, STDIN_OPEN, TTY,
            CPU_SET, CPU_SHARES, CPU_QUOTA, EXTRA_HOSTS, DEVICES, CONTAINERNAME, DEPENDS_ON, MEM_SWAP_LIMIT, SHM_SIZE, NETWORKS);

    private static final Logger log = Logger.getLogger(ContainerBuilder.class.getName());

    private Random random = new Random();

    private CubeContainer configuration;
    private Path dockerComposeRootLocation;
    GitOperations gitOperations;

    public ContainerBuilder(Path dockerComposeRootLocation) {
        this(dockerComposeRootLocation, new CubeContainer());
    }

    protected ContainerBuilder(Path dockerComposeRootLocation, CubeContainer configuration) {
        this.dockerComposeRootLocation = dockerComposeRootLocation;
        this.configuration = configuration;
    }

    public CubeContainer build(Map<String, Object> dockerComposeContainerDefinition) {
        return build(dockerComposeContainerDefinition, null);
    }

    @SuppressWarnings("unchecked")
    public CubeContainer build(Map<String, Object> dockerComposeContainerDefinition, String version) {
        if(dockerComposeContainerDefinition.containsKey(EXTENDS)) {
            Map<String, Object> extendsDefinition = asMap(dockerComposeContainerDefinition, EXTENDS);
            this.extend(Paths.get(asString(extendsDefinition, "file")), asString(extendsDefinition, "service"));
        }
        if (dockerComposeContainerDefinition.containsKey(IMAGE)) {
            this.addImage(asString(dockerComposeContainerDefinition, IMAGE));
        }
        if (dockerComposeContainerDefinition.containsKey(BUILD)) {
            if (DockerComposeConverter.DOCKER_COMPOSE_VERSION_2_VALUE.equals(version)) {
                Object o = dockerComposeContainerDefinition.get(BUILD);
                if (o instanceof String) {
                    String dockerfile = dockerComposeContainerDefinition.containsKey(DOCKERFILE) ? asString(dockerComposeContainerDefinition, DOCKERFILE) : null;
                    this.addBuild(asString(dockerComposeContainerDefinition, BUILD), dockerfile);
                } else if (o instanceof Map) {
                    Map<String, Object> buildDefinition = asMap(dockerComposeContainerDefinition, BUILD);

                    String context = buildDefinition.containsKey(CONTEXT) ? asString(buildDefinition, CONTEXT) : null;

                    final String dockerfile = buildDefinition.containsKey(DOCKERFILE) ? asString(buildDefinition, DOCKERFILE) : null;
                    if (context != null) {
                        File directory = new File(context);
                        if (directory.isDirectory()) {
                            this.addBuild(asString(buildDefinition, CONTEXT),
                                    dockerfile);
                        } else {
                            // Make GitOperations lazy because it will help for testing purposes and to avoid instantiate JGit classes when Git is not required-
                            // In this way, if you don't need Git, you don't need to add jgit dependency
                            if (gitOperations == null) {
                                log.log(Level.INFO, String.format("Starting cloning git repository %s defined in docker-compose", context));
                                GitOperations gitOperations = new GitOperations();
                                File clonedDirectory = gitOperations.cloneRepo(context);
                                log.log(Level.INFO, String.format("Finished cloning git repository %s defined in docker-compose", context));
                                this.addBuild(clonedDirectory.getParentFile().getAbsolutePath(), dockerfile);
                            }
                        }
                    } else {
                        log.log(Level.WARNING, "build configuration is provided as object but no context definition is found.");
                    }
                }
            } else {
                String dockerfile = dockerComposeContainerDefinition.containsKey(DOCKERFILE) ? asString(dockerComposeContainerDefinition, DOCKERFILE) : null;
                this.addBuild(asString(dockerComposeContainerDefinition, BUILD), dockerfile);
            }
        }
        if (dockerComposeContainerDefinition.containsKey(COMMAND)) {
            if (dockerComposeContainerDefinition.get(COMMAND) instanceof List) {
                this.addCommands(asListOfString(dockerComposeContainerDefinition, COMMAND));
            } else {
                this.addCommand(asString(dockerComposeContainerDefinition, COMMAND));
            }
        }
        if (dockerComposeContainerDefinition.containsKey(DEPENDS_ON)) {
            this.addDependsOn(asListOfString(dockerComposeContainerDefinition, DEPENDS_ON));
        }
        if (dockerComposeContainerDefinition.containsKey(LINKS)) {
            this.addLinks(asListOfString(dockerComposeContainerDefinition, LINKS));
        }
        if (dockerComposeContainerDefinition.containsKey(EXTERNAL_LINKS)) {
            this.addLinks(asListOfString(dockerComposeContainerDefinition, EXTERNAL_LINKS));
        }
        if (dockerComposeContainerDefinition.containsKey(PORTS)) {
            this.addPorts(asListOfString(dockerComposeContainerDefinition, PORTS));
        }
        if (dockerComposeContainerDefinition.containsKey(EXPOSE)) {
            this.addExpose(asListOfString(dockerComposeContainerDefinition, EXPOSE));
        }
        if (dockerComposeContainerDefinition.containsKey(VOLUMES)) {
            Collection<String> volumes = asListOfString(dockerComposeContainerDefinition, VOLUMES);
            this.addVolumes(volumes);
            this.addBinds(normalizePaths(volumes));
        }
        if(dockerComposeContainerDefinition.containsKey(LABELS)) {
            this.addLabels(asMapOfStrings(dockerComposeContainerDefinition, LABELS));
        }
        if (dockerComposeContainerDefinition.containsKey(VOLUMES_FROM)) {
            this.addVolumesFrom(asListOfString(dockerComposeContainerDefinition, VOLUMES_FROM));
        }
        if (dockerComposeContainerDefinition.containsKey(ENVIRONMENT)) {
            try {
                this.addEnvironment(asListOfString(dockerComposeContainerDefinition, ENVIRONMENT));
            } catch (ClassCastException e) {
                this.addEnvironment(asListOfString(asMap(dockerComposeContainerDefinition, ENVIRONMENT)));
            }
        }
        if (dockerComposeContainerDefinition.containsKey(ENV_FILE)) {
            if(dockerComposeContainerDefinition.get(ENV_FILE) instanceof List) {
                this.addEnvFile(asListOfString(dockerComposeContainerDefinition, ENV_FILE));
            } else {
                this.addEnvFile(Arrays.asList(asString(dockerComposeContainerDefinition, ENV_FILE)));
            }
        }
        if (dockerComposeContainerDefinition.containsKey(NET)) {
            this.addNet(asString(dockerComposeContainerDefinition, NET));
        }
        if (dockerComposeContainerDefinition.containsKey(EXTRA_HOSTS)) {
            this.addExtraHosts(asListOfString(dockerComposeContainerDefinition, EXTRA_HOSTS));
        }
        if (dockerComposeContainerDefinition.containsKey(DNS)) {
            Object dns = dockerComposeContainerDefinition.get(DNS);
            if (dns instanceof List) {
                this.addDns((List<String>) dns);
            } else {
                this.addDns((String) dns);
            }
        }
        if (dockerComposeContainerDefinition.containsKey(CAP_ADD)) {
            this.addCapAdd(asListOfString(dockerComposeContainerDefinition, CAP_ADD));
        }
        if (dockerComposeContainerDefinition.containsKey(CAP_DROP)) {
            this.addCapDrop(asListOfString(dockerComposeContainerDefinition, CAP_DROP));
        }
        if (dockerComposeContainerDefinition.containsKey(DNS_SEARCH)) {
            Object dns = dockerComposeContainerDefinition.get(DNS_SEARCH);
            if (dns instanceof List) {
                this.addDnsSearch((List<String>) dns);
            } else {
                this.addDnsSearch((String) dns);
            }
        }
        if (dockerComposeContainerDefinition.containsKey(WORKING_DIR)) {
            this.addWorkingDir(asString(dockerComposeContainerDefinition, WORKING_DIR));
        }
        if (dockerComposeContainerDefinition.containsKey(ENTRYPOINT)) {
            try {
                this.addEntrypoint(asString(dockerComposeContainerDefinition, ENTRYPOINT));
            } catch (ClassCastException e) {
                this.addEntrypoint(asListOfString(dockerComposeContainerDefinition, ENTRYPOINT));
            }
        }
        if (dockerComposeContainerDefinition.containsKey(USER)) {
            this.addUser(asString(dockerComposeContainerDefinition, USER));
        }
        if (dockerComposeContainerDefinition.containsKey(HOSTNAME)) {
            this.addHostname(asString(dockerComposeContainerDefinition, HOSTNAME));
        }
        if (dockerComposeContainerDefinition.containsKey(MEM_LIMIT)) {
            this.addMemLimit(asLong(dockerComposeContainerDefinition, MEM_LIMIT));
        }
        if (dockerComposeContainerDefinition.containsKey(MEM_SWAP_LIMIT)) {
            this.addMemSwapLimit(asLong(dockerComposeContainerDefinition, MEM_SWAP_LIMIT));
        }
        if (dockerComposeContainerDefinition.containsKey(SHM_SIZE)) {
            this.addShmSize(asLong(dockerComposeContainerDefinition, SHM_SIZE));
        }
        if (dockerComposeContainerDefinition.containsKey(PRIVILEGED)) {
            this.addPrivileged(asBoolean(dockerComposeContainerDefinition, PRIVILEGED));
        }
        if (dockerComposeContainerDefinition.containsKey(RESTART)) {
            this.addRestart(asString(dockerComposeContainerDefinition, RESTART));
        }
        if (dockerComposeContainerDefinition.containsKey(STDIN_OPEN)) {
            this.addStdinOpen(asBoolean(dockerComposeContainerDefinition, STDIN_OPEN));
        }
        if (dockerComposeContainerDefinition.containsKey(TTY)) {
            this.addTty(asBoolean(dockerComposeContainerDefinition, TTY));
        }
        if (dockerComposeContainerDefinition.containsKey(CPU_SHARES)) {
            this.addCpuShares(asInt(dockerComposeContainerDefinition, CPU_SHARES));
        }
        if (dockerComposeContainerDefinition.containsKey(CPU_SET)) {
            this.addCpuSet(asString(dockerComposeContainerDefinition, CPU_SET));
        }
        if (dockerComposeContainerDefinition.containsKey(CPU_QUOTA)) {
            this.addCpuQuota(asInt(dockerComposeContainerDefinition, CPU_QUOTA));
        }
        if (dockerComposeContainerDefinition.containsKey(DEVICES)) {
            this.addDevices(asListOfString(dockerComposeContainerDefinition, DEVICES));
        }
        if (dockerComposeContainerDefinition.containsKey(DOMAINNAME)) {
           this.addDomainName(asString(dockerComposeContainerDefinition, DOMAINNAME));
        }
        if (dockerComposeContainerDefinition.containsKey(READ_ONLY)) {
            this.addReadOnly(asBoolean(dockerComposeContainerDefinition, READ_ONLY));
        }

        if (dockerComposeContainerDefinition.containsKey(CONTAINERNAME)) {
            this.addContainerName(asString(dockerComposeContainerDefinition, CONTAINERNAME));
        }

        if (dockerComposeContainerDefinition.containsKey(NETWORKS)) {
            if (dockerComposeContainerDefinition.get(NETWORKS) instanceof ArrayList) {
                this.addNetworks(asListOfString(dockerComposeContainerDefinition, NETWORKS));
            } else {
                Map<String, Object> networks = asMap(dockerComposeContainerDefinition, NETWORKS);
                this.addNetworks(networks.keySet());
            }

        }

        this.logUnsupportedOperations(dockerComposeContainerDefinition.keySet());
        return this.build();
    }

    private Collection<String> normalizePaths(Collection<String> volumes) {
        final ArrayList<String> expanded = new ArrayList<>(volumes.size());

        for (final String volume : volumes) {
            final String[] split = volume.split(":");
            try {
                expanded.add(volume.replace(split[0], normalizePath(split[0])));
            } catch (IOException e) {
                log.log(Level.WARNING, "Failed to normalize volume: " + volume, e);
                expanded.add(volume);
            }
        }
        return expanded;
    }

    private String normalizePath(String bind) throws IOException {
        if (bind.startsWith("//")) {
            //Windows escaped syntax
            bind = bind.substring(2).replaceFirst("/", ":/");
        }

        String absolutePath = new File(bind).getCanonicalPath().replace("\\", "/");

        if (':' == absolutePath.charAt(1)) {
            //Windows escaped syntax
            absolutePath = "//" + absolutePath.replaceFirst(":", "");
        }

        return absolutePath;
    }

    private ContainerBuilder addNetworks(Collection<String> networks) {
        if (configuration.getNetworks() != null) {
            configuration.getNetworks().addAll(networks);
        } else {
            configuration.setNetworks(new HashSet<>(networks));
        }

        return this;
    }

    private ContainerBuilder addShmSize(long shmSize) {
        configuration.setShmSize(shmSize);
        return this;
    }

    private ContainerBuilder addMemSwapLimit(long memSwapLimit) {
        configuration.setMemorySwap(memSwapLimit);
        return this;
    }


    private ContainerBuilder addDevices(Collection<String> devices) {
        Collection<Device> devicesDefinition = new HashSet<Device>();
        for (String device : devices) {
            String[] deviceSplitted = device.split(":");
            Device def = new Device();
            switch(deviceSplitted.length) {
                case 3: {
                    ///dev/ttyUSB0:/dev/ttyUSB0:rw
                    def.setcGroupPermissions(deviceSplitted[2]);
                }
                case 2: {
                    ///dev/ttyUSB0:/dev/ttyUSB0
                    def.setPathOnHost(deviceSplitted[0]);
                    def.setPathInContainer(deviceSplitted[1]);
                    break;
                }
                default: {
                    throw new IllegalArgumentException(String.format("Device definition %s is incorrect. It should follow the format <hostPath>:<containerPath>:(optional)<permissions>", device));
                }
            }
            devicesDefinition.add(def);
        }
        configuration.setDevices(devicesDefinition);
        return this;
    }

    public ContainerBuilder addExtraHosts(Collection<String> extraHosts) {
        if (configuration.getExtraHosts() != null) {
            Collection<String> oldExtraHosts = configuration.getExtraHosts();
            oldExtraHosts.addAll(extraHosts);
        } else {
            configuration.setExtraHosts(new HashSet<String>(extraHosts));
        }
        return this;
    }

    public ContainerBuilder addImage(String image) {
        configuration.setImage(Image.valueOf(image));
        return this;
    }

    public ContainerBuilder addContainerName(String name) {
        configuration.setContainerName(name);
        return this;
    }

    public ContainerBuilder addReadOnly(boolean b) {
        configuration.setReadonlyRootfs(b);
        return this;
    }

    public ContainerBuilder addBuild(String buildPath, String dockerfile) {
        //. directory is the root of the project, but docker-compose . means relative to docker-compose file, so we resolve the full path
        //and to no add full path we relativize to docker-compose

        Path buildPathPath = Paths.get(buildPath);

        Path calculatedPath;
        if (buildPathPath.isAbsolute()) {
            calculatedPath = buildPathPath;
        } else {
            Path fullDirectory = this.dockerComposeRootLocation.resolve(buildPath);
            calculatedPath = this.dockerComposeRootLocation.relativize(fullDirectory);
        }

        BuildImage buildImage = new BuildImage(calculatedPath.toString(), dockerfile, true, true);
        configuration.setBuildImage(buildImage);
        return this;
    }

    public ContainerBuilder addCommand(String command) {
        addCommands(Arrays.asList(command.split("\\ ")));
        return this;
    }

    public ContainerBuilder addCommands(Collection<String> commands) {
        configuration.setCmd(commands);
        return this;
    }

    public ContainerBuilder addDependsOn(Collection<String> dependsOn) {
        Collection<String> listOfDependsOn = new HashSet<>();
        for (String link : dependsOn) {
            listOfDependsOn.add(link);
        }

        if (configuration.getDependsOn() != null) {
            Collection<String> oldDependsOn = configuration.getDependsOn();
            oldDependsOn.addAll(listOfDependsOn);
        } else {
            configuration.setDependsOn(listOfDependsOn);
        }
        return this;
    }

    public ContainerBuilder addLinks(Collection<String> links) {
        Collection<Link> listOfLinks = new HashSet<Link>();
        for (String link : links) {
            listOfLinks.add(Link.valueOf(link));
        }

        if (configuration.getLinks() != null) {
            Collection<Link> oldLinks = configuration.getLinks();
            oldLinks.addAll(listOfLinks);
        } else {
            configuration.setLinks(listOfLinks);
        }
        return this;
    }

    public ContainerBuilder addPorts(Collection<String> ports) {
        Collection<PortBinding> listOfPorts = new HashSet<>();
        for (String port : ports) {
            String[] elements = port.split(":");
            switch (elements.length) {
                case 1: {
                    //random host port
                    if (port.contains("-")) {
                        getExpandedPorts(port).stream()
                                .forEach(expandedPort -> listOfPorts.add(PortBinding.valueOf(getRandomPort() + "->" + expandedPort)));
                    } else {
                        listOfPorts.add(PortBinding.valueOf(getRandomPort() + "->" + elements[0]));
                    }
                    break;
                }
                case 2: {
                    //hostport:containerport
                    if (port.contains("-")) {

                        listOfPorts.addAll(addPairPortRange(elements[0], elements[1], null));

                    } else {
                        listOfPorts.add(PortBinding.valueOf(port.replaceAll(":", "->")));
                    }
                    break;
                }
                case 3: {
                    //host:hostport:containerport
                    if (port.contains("-")) {
                        listOfPorts.addAll(addPairPortRange(elements[1], elements[2], elements[0]));
                    } else {
                        listOfPorts.add(PortBinding.valueOf(elements[0] + ":" + elements[1] + "->" + elements[2]));
                    }
                    break;
                }
            }
        }

        if (configuration.getPortBindings() != null) {
            Collection<PortBinding> oldPortBindings = configuration.getPortBindings();
            oldPortBindings.addAll(listOfPorts);
        } else {
            configuration.setPortBindings(listOfPorts);
        }
        return this;
    }

    private Collection<PortBinding> addPairPortRange(String hostRangePorts, String containerRangePorts, String host) {
        Collection<PortBinding> listOfPorts = new ArrayList<>();
        final List<String> expandedHostPorts = getExpandedPorts(hostRangePorts);
        final List<String> expandedContainerPorts = getExpandedPorts(containerRangePorts);

        if (expandedContainerPorts.size() != expandedHostPorts.size()) {
            throw new IllegalArgumentException("Port ranges from host and container side should contain same number of ports");
        }

        for (int i=0; i < expandedHostPorts.size(); i++) {
            if (host == null) {
                listOfPorts.add(PortBinding.valueOf(expandedHostPorts.get(i) + "->" + expandedContainerPorts.get(i)));
            } else {
                listOfPorts.add(PortBinding.valueOf(host + ":" + expandedHostPorts.get(i) + "->" + expandedContainerPorts.get(i)));
            }
        }

        return listOfPorts;
    }

    private List<String> getExpandedPorts(String expression) {
        String[] portRange = expression.split("-");

        if (portRange.length != 2) {
            throw new IllegalArgumentException("Expected Port Range expression but found " + expression);
        }

        int initialPort = Integer.parseInt(portRange[0].trim());
        int endPort = Integer.parseInt(portRange[1].trim()) + 1;

        return IntStream.range(initialPort, endPort).boxed()
                .map(String::valueOf)
                .collect(Collectors.toList());

    }

    public ContainerBuilder addExpose(Collection<String> exposes) {
        Collection<ExposedPort> ports = new HashSet<ExposedPort>();
        for(String exposed : exposes) {
            ports.add(ExposedPort.valueOf(exposed));
        }
        if (configuration.getExposedPorts() != null) {
            Collection<ExposedPort> oldExposedPorts = configuration.getExposedPorts();
            oldExposedPorts.addAll(ports);
        } else {
            configuration.setExposedPorts(ports);
        }
        return this;
    }

    public ContainerBuilder addVolumes(Collection<String> volumes) {
        if (configuration.getVolumes() != null) {
            Collection<String> oldVolumes = configuration.getVolumes();
            oldVolumes.addAll(volumes);
        } else {
            configuration.setVolumes(new HashSet<>(volumes));
        }
        return this;
    }

    public ContainerBuilder addBinds(Collection<String> volumes) {
        if (configuration.getBinds() != null) {
            Collection<String> oldBinds = configuration.getBinds();
            oldBinds.addAll(volumes);
        } else {
            configuration.setBinds(new HashSet<>(volumes));
        }

        return this;
    }

    public ContainerBuilder addVolumesFrom(Collection<String> volumesFrom) {
        if (configuration.getVolumesFrom() != null) {
            Collection<String> oldVolumes = configuration.getVolumesFrom();
            oldVolumes.addAll(volumesFrom);
        } else {
            configuration.setVolumesFrom(new HashSet<String>(volumesFrom));
        }
        return this;
    }

    public ContainerBuilder addLabels(Map<String, String> labels) {
        //TODO now only support for array approach and not dictionary
        if (configuration.getLabels() != null) {
            Map<String, String> oldLabels = configuration.getLabels();
            oldLabels.putAll(labels);
        } else {
            configuration.setLabels(labels);
        }
        return this;
    }

    public ContainerBuilder addEnvironment(Collection<String> environments) {
        //TODO now only support for array approach
        addEnvironment(getProperties(environments));
        return this;
    }

    private void addEnvironment(Properties properties) {
        if (configuration.getEnv() != null) {
            Properties oldProperties = getProperties(configuration.getEnv());
            oldProperties.putAll(properties);
            configuration.setEnv(toEnvironment(oldProperties));
        } else {
            configuration.setEnv(toEnvironment(properties));
        }
    }

    public ContainerBuilder addEnvFile(Collection<String> environmentPaths) {
        for (String environmentPath : environmentPaths) {
            try {
                Properties properties = new Properties();
                Path environmentLocation = Paths.get(environmentPath);
                File environmentFile = this.dockerComposeRootLocation.resolve(environmentLocation).toFile();
                FileInputStream inStream = new FileInputStream(environmentFile);
                properties.load(inStream);
                inStream.close();
                addEnvironment(properties);
            } catch (IOException e) {
                throw new IllegalArgumentException(e);
            }
        }
        return this;
    }

    public ContainerBuilder addNet(String net) {
        configuration.setNetworkMode(net);
        return this;
    }

    public ContainerBuilder addDns(String dns) {
        configuration.setDns(Arrays.asList(dns));
        return this;
    }

    public ContainerBuilder addDns(Collection<String> dns) {
        if (configuration.getDns() != null) {
            Collection<String> oldDns = configuration.getDns();
            oldDns.addAll(dns);
        } else {
            configuration.setDns(new HashSet<String>(dns));
        }
        return this;
    }

    public ContainerBuilder addCapAdd(Collection<String> capAdds) {
        if (configuration.getCapAdd() != null) {
            Collection<String> oldCapAdd = configuration.getCapAdd();
            oldCapAdd.addAll(capAdds);
        } else {
            configuration.setCapAdd(new HashSet<String>(capAdds));
        }
        return this;
    }

    public ContainerBuilder addCapDrop(Collection<String> capDrops) {
        if (configuration.getCapDrop() != null) {
            Collection<String> oldCapDrops = configuration.getCapDrop();
            oldCapDrops.addAll(capDrops);
        } else {
            configuration.setCapDrop(new HashSet<String>(capDrops));
        }
        return this;
    }

    public ContainerBuilder addDnsSearch(String dnsSearch) {
        configuration.setDnsSearch(Arrays.asList(dnsSearch));
        return this;
    }

    public ContainerBuilder addDnsSearch(Collection<String> dnsSearch) {
        if (configuration.getDnsSearch() != null) {
            Collection<String> oldDnsSearch = configuration.getDnsSearch();
            oldDnsSearch.addAll(dnsSearch);
        } else {
            configuration.setDnsSearch(new HashSet<String>(dnsSearch));
        }
        return this;
    }

    public ContainerBuilder addCpuShares(int cpuShares) {
        configuration.setCpuShares(cpuShares);
        return this;
    }

    private ContainerBuilder addCpuSet(String cpuSet) {
        configuration.setCpuSet(cpuSet);
        return this;
    }

    public ContainerBuilder addCpuQuota(int cpuQuota) {
        configuration.setCpuQuota(cpuQuota);
        return this;
    }

    public ContainerBuilder addTty(boolean tty) {
        configuration.setTty(tty);
        return this;
    }

    public ContainerBuilder addStdinOpen(boolean stdinOpen) {
        configuration.setStdinOpen(stdinOpen);
        return this;
    }

    public ContainerBuilder extend(Path location, String service) {
        Path extendLocation = this.dockerComposeRootLocation.resolve(location);
        DockerCompositions dockerCompositions = DockerComposeConverter.create(extendLocation).convert();
        CubeContainer cubeContainer = dockerCompositions.getContainers().get(service);

        if (cubeContainer == null) {
            throw new IllegalArgumentException(String.format("Service name %s is not present at %s", service, extendLocation.toAbsolutePath()));
        } else {
            ContainerBuilder containerBuilder = new ContainerBuilder(dockerComposeRootLocation, configuration);
            configuration.merge(cubeContainer);
        }
        return this;
    }

    public ContainerBuilder addRestart(String restart) {
        RestartPolicy restartPolicy = new RestartPolicy();
        if (restart.startsWith("on-failure")) {
            String[] element = restart.split(":");
            if (element.length == 1) {
                restartPolicy.setName(restart);
                restartPolicy.setMaximumRetryCount(0);
            } else {
                if (element.length == 2) {
                    restartPolicy.setName(element[0]);
                    restartPolicy.setMaximumRetryCount(Integer.parseInt(element[1]));
                } else {
                    throw new IllegalArgumentException("on-failure restart should be on-failure or with optional retries on-failure:retries");
                }
            }
        } else {
            restartPolicy.setName(restart);
        }
        configuration.setRestartPolicy(restartPolicy);
        return this;
    }

    public ContainerBuilder addPrivileged(boolean privileged) {
        configuration.setPrivileged(privileged);
        return this;
    }

    public ContainerBuilder addMemLimit(Long memLimit) {
        configuration.setMemoryLimit(memLimit);
        return this;
    }

    public ContainerBuilder addDomainName(String domainName) {
        configuration.setDomainName(domainName);
        return this;
    }

    public ContainerBuilder addHostname(String hostname) {
        configuration.setHostName(hostname);
        return this;
    }

    public ContainerBuilder addUser(String user) {
        configuration.setUser(user);
        return this;
    }

    public ContainerBuilder addEntrypoint(String entrypoint) {
        configuration.setEntryPoint(Arrays.asList(entrypoint));
        return this;
    }

    public ContainerBuilder addEntrypoint(Collection<String> entrypoint) {
        configuration.setEntryPoint(entrypoint);
        return this;
    }

    public ContainerBuilder addWorkingDir(String workingDir) {
        configuration.setWorkingDir(workingDir);
        return this;
    }

    public CubeContainer buildFromExtension() {
        return this.configuration;
    }

    public CubeContainer build() {
        return configuration;
    }

    private String getRandomPort() {
        int shiftedRandomPort = random.nextInt(61000 - 32768);
        return Integer.toString(shiftedRandomPort + 32768);
    }

    private Properties getProperties(Collection<String> properties) {
        Properties allProperties = new Properties();
        for (String property : properties) {
            int keySeparator = property.indexOf("=");
            allProperties.put(property.substring(0, keySeparator), property.substring(keySeparator + 1, property.length()));
        }
        return allProperties;
    }

    private Collection<String> toEnvironment(Properties properties) {
        Collection<String> listOfEnvironment = new HashSet<String>();
        Set<Entry<Object, Object>> entrySet = properties.entrySet();
        for (Entry<Object, Object> entry : entrySet) {
            listOfEnvironment.add(entry.getKey() + "=" + entry.getValue());
        }
        return listOfEnvironment;
    }

    private void logUnsupportedOperations(Set<String> keys) {
        for (String key : keys) {
            if(!AVAILABLE_COMMANDS.contains(key)) {
                log.info(String.format("Key: %s is not implemented in Cube.", keys));
            }
        }
    }
}

