/*
 * 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.PrintWriter;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;
import jdk.nashorn.internal.codegen.types.Range;
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;

import static jdk.nashorn.internal.codegen.CompilerConstants.RETURN;

/**
 * Maps a name to specific data.
 */

public final class Symbol implements Comparable<Symbol> {
    /** Symbol kinds. Kind ordered by precedence. */
    public static final int IS_TEMP     = 1;
    /** Is this Global */
    public static final int IS_GLOBAL   = 2;
    /** Is this a variable */
    public static final int IS_VAR      = 3;
    /** Is this a parameter */
    public static final int IS_PARAM    = 4;
    /** Is this a constant */
    public static final int IS_CONSTANT = 5;
    /** Mask for kind flags */
    public static final int KINDMASK = (1 << 3) - 1; // Kinds are represented by lower three bits

    /** Is this scope */
    public static final int IS_SCOPE             = 1 <<  4;
    /** Is this a this symbol */
    public static final int IS_THIS              = 1 <<  5;
    /** Can this symbol ever be undefined */
    public static final int CAN_BE_UNDEFINED     = 1 <<  6;
    /** Is this symbol always defined? */
    public static final int IS_ALWAYS_DEFINED    = 1 <<  8;
    /** Can this symbol ever have primitive types */
    public static final int CAN_BE_PRIMITIVE     = 1 <<  9;
    /** Is this a let */
    public static final int IS_LET               = 1 << 10;
    /** Is this an internal symbol, never represented explicitly in source code */
    public static final int IS_INTERNAL          = 1 << 11;
    /** Is this a function self-reference symbol */
    public static final int IS_FUNCTION_SELF     = 1 << 12;
    /** Is this a specialized param? */
    public static final int IS_SPECIALIZED_PARAM = 1 << 13;
    /** Is this symbol a shared temporary? */
    public static final int IS_SHARED            = 1 << 14;
    /** Is this a function declaration? */
    public static final int IS_FUNCTION_DECLARATION = 1 << 15;

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

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

    /** Type of symbol. */
    private Type type;

    /** Local variable slot. -1 indicates external property. */
    private int slot;

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

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

    /** Range for symbol */
    private Range range;

    /** 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 (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 (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
     * @param type  type of this symbol
     * @param slot  bytecode slot for this symbol
     */
    protected Symbol(final String name, final int flags, final Type type, final int slot) {
        this.name       = name;
        this.flags      = flags;
        this.type       = type;
        this.slot       = slot;
        this.fieldIndex = -1;
        this.range      = Range.createUnknownRange();
        trace("CREATE SYMBOL");
    }

    /**
     * Constructor
     *
     * @param name  name of symbol
     * @param flags symbol flags
     */
    public Symbol(final String name, final int flags) {
        this(name, flags, Type.UNKNOWN, -1);
    }

    /**
     * Constructor
     *
     * @param name  name of symbol
     * @param flags symbol flags
     * @param type  type of this symbol
     */
    public Symbol(final String name, final int flags, final Type type) {
        this(name, flags, type, -1);
    }

    private Symbol(final Symbol base, final String name, final int flags) {
        this.flags = flags;
        this.name  = name;

        this.fieldIndex = base.fieldIndex;
        this.slot       = base.slot;
        this.type       = base.type;
        this.useCount   = base.useCount;
        this.range      = base.range;
    }

    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();
    }

    /**
     * Return the type for this symbol. Normally, if there is no type override,
     * this is where any type for any node is stored. If the node has a TypeOverride,
     * it may override this, e.g. when asking for a scoped field as a double
     *
     * @return symbol type
     */
    public final Type getSymbolType() {
        return type;
    }

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

    void print(final PrintWriter stream) {
        final String printName = align(name, 20);
        final String printType = align(type.toString(), 10);
        final String printSlot = align(slot == -1 ? "none" : "" + slot, 10);
        String printFlags = "";

        switch (flags & KINDMASK) {
        case IS_TEMP:
            printFlags = "temp " + printFlags;
            break;
        case IS_GLOBAL:
            printFlags = "global " + printFlags;
            break;
        case IS_VAR:
            printFlags = "var " + printFlags;
            break;
        case IS_PARAM:
            printFlags = "param " + printFlags;
            break;
        case IS_CONSTANT:
            printFlags = "CONSTANT " + printFlags;
            break;
        default:
            break;
        }

        if (isScope()) {
            printFlags += "scope ";
        }

        if (isInternal()) {
            printFlags += "internal ";
        }

        if (isLet()) {
            printFlags += "let ";
        }

        if (isThis()) {
            printFlags += "this ";
        }

        if (!canBeUndefined()) {
            printFlags += "always_def ";
        }

        if (canBePrimitive()) {
            printFlags += "can_be_prim ";
        }

        stream.print(printName + ": " + printType + ", " + printSlot + ", " + printFlags);
        stream.println();
    }

    /**
     * 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.
     */
    public void setNeedsSlot(final boolean needsSlot) {
        setSlot(needsSlot ? 0 : -1);
    }

    /**
     * Return the number of slots required for the symbol.
     *
     * @return Number of slots.
     */
    public int slotCount() {
        return type.isCategory2() ? 2 : 1;
    }

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

        sb.append(name).
            append(' ').
            append('(').
            append(getSymbolType().getTypeClass().getSimpleName()).
            append(')');

        if (hasSlot()) {
            sb.append(' ').
                append('(').
                append("slot=").
                append(slot).
                append(')');
        }

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

        if (canBePrimitive()) {
            sb.append(" P?");
        }

        return sb.toString();
    }

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

    /**
     * Does this symbol have an allocated bytecode slot. If not, it is scope
     * and must be loaded from memory upon access
     *
     * @return true if this symbol has a local bytecode slot
     */
    public boolean hasSlot() {
        return slot >= 0;
    }

    /**
     * Check if this is a temporary symbol
     * @return true if temporary
     */
    public boolean isTemp() {
        return (flags & KINDMASK) == IS_TEMP;
    }

    /**
     * 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) == IS_SCOPE;
    }

    /**
     * Returns true if this symbol is a temporary that is being shared across expressions.
     * @return true if this symbol is a temporary that is being shared across expressions.
     */
    public boolean isShared() {
        return (flags & IS_SHARED) == IS_SHARED;
    }

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

    /**
     * Creates an unshared copy of a symbol. The symbol must be currently shared.
     * @param newName the name for the new symbol.
     * @return a new, unshared symbol.
     */
    public Symbol createUnshared(final String newName) {
        assert isShared();
        return new Symbol(this, newName, flags & ~IS_SHARED);
    }

    /**
     * Flag this symbol as scope as described in {@link Symbol#isScope()}
     */
    /**
     * Flag this symbol as scope as described in {@link Symbol#isScope()}
     */
     public void setIsScope() {
        if (!isScope()) {
            trace("SET IS SCOPE");
            assert !isShared();
            flags |= IS_SCOPE;
        }
    }

     /**
      * Mark this symbol as one being shared by multiple expressions. The symbol must be a temporary.
      */
     public void setIsShared() {
         if (!isShared()) {
             assert isTemp();
             trace("SET IS SHARED");
             flags |= IS_SHARED;
         }
     }


    /**
     * Mark this symbol as a function declaration.
     */
    public void setIsFunctionDeclaration() {
        if (!isFunctionDeclaration()) {
            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 symbol is always defined, which overrides all canBeUndefined tags
     * @return true if always defined
     */
    public boolean isAlwaysDefined() {
        return isParam() || (flags & IS_ALWAYS_DEFINED) == IS_ALWAYS_DEFINED;
    }

    /**
     * Get the range for this symbol
     * @return range for symbol
     */
    public Range getRange() {
        return range;
    }

    /**
     * Set the range for this symbol
     * @param range range
     */
    public void setRange(final Range range) {
        this.range = range;
    }

    /**
     * Check if this symbol represents a return value with a known non-generic type.
     * @return true if specialized return value
     */
    public boolean isNonGenericReturn() {
        return getName().equals(RETURN.symbolName()) && type != Type.OBJECT;
    }

    /**
     * Check if this symbol is a function parameter of known
     * narrowest type
     * @return true if parameter
     */
    public boolean isSpecializedParam() {
        return (flags & IS_SPECIALIZED_PARAM) == IS_SPECIALIZED_PARAM;
    }

    /**
     * Check whether this symbol ever has primitive assignments. Conservative
     * @return true if primitive assignments exist
     */
    public boolean canBePrimitive() {
        return (flags & CAN_BE_PRIMITIVE) == CAN_BE_PRIMITIVE;
    }

    /**
     * Check if this symbol can ever be undefined
     * @return true if can be undefined
     */
    public boolean canBeUndefined() {
        return (flags & CAN_BE_UNDEFINED) == CAN_BE_UNDEFINED;
    }

    /**
     * Flag this symbol as potentially undefined in parts of the program
     */
    public void setCanBeUndefined() {
        assert type.isObject() : type;
        if (isAlwaysDefined()) {
            return;
        } else if (!canBeUndefined()) {
            assert !isShared();
            flags |= CAN_BE_UNDEFINED;
        }
    }

    /**
     * Flag this symbol as potentially primitive
     * @param type the primitive type it occurs with, currently unused but can be used for width guesses
     */
    public void setCanBePrimitive(final Type type) {
        if(!canBePrimitive()) {
            assert !isShared();
            flags |= CAN_BE_PRIMITIVE;
        }
    }

    /**
     * Check if this symbol is a constant
     * @return true if a constant
     */
    public boolean isConstant() {
        return (flags & KINDMASK) == IS_CONSTANT;
    }

    /**
     * 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) == IS_LET;
    }

    /**
     * Flag this symbol as a let
     */
    public void setIsLet() {
        if(!isLet()) {
            assert !isShared();
            flags |= IS_LET;
        }
    }

    /**
     * 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) == IS_FUNCTION_SELF;
    }

    /**
     * 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
     */
    public void setFieldIndex(final int fieldIndex) {
        if(this.fieldIndex != fieldIndex) {
            assert !isShared();
            this.fieldIndex = fieldIndex;
        }
    }

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

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

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

    /**
     * Get the byte code slot for this symbol
     * @return byte code slot, or -1 if no slot allocated/possible
     */
    public int getSlot() {
        return slot;
    }

    /**
     * Increase the symbol's use count by one.
     */
    public void increaseUseCount() {
        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 slot valid bytecode slot, or -1 if not available
     */
    public void setSlot(final int slot) {
        if (slot != this.slot) {
            assert !isShared();
            trace("SET SLOT " + slot);
            this.slot = slot;
        }
    }

    /**
     * Assign a specific subclass of Object to the symbol
     *
     * @param type  the type
     */
    public void setType(final Class<?> type) {
        assert !type.isPrimitive() && !Number.class.isAssignableFrom(type) : "Class<?> types can only be subclasses of object";
        setType(Type.typeFor(type));
    }

    /**
     * Assign a type to the symbol
     *
     * @param type the type
     */
    public void setType(final Type type) {
        setTypeOverride(Type.widest(this.type, type));
    }

    /**
     * Returns true if calling {@link #setType(Type)} on this symbol would effectively change its type.
     * @param newType the new type to test for
     * @return true if setting this symbols type to a new value would effectively change its type.
     */
    public boolean wouldChangeType(final Type newType) {
        return Type.widest(this.type, newType) != this.type;
    }

    /**
     * Only use this if you know about an existing type
     * constraint - otherwise a type can only be
     * widened
     *
     * @param type  the type
     */
    public void setTypeOverride(final Type type) {
        final Type old = this.type;
        if (old != type) {
            assert !isShared();
            trace("TYPE CHANGE: " + old + "=>" + type + " == " + type);
            this.type = type;
        }
    }

    /**
     * Sets the type of the symbol to the specified type. If the type would be changed, but this symbol is a shared
     * temporary, it will instead return a different temporary symbol of the requested type from the passed temporary
     * symbols. That way, it never mutates the type of a shared temporary.
     * @param type the new type for the symbol
     * @param ts a holder of temporary symbols
     * @return either this symbol, or a different symbol if this symbol is a shared temporary and it type would have to
     * be changed.
     */
    public Symbol setTypeOverrideShared(final Type type, final TemporarySymbols ts) {
        if(getSymbolType() != type) {
            if(isShared()) {
                assert !hasSlot();
                return ts.getTypedTemporarySymbol(type);
            }
            setTypeOverride(type);
        }
        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
     */
    public static void setSymbolIsScope(final LexicalContext lc, final Symbol symbol) {
        symbol.setIsScope();
        if (!symbol.isGlobal()) {
            lc.setBlockNeedsScope(lc.getDefiningBlock(symbol));
        }
    }

    private void trace(final String desc) {
        if (TRACE_SYMBOLS != null && (TRACE_SYMBOLS.isEmpty() || TRACE_SYMBOLS.contains(name))) {
            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());
            }
        }
    }
}
