/*
 * Copyright (c) 2010, 2013, 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.internal.runtime;

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.logging.Level;
import jdk.nashorn.internal.codegen.Namespace;
import jdk.nashorn.internal.runtime.linker.NashornCallSiteDescriptor;
import jdk.nashorn.internal.runtime.options.KeyValueOption;
import jdk.nashorn.internal.runtime.options.LoggingOption;
import jdk.nashorn.internal.runtime.options.LoggingOption.LoggerInfo;
import jdk.nashorn.internal.runtime.options.Option;
import jdk.nashorn.internal.runtime.options.Options;

/**
 * Script environment consists of command line options, arguments, script files
 * and output and error writers, top level Namespace etc.
 */
public final class ScriptEnvironment {
    // Primarily intended to be used in test environments so that eager compilation tests work without an
    // error when tested with optimistic compilation.
    private static final boolean ALLOW_EAGER_COMPILATION_SILENT_OVERRIDE = Options.getBooleanProperty(
            "nashorn.options.allowEagerCompilationSilentOverride", false);

    /** Output writer for this environment */
    private final PrintWriter out;

    /** Error writer for this environment */
    private final PrintWriter err;

    /** Top level namespace. */
    private final Namespace namespace;

    /** Current Options object. */
    private final Options options;

    /** Size of the per-global Class cache size */
    public final int     _class_cache_size;

    /** -classpath value. */
    public final String  _classpath;

    /** Only compile script, do not run it or generate other ScriptObjects */
    public final boolean _compile_only;

    /** Accept "const" keyword and treat it as variable. Interim feature */
    public final boolean _const_as_var;

    /** Accumulated callsite flags that will be used when bootstrapping script callsites */
    public final int     _callsite_flags;

    /** Generate line number table in class files */
    public final boolean _debug_lines;

    /** Put all variables in scopes to make them debuggable */
    public final boolean _debug_scopes;

    /** Directory in which source files and generated class files are dumped */
    public final String  _dest_dir;

    /** Display stack trace upon error, default is false */
    public final boolean _dump_on_error;

    /** Invalid lvalue expressions should be reported as early errors */
    public final boolean _early_lvalue_error;

    /** Empty statements should be preserved in the AST */
    public final boolean _empty_statements;

    /** Show full Nashorn version */
    public final boolean _fullversion;

    /** Launch using as fx application */
    public final boolean _fx;

    /** Use single Global instance per jsr223 engine instance. */
    public final boolean _global_per_engine;

    /** Enable experimental ECMAScript 6 features. */
    public final boolean _es6;

    /** do not show deprecation warning for nashorn engine and jjs usage. */
    public final boolean _no_deprecation_warning;

    /** Number of times a dynamic call site has to be relinked before it is
     * considered unstable (and thus should be linked as if it were megamorphic).
     */
    public final int _unstable_relink_threshold;

    /** Argument passed to compile only if optimistic compilation should take place */
    public static final String COMPILE_ONLY_OPTIMISTIC_ARG = "optimistic";

    /**
     *  Behavior when encountering a function declaration in a lexical context where only statements are acceptable
     * (function declarations are source elements, but not statements).
     */
    public enum FunctionStatementBehavior {
        /**
         * Accept the function declaration silently and treat it as if it were a function expression assigned to a local
         * variable.
         */
        ACCEPT,
        /**
         * Log a parser warning, but accept the function declaration and treat it as if it were a function expression
         * assigned to a local variable.
         */
        WARNING,
        /**
         * Raise a {@code SyntaxError}.
         */
        ERROR
    }

    /**
     * Behavior when encountering a function declaration in a lexical context where only statements are acceptable
     * (function declarations are source elements, but not statements).
     */
    public final FunctionStatementBehavior _function_statement;

    /** Should lazy compilation take place */
    public final boolean _lazy_compilation;

    /** Should optimistic types be used */
    public final boolean _optimistic_types;

    /** Create a new class loaded for each compilation */
    public final boolean _loader_per_compile;

    /** --module-path, if any */
    public final String _module_path;

    /** --add-modules, if any */
    public final String _add_modules;

    /** Do not support Java support extensions. */
    public final boolean _no_java;

    /** Do not support non-standard syntax extensions. */
    public final boolean _no_syntax_extensions;

    /** Do not support typed arrays. */
    public final boolean _no_typed_arrays;

    /** Only parse the source code, do not compile */
    public final boolean _parse_only;

    /** Enable disk cache for compiled scripts */
    public final boolean _persistent_cache;

    /** Print the AST before lowering */
    public final boolean _print_ast;

    /** Print the AST after lowering */
    public final boolean _print_lower_ast;

    /** Print resulting bytecode for script */
    public final boolean _print_code;

    /** Directory (optional) to print files to */
    public final String _print_code_dir;

    /** List of functions to write to the print code dir, optional */
    public final String _print_code_func;

    /** Print function will no print newline characters */
    public final boolean _print_no_newline;

    /** Print AST in more human readable form */
    public final boolean _print_parse;

    /** Print AST in more human readable form after Lowering */
    public final boolean _print_lower_parse;

    /** print symbols and their contents for the script */
    public final boolean _print_symbols;

    /** is this environment in scripting mode? */
    public final boolean _scripting;

    /** is this environment in strict mode? */
    public final boolean _strict;

    /** print version info of Nashorn */
    public final boolean _version;

    /** should code verification be done of generated bytecode */
    public final boolean _verify_code;

    /** time zone for this environment */
    public final TimeZone _timezone;

    /** Local for error messages */
    public final Locale _locale;

    /** Logging */
    public final Map<String, LoggerInfo> _loggers;

    /** Timing */
    public final Timing _timing;

    /** Whether to use anonymous classes. See {@link #useAnonymousClasses(int)}. */
    private final AnonymousClasses _anonymousClasses;
    private enum AnonymousClasses {
        AUTO,
        OFF,
        ON
    }

    /** Size threshold up to which we use anonymous classes in {@link AnonymousClasses#AUTO} setting */
    private final int _anonymous_classes_threshold;

    /** Default value for anonymous class threshold */
    private final static int DEFAULT_ANON_CLASS_THRESHOLD = 512;

    /**
     * Constructor
     *
     * @param options a Options object
     * @param out output print writer
     * @param err error print writer
     */
    @SuppressWarnings("unused")
    public ScriptEnvironment(final Options options, final PrintWriter out, final PrintWriter err) {
        this.out = out;
        this.err = err;
        this.namespace = new Namespace();
        this.options = options;

        _class_cache_size     = options.getInteger("class.cache.size");
        _classpath            = options.getString("classpath");
        _compile_only         = options.getBoolean("compile.only");
        _const_as_var         = options.getBoolean("const.as.var");
        _debug_lines          = options.getBoolean("debug.lines");
        _debug_scopes         = options.getBoolean("debug.scopes");
        _dest_dir             = options.getString("d");
        _dump_on_error        = options.getBoolean("doe");
        _early_lvalue_error   = options.getBoolean("early.lvalue.error");
        _empty_statements     = options.getBoolean("empty.statements");
        _fullversion          = options.getBoolean("fullversion");
        if (options.getBoolean("function.statement.error")) {
            _function_statement = FunctionStatementBehavior.ERROR;
        } else if (options.getBoolean("function.statement.warning")) {
            _function_statement = FunctionStatementBehavior.WARNING;
        } else {
            _function_statement = FunctionStatementBehavior.ACCEPT;
        }
        _fx                   = options.getBoolean("fx");
        _global_per_engine    = options.getBoolean("global.per.engine");
        _optimistic_types     = options.getBoolean("optimistic.types");
        final boolean lazy_compilation = options.getBoolean("lazy.compilation");
        if (!lazy_compilation && _optimistic_types) {
            if (!ALLOW_EAGER_COMPILATION_SILENT_OVERRIDE) {
                throw new IllegalStateException(
                        ECMAErrors.getMessage(
                                "config.error.eagerCompilationConflictsWithOptimisticTypes",
                                options.getOptionTemplateByKey("lazy.compilation").getName(),
                                options.getOptionTemplateByKey("optimistic.types").getName()));
            }
            _lazy_compilation = true;
        } else {
            _lazy_compilation = lazy_compilation;
        }
        _loader_per_compile   = options.getBoolean("loader.per.compile");
        _module_path          = options.getString("module.path");
        _add_modules          = options.getString("add.modules");
        _no_java              = options.getBoolean("no.java");
        _no_syntax_extensions = options.getBoolean("no.syntax.extensions");
        _no_typed_arrays      = options.getBoolean("no.typed.arrays");
        _parse_only           = options.getBoolean("parse.only");
        _persistent_cache     = options.getBoolean("persistent.code.cache");
        _print_ast            = options.getBoolean("print.ast");
        _print_lower_ast      = options.getBoolean("print.lower.ast");
        _print_code           = options.getString("print.code") != null;
        _print_no_newline     = options.getBoolean("print.no.newline");
        _print_parse          = options.getBoolean("print.parse");
        _print_lower_parse    = options.getBoolean("print.lower.parse");
        _print_symbols        = options.getBoolean("print.symbols");
        _scripting            = options.getBoolean("scripting");
        _strict               = options.getBoolean("strict");
        _version              = options.getBoolean("version");
        _verify_code          = options.getBoolean("verify.code");
        _no_deprecation_warning = options.getBoolean("no.deprecation.warning");

        final int configuredUrt = options.getInteger("unstable.relink.threshold");
        // The default for this property is -1, so we can easily detect when
        // it is not specified on command line.
        if (configuredUrt < 0) {
            // In this case, use a default of 8, or 16 for optimistic types.
            // Optimistic types come with dual fields, and in order to get
            // performance on benchmarks with a lot of object instantiation and
            // then field reassignment, it can take slightly more relinks to
            // become stable with type changes swapping out an entire property
            // map and making a map guard fail. Also, honor the "nashorn.*"
            // system property for now. It was documented in DEVELOPER_README
            // so we should recognize it for the time being.
            _unstable_relink_threshold = Options.getIntProperty(
                    "nashorn.unstable.relink.threshold",
                    _optimistic_types ? 16 : 8);
        } else {
            _unstable_relink_threshold = configuredUrt;
        }

        final String anonClasses = options.getString("anonymous.classes");
        if (anonClasses == null || anonClasses.equals("auto")) {
            _anonymousClasses = AnonymousClasses.AUTO;
        } else if (anonClasses.equals("true")) {
            _anonymousClasses = AnonymousClasses.ON;
        } else if (anonClasses.equals("false")) {
            _anonymousClasses = AnonymousClasses.OFF;
        } else {
            throw new RuntimeException("Unsupported value for anonymous classes: " + anonClasses);
        }

        this._anonymous_classes_threshold = Options.getIntProperty(
                "nashorn.anonymous.classes.threshold", DEFAULT_ANON_CLASS_THRESHOLD);

        final String language = options.getString("language");
        if (language == null || language.equals("es5")) {
            _es6 = false;
        } else if (language.equals("es6")) {
            _es6 = true;
        } else {
            throw new RuntimeException("Unsupported language: " + language);
        }

        String dir = null;
        String func = null;
        final String pc = options.getString("print.code");
        if (pc != null) {
            final StringTokenizer st = new StringTokenizer(pc, ",");
            while (st.hasMoreTokens()) {
                final StringTokenizer st2 = new StringTokenizer(st.nextToken(), ":");
                while (st2.hasMoreTokens()) {
                    final String cmd = st2.nextToken();
                    if ("dir".equals(cmd)) {
                        dir = st2.nextToken();
                    } else if ("function".equals(cmd)) {
                        func = st2.nextToken();
                    }
                }
            }
        }
        _print_code_dir = dir;
        _print_code_func = func;

        int callSiteFlags = 0;
        if (options.getBoolean("profile.callsites")) {
            callSiteFlags |= NashornCallSiteDescriptor.CALLSITE_PROFILE;
        }

        if (options.get("trace.callsites") instanceof KeyValueOption) {
            callSiteFlags |= NashornCallSiteDescriptor.CALLSITE_TRACE;
            final KeyValueOption kv = (KeyValueOption)options.get("trace.callsites");
            if (kv.hasValue("miss")) {
                callSiteFlags |= NashornCallSiteDescriptor.CALLSITE_TRACE_MISSES;
            }
            if (kv.hasValue("enterexit") || (callSiteFlags & NashornCallSiteDescriptor.CALLSITE_TRACE_MISSES) == 0) {
                callSiteFlags |= NashornCallSiteDescriptor.CALLSITE_TRACE_ENTEREXIT;
            }
            if (kv.hasValue("objects")) {
                callSiteFlags |= NashornCallSiteDescriptor.CALLSITE_TRACE_VALUES;
            }
        }
        this._callsite_flags = callSiteFlags;

        final Option<?> timezoneOption = options.get("timezone");
        if (timezoneOption != null) {
            this._timezone = (TimeZone)timezoneOption.getValue();
        } else {
            this._timezone  = TimeZone.getDefault();
        }

        final Option<?> localeOption = options.get("locale");
        if (localeOption != null) {
            this._locale = (Locale)localeOption.getValue();
        } else {
            this._locale = Locale.getDefault();
        }

        final LoggingOption loggingOption = (LoggingOption)options.get("log");
        this._loggers = loggingOption == null ? new HashMap<String, LoggerInfo>() : loggingOption.getLoggers();

        final LoggerInfo timeLoggerInfo = _loggers.get(Timing.getLoggerName());
        this._timing = new Timing(timeLoggerInfo != null && timeLoggerInfo.getLevel() != Level.OFF);
    }

    /**
     * Get the output stream for this environment
     * @return output print writer
     */
    public PrintWriter getOut() {
        return out;
    }

    /**
     * Get the error stream for this environment
     * @return error print writer
     */
    public PrintWriter getErr() {
        return err;
    }

    /**
     * Get the namespace for this environment
     * @return namespace
     */
    public Namespace getNamespace() {
        return namespace;
    }

    /**
     * Return the JavaScript files passed to the program
     *
     * @return a list of files
     */
    public List<String> getFiles() {
        return options.getFiles();
    }

    /**
     * Return the user arguments to the program, i.e. those trailing "--" after
     * the filename
     *
     * @return a list of user arguments
     */
    public List<String> getArguments() {
        return options.getArguments();
    }

    /**
     * Check if there is a logger registered for a particular name: typically
     * the "name" attribute of a Loggable annotation on a class
     *
     * @param name logger name
     * @return true, if a logger exists for that name, false otherwise
     */
    public boolean hasLogger(final String name) {
        return _loggers.get(name) != null;
    }

    /**
     * Check if compilation/runtime timings are enabled
     * @return true if enabled
     */
    public boolean isTimingEnabled() {
        return _timing != null ? _timing.isEnabled() : false;
    }

    /**
     * Returns true if compilation should use anonymous classes.
     * @param sourceLength length of source being compiled.
     * @return true if anonymous classes should be used
     */
    public boolean useAnonymousClasses(final int sourceLength) {
        return _anonymousClasses == AnonymousClasses.ON
                || (_anonymousClasses == AnonymousClasses.AUTO && sourceLength <= _anonymous_classes_threshold);
    }

}
