/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.wayang.core.plugin;

import org.apache.commons.lang3.Validate;
import org.apache.wayang.core.api.Configuration;
import org.apache.wayang.core.api.exception.WayangException;
import org.apache.wayang.core.mapping.Mapping;
import org.apache.wayang.core.optimizer.channels.ChannelConversion;
import org.apache.wayang.core.platform.Platform;
import org.apache.wayang.core.util.ReflectionUtils;
import org.apache.wayang.core.util.fs.FileSystem;
import org.apache.wayang.core.util.fs.FileSystems;
import org.yaml.snakeyaml.Yaml;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;

/**
 * This {@link Plugin} can be arbitrarily customized.
 */
public class DynamicPlugin implements Plugin {

    private final Set<Platform> requiredPlatforms = new HashSet<>(), excludedRequiredPlatforms = new HashSet<>();

    private final Set<Mapping> mappings = new HashSet<>(), excludedMappings = new HashSet<>();

    private final Set<ChannelConversion> channelConversions = new HashSet<>(), excludedChannelConversions = new HashSet<>();

    private final Map<String, String> properties = new HashMap<>();

    /**
     * Loads a YAML file of the following format:
     * <pre>{@code
     * # mappings to be loaded by this plugin
     * mappings:
     *   include:
     *     - <expression that evaluates to a Mapping instance or Collection of such>
     *     - ...
     *   exclude:
     *     - <expression that evaluates to a Mapping instance or Collection of such>
     *     - ...
     *
     * # channel conversions to be loaded by this plugin
     * conversions:
     *   include:
     *     - <expression that evaulates to a ChannelConversion instance or Collection of such>
     *     - ..
     *   exclude:
     *     - <expression that evaulates to a ChannelConversion instance or Collection of such>
     *     - ..
     *
     * # properties configured by this plugin
     * properties:
     *   <property name>: <property value>
     *
     * # required platforms
     * platforms:
     *   include:
     *     - <expression that evaluates to a Platform instance or Collection of such>
     *     - ..
     *   exclude:
     *     - <expression that evaluates to a Platform instance or Collection of such>
     *     - ..
     *
     * }</pre>
     *
     * @param yamlUrl an URL to a YAML file of the above format
     * @return the configured instance
     * @see ReflectionUtils#evaluate(String) Supported expressions
     */
    public static DynamicPlugin loadYaml(String yamlUrl) {
        // Load YAML file.
        final FileSystem fileSystem = FileSystems.getFileSystem(yamlUrl).orElseThrow(
                () -> new WayangException(String.format("No filesystem for %s.", yamlUrl))
        );
        Object yaml;
        try (final InputStream inputStream = fileSystem.open(yamlUrl)) {
            yaml = new Yaml().load(inputStream);
        } catch (IOException e) {
            throw new WayangException(String.format("Could not load %s.", yamlUrl));
        }

        DynamicPlugin plugin = new DynamicPlugin();
        try {
            // Evaluate YAML file.
            DynamicPlugin.<Map<String, Object>>ifPresent(yaml, Map.class, values -> {
                // Platforms.
                DynamicPlugin.<Map<String, Object>>ifPresent(values.get("platforms"), Map.class, platforms -> {
                    DynamicPlugin.<List<String>>ifPresent(platforms.get("include"), List.class, expressions -> {
                        for (String expression : expressions) {
                            Object eval = ReflectionUtils.evaluate(expression);
                            if (eval instanceof Platform) {
                                plugin.addRequiredPlatform((Platform) eval);
                            } else {
                                Collection<?> platformCollection = (Collection<?>) eval;
                                for (Object platform : platformCollection) {
                                    plugin.addRequiredPlatform((Platform) platform);
                                }
                            }
                        }
                    });
                    DynamicPlugin.<List<String>>ifPresent(platforms.get("exclude"), List.class, expressions -> {
                        for (String expression : expressions) {
                            Object eval = ReflectionUtils.evaluate(expression);
                            if (eval instanceof Platform) {
                                plugin.excludeRequiredPlatform((Platform) eval);
                            } else {
                                Collection<?> platformCollection = (Collection<?>) eval;
                                for (Object platform : platformCollection) {
                                    plugin.excludeRequiredPlatform((Platform) platform);
                                }
                            }
                        }
                    });
                });

                // Mappings.
                DynamicPlugin.<Map<String, Object>>ifPresent(values.get("mappings"), Map.class, mappings -> {
                    DynamicPlugin.<List<String>>ifPresent(mappings.get("include"), List.class, expressions -> {
                        for (String expression : expressions) {
                            Object eval = ReflectionUtils.evaluate(expression);
                            if (eval instanceof Mapping) {
                                plugin.addMapping((Mapping) eval);
                            } else {
                                Collection<?> collection = (Collection<?>) eval;
                                for (Object element : collection) {
                                    plugin.addMapping((Mapping) element);
                                }
                            }
                        }
                    });
                    DynamicPlugin.<List<String>>ifPresent(mappings.get("exclude"), List.class, expressions -> {
                        for (String expression : expressions) {
                            Object eval = ReflectionUtils.evaluate(expression);
                            if (eval instanceof Mapping) {
                                plugin.excludeMapping((Mapping) eval);
                            } else {
                                Collection<?> collection = (Collection<?>) eval;
                                for (Object element : collection) {
                                    plugin.excludeMapping((Mapping) element);
                                }
                            }
                        }
                    });
                });

                // ChannelConversions.
                DynamicPlugin.<Map<String, Object>>ifPresent(values.get("conversions"), Map.class, conversions -> {
                    DynamicPlugin.<List<String>>ifPresent(conversions.get("include"), List.class, expressions -> {
                        for (String expression : expressions) {
                            Object eval = ReflectionUtils.evaluate(expression);
                            if (eval instanceof ChannelConversion) {
                                plugin.addChannelConversion((ChannelConversion) eval);
                            } else {
                                Collection<?> collection = (Collection<?>) eval;
                                for (Object element : collection) {
                                    plugin.addChannelConversion((ChannelConversion) element);
                                }
                            }
                        }
                    });
                    DynamicPlugin.<List<String>>ifPresent(conversions.get("exclude"), List.class, expressions -> {
                        for (String expression : expressions) {
                            Object eval = ReflectionUtils.evaluate(expression);
                            if (eval instanceof ChannelConversion) {
                                plugin.excludeChannelConversion((ChannelConversion) eval);
                            } else {
                                Collection<?> collection = (Collection<?>) eval;
                                for (Object element : collection) {
                                    plugin.excludeChannelConversion((ChannelConversion) element);
                                }
                            }
                        }
                    });
                });

                // Properties.
                DynamicPlugin.<Map<String, Object>>ifPresent(values.get("properties"), Map.class, properties -> {
                    properties.forEach(plugin::addProperty);
                });
            });

            return plugin;
        } catch (Exception e) {
            throw new WayangException(String.format("Configuration file %s seems to be corrupt.", yamlUrl), e);
        }
    }


    /**
     * Checks and casts the given {@link Object}. If it is not {@code null}, feed it to a {@link Consumer}.
     *
     * @param o        to be casted
     * @param t        to that should be casted
     * @param consumer accepts the casted {@code o} unless it is {@code null}
     */
    @SuppressWarnings("unchecked")
    public static <T> void ifPresent(Object o, Class<? super T> t, Consumer<T> consumer) {
        if (o == null) return;
        Validate.isInstanceOf(t, o, "Expected %s to be of type %s (is %s).", o, t, o.getClass());
        consumer.accept((T) o);
    }

    public void addRequiredPlatform(Platform platform) {
        this.requiredPlatforms.add(platform);
        this.excludedRequiredPlatforms.remove(platform);
    }

    public void excludeRequiredPlatform(Platform platform) {
        this.excludedRequiredPlatforms.add(platform);
        this.requiredPlatforms.remove(platform);
    }

    @Override
    public Collection<Platform> getRequiredPlatforms() {
        return this.requiredPlatforms;
    }

    @Override
    public Collection<Platform> getExcludedRequiredPlatforms() {
        return this.excludedRequiredPlatforms;
    }

    public void addMapping(Mapping mapping) {
        this.mappings.add(mapping);
        this.excludedMappings.remove(mapping);
    }

    public void excludeMapping(Mapping mapping) {
        this.excludedMappings.add(mapping);
        this.mappings.remove(mapping);
    }

    @Override
    public Collection<Mapping> getMappings() {
        return this.mappings;
    }

    @Override
    public Collection<Mapping> getExcludedMappings() {
        return this.excludedMappings;
    }

    public void addChannelConversion(ChannelConversion channelConversion) {
        this.channelConversions.add(channelConversion);
        this.excludedChannelConversions.remove(channelConversion);
    }

    public void excludeChannelConversion(ChannelConversion channelConversion) {
        this.excludedChannelConversions.add(channelConversion);
        this.channelConversions.remove(channelConversion);
    }

    @Override
    public Collection<ChannelConversion> getChannelConversions() {
        return this.channelConversions;
    }

    @Override
    public Collection<ChannelConversion> getExcludedChannelConversions() {
        return this.excludedChannelConversions;
    }

    public void addProperty(String key, Object value) {
        if (value == null) {
            this.properties.remove(key);
        } else {
            this.properties.put(key, Objects.toString(value));
        }
    }

    @Override
    public void setProperties(Configuration configuration) {
        properties.forEach(configuration::setProperty);
    }
}
