/*
 * Copyright 2008 Google Inc.
 * 
 * Licensed 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.jetbrains.kotlin.js.backend.ast;

/*
 * Taken from GWT project with modifications.
 * Original:
 *  repository: https://gwt.googlesource.com/gwt
 *  revision: e32bf0a95029165d9e6ab457c7ee7ca8b07b908c
 *  file: dev/core/src/com/google/gwt/dev/js/ast/JsModVisitor.java
 */

import com.intellij.util.SmartList;
import org.jetbrains.annotations.Nullable;

import java.util.*;

/**
 * A visitor for iterating through and modifying an AST.
 */
public class JsVisitorWithContextImpl extends JsVisitorWithContext {

    protected final Stack<JsContext<JsStatement>> statementContexts = new Stack<>();

    public class ListContext<T extends JsNode> extends JsContext<T> {
        private List<T> nodes;
        private int index;

        // Those are reset in every iteration of traverse()
        private final List<T> previous = new SmartList<>();
        private final List<T> next = new SmartList<>();
        private boolean removed = false;

        @Override
        public <R extends T> void addPrevious(R node) {
            previous.add(node);
        }

        @Override
        public void removeMe() {
            removed = true;
        }

        @Override
        public <R extends T> void replaceMe(R node) {
            checkReplacement(nodes.get(index), node);
            nodes.set(index, node);
            removed = false;
        }
        
        @Nullable
        @Override
        public T getCurrentNode() {
            if (!removed && index < nodes.size()) {
                return nodes.get(index);
            }

            return null;
        }

        public void traverse(List<T> nodes) {
            assert previous.isEmpty(): "addPrevious() was called before traverse()";
            assert next.isEmpty(): "addNext() was called before traverse()";
            this.nodes = nodes;

            for (index = 0; index < nodes.size(); index++) {
                removed = false;
                previous.clear();
                next.clear();
                doTraverse(getCurrentNode(), this);

                if (!previous.isEmpty()) {
                    nodes.addAll(index, previous);
                    index += previous.size();
                }

                if (removed) {
                    nodes.remove(index);
                    index--;
                }

                if (!next.isEmpty()) {
                    nodes.addAll(index + 1, next);
                    index += next.size();
                }
            }

            previous.clear();
            next.clear();
        }
    }

    private class LvalueContext extends NodeContext<JsExpression> {
    }

    private class NodeContext<T extends JsNode> extends JsContext<T> {
        protected T node;

        @Override
        public void removeMe() {
            throw new UnsupportedOperationException();
        }

        @Override
        public <R extends T> void replaceMe(R node) {
            checkReplacement(this.node, node);
            this.node = node;
        }

        @Nullable
        @Override
        public T getCurrentNode() {
            return node;
        }

        protected T traverse(T node) {
            this.node = node;
            doTraverse(node, this);
            return this.node;
        }
    }

    private static void checkReplacement(@SuppressWarnings("UnusedParameters") JsNode origNode, JsNode newNode) {
        if (newNode == null) throw new RuntimeException("Cannot replace with null");
    }

    @Override
    protected <T extends JsNode> T doAccept(T node) {
        return new NodeContext<T>().traverse(node);
    }

    @Override
    protected JsExpression doAcceptLvalue(JsExpression expr) {
        return new LvalueContext().traverse(expr);
    }

    @Override
    protected <T extends JsStatement> JsStatement doAcceptStatement(T statement) {
        List<JsStatement> statements = new SmartList<>(statement);
        doAcceptStatementList(statements);

        if (statements.size() == 1) {
            return statements.get(0);
        }

        return new JsBlock(statements);
    }

    @Override
    protected void doAcceptStatementList(List<JsStatement> statements) {
        ListContext<JsStatement> context = new ListContext<>();
        statementContexts.push(context);
        context.traverse(statements);
        statementContexts.pop();
    }

    @Override
    protected <T extends JsNode> void doAcceptList(List<T> collection) {
        new ListContext<T>().traverse(collection);
    }

    @Override
    protected <T extends JsNode> void doTraverse(T node, JsContext ctx) {
        node.traverse(this, ctx);
    }

}
