package org.testcontainers;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.InternalServerErrorException;
import com.github.dockerjava.api.NotFoundException;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.model.Frame;
import com.github.dockerjava.api.model.Image;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.command.LogContainerResultCallback;
import com.github.dockerjava.core.command.PullImageResultCallback;
import lombok.Synchronized;
import org.slf4j.Logger;
import org.testcontainers.dockerclient.DockerClientConfigUtils;
import org.testcontainers.dockerclient.DockerConfigurationStrategy;
import org.testcontainers.dockerclient.DockerMachineConfigurationStrategy;
import org.testcontainers.dockerclient.EnvironmentAndSystemPropertyConfigurationStrategy;
import org.testcontainers.dockerclient.UnixSocketConfigurationStrategy;

import java.util.List;

import static java.util.Arrays.asList;
import static org.slf4j.LoggerFactory.getLogger;

/**
 * Singleton class that provides initialized Docker clients.
 * <p>
 * The correct client configuration to use will be determined on first use, and cached thereafter.
 */
public class DockerClientFactory {

    private static DockerClientFactory instance;
    private static final Logger LOGGER = getLogger(DockerClientFactory.class);

    // Cached client configuration
    private DockerClientConfig config;
    private boolean preconditionsChecked = false;

    private static final List<DockerConfigurationStrategy> CONFIGURATION_STRATEGIES =
            asList(new EnvironmentAndSystemPropertyConfigurationStrategy(),
                    new DockerMachineConfigurationStrategy(),
                    new UnixSocketConfigurationStrategy());

    /**
     * Private constructor
     */
    private DockerClientFactory() {

    }

    /**
     * Obtain an instance of the DockerClientFactory.
     *
     * @return the singleton instance of DockerClientFactory
     */
    public synchronized static DockerClientFactory instance() {
        if (instance == null) {
            instance = new DockerClientFactory();
        }

        return instance;
    }

    /**
     * @return a new initialized Docker client
     */
    @Synchronized
    public DockerClient client() {
        if (config == null) {
            config = DockerConfigurationStrategy.getFirstValidConfig(CONFIGURATION_STRATEGIES);
        }

        DockerClient client = DockerClientBuilder.getInstance(config).build();

        if (!preconditionsChecked) {
            String version = client.versionCmd().exec().getVersion();
            checkVersion(version);
            checkDiskSpaceAndHandleExceptions(client);
            preconditionsChecked = true;
        }

        // Ping, to fail fast if our docker environment has gone away
        client.pingCmd().exec();

        return client;
    }

    /**
     * @param config docker client configuration to extract the host IP address from
     * @return the IP address of the host running Docker
     */
    private String dockerHostIpAddress(DockerClientConfig config) {
        return DockerClientConfigUtils.getDockerHostIpAddress(config);
    }

    /**
     * @return the IP address of the host running Docker
     */
    public String dockerHostIpAddress() {
        return dockerHostIpAddress(config);
    }

    private void checkVersion(String version) {
        String[] splitVersion = version.split("\\.");
        if (Integer.valueOf(splitVersion[0]) <= 1 && Integer.valueOf(splitVersion[1]) < 6) {
            throw new IllegalStateException("Docker version 1.6.0+ is required, but version " + version + " was found");
        }
    }

    private void checkDiskSpaceAndHandleExceptions(DockerClient client) {
        try {
            checkDiskSpace(client);
        } catch (NotEnoughDiskSpaceException e) {
            throw e;
        } catch (Exception e) {
            LOGGER.warn("Encountered and ignored error while checking disk space", e);
        }
    }

    /**
     * Check whether this docker installation is likely to have disk space problems
     * @param client an active Docker client
     */
    private void checkDiskSpace(DockerClient client) {

        List<Image> images = client.listImagesCmd().exec();
        if (!images.stream().anyMatch(it -> asList(it.getRepoTags()).contains("alpine:3.2"))) {
            PullImageResultCallback callback = client.pullImageCmd("alpine:3.2").exec(new PullImageResultCallback());
            callback.awaitSuccess();
        }

        CreateContainerResponse createContainerResponse = client.createContainerCmd("alpine:3.2").withCmd("df", "-P").exec();
        String id = createContainerResponse.getId();

        client.startContainerCmd(id).exec();

        client.waitContainerCmd(id).exec();

        LogContainerResultCallback callback = client.logContainerCmd(id).withStdOut().exec(new LogContainerCallback());
        try {
            callback.awaitCompletion();
            String logResults = callback.toString();

            int use = 0;
            String[] lines = logResults.split("\n");
            for (String line : lines) {
                String[] fields = line.split("\\s+");
                if (fields[5].equals("/")) {
                    use = Integer.valueOf(fields[4].replace("%", ""));
                }
            }

            LOGGER.info("Disk utilization in Docker environment is {}%", use);

            if (use > 90) {
                LOGGER.warn("Disk utilization Docker environment is over 90% - execution is unlikely to succeed so will be aborted.");
                throw new NotEnoughDiskSpaceException("Not enough disk space in Docker environment");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                client.removeContainerCmd(id).withRemoveVolumes(true).withForce(true).exec();
            } catch (NotFoundException | InternalServerErrorException ignored) {

            }
        }
    }

    private static class NotEnoughDiskSpaceException extends RuntimeException {
        NotEnoughDiskSpaceException(String message) {
            super(message);
        }
    }
}

class LogContainerCallback extends LogContainerResultCallback {
    private final StringBuffer log = new StringBuffer();

    @Override
    public void onNext(Frame frame) {
        log.append(new String(frame.getPayload()));
        super.onNext(frame);
    }

    @Override
    public String toString() {
        return log.toString();
    }
}
@@ -5156,24 +5156,57 @@
 oString();%0A%0A
+            int availableKB = 0;%0A
             
@@ -5406,24 +5406,86 @@
 als(%22/%22)) %7B%0A
+                    availableKB = Integer.valueOf(fields%5B3%5D);%0A
             
@@ -5566,32 +5566,82 @@
  %7D%0A            %7D
+%0A            int availableMB = availableKB / 1024;
 %0A%0A            LO
@@ -5700,14 +5700,45 @@
  %7B%7D%25
-%22, use
+ (%7B%7D MB available)%22, use, availableMB
 );%0A%0A
@@ -5757,16 +5757,26 @@
 if (
-use %3E 90
+availableMB %3C 2048
 ) %7B%0A
@@ -5802,61 +5802,56 @@
 GER.
-warn(%22Disk utilization Docker environment is over 90%25
+error(%22Docker environment has less than 2GB free
  - e

Replace % based disk space check with an absolute 2GB minimum limit. Refs #95.