/*
 * 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.meecrowave.runner;

import org.apache.catalina.Server;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardServer;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.ParseException;
import org.apache.meecrowave.Meecrowave;
import org.apache.meecrowave.runner.cli.CliOption;
import org.apache.xbean.recipe.ObjectRecipe;

import javax.enterprise.inject.Vetoed;
import java.io.File;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static java.util.Optional.ofNullable;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

@Vetoed
public class Cli implements Runnable, AutoCloseable {
    private final String[] args;
    private volatile Meecrowave instance;
    private volatile boolean closed;

    public Cli(final String[] args) {
        this.args = args;
    }

    public void run() {
        final ParsedCommand parsedCommand = new ParsedCommand(args).invoke();
        if (parsedCommand.isFailed()) {
            return;
        }

        final Meecrowave.Builder builder = parsedCommand.getBuilder();
        final CommandLine line = parsedCommand.getLine();
        try (final Meecrowave meecrowave = new Meecrowave(builder)) {
            synchronized (this) {
                if (closed) {
                    return;
                }
                this.instance = meecrowave;
            }

            final String ctx = line.getOptionValue("context", "");
            final String fixedCtx = !ctx.isEmpty() && !ctx.startsWith("/") ? '/' + ctx : ctx;
            final String war = line.getOptionValue("webapp");
            meecrowave.start();
            if (war == null) {
                meecrowave.deployClasspath(new Meecrowave.DeploymentMeta(ctx, ofNullable(line.getOptionValue("docbase")).map(File::new).orElse(null), null));
            } else {
                meecrowave.deployWebapp(fixedCtx, new File(war));
            }
            doWait(meecrowave, line);
        }
    }

    protected void doWait(final Meecrowave meecrowave, final CommandLine line) {
        meecrowave.getTomcat().getServer().await();
    }

    @Override
    public synchronized void close() {
        if (closed) {
            return;
        }
        this.closed = true;
        if (instance != null) {
            final Server server = instance.getTomcat().getServer();
            if (StandardServer.class.isInstance(server)) {
                StandardServer.class.cast(server).stopAwait();
            }
        }
    }

    public static void main(final String[] args) {
        new Cli(args).run();
    }

    // utility when user wraps the command, it enables him to manage the instance but reuse most of our options
    public static Meecrowave.Builder create(final String[] args) {
        final ParsedCommand command = new ParsedCommand(args).invoke();
        if (command.isFailed()) {
            return null;
        }
        return command.getBuilder();
    }

    private static void bind(final Meecrowave.Builder builder, final CommandLine line, final List<Field> fields, final Object config) {
        fields.forEach(f -> {
            final CliOption opt = f.getAnnotation(CliOption.class);
            final Optional<String> first = Stream.of(Stream.of(opt.name()), Stream.of(opt.alias()))
                    .flatMap(a -> a)
                    .filter(line::hasOption)
                    .findFirst();
            if (first.isPresent()) {
                final String name = first.get();
                ofNullable(f.getType() == boolean.class ?
                        ofNullable(line.getOptionValue(name)).map(Boolean::parseBoolean).orElse(true) :
                        toValue(builder, name, line.getOptionValues(name), f.getType()))
                        .ifPresent(v -> {
                            if (!f.isAccessible()) {
                                f.setAccessible(true);
                            }
                            try {
                                f.set(config, v);
                            } catch (final IllegalAccessException e) {
                                throw new IllegalStateException(e);
                            }
                        });
            }
        });
    }

    private static Object toValue(final Meecrowave.Builder builder, final String name, final String[] optionValues, final Class<?> type) {
        if (optionValues == null || optionValues.length == 0) {
            return null;
        }

        // decode options while it is strings
        IntStream.range(0, optionValues.length)
                .forEach(i -> optionValues[i] = builder.getExtension(Meecrowave.ValueTransformers.class).apply(optionValues[i]));

        if (String.class == type) {
            return optionValues[0];
        }
        if (int.class == type) {
            return Integer.parseInt(optionValues[0]);
        }
        if (File.class == type) {
            return new File(optionValues[0]);
        }
        if (Properties.class == type) {
            final Properties props = new Properties();
            Stream.of(optionValues).map(v -> v.split("=")).forEach(v -> props.setProperty(v[0], v[1]));
            return props;
        }
        if (Map.class == type) {
            final Map<String, String> props = new HashMap<>();
            Stream.of(optionValues).map(v -> v.split("=")).forEach(v -> props.put(v[0], v[1]));
            return props;
        }

        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
        switch (name) {
            case "realm": // org.foo.Impl:attr1=val1;attr2=val2
                try {
                    int end = optionValues[0].indexOf(':');
                    if (end < 0) {
                        return loader.loadClass(optionValues[0]).newInstance();
                    }
                    final ObjectRecipe recipe = new ObjectRecipe(optionValues[0].substring(0, end));
                    Stream.of(optionValues[0].substring(end + 1, optionValues[0].length()).split(";"))
                            .map(v -> v.split("="))
                            .forEach(v -> recipe.setProperty(v[0], v[1]));
                    return recipe.create(loader);
                } catch (final Exception cnfe) {
                    throw new IllegalArgumentException(optionValues[0]);
                }
            case "security-constraint": // attr1=val1;attr2=val2
                return Stream.of(optionValues)
                        .map(item -> {
                            try {
                                final ObjectRecipe recipe = new ObjectRecipe(Meecrowave.SecurityConstaintBuilder.class);
                                Stream.of(item.split(";"))
                                        .map(v -> v.split("="))
                                        .forEach(v -> recipe.setProperty(v[0], v[1]));
                                return recipe.create(loader);
                            } catch (final Exception cnfe) {
                                throw new IllegalArgumentException(optionValues[0]);
                            }
                        }).collect(toList());
            case "login-config":
                try {
                    final ObjectRecipe recipe = new ObjectRecipe(Meecrowave.LoginConfigBuilder.class);
                    Stream.of(optionValues[0].split(";"))
                            .map(v -> v.split("="))
                            .forEach(v -> recipe.setProperty(v[0], v[1]));
                    return recipe.create(loader);
                } catch (final Exception cnfe) {
                    throw new IllegalArgumentException(optionValues[0]);
                }
            case "connector": // org.foo.Impl:attr1=val1;attr2=val2
                return Stream.of(optionValues)
                        .map(v -> {
                            try {
                                int end = v.indexOf(':');
                                if (end < 0) {
                                    return new Connector(v);
                                }
                                final Connector connector = new Connector(optionValues[0].substring(0, end));
                                Stream.of(v.substring(end + 1, v.length()).split(";"))
                                        .map(i -> i.split("="))
                                        .forEach(i -> connector.setProperty(i[0], i[1]));
                                return connector;
                            } catch (final Exception cnfe) {
                                throw new IllegalArgumentException(optionValues[0]);
                            }
                        }).collect(toList());
            default:
                throw new IllegalArgumentException("Unsupported " + name);
        }
    }

    public interface Options {
    }

    private static final class ParsedCommand {
        private final String[] args;
        private boolean failed;
        private CommandLine line;
        private Meecrowave.Builder builder;

        private ParsedCommand(final String... args) {
            this.args = args;
        }

        private static void help(final org.apache.commons.cli.Options options) {
            new HelpFormatter().printHelp("java -jar meecrowave-runner.jar", options);
        }

        private static Meecrowave.Builder buildConfig(final CommandLine line, final List<Field> fields,
                                                      final Map<Object, List<Field>> propertiesOptions) {
            final Meecrowave.Builder config = new Meecrowave.Builder();
            bind(config, line, fields, config);
            propertiesOptions.forEach((o, f) -> {
                bind(config, line, f, o);
                config.setExtension(o.getClass(), o);
            });
            return config;
        }

        boolean isFailed() {
            return failed;
        }

        public CommandLine getLine() {
            return line;
        }

        public Meecrowave.Builder getBuilder() {
            return builder;
        }

        public ParsedCommand invoke() {
            final org.apache.commons.cli.Options options = new org.apache.commons.cli.Options();
            options.addOption(null, "help", false, "Show help");
            options.addOption(null, "context", true, "The context to use to deploy the webapp");
            options.addOption(null, "webapp", true, "Location of the webapp, if not set the classpath will be deployed");
            options.addOption(null, "docbase", true, "Location of the docbase for a classpath deployment");
            final List<Field> fields = Stream.of(Meecrowave.Builder.class.getDeclaredFields())
                    .filter(f -> f.isAnnotationPresent(CliOption.class))
                    .collect(toList());
            final Map<Object, List<Field>> propertiesOptions = StreamSupport.stream(ServiceLoader.load(Options.class).spliterator(), false)
                    .collect(toMap(identity(), o -> Stream.of(o.getClass().getDeclaredFields()).filter(f -> f.isAnnotationPresent(CliOption.class)).collect(toList())));
            fields.forEach(f -> {
                final CliOption opt = f.getAnnotation(CliOption.class);
                final String description = opt.description();
                options.addOption(null, opt.name(), true /*even for booleans otherwise no way to set false for true by default ones*/, description);
                Stream.of(opt.alias()).forEach(a -> options.addOption(null, a, true, description));
            });
            propertiesOptions.values().forEach(all -> all.forEach(f -> {
                final CliOption opt = f.getAnnotation(CliOption.class);
                final String description = opt.description();
                options.addOption(null, opt.name(), true, description);
                Stream.of(opt.alias()).forEach(a -> options.addOption(null, a, true, description));
            }));

            final CommandLineParser parser = new DefaultParser();
            try {
                line = parser.parse(options, args, true);
            } catch (final ParseException exp) {
                help(options);
                failed = true;
                return this;
            }

            if (line.hasOption("help")) {
                help(options);
                failed = true;
                return this;
            }

            builder = buildConfig(line, fields, propertiesOptions);
            failed = false;
            return this;
        }
    }
}
