/*
 * Copyright 2014 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Eclipse Public License version 1.0, available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.jboss.windup.config;

import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;

import javax.enterprise.inject.Vetoed;

import org.jboss.windup.config.exception.IllegalTypeArgumentException;
import org.jboss.windup.config.operation.Iteration;
import org.jboss.windup.graph.model.WindupVertexFrame;

/**
 * A variables stack - keeps few layers of "key"->[vertices] maps, one per rule execution level, {@link Iteration} and
 * {@link RuleSubset}.
 * 
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 */
@Vetoed
public class Variables
{
    public static int SEARCH_ALL_LAYERS = Integer.MAX_VALUE;

    private final Deque<Map<String, Iterable<? extends WindupVertexFrame>>> deque = new LinkedList<>();

    private Variables()
    {
    }

    /**
     * Get an instance of the {@link Variables} stack from the given {@link GraphRewrite} event context.
     */
    public static Variables instance(GraphRewrite event)
    {
        Variables instance = (Variables) event.getRewriteContext().get(Variables.class);
        if (instance == null)
        {
            instance = new Variables();
            event.getRewriteContext().put(Variables.class, instance);
        }
        return instance;
    }

    /**
     * Add new {@link Variables} layer on top of the stack.
     */
    public void push()
    {
        Map<String, Iterable<? extends WindupVertexFrame>> newFrame = new HashMap<>();
        deque.push(newFrame);
    }

    /**
     * Push the given {@link Variables} layer on top of the stack.
     */
    public void push(Map<String, Iterable<? extends WindupVertexFrame>> frame)
    {
        deque.push(frame);
    }

    /**
     * Remove the top {@link Variables} layer from the the stack.
     */
    public Map<String, Iterable<? extends WindupVertexFrame>> pop()
    {
        Map<String, Iterable<? extends WindupVertexFrame>> frame = deque.pop();
        return frame;
    }

    /**
     * Get the top {@link Variables} layer from the stack.
     */
    public Map<String, Iterable<? extends WindupVertexFrame>> peek()
    {
        return deque.peek();
    }

    /**
     * Type-safe wrapper around setVariable which sets only one framed vertex.
     */
    public void setSingletonVariable(String name, WindupVertexFrame frame)
    {
        setVariable(name, Collections.singletonList(frame));
    }

    /**
     * Set a variable in the top variables layer to given "collection" of the vertex frames. Can't be reassigned -
     * throws on attempt to reassign.
     */
    public void setVariable(String name, Iterable<? extends WindupVertexFrame> frames)
    {
        Map<String, Iterable<? extends WindupVertexFrame>> frame = peek();
        if (!Iteration.DEFAULT_VARIABLE_LIST_STRING.equals(name) && findVariable(name) != null)
        {
            throw new IllegalArgumentException("Variable \"" + name
                        + "\" has already been assigned and cannot be reassigned");
        }

        frame.put(name, frames);
    }

    /**
     * Remove a variable in the top variables layer.
     */
    public void removeVariable(String name)
    {
        Map<String, Iterable<? extends WindupVertexFrame>> frame = peek();
        frame.remove(name);
    }

    /**
     * Wrapper around {@link #findVariable(String)} which gives only one framed vertex, and checks if there is 0 or 1;
     * throws otherwise.
     */
    @SuppressWarnings("unchecked")
    public <T extends WindupVertexFrame> T findSingletonVariable(String name)
    {
        Iterable<? extends WindupVertexFrame> frames = findVariable(name);
        if (null == frames)
        {
            throw new IllegalStateException("Variable not found: \"" + name + "\"");
        }

        Iterator<? extends WindupVertexFrame> iterator = frames.iterator();
        if (!iterator.hasNext())
        {
            return null;
        }

        WindupVertexFrame obj = iterator.next();

        if (iterator.hasNext())
        {
            throw new IllegalStateException("More than one frame present "
                        + "under presumed singleton variable: " + name);
        }
        return (T) obj;
    }

    /**
     * Type-safe wrapper around {@link #findVariable(String)} returns a unique {@link WindupVertexFrame}.
     * 
     * @throws IllegalStateException If more than one frame was found.
     */
    @SuppressWarnings("unchecked")
    public <FRAMETYPE extends WindupVertexFrame> FRAMETYPE findSingletonVariable(Class<FRAMETYPE> type, String name)
    {
        WindupVertexFrame frame = findSingletonVariable(name);

        if (type != null && !type.isAssignableFrom(frame.getClass()))
        {
            throw new IllegalTypeArgumentException(name, type, frame.getClass());
        }

        return (FRAMETYPE) frame;
    }

    /**
     * Searches the variables layers, top to bottom, for given name, and returns if found; null otherwise.
     */
    public Iterable<? extends WindupVertexFrame> findVariable(String name)
    {
        return findVariable(name, SEARCH_ALL_LAYERS);
    }

    /**
     * Searches the variables layers, top to bottom, for given name, and returns if found; null otherwise.
     *
     * If maxDepth is set to {@link Variables#SEARCH_ALL_LAYERS}, then search all layers.
     */
    public Iterable<? extends WindupVertexFrame> findVariable(String name, int maxDepth)
    {
        int currentDepth = 0;
        Iterable<? extends WindupVertexFrame> result = null;
        for (Map<String, Iterable<? extends WindupVertexFrame>> frame : deque)
        {
            result = frame.get(name);
            if (result != null)
            {
                break;
            }
            currentDepth++;
            if (currentDepth >= maxDepth)
                break;
        }
        return result;
    }

    /**
     * Searches the variables layers, top to bottom, for the iterable having all of it's items of the given type. Return
     * null if not found.
     */
    @SuppressWarnings("unchecked")
    public <T extends WindupVertexFrame> Iterable<T> findVariableOfType(Class<T> type)
    {
        for (Map<String, Iterable<? extends WindupVertexFrame>> topOfStack : deque)
        {
            for (Iterable<? extends WindupVertexFrame> frames : topOfStack.values())
            {
                boolean empty = true;
                for (WindupVertexFrame frame : frames)
                {
                    if (!type.isAssignableFrom(frame.getClass()))
                    {
                        break;
                    }
                    else
                    {
                        empty = false;
                    }
                }
                // now we know all the frames are of the chosen type
                if (!empty)
                    return (Iterable<T>) frames;
            }
        }
        return null;
    }

    @Override
    public String toString()
    {
        return "Variables [depth=" + deque.size() + "]";
    }
}
