/*
 * Copyright 2016 Adrian Sutton
 *
 * Licensed 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 net.symphonious.commonjs;

import net.symphonious.commonjs.sourcemap.SourceMapBuilder;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * A CommonJS compiler - given a set of module IDs, it will combine them and all their dependencies into a single JavaScript file that can be shipped to the browser.
 * <p>An implementation of {@link ModuleLoader} must be provided that when given an module ID, returns the source code for that module or <code>null</code> if the module does not exist.</p>
 */
public class CommonJsCompiler
{
    private final ModuleLoader moduleLoader;

    /**
     * Create a new CommonJsCompiler with the supplied {@link ModuleLoader}.
     *
     * @param moduleLoader the implementation of {@link ModuleLoader} to use to load source code for the modules.
     */
    public CommonJsCompiler(final ModuleLoader moduleLoader)
    {
        this.moduleLoader = moduleLoader;
    }

    /**
     * Load a set of modules, combined with their dependencies (transitively) and combine them all into a single JavaScript file.
     *
     * @param moduleIds the module IDs for the modules to load.
     * @return a String of JavaScript that combines the requested modules plus their transitive dependencies.
     * @throws IOException if an error occurs while loading a module.
     */
    public String compile(final String... moduleIds) throws IOException
    {
        final Writer out = new StringWriter();
        compile(out, moduleIds);
        return out.toString();
    }

    /**
     * Load a set of modules, combined with their dependencies (transitively) and combine them all into a single JavaScript file.
     *
     * @param out       the writer to use to output the compiled JavaScript.
     * @param moduleIds the module IDs for the modules to load.
     * @throws IOException if an error occurs while loading a module.
     */
    public void compile(final Writer out, final String... moduleIds) throws IOException
    {
        compile(out, Optional.empty(), moduleIds);
    }

    public void compile(final Writer sourceOut, final Writer sourceMapOut, final String... moduleIds) throws IOException
    {
        compile(sourceOut, Optional.of(sourceMapOut), moduleIds);
    }

    private void compile(final Writer out, final Optional<Writer> sourceMapWriter, final String... moduleIds) throws IOException
    {
        final Optional<SourceMapBuilder> sourceMapBuilder = sourceMapWriter.map(writer -> new SourceMapBuilder());
        final ModuleSet modules = new ModuleSet(moduleLoader);
        for (final String moduleId : moduleIds)
        {
            modules.addModule(moduleId);
        }

        try (BufferedReader in = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("machinery.js"), StandardCharsets.UTF_8)))
        {
            for (int c = in.read(); c >= 0; c = in.read())
            {
                if (c == '\n')
                {
                    sourceMapBuilder.ifPresent(SourceMapBuilder::skipLine);
                }
                out.write(c);
            }
        }

        out.write("(");
        out.write(Stream.of(modules.getModules())
                   .map(moduleInfo -> {
                       sourceMapBuilder.ifPresent(SourceMapBuilder::skipLine);
                       sourceMapBuilder.ifPresent(builder -> builder.appendModule(moduleInfo.getModuleId(), moduleInfo.getSource()));
                       return "'" + moduleInfo.getModuleId() + "': function(module, exports, require) {\n" +
                              moduleInfo.getSource() +
                              "\n}";
                   })
                   .collect(Collectors.joining(",", "{", "}")));
        out.write(", ");
        out.write(Stream.of(moduleIds)
                   .map(id -> "'" + id + "'")
                   .collect(Collectors.joining(",", "[", "]")));
        out.write(");\n");
        if (sourceMapBuilder.isPresent())
        {
            sourceMapWriter.get().append(sourceMapBuilder.get().generateSourceMap());
        }
        out.write("//# sourceMappingURL=test.js.map");
    }
}

@@ -2716,38 +2716,42 @@
 d compile(final 
-Writer
+Appendable
  out, final Stri
@@ -2858,64 +2858,583 @@
     
-public void compile(final Writer sourceOut, final Writer
+/**%0A     * Load a set of modules, combined with their dependencies (transitively) and combine them all into a single JavaScript file,%0A     * creating a source map that includes the original sources.%0A     *%0A     * @param sourceOut appendable to write the combined JavaScript to.%0A     * @param sourceMapOut appendable to write the sourcemap to.%0A     * @param moduleIds the module IDs for the modules to load.%0A     * @throws IOException if an error occurs while loading a module or writing the output.%0A     */%0A    public void compile(final Appendable sourceOut, final Appendable
  sou
@@ -3596,30 +3596,34 @@
 mpile(final 
-Writer
+Appendable
  out, final 
@@ -3631,22 +3631,26 @@
 ptional%3C
-Writer
+Appendable
 %3E source
@@ -4394,22 +4394,29 @@
     out.
-write(
+append((char)
 c);%0A    
@@ -4448,21 +4448,22 @@
     out.
-write
+append
 (%22(%22);%0A 
@@ -4465,37 +4465,38 @@
 %22);%0A        out.
-write
+append
 (Stream.of(modul
@@ -5065,21 +5065,22 @@
     out.
-write
+append
 (%22, %22);%0A
@@ -5091,21 +5091,22 @@
     out.
-write
+append
 (Stream.
@@ -5242,21 +5242,22 @@
     out.
-write
+append
 (%22);%5Cn%22)
@@ -5418,21 +5418,22 @@
     out.
-write
+append
 (%22//# so

Add documentation for using the sourcemap generating version of compilation.