/*
 * Copyright (c) 2011, 2019, 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 org.graalvm.compiler.java;

import org.graalvm.compiler.bytecode.Bytecodes;
import org.graalvm.compiler.java.BciBlockMapping.BciBlock;

/**
 * Represents a subroutine entered via {@link Bytecodes#JSR} and exited via {@link Bytecodes#RET}.
 */
public final class JsrScope {

    /**
     * The scope outside of any JSR/RET subroutine.
     */
    public static final JsrScope EMPTY_SCOPE = new JsrScope();

    private final char returnAddress;

    private final JsrScope parent;

    private final BciBlock jsrEntryBlock;

    private JsrScope(int returnBci, BciBlock jsrEntryBlock, JsrScope parent) {
        this.returnAddress = (char) returnBci;
        this.parent = parent;
        this.jsrEntryBlock = jsrEntryBlock;
    }

    private JsrScope() {
        this.returnAddress = 0;
        this.parent = null;
        this.jsrEntryBlock = null;
    }

    public int nextReturnAddress() {
        return returnAddress;
    }

    public BciBlock getJsrEntryBlock() {
        return jsrEntryBlock;
    }

    /**
     * Enters a new subroutine from the current scope represented by this object.
     *
     * @param returnBci the bytecode address returned to when leaving the new scope
     * @return an object representing the newly entered scope
     */
    public JsrScope push(int returnBci, BciBlock newJsrEntryBlock) {
        if (returnBci == 0) {
            throw new IllegalArgumentException("A bytecode subroutine cannot have a return address of 0");
        }
        if (returnBci < 1 || returnBci > 0xFFFF) {
            throw new IllegalArgumentException("Bytecode subroutine return address cannot be encoded as a char: " + returnBci);
        }
        return new JsrScope(returnBci, newJsrEntryBlock, this);
    }

    public JsrScope push(int returnBci) {
        return push(returnBci, null);
    }

    /**
     * Determines if this is the scope outside of any JSR/RET subroutine.
     */
    public boolean isEmpty() {
        return returnAddress == 0;
    }

    /**
     * Gets the ancestry of this scope starting with the {@link #returnAddress} of this scope's most
     * distant ancestor and ending with the {@link #returnAddress} of this object.
     *
     * @return a String where each character is a 16-bit BCI. This value can be converted to an
     *         {@code int[]} with {@code value.chars().toArray()}.
     */
    public String getAncestry() {
        String result = "";
        for (JsrScope s = this; s != null; s = s.parent) {
            if (!s.isEmpty()) {
                result = s.returnAddress + result;
            }
        }
        return result;
    }

    /**
     * Determines if the {@linkplain #getAncestry() ancestry} of this scope is a prefix of the
     * ancestry of {@code other}.
     */
    public boolean isPrefixOf(JsrScope other) {
        if (isEmpty()) {
            return true;
        }
        String ancestry = getAncestry();
        String otherAncestry = other.getAncestry();
        return otherAncestry.startsWith(ancestry);
    }

    /**
     * Gets this scope's parent.
     *
     * @return this scope's parent or {@link #EMPTY_SCOPE} if this is the {@link #EMPTY_SCOPE}
     */
    public JsrScope pop() {
        if (isEmpty()) {
            return this;
        }
        return parent;
    }

    @Override
    public int hashCode() {
        int hc = returnAddress;
        JsrScope ancestor = parent;
        while (ancestor != null) {
            hc = hc ^ ancestor.returnAddress;
            ancestor = ancestor.parent;
        }
        return hc;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj != null && getClass() == obj.getClass()) {
            JsrScope ancestor = this;
            JsrScope otherAncestor = (JsrScope) obj;
            while (ancestor != null) {
                if (otherAncestor == null) {
                    return false;
                }
                if (otherAncestor.returnAddress != ancestor.returnAddress) {
                    return false;
                }
                ancestor = ancestor.parent;
                otherAncestor = otherAncestor.parent;
            }
            if (otherAncestor == null) {
                return true;
            }
        }
        return false;
    }

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

        for (JsrScope ancestor = this; ancestor != null; ancestor = ancestor.parent) {
            if (!ancestor.isEmpty()) {
                if (sb.length() != 0) {
                    sb.append(", ");
                }
                sb.append((int) ancestor.returnAddress);
            }
        }
        return "[" + sb + "]";
    }
}
