/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.jexl3.internal;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * A script scope, stores the declaration of parameters and local variables as symbols.
 * @since 3.0
 */
public final class Scope {
    /**
     * The parent scope.
     */
    private Scope parent = null;
    /**
     * The number of parameters.
     */
    private int parms;
    /**
     * The number of local variables.
     */
    private int vars;
    /**
     * The map of named variables aka script parameters and local variables.
     * Each parameter is associated to a symbol and is materialized as an offset in the stacked array used
     * during evaluation.
     */
    private Map<String, Integer> namedVariables = null;
    /**
     * The map of local hoisted variables to parent scope variables, ie closure.
     */
    private Map<Integer, Integer> hoistedVariables = null;

    /**
     * Creates a new scope with a list of parameters.
     * @param scope the parent scope if any
     * @param parameters the list of parameters
     */
    public Scope(Scope scope, String... parameters) {
        if (parameters != null) {
            parms = parameters.length;
            namedVariables = new LinkedHashMap<String, Integer>();
            for (int p = 0; p < parms; ++p) {
                namedVariables.put(parameters[p], p);
            }
        } else {
            parms = 0;
        }
        vars = 0;
        parent = scope;
    }

    @Override
    public int hashCode() {
        return namedVariables == null ? 0 : parms ^ namedVariables.hashCode();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Scope)) {
            return false;
        }
        Scope scope = (Scope) o;
        if (parms != scope.parms) {
            return false;
        }
        if (namedVariables == null) {
            return scope.namedVariables == null;
        }
        return namedVariables.equals(scope.namedVariables);
    }

    /**
     * Checks whether an identifier is a local variable or argument, ie a symbol.
     * If this fails, attempt to solve by hoisting parent stacked.
     * @param name the symbol name
     * @return the symbol index
     */
    public Integer getSymbol(String name) {
        return getSymbol(name, true);
    }

    /**
     * Checks whether an identifier is a local variable or argument, ie a symbol.
     * @param name the symbol name
     * @param hoist whether solving by hoisting parent stacked is allowed
     * @return the symbol index
     */
    private Integer getSymbol(String name, boolean hoist) {
        Integer register = namedVariables != null ? namedVariables.get(name) : null;
        if (register == null && hoist && parent != null) {
            Integer pr = parent.getSymbol(name, false);
            if (pr != null) {
                if (hoistedVariables == null) {
                    hoistedVariables = new LinkedHashMap<Integer, Integer>();
                }
                if (namedVariables == null) {
                    namedVariables = new LinkedHashMap<String, Integer>();
                }
                register = Integer.valueOf(namedVariables.size());
                namedVariables.put(name, register);
                hoistedVariables.put(register, pr);
            }
        }
        return register;
    }

    /**
     * Checks whether a given symbol is hoisted.
     * @param symbol the symbol number
     * @return true if hoisted, false otherwise
     */
    public boolean isHoistedSymbol(int symbol) {
        return hoistedVariables != null && hoistedVariables.containsKey(symbol);
    }

    /**
     * Declares a parameter.
     * <p>
     * This method creates an new entry in the symbol map.
     * </p>
     * @param name the parameter name
     */
    public void declareParameter(String name) {
        if (namedVariables == null) {
            namedVariables = new LinkedHashMap<String, Integer>();
        } else if (vars > 0) {
            throw new IllegalStateException("cant declare parameters after variables");
        }
        Integer register = namedVariables.get(name);
        if (register == null) {
            register = Integer.valueOf(namedVariables.size());
            namedVariables.put(name, register);
            parms += 1;
        }
    }

    /**
     * Declares a local variable.
     * <p>
     * This method creates an new entry in the symbol map.
     * </p>
     * @param name the variable name
     * @return the register index storing this variable
     */
    public Integer declareVariable(String name) {
        if (namedVariables == null) {
            namedVariables = new LinkedHashMap<String, Integer>();
        }
        Integer register = namedVariables.get(name);
        if (register == null) {
            register = Integer.valueOf(namedVariables.size());
            namedVariables.put(name, register);
            vars += 1;
        }
        return register;
    }

    /**
     * Creates a frame by copying values up to the number of parameters.
     * <p>This captures the hoisted variables values.</p>
     * @param frame the caller frame
     * @return the arguments array
     */
    public Frame createFrame(Frame frame) {
        if (namedVariables != null) {
            Object[] arguments = new Object[namedVariables.size()];
            if (frame != null && hoistedVariables != null && parent != null) {
                for (Map.Entry<Integer, Integer> hoist : hoistedVariables.entrySet()) {
                    Integer target = hoist.getKey();
                    Integer source = hoist.getValue();
                    Object arg = frame.get(source.intValue());
                    arguments[target.intValue()] = arg;
                }
            }
            return new Frame(this, arguments, 0);
        } else {
            return null;
        }
    }

    /**
     * Gets the hoisted index of a given symbol, ie the target index of a symbol in a child frame.
     * @param symbol the symbol index
     * @return the target symbol index or null if the symbol is not hoisted
     */
    public Integer getHoisted(int symbol) {
        if (hoistedVariables != null) {
            for (Map.Entry<Integer, Integer> hoist : hoistedVariables.entrySet()) {
                Integer source = hoist.getValue();
                if (source == symbol) {
                    return hoist.getKey();
                }
            }
        }
        return null;
    }

    /**
     * Gets the (maximum) number of arguments this script expects.
     * @return the number of parameters
     */
    public int getArgCount() {
        return parms;
    }

    /**
     * Gets this script symbols names, i.e. parameters and local variables.
     * @return the symbol names
     */
    public String[] getSymbols() {
        return namedVariables != null ? namedVariables.keySet().toArray(new String[0]) : new String[0];
    }

    /**
     * Gets this script parameters, i.e. symbols assigned before creating local variables.
     * @return the parameter names
     */
    public String[] getParameters() {
        if (namedVariables != null && parms > 0) {
            String[] pa = new String[parms];
            int p = 0;
            for (Map.Entry<String, Integer> entry : namedVariables.entrySet()) {
                if (entry.getValue().intValue() < parms) {
                    pa[p++] = entry.getKey();
                }
            }
            return pa;
        } else {
            return null;
        }
    }

    /**
     * Gets this script local variable, i.e. symbols assigned to local variables excluding hoisted variables.
     * @return the local variable names
     */
    public String[] getLocalVariables() {
        if (namedVariables != null && vars > 0) {
            String[] pa = new String[parms - (hoistedVariables == null? 0 : hoistedVariables.size())];
            int p = 0;
            for (Map.Entry<String, Integer> entry : namedVariables.entrySet()) {
                int symnum = entry.getValue().intValue();
                if (symnum >= parms && (hoistedVariables == null || !hoistedVariables.containsKey(symnum))) {
                    pa[p++] = entry.getKey();
                }
            }
            return pa;
        } else {
            return null;
        }
    }

    /**
     * A call frame, created from a scope, stores the arguments and local variables in a "stack frame" (sic).
     * @since 3.0
     */
    public static final class Frame {
        /** The scope. */
        private final Scope scope;
        /** The actual stack frame. */
        private final Object[] stack;
        /** Number of curried parameters. */
        private int curried = 0;

        /**
         * Creates a new frame.
         * @param s the scope
         * @param r the stack frame
         * @param c the number of curried parameters
         */
        public Frame(Scope s, Object[] r, int c) {
            scope = s;
            stack = r;
            curried = c;
        }

        /**
         * Gets the scope.
         * @return this frame scope
         */
        public Scope getScope() {
            return scope;
        }

        @Override
        public int hashCode() {
            return Arrays.deepHashCode(this.stack);
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final Frame other = (Frame) obj;
            return Arrays.deepEquals(this.stack, other.stack);
        }

        /**
         * Gets a value.
         * @param s the offset in this frame
         * @return the stacked value
         */
        public Object get(int s) {
            return stack[s];
        }

        /**
         * Sets a value.
         * @param r the offset in this frame
         * @param value the value to set in this frame
         */
        public void set(int r, Object value) {
            stack[r] = value;
        }

        /**
         * Assign values to this frame.
         * @param values the values
         * @return this frame
         */
        public Frame assign(Object... values) {
            if (stack != null && values != null && values.length > 0) {
                Object[] copy = stack.clone();
                int ncopy = Math.min(copy.length - curried, values.length);
                System.arraycopy(values, 0, copy, curried, ncopy);
                return new Frame(scope, copy, curried + ncopy);
            }
            return this;
        }
    }
}
