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

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.arquillian.cube.docker.impl.client.config.CubeContainer;
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.Network;
import org.arquillian.cube.docker.impl.client.config.PortBinding;
import org.arquillian.cube.docker.impl.docker.compose.DockerComposeEnvironmentVarResolver;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.introspector.Property;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeId;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.ScalarNode;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Represent;
import org.yaml.snakeyaml.representer.Representer;

public final class ConfigUtil {

    private static final String NETWORKS = "networks";

    private static final String CONTAINERS = "containers";

    private ConfigUtil() {
    }

    public static String[] trim(String[] data) {
        List<String> result = new ArrayList<String>();
        for (String val : data) {
            String trimmed = val.trim();
            if (!trimmed.isEmpty()) {
                result.add(trimmed);
            }
        }
        return result.toArray(new String[] {});
    }

    public static String[] reverse(String[] cubeIds) {
        String[] result = new String[cubeIds.length];
        int n = cubeIds.length - 1;
        for (int i = 0; i < cubeIds.length; i++) {
            result[n--] = cubeIds[i];
        }
        return result;
    }

    public static String dump(DockerCompositions containers) {
        DumperOptions dumperOptions = new DumperOptions();
        dumperOptions.setPrettyFlow(true);
        dumperOptions.setDefaultFlowStyle(FlowStyle.BLOCK);
        Yaml yaml = new Yaml(new CubeRepresenter(dumperOptions));
        return yaml.dump(containers);
    }

    public static DockerCompositions load(String content) {
        return load(new ByteArrayInputStream(content.getBytes()));
    }

    @SuppressWarnings("unchecked")
    public static DockerCompositions load(InputStream inputStream) {
        // TODO: Figure out how to map root Map<String, Type> objects. Workaround by mapping it to Map structure then dumping it into individual objects

        //Apply Docker compose substitutions to cube format as well

        final String content = DockerComposeEnvironmentVarResolver.replaceParameters(inputStream);

        LoaderOptions loaderOptions = new LoaderOptions();
        loaderOptions.setAllowDuplicateKeys(false);
        loaderOptions.setProcessComments(true);
        Yaml yaml = new Yaml(new CubeConstructor(loaderOptions));
        Map<String, Object> rawLoad = (Map<String, Object>) yaml.load(content);

        DockerCompositions containers = new DockerCompositions();

        for (Map.Entry<String, Object> rawLoadEntry : rawLoad.entrySet()) {
            if (NETWORKS.equals(rawLoadEntry.getKey())) {
                Map<String, Object> rawNetworks = (Map<String, Object>) rawLoadEntry.getValue();
                for (Map.Entry<String, Object> rawNetworkEntry : rawNetworks.entrySet()) {
                    Network network = yaml.loadAs(yaml.dump(rawNetworkEntry.getValue()), Network.class);
                    containers.add(rawNetworkEntry.getKey(), network);
                }
            } else if (CONTAINERS.equals(rawLoadEntry.getKey())) {
                Map<String, Object> rawContainers = (Map<String, Object>) rawLoadEntry.getValue();
                for (Map.Entry<String, Object> rawContainerEntry : rawContainers.entrySet()) {
                    CubeContainer cubeContainer =
                        yaml.loadAs(yaml.dump(rawContainerEntry.getValue()), CubeContainer.class);
                    containers.add(rawContainerEntry.getKey(), cubeContainer);
                }
            } else {
                CubeContainer container = yaml.loadAs(yaml.dump(rawLoadEntry.getValue()), CubeContainer.class);
                containers.add(rawLoadEntry.getKey(), container);
            }
        }
        return applyExtendsRules(containers);
    }

    private static DockerCompositions applyExtendsRules(DockerCompositions dockerCompositions) {

        for (Map.Entry<String, CubeContainer> containerEntry : dockerCompositions.getContainers().entrySet()) {
            CubeContainer container = containerEntry.getValue();
            if (container.getExtends() != null) {
                String extendsContainer = container.getExtends();
                if (dockerCompositions.get(extendsContainer) == null) {
                    throw new IllegalArgumentException(
                        containerEntry.getKey() + " extends a non existing container definition " + extendsContainer);
                }
                CubeContainer extendedContainer = dockerCompositions.get(extendsContainer);
                container.merge(extendedContainer);
            }
        }
        return dockerCompositions;
    }

    private static class CubeRepresenter extends Representer {
        public CubeRepresenter(DumperOptions options) {
            super(options);
            this.representers.put(PortBinding.class, new ToStringRepresent());
            this.representers.put(ExposedPort.class, new ToStringRepresent());
            this.representers.put(Image.class, new ToStringRepresent());
            this.representers.put(Link.class, new ToStringRepresent());
            addClassTag(DockerCompositions.class, Tag.MAP);
        }

        @Override
        protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue,
            Tag customTag) {
            if (propertyValue == null) {
                return null;
            }
            return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag);
        }

        public class ToStringRepresent implements Represent {
            @Override
            public Node representData(Object data) {
                return representScalar(Tag.STR, data.toString());
            }
        }
    }

    public static class CubeConstructor extends Constructor {
        public CubeConstructor(Class<? extends Object> theRoot, LoaderOptions loadingConfig) {
            super(theRoot,loadingConfig);
        }
        public CubeConstructor(LoaderOptions loadingConfig) {
            super(loadingConfig);
        }
        public CubeConstructor(String theRoot, LoaderOptions loadingConfig) throws ClassNotFoundException {
            super(theRoot,loadingConfig);
        }
        public CubeConstructor(TypeDescription theRoot, Collection<TypeDescription> moreTDs,
                           LoaderOptions loadingConfig) {
            super (theRoot, moreTDs,loadingConfig);
        }
        public CubeConstructor(TypeDescription theRoot, LoaderOptions loadingConfig) {
            super(theRoot,loadingConfig);
        }

        private class CubeMapping extends Constructor.ConstructScalar {

            @Override
            public Object construct(Node node) {
                if (node.getType() == PortBinding.class) {
                    String value = constructScalar((ScalarNode) node).toString();
                    return PortBinding.valueOf(value);
                } else if (node.getType() == ExposedPort.class) {
                    String value = constructScalar((ScalarNode) node).toString();
                    return ExposedPort.valueOf(value);
                } else if (node.getType() == Image.class) {
                    String value = constructScalar((ScalarNode) node).toString();
                    return Image.valueOf(value);
                } else if (node.getType() == Link.class) {
                    String value = constructScalar((ScalarNode) node).toString();
                    return Link.valueOf(value);
                }
                return super.construct(node);
            }
        }
    }
}
