package com.atlassian.bitbucket.mesh.process;

import com.atlassian.bitbucket.mesh.throttle.ThrottledResource;
import com.atlassian.bitbucket.mesh.util.BuilderSupport;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.*;

import static java.util.Objects.requireNonNull;

public class ProcessParameters<T> {

    private final List<String> arguments;
    private final ProcessCompletionHandler completionHandler;
    private final Map<String, String> environment;
    private final ThrottledResource throttledResource;
    private final ProcessStdioHandler<T> stdioHandler;

    private Duration executionInterval;
    private Duration idleInterval;
    private Path workDir;

    private ProcessParameters(Builder<T> builder) {
        arguments = builder.arguments.build();
        completionHandler = builder.completionHandler;
        environment = builder.environment;
        executionInterval = builder.executionInterval;
        throttledResource = builder.throttledResource;
        stdioHandler = builder.stdioHandler;
        idleInterval = builder.idleInterval;
        workDir = builder.workDir;
    }

    public void environmentPutIfAbsent(@Nonnull String key, @Nonnull String value) {
        requireNonBlankAndNoNullChar(key, "key");
        requireNoNullChars(value);
        environment.putIfAbsent(key, value);
    }

    @Nonnull
    public List<String> getArguments() {
        return arguments;
    }

    @Nonnull
    public ProcessCompletionHandler getCompletionHandler() {
        return completionHandler;
    }

    @Nonnull
    public Map<String, String> getEnvironment() {
        return Collections.unmodifiableMap(environment);
    }

    @Nullable
    public Duration getExecutionInterval() {
        return executionInterval;
    }

    @Nonnull
    public ProcessStdioHandler<T> getStdioHandler() {
        return stdioHandler;
    }

    @Nullable
    public Duration getIdleInterval() {
        return idleInterval;
    }

    @Nullable
    public ThrottledResource getThrottledResource() {
        return throttledResource;
    }

    @Nullable
    public Path getWorkDir() {
        return workDir;
    }

    public void setExecutionInterval(@Nullable Duration executionInterval) {
        this.executionInterval = executionInterval;
    }

    public void setIdleInterval(@Nullable Duration idleInterval) {
        this.idleInterval = idleInterval;
    }

    public void setWorkDir(@Nullable Path workDir) {
        this.workDir = workDir;
    }

    @Override
    public String toString() {
        StringJoiner joiner = new StringJoiner(" ");
        joiner.add(Paths.get(arguments.get(0)).getFileName().toString());
        Iterables.skip(arguments, 1).forEach(joiner::add);

        return joiner.toString();
    }

    public static class Builder<T> extends BuilderSupport {

        private final ImmutableList.Builder<String> arguments;
        private final ProcessCompletionHandler completionHandler;
        private final Map<String, String> environment;
        private final ProcessStdioHandler<T> stdioHandler;

        private Duration executionInterval;
        private Duration idleInterval;
        private ThrottledResource throttledResource;
        private Path workDir;

        public Builder(@Nonnull ProcessStdioHandler<T> stdioHandler,
                       @Nonnull ProcessCompletionHandler completionHandler) {
            this.completionHandler = requireNonNull(completionHandler, "completionHandler");
            this.stdioHandler = requireNonNull(stdioHandler, "stdioHandler");

            arguments = ImmutableList.builder();
            environment = new HashMap<>();
        }

        @Nonnull
        public Builder<T> argument(@Nullable String value) {
            addIf(Builder::nonNullAndNoNullChar, arguments, value);

            return this;
        }

        @Nonnull
        public Builder<T> arguments(@Nullable Iterable<String> values) {
            addIf(Builder::nonNullAndNoNullChar, arguments, values);

            return this;
        }

        @Nonnull
        public Builder<T> arguments(@Nullable String value, String... values) {
            addIf(Builder::nonNullAndNoNullChar, arguments, value, values);

            return this;
        }

        @Nonnull
        public ProcessParameters<T> build() {
            return new ProcessParameters<>(this);
        }

        @Nonnull
        public Builder<T> environment(@Nonnull Map<String, String> map) {
            requireNonNull(map, "environment").forEach((key, value) -> {
                requireNonBlankAndNoNullChar(key, "key");
                requireNoNullChars(value);
            });
            environment.putAll(map);

            return this;
        }

        @Nonnull
        public Builder<T> environment(@Nonnull String key, @Nonnull String value) {
            environment.put(requireNonBlankAndNoNullChar(key, "key"), requireNoNullChars(value));

            return this;
        }

        @Nonnull
        public Builder<T> executionInterval(@Nullable Duration value) {
            executionInterval = value;

            return this;
        }

        @Nonnull
        public Builder<T> idleInterval(@Nullable Duration value) {
            idleInterval = value;

            return this;
        }

        @Nonnull
        public Builder<T> throttled(@Nullable ThrottledResource value) {
            throttledResource = value;

            return this;
        }

        @Nonnull
        public Builder<T> workDir(@Nullable Path value) {
            workDir = value;

            return this;
        }

        private static boolean nonNullAndNoNullChar(String value) {
            if (value == null) {
                return false;
            }
            requireNoNullChars(value);
            return true;
        }
    }

    private static String requireNoNullChars(String value) {
        if (value.indexOf(0) >= 0) {
            throw new IllegalArgumentException("Unsupported \\0 character detected: " + value);
        }
        return value;
    }

    private static String requireNonBlankAndNoNullChar(String value, String name) {
        requireNonNull(value, name);
        if (value.trim().isEmpty()) {
            throw new IllegalArgumentException("A non-blank " + StringUtils.defaultString(name, "value") + " is required");
        }
        requireNoNullChars(value);
        return value;
    }
}
