/*
 * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.nashorn.api.tree;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.net.URL;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import jdk.nashorn.api.scripting.NashornException;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import jdk.nashorn.internal.ir.FunctionNode;
import jdk.nashorn.internal.runtime.Context;
import jdk.nashorn.internal.runtime.ErrorManager;
import jdk.nashorn.internal.runtime.JSType;
import jdk.nashorn.internal.runtime.ParserException;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.Source;
import jdk.nashorn.internal.runtime.options.Options;

final class ParserImpl implements Parser {

    private final ScriptEnvironment env;
    private final boolean moduleMode;

    ParserImpl(final String... args) throws IllegalArgumentException {
        Objects.requireNonNull(args);

        // handle the parser specific "--es6-module" option
        boolean seenModuleOption = false;
        for (int idx = 0; idx < args.length; idx++) {
            final String opt = args[idx];
            if (opt.equals("--es6-module")) {
                seenModuleOption = true;
                /*
                 * Nashorn parser does not understand parser API specific
                 * option. This option implies --language=es6. So, we change
                 * the option to --language=es6. Note that if user specified
                 * --language=es6 explicitly, that is okay. Nashorn tolerates
                 * repeated options!
                 */
                args[idx] = "--language=es6";
                break;
            }
        }
        this.moduleMode = seenModuleOption;

        // append "--parse-only to signal to the Nashorn that it
        // is being used in "parse only" mode.
        final String[] newArgs = Arrays.copyOf(args, args.length + 1, String[].class);
        newArgs[args.length] = "--parse-only";
        final Options options = new Options("nashorn");
        options.process(newArgs);
        this.env = new ScriptEnvironment(options,
                new PrintWriter(System.out), new PrintWriter(System.err));
    }

    @Override
    public CompilationUnitTree parse(final File file, final DiagnosticListener listener) throws IOException, NashornException {
        if (moduleMode) {
            return parseModule(file, listener);
        }
        final Source src = Source.sourceFor(Objects.requireNonNull(file).getName(), file);
        return translate(makeParser(src, listener).parse());
    }

    @Override
    public CompilationUnitTree parse(final Path path, final DiagnosticListener listener) throws IOException, NashornException {
        if (moduleMode) {
            return parseModule(path, listener);
        }
        final Source src = Source.sourceFor(Objects.requireNonNull(path).toString(), path);
        return translate(makeParser(src, listener).parse());
    }

    @Override
    public CompilationUnitTree parse(final URL url, final DiagnosticListener listener) throws IOException, NashornException {
        if (moduleMode) {
            return parseModule(url, listener);
        }
        final Source src = Source.sourceFor(url.toString(), url);
        return translate(makeParser(src, listener).parse());
    }

    @Override
    public CompilationUnitTree parse(final String name, final Reader reader, final DiagnosticListener listener) throws IOException, NashornException {
        if (moduleMode) {
            return parseModule(name, reader, listener);
        }
        final Source src = Source.sourceFor(Objects.requireNonNull(name), Objects.requireNonNull(reader));
        return translate(makeParser(src, listener).parse());
    }

    @Override
    public CompilationUnitTree parse(final String name, final String code, final DiagnosticListener listener) throws NashornException {
        if (moduleMode) {
            return parseModule(name, code, listener);
        }
        final Source src = Source.sourceFor(name, code);
        return translate(makeParser(src, listener).parse());
    }

    @Override
    public CompilationUnitTree parse(final ScriptObjectMirror scriptObj, final DiagnosticListener listener) throws NashornException {
        if (moduleMode) {
            return parseModule(scriptObj, listener);
        }
        final Map<?, ?> map = Objects.requireNonNull(scriptObj);
        if (map.containsKey("script") && map.containsKey("name")) {
            final String script = JSType.toString(map.get("script"));
            final String name = JSType.toString(map.get("name"));
            final Source src = Source.sourceFor(name, script);
            return translate(makeParser(src, listener).parse());
        } else {
            throw new IllegalArgumentException("can't find 'script' and 'name' properties");
        }
    }

    private CompilationUnitTree parseModule(final File file, final DiagnosticListener listener) throws IOException, NashornException {
        final Source src = Source.sourceFor(Objects.requireNonNull(file).getName(), file);
        return makeModule(src, listener);
    }

    private CompilationUnitTree parseModule(final Path path, final DiagnosticListener listener) throws IOException, NashornException {
        final Source src = Source.sourceFor(Objects.requireNonNull(path).toString(), path);
        return makeModule(src, listener);
    }

    private CompilationUnitTree parseModule(final URL url, final DiagnosticListener listener) throws IOException, NashornException {
        final Source src = Source.sourceFor(url.toString(), url);
        return makeModule(src, listener);
    }

    private CompilationUnitTree parseModule(final String name, final Reader reader, final DiagnosticListener listener) throws IOException, NashornException {
        final Source src = Source.sourceFor(Objects.requireNonNull(name), Objects.requireNonNull(reader));
        return makeModule(src, listener);
    }

    private CompilationUnitTree parseModule(final String name, final String code, final DiagnosticListener listener) throws NashornException {
        final Source src = Source.sourceFor(name, code);
        return makeModule(src, listener);
    }

    private CompilationUnitTree parseModule(final ScriptObjectMirror scriptObj, final DiagnosticListener listener) throws NashornException {
        final Map<?, ?> map = Objects.requireNonNull(scriptObj);
        if (map.containsKey("script") && map.containsKey("name")) {
            final String script = JSType.toString(map.get("script"));
            final String name = JSType.toString(map.get("name"));
            final Source src = Source.sourceFor(name, script);
            return makeModule(src, listener);
        } else {
            throw new IllegalArgumentException("can't find 'script' and 'name' properties");
        }
    }

    private CompilationUnitTree makeModule(final Source src, final DiagnosticListener listener) {
        final FunctionNode modFunc = makeParser(src, listener).parseModule(src.getName());
        return new IRTranslator().translate(modFunc);
    }

    private jdk.nashorn.internal.parser.Parser makeParser(final Source source, final DiagnosticListener listener) {
        final ErrorManager errMgr = listener != null ? new ListenerErrorManager(listener) : new Context.ThrowErrorManager();
        return new jdk.nashorn.internal.parser.Parser(env, source, errMgr);
    }

    private static class ListenerErrorManager extends ErrorManager {

        private final DiagnosticListener listener;

        ListenerErrorManager(final DiagnosticListener listener) {
            // null check
            listener.getClass();
            this.listener = listener;
        }

        @Override
        public void error(final String msg) {
            error(new ParserException(msg));
        }

        @Override
        public void error(final ParserException e) {
            listener.report(new DiagnosticImpl(e, Diagnostic.Kind.ERROR));
        }

        @Override
        public void warning(final String msg) {
            warning(new ParserException(msg));
        }

        @Override
        public void warning(final ParserException e) {
            listener.report(new DiagnosticImpl(e, Diagnostic.Kind.WARNING));
        }
    }

    private static CompilationUnitTree translate(final FunctionNode node) {
        return new IRTranslator().translate(node);
    }
}