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

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Stream;

import org.apache.commons.configuration.CompositeConfiguration;
import org.apache.commons.configuration.MapConfiguration;
import org.apache.meecrowave.Meecrowave;
import org.apache.meecrowave.doc.generator.ArquillianConfiguration;
import org.apache.meecrowave.doc.generator.CliConfiguration;
import org.apache.meecrowave.doc.generator.Configuration;
import org.apache.meecrowave.doc.generator.Downloads;
import org.apache.meecrowave.doc.generator.GradleConfiguration;
import org.apache.meecrowave.doc.generator.LetsEncryptConfiguration;
import org.apache.meecrowave.doc.generator.MavenConfiguration;
import org.apache.meecrowave.doc.generator.OAuth2Configuration;
import org.apache.meecrowave.doc.generator.ProxyConfiguration;
import org.jbake.app.Oven;
import org.jbake.app.configuration.ConfigUtil;
import org.jbake.app.configuration.DefaultJBakeConfiguration;

import com.orientechnologies.orient.core.Orient;

public class JBake {
    private JBake() {
        // no-op
    }

    // if you want to switch off PDF generation use as arguments: src/main/jbake target/site-tmp true false
    public static void main(final String[] args) throws Exception {
        System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "64"); // try to have parallelStream better than default

        final File source = args == null || args.length < 1 ? new File("src/main/jbake") : new File(args[0]);
        final File pdfSource = new File(source, "content");
        final File destination = args == null || args.length < 2 ? new File("target/site-tmp") : new File(args[1]);
        final boolean startHttp = args == null || args.length < 2 || Boolean.parseBoolean(args[2]); // by default we dev
        final boolean skipPdf = args != null && args.length > 3 && !Boolean.parseBoolean(args[3]); // by default...too slow sorry
        final boolean updateDownloads = args != null && args.length > 4 && Boolean.parseBoolean(args[4]); // grabs central

        // generation of dynamic content
        new Configuration().run();
        new CliConfiguration().run();
        new ArquillianConfiguration().run();
        new MavenConfiguration().run();
        new OAuth2Configuration().run();
        new LetsEncryptConfiguration().run();
        new GradleConfiguration().run();
        new ProxyConfiguration().run();

        if (updateDownloads) {
            final ByteArrayOutputStream tableContent = new ByteArrayOutputStream();
            try (final PrintStream stream = new PrintStream(tableContent)) {
                Downloads.doMain(stream);
            }
            try (final Writer writer = new FileWriter(new File(source, "content/download.adoc"))) {
                writer.write("= Downloads\n"
                        + ":jbake-generated: true\n"
                        + ":jbake-date: 2017-07-24\n"
                        + ":jbake-type: page\n"
                        + ":jbake-status: published\n"
                        + ":jbake-meecrowavepdf:\n"
                        + ":jbake-meecrowavecolor: body-blue\n"
                        + ":icons: font\n"
                        + "\n"
                        + "License under Apache License v2 (ALv2).\n"
                        + "\n"
                        + "[.table.table-bordered,options=\"header\"]\n"
                        + "|===\n"
                        + "|Name|Version|Date|Size|Type|Links\n");
                writer.write(new String(tableContent.toByteArray(), StandardCharsets.UTF_8));
                writer.write("\n|===\n");
            }
        }

        final Runnable build = () -> {
            System.out.println("Building Meecrowave website in " + destination);
            final Orient orient = Orient.instance();
            try {
                orient.startup();

                final Oven oven = new Oven(source, destination, new CompositeConfiguration() {{
                    final CompositeConfiguration config = new CompositeConfiguration();
                    config.addConfiguration(new MapConfiguration(new HashMap<String, Object>() {{
                        put("asciidoctor.attributes", new ArrayList<String>() {{
                            add("source-highlighter=highlightjs");
                            add("highlightjs-theme=idea");
                            add("context_rootpath=/meecrowave");
                        }});
                    }}));
                    config.addConfiguration(DefaultJBakeConfiguration.class.cast(
                            new ConfigUtil().loadConfig(source)).getCompositeConfiguration());
                    addConfiguration(config);
                }}, true);
                oven.setupPaths();

                System.out.println("  > baking");
                oven.bake();

                if (!skipPdf) {
                    System.out.println("  > pdfifying");
                    PDFify.generatePdf(pdfSource, destination);
                }

                System.out.println("  > done :)");
            } catch (final Exception e) {
                e.printStackTrace();
            } finally {
                orient.shutdown();
            }
        };

        build.run();
        if (startHttp) {
            final Path watched = source.toPath();
            final WatchService watchService = watched.getFileSystem().newWatchService();
            watched.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
            final AtomicBoolean run = new AtomicBoolean(true);
            final AtomicLong render = new AtomicLong(-1);
            final Thread renderingThread = new Thread() {
                {
                    setName("jbake-renderer");
                }

                @Override
                public void run() {
                    long last = System.currentTimeMillis();
                    while (run.get()) {
                        if (render.get() > last) {
                            last = System.currentTimeMillis();
                            try {
                                build.run();
                            } catch (final Throwable oops) {
                                oops.printStackTrace();
                            }
                        }
                        try {
                            sleep(TimeUnit.SECONDS.toMillis(1));
                        } catch (final InterruptedException e) {
                            Thread.interrupted();
                            break;
                        }
                    }
                    System.out.println("Exiting renderer");
                }
            };
            final Thread watcherThread = new Thread() {
                {
                    setName("jbake-file-watcher");
                }

                @Override
                public void run() {
                    while (run.get()) {
                        try {
                            final WatchKey key = watchService.poll(1, TimeUnit.SECONDS);
                            if (key == null) {
                                continue;
                            }

                            for (final WatchEvent<?> event : key.pollEvents()) {
                                final WatchEvent.Kind<?> kind = event.kind();
                                if (kind != ENTRY_CREATE && kind != ENTRY_DELETE && kind != ENTRY_MODIFY) {
                                    continue; // unlikely but better to protect ourself
                                }

                                final Path updatedPath = Path.class.cast(event.context());
                                if (kind == ENTRY_DELETE || updatedPath.toFile().isFile()) {
                                    final String path = updatedPath.toString();
                                    if (!path.contains("___jb") && !path.endsWith("~")) {
                                        render.set(System.currentTimeMillis());
                                    }
                                }
                            }
                            key.reset();
                        } catch (final InterruptedException e) {
                            Thread.interrupted();
                            run.compareAndSet(true, false);
                        } catch (final ClosedWatchServiceException cwse) {
                            if (!run.get()) {
                                throw new IllegalStateException(cwse);
                            }
                        }
                    }
                    System.out.println("Exiting file watcher");
                }
            };

            renderingThread.start();
            watcherThread.start();

            final Runnable onQuit = () -> {
                run.compareAndSet(true, false);
                Stream.of(watcherThread, renderingThread).forEach(thread -> {
                    try {
                        thread.join();
                    } catch (final InterruptedException e) {
                        Thread.interrupted();
                    }
                });
                try {
                    watchService.close();
                } catch (final IOException ioe) {
                    // not important
                }
            };

            try (final Meecrowave container = new Meecrowave(new Meecrowave.Builder() {{
                setWebResourceCached(false);
            }}) {{
                start();
                deployWebapp("/meecrowave", destination);
            }}) {
                System.out.println("Started on http://localhost:" + container.getConfiguration().getHttpPort() + "/meecrowave");

                final Scanner console = new Scanner(System.in);
                String cmd;
                while (((cmd = console.nextLine())) != null) {
                    if ("quit".equals(cmd)) {
                        break;
                    } else if ("r".equals(cmd) || "rebuild".equals(cmd) || "build".equals(cmd) || "b".equals(cmd)) {
                        render.set(System.currentTimeMillis());
                    } else {
                        System.err.println("Ignoring " + cmd + ", please use 'build' or 'quit'");
                    }
                }
            }
            onQuit.run();
        }
    }
}
