package org.testcontainers;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.exception.InternalServerErrorException;
import com.github.dockerjava.api.exception.NotFoundException;
import com.github.dockerjava.api.model.Frame;
import com.github.dockerjava.api.model.Image;
import com.github.dockerjava.api.model.Info;
import com.github.dockerjava.core.command.LogContainerResultCallback;
import com.github.dockerjava.core.command.PullImageResultCallback;
import com.github.dockerjava.core.command.WaitContainerResultCallback;
import lombok.Synchronized;
import org.slf4j.Logger;
import org.testcontainers.dockerclient.*;

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 DockerClientProviderStrategy strategy;
    private boolean preconditionsChecked = false;

    private static final List<DockerClientProviderStrategy> CONFIGURATION_STRATEGIES =
            asList(new EnvironmentAndSystemPropertyClientProviderStrategy(),
                    new ProxiedUnixSocketClientProviderStrategy(),
                    new UnixSocketClientProviderStrategy(),
                    new DockerMachineClientProviderStrategy());

    /**
     * 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
     */
    public DockerClient client() {
        return client(true);
    }

    /**
     *
     * @param failFast fail if client fails to ping Docker daemon
     * @return a new initialized Docker client
     */
    @Synchronized
    public DockerClient client(boolean failFast) {

        if (strategy == null) {
            strategy = DockerClientProviderStrategy.getFirstValidStrategy(CONFIGURATION_STRATEGIES);
        }

        DockerClient client = strategy.getClient();

        if (!preconditionsChecked) {
            Info dockerInfo = client.infoCmd().exec();
            LOGGER.info("Connected to docker: \n" +
                    "  Server Version: " + dockerInfo.getServerVersion() + "\n" +
                    "  Operating System: " + dockerInfo.getOperatingSystem() + "\n" +
                    "  Total Memory: " + dockerInfo.getMemTotal() / (1024 * 1024) + " MB");

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

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

        return client;
    }

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

    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();

        LogContainerResultCallback callback = client.logContainerCmd(id).withStdOut(true).exec(new LogContainerCallback());
        try {

            WaitContainerResultCallback waitCallback = new WaitContainerResultCallback();
            client.waitContainerCmd(id).exec(waitCallback);
            waitCallback.awaitStarted();

            callback.awaitCompletion();
            String logResults = callback.toString();

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

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

            if (availableMB < 2048) {
                LOGGER.error("Docker environment has less than 2GB free - 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();
    }
}
@@ -407,16 +407,64 @@
 l.Info;%0A
+import com.github.dockerjava.api.model.Version;%0A
 import c
@@ -2747,32 +2747,90 @@
 foCmd().exec();%0A
+            Version version = client.versionCmd().exec();%0A
             LOGG
@@ -2861,24 +2861,24 @@
 cker: %5Cn%22 +%0A
-
             
@@ -2939,32 +2939,105 @@
 sion() + %22%5Cn%22 +%0A
+                    %22  API Version: %22 + version.getApiVersion() + %22%5Cn%22 +%0A
                 
@@ -3203,78 +3203,8 @@
 );%0A%0A
-            String version = client.versionCmd().exec().getVersion();%0A
     
@@ -3231,16 +3231,29 @@
 (version
+.getVersion()
 );%0A     

Add logging of Docker API version for diagnostics