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

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;
import jdk.nashorn.internal.codegen.types.Type;
import jdk.nashorn.internal.runtime.Context;
import jdk.nashorn.internal.runtime.Debug;
import jdk.nashorn.internal.runtime.options.Options;

/**
 * Symbol is a symbolic address for a value ("variable" if you wish). Identifiers in JavaScript source, as well as
 * certain synthetic variables created by the compiler are represented by Symbol objects. Symbols can address either
 * local variable slots in bytecode ("slotted symbol"), or properties in scope objects ("scoped symbol"). A symbol can
 * also end up being defined but then not used during symbol assignment calculations; such symbol will be neither
 * scoped, nor slotted; it represents a dead variable (it might be written to, but is never read). Finally, a symbol can
 * be both slotted and in scope. This special case can only occur with bytecode method parameters. They all come in as
 * slotted, but if they are used by a nested function (or eval) then they will be copied into the scope object, and used
 * from there onwards. Two further special cases are parameters stored in {@code NativeArguments} objects and parameters
 * stored in {@code Object[]} parameter to variable-arity functions. Those use the {@code #getFieldIndex()} property to
 * refer to their location.
 */

public final class Symbol implements Comparable<Symbol>, Cloneable, Serializable {
    private static final long serialVersionUID = 1L;

    /** Is this Global */
    public static final int IS_GLOBAL   = 1;
    /** Is this a variable */
    public static final int IS_VAR      = 2;
    /** Is this a parameter */
    public static final int IS_PARAM    = 3;
    /** Mask for kind flags */
    public static final int KINDMASK = (1 << 2) - 1; // Kinds are represented by lower two bits

    /** Is this symbol in scope */
    public static final int IS_SCOPE                = 1 <<  2;
    /** Is this a this symbol */
    public static final int IS_THIS                 = 1 <<  3;
    /** Is this a let */
    public static final int IS_LET                  = 1 <<  4;
    /** Is this a const */
    public static final int IS_CONST                = 1 <<  5;
    /** Is this an internal symbol, never represented explicitly in source code */
    public static final int IS_INTERNAL             = 1 <<  6;
    /** Is this a function self-reference symbol */
    public static final int IS_FUNCTION_SELF        = 1 <<  7;
    /** Is this a function declaration? */
    public static final int IS_FUNCTION_DECLARATION = 1 <<  8;
    /** Is this a program level symbol? */
    public static final int IS_PROGRAM_LEVEL        = 1 <<  9;
    /** Are this symbols' values stored in local variable slots? */
    public static final int HAS_SLOT                = 1 << 10;
    /** Is this symbol known to store an int value ? */
    public static final int HAS_INT_VALUE           = 1 << 11;
    /** Is this symbol known to store a double value ? */
    public static final int HAS_DOUBLE_VALUE        = 1 << 12;
    /** Is this symbol known to store an object value ? */
    public static final int HAS_OBJECT_VALUE        = 1 << 13;
    /** Is this symbol seen a declaration? Used for block scoped LET and CONST symbols only. */
    public static final int HAS_BEEN_DECLARED       = 1 << 14;

    /** Null or name identifying symbol. */
    private final String name;

    /** Symbol flags. */
    private int flags;

    /** First bytecode method local variable slot for storing the value(s) of this variable. -1 indicates the variable
     * is not stored in local variable slots or it is not yet known. */
    private transient int firstSlot = -1;

    /** Field number in scope or property; array index in varargs when not using arguments object. */
    private transient int fieldIndex = -1;

    /** Number of times this symbol is used in code */
    private int useCount;

    /** Debugging option - dump info and stack trace when symbols with given names are manipulated */
    private static final Set<String> TRACE_SYMBOLS;
    private static final Set<String> TRACE_SYMBOLS_STACKTRACE;

    static {
        final String stacktrace = Options.getStringProperty("nashorn.compiler.symbol.stacktrace", null);
        final String trace;
        if (stacktrace != null) {
            trace = stacktrace; //stacktrace always implies trace as well
            TRACE_SYMBOLS_STACKTRACE = new HashSet<>();
            for (final StringTokenizer st = new StringTokenizer(stacktrace, ","); st.hasMoreTokens(); ) {
                TRACE_SYMBOLS_STACKTRACE.add(st.nextToken());
            }
        } else {
            trace = Options.getStringProperty("nashorn.compiler.symbol.trace", null);
            TRACE_SYMBOLS_STACKTRACE = null;
        }

        if (trace != null) {
            TRACE_SYMBOLS = new HashSet<>();
            for (final StringTokenizer st = new StringTokenizer(trace, ","); st.hasMoreTokens(); ) {
                TRACE_SYMBOLS.add(st.nextToken());
            }
        } else {
            TRACE_SYMBOLS = null;
        }
    }

    /**
     * Constructor
     *
     * @param name  name of symbol
     * @param flags symbol flags
     */
    public Symbol(final String name, final int flags) {
        this.name       = name;
        this.flags      = flags;
        if(shouldTrace()) {
            trace("CREATE SYMBOL " + name);
        }
    }

    @Override
    public Symbol clone() {
        try {
            return (Symbol)super.clone();
        } catch (final CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }

    private static String align(final String string, final int max) {
        final StringBuilder sb = new StringBuilder();
        sb.append(string.substring(0, Math.min(string.length(), max)));

        while (sb.length() < max) {
            sb.append(' ');
        }
        return sb.toString();
    }

    /**
     * Debugging .
     *
     * @param stream Stream to print to.
     */

    void print(final PrintWriter stream) {
        final StringBuilder sb = new StringBuilder();

        sb.append(align(name, 20)).
            append(": ").
            append(", ").
            append(align(firstSlot == -1 ? "none" : "" + firstSlot, 10));

        switch (flags & KINDMASK) {
        case IS_GLOBAL:
            sb.append(" global");
            break;
        case IS_VAR:
            if (isConst()) {
                sb.append(" const");
            } else if (isLet()) {
                sb.append(" let");
            } else {
                sb.append(" var");
            }
            break;
        case IS_PARAM:
            sb.append(" param");
            break;
        default:
            break;
        }

        if (isScope()) {
            sb.append(" scope");
        }

        if (isInternal()) {
            sb.append(" internal");
        }

        if (isThis()) {
            sb.append(" this");
        }

        if (isProgramLevel()) {
            sb.append(" program");
        }

        sb.append('\n');

        stream.print(sb.toString());
    }

    /**
     * Compare the the symbol kind with another.
     *
     * @param other Other symbol's flags.
     * @return True if symbol has less kind.
     */
    public boolean less(final int other) {
        return (flags & KINDMASK) < (other & KINDMASK);
    }

    /**
     * Allocate a slot for this symbol.
     *
     * @param needsSlot True if symbol needs a slot.
     * @return the symbol
     */
    public Symbol setNeedsSlot(final boolean needsSlot) {
        if(needsSlot) {
            assert !isScope();
            flags |= HAS_SLOT;
        } else {
            flags &= ~HAS_SLOT;
        }
        return this;
    }

    /**
     * Return the number of slots required for the symbol.
     *
     * @return Number of slots.
     */
    public int slotCount() {
        return ((flags & HAS_INT_VALUE)    == 0 ? 0 : 1) +
               ((flags & HAS_DOUBLE_VALUE) == 0 ? 0 : 2) +
               ((flags & HAS_OBJECT_VALUE) == 0 ? 0 : 1);
    }

    private boolean isSlotted() {
        return firstSlot != -1 && ((flags & HAS_SLOT) != 0);
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();

        sb.append(name).
            append(' ');

        if (hasSlot()) {
            sb.append(' ').
                append('(').
                append("slot=").
                append(firstSlot).append(' ');
            if((flags & HAS_INT_VALUE) != 0) { sb.append('I'); }
            if((flags & HAS_DOUBLE_VALUE) != 0) { sb.append('D'); }
            if((flags & HAS_OBJECT_VALUE) != 0) { sb.append('O'); }
            sb.append(')');
        }

        if (isScope()) {
            if(isGlobal()) {
                sb.append(" G");
            } else {
                sb.append(" S");
            }
        }

        return sb.toString();
    }

    @Override
    public int compareTo(final Symbol other) {
        return name.compareTo(other.name);
    }

    /**
     * Does this symbol have an allocated bytecode slot? Note that having an allocated bytecode slot doesn't necessarily
     * mean the symbol's value will be stored in it. Namely, a function parameter can have a bytecode slot, but if it is
     * in scope, then the bytecode slot will not be used. See {@link #isBytecodeLocal()}.
     *
     * @return true if this symbol has a local bytecode slot
     */
    public boolean hasSlot() {
        return (flags & HAS_SLOT) != 0;
    }

    /**
     * Is this symbol a local variable stored in bytecode local variable slots? This is true for a slotted variable that
     * is not in scope. (E.g. a parameter that is in scope is slotted, but it will not be a local variable).
     * @return true if this symbol is using bytecode local slots for its storage.
     */
    public boolean isBytecodeLocal() {
        return hasSlot() && !isScope();
    }

    /**
     * Returns true if this symbol is dead (it is a local variable that is statically proven to never be read in any type).
     * @return true if this symbol is dead
     */
    public boolean isDead() {
        return (flags & (HAS_SLOT | IS_SCOPE)) == 0;
    }

    /**
     * Check if this is a symbol in scope. Scope symbols cannot, for obvious reasons
     * be stored in byte code slots on the local frame
     *
     * @return true if this is scoped
     */
    public boolean isScope() {
        assert (flags & KINDMASK) != IS_GLOBAL || (flags & IS_SCOPE) == IS_SCOPE : "global without scope flag";
        return (flags & IS_SCOPE) != 0;
    }

    /**
     * Check if this symbol is a function declaration
     * @return true if a function declaration
     */
    public boolean isFunctionDeclaration() {
        return (flags & IS_FUNCTION_DECLARATION) != 0;
    }

    /**
     * Flag this symbol as scope as described in {@link Symbol#isScope()}
     * @return the symbol
     */
    public Symbol setIsScope() {
        if (!isScope()) {
            if(shouldTrace()) {
                trace("SET IS SCOPE");
            }
            flags |= IS_SCOPE;
            if(!isParam()) {
                flags &= ~HAS_SLOT;
            }
        }
        return this;
    }

    /**
     * Mark this symbol as a function declaration.
     */
    public void setIsFunctionDeclaration() {
        if (!isFunctionDeclaration()) {
            if(shouldTrace()) {
                trace("SET IS FUNCTION DECLARATION");
            }
            flags |= IS_FUNCTION_DECLARATION;
        }
    }

    /**
     * Check if this symbol is a variable
     * @return true if variable
     */
    public boolean isVar() {
        return (flags & KINDMASK) == IS_VAR;
    }

    /**
     * Check if this symbol is a global (undeclared) variable
     * @return true if global
     */
    public boolean isGlobal() {
        return (flags & KINDMASK) == IS_GLOBAL;
    }

    /**
     * Check if this symbol is a function parameter
     * @return true if parameter
     */
    public boolean isParam() {
        return (flags & KINDMASK) == IS_PARAM;
    }

    /**
     * Check if this is a program (script) level definition
     * @return true if program level
     */
    public boolean isProgramLevel() {
        return (flags & IS_PROGRAM_LEVEL) != 0;
    }

    /**
     * Check if this symbol is a constant
     * @return true if a constant
     */
    public boolean isConst() {
        return (flags & IS_CONST) != 0;
    }

    /**
     * Check if this is an internal symbol, without an explicit JavaScript source
     * code equivalent
     * @return true if internal
     */
    public boolean isInternal() {
        return (flags & IS_INTERNAL) != 0;
    }

    /**
     * Check if this symbol represents {@code this}
     * @return true if this
     */
    public boolean isThis() {
        return (flags & IS_THIS) != 0;
    }

    /**
     * Check if this symbol is a let
     * @return true if let
     */
    public boolean isLet() {
        return (flags & IS_LET) != 0;
    }

    /**
     * Flag this symbol as a function's self-referencing symbol.
     * @return true if this symbol as a function's self-referencing symbol.
     */
    public boolean isFunctionSelf() {
        return (flags & IS_FUNCTION_SELF) != 0;
    }

    /**
     * Is this a block scoped symbol
     * @return true if block scoped
     */
    public boolean isBlockScoped() {
        return isLet() || isConst();
    }

    /**
     * Has this symbol been declared
     * @return true if declared
     */
    public boolean hasBeenDeclared() {
        return (flags & HAS_BEEN_DECLARED) != 0;
    }

    /**
     * Mark this symbol as declared
     */
    public void setHasBeenDeclared() {
        if (!hasBeenDeclared()) {
            flags |= HAS_BEEN_DECLARED;
        }
    }

    /**
     * Get the index of the field used to store this symbol, should it be an AccessorProperty
     * and get allocated in a JO-prefixed ScriptObject subclass.
     *
     * @return field index
     */
    public int getFieldIndex() {
        assert fieldIndex != -1 : "fieldIndex must be initialized " + fieldIndex;
        return fieldIndex;
    }

    /**
     * Set the index of the field used to store this symbol, should it be an AccessorProperty
     * and get allocated in a JO-prefixed ScriptObject subclass.
     *
     * @param fieldIndex field index - a positive integer
     * @return the symbol
     */
    public Symbol setFieldIndex(final int fieldIndex) {
        if (this.fieldIndex != fieldIndex) {
            this.fieldIndex = fieldIndex;
        }
        return this;
    }

    /**
     * Get the symbol flags
     * @return flags
     */
    public int getFlags() {
        return flags;
    }

    /**
     * Set the symbol flags
     * @param flags flags
     * @return the symbol
     */
    public Symbol setFlags(final int flags) {
        if (this.flags != flags) {
            this.flags = flags;
        }
        return this;
    }

    /**
     * Set a single symbol flag
     * @param flag flag to set
     * @return the symbol
     */
    public Symbol setFlag(final int flag) {
        if ((this.flags & flag) == 0) {
            this.flags |= flag;
        }
        return this;
    }

    /**
     * Clears a single symbol flag
     * @param flag flag to set
     * @return the symbol
     */
    public Symbol clearFlag(final int flag) {
        if ((this.flags & flag) != 0) {
            this.flags &= ~flag;
        }
        return this;
    }

    /**
     * Get the name of this symbol
     * @return symbol name
     */
    public String getName() {
        return name;
    }

    /**
     * Get the index of the first bytecode slot for this symbol
     * @return byte code slot
     */
    public int getFirstSlot() {
        assert isSlotted();
        return firstSlot;
    }

    /**
     * Get the index of the bytecode slot for this symbol for storing a value of the specified type.
     * @param type the requested type
     * @return byte code slot
     */
    public int getSlot(final Type type) {
        assert isSlotted();
        int typeSlot = firstSlot;
        if(type.isBoolean() || type.isInteger()) {
            assert (flags & HAS_INT_VALUE) != 0;
            return typeSlot;
        }
        typeSlot += ((flags & HAS_INT_VALUE) == 0 ? 0 : 1);
        if(type.isNumber()) {
            assert (flags & HAS_DOUBLE_VALUE) != 0;
            return typeSlot;
        }
        assert type.isObject();
        assert (flags & HAS_OBJECT_VALUE) != 0 : name;
        return typeSlot + ((flags & HAS_DOUBLE_VALUE) == 0 ? 0 : 2);
    }

    /**
     * Returns true if this symbol has a local variable slot for storing a value of specific type.
     * @param type the type
     * @return true if this symbol has a local variable slot for storing a value of specific type.
     */
    public boolean hasSlotFor(final Type type) {
        if(type.isBoolean() || type.isInteger()) {
            return (flags & HAS_INT_VALUE) != 0;
        } else if(type.isNumber()) {
            return (flags & HAS_DOUBLE_VALUE) != 0;
        }
        assert type.isObject();
        return (flags & HAS_OBJECT_VALUE) != 0;
    }

    /**
     * Marks this symbol as having a local variable slot for storing a value of specific type.
     * @param type the type
     */
    public void setHasSlotFor(final Type type) {
        if(type.isBoolean() || type.isInteger()) {
            setFlag(HAS_INT_VALUE);
        } else if(type.isNumber()) {
            setFlag(HAS_DOUBLE_VALUE);
        } else {
            assert type.isObject();
            setFlag(HAS_OBJECT_VALUE);
        }
    }

    /**
     * Increase the symbol's use count by one.
     */
    public void increaseUseCount() {
        if (isScope()) { // Avoid dirtying a cache line; we only need the use count for scoped symbols
            useCount++;
        }
    }

    /**
     * Get the symbol's use count
     * @return the number of times the symbol is used in code.
     */
    public int getUseCount() {
        return useCount;
    }

    /**
     * Set the bytecode slot for this symbol
     * @param  firstSlot valid bytecode slot
     * @return the symbol
     */
    public Symbol setFirstSlot(final int firstSlot) {
        assert firstSlot >= 0 && firstSlot <= 65535;
        if (firstSlot != this.firstSlot) {
            if(shouldTrace()) {
                trace("SET SLOT " + firstSlot);
            }
            this.firstSlot = firstSlot;
        }
        return this;
    }

    /**
     * From a lexical context, set this symbol as needing scope, which
     * will set flags for the defining block that will be written when
     * block is popped from the lexical context stack, used by codegen
     * when flags need to be tagged, but block is in the
     * middle of evaluation and cannot be modified.
     *
     * @param  lc     lexical context
     * @param  symbol symbol
     * @return the symbol
     */
    public static Symbol setSymbolIsScope(final LexicalContext lc, final Symbol symbol) {
        symbol.setIsScope();
        if (!symbol.isGlobal()) {
            lc.setBlockNeedsScope(lc.getDefiningBlock(symbol));
        }
        return symbol;
    }

    private boolean shouldTrace() {
        return TRACE_SYMBOLS != null && (TRACE_SYMBOLS.isEmpty() || TRACE_SYMBOLS.contains(name));
    }

    private void trace(final String desc) {
        Context.err(Debug.id(this) + " SYMBOL: '" + name + "' " + desc);
        if (TRACE_SYMBOLS_STACKTRACE != null && (TRACE_SYMBOLS_STACKTRACE.isEmpty() || TRACE_SYMBOLS_STACKTRACE.contains(name))) {
            new Throwable().printStackTrace(Context.getCurrentErr());
        }
    }

    private void readObject(final ObjectInputStream in) throws ClassNotFoundException, IOException {
        in.defaultReadObject();
        firstSlot = -1;
        fieldIndex = -1;
    }
}
