/******************************************************************************
* Copyright (C) 2006-2013  IFS Institute for Software and others
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Original authors:
*     Dennis Hunziker
*     Ueli Kistler
*     Reto Schuettel
*     Robin Stocker
* Contributors:
*     Fabio Zadrozny <fabiofz@gmail.com> - initial implementation
******************************************************************************/
/* 
 * Copyright (C) 2006, 2007  Dennis Hunziker, Ueli Kistler
 * Copyright (C) 2007  Reto Schuettel, Robin Stocker
 *
 * IFS Institute for Software, HSR Rapperswil, Switzerland
 * 
 */

package org.python.pydev.refactoring.ast.visitors.selection;

import java.util.List;

import org.eclipse.jface.text.ITextSelection;
import org.python.pydev.parser.jython.ISpecialStr;
import org.python.pydev.parser.jython.SimpleNode;
import org.python.pydev.parser.jython.ast.Attribute;
import org.python.pydev.parser.jython.ast.Call;
import org.python.pydev.parser.jython.ast.ClassDef;
import org.python.pydev.parser.jython.ast.For;
import org.python.pydev.parser.jython.ast.FunctionDef;
import org.python.pydev.parser.jython.ast.If;
import org.python.pydev.parser.jython.ast.Module;
import org.python.pydev.parser.jython.ast.Str;
import org.python.pydev.parser.jython.ast.TryExcept;
import org.python.pydev.parser.jython.ast.VisitorBase;
import org.python.pydev.parser.jython.ast.While;
import org.python.pydev.parser.jython.ast.aliasType;
import org.python.pydev.parser.jython.ast.argumentsType;
import org.python.pydev.parser.jython.ast.decoratorsType;
import org.python.pydev.parser.jython.ast.excepthandlerType;
import org.python.pydev.parser.jython.ast.exprType;
import org.python.pydev.parser.jython.ast.keywordType;
import org.python.pydev.parser.jython.ast.stmtType;
import org.python.pydev.parser.jython.ast.suiteType;
import org.python.pydev.refactoring.ast.adapters.ModuleAdapter;
import org.python.pydev.shared_core.structure.FastStack;

public class SelectionExtenderVisitor extends VisitorBase {

    private ModuleAdapter module;

    private ITextSelection selection;

    private FastStack<SimpleNode> stmtExprStack;

    private SimpleNode extendNodeInSelection;

    public SelectionExtenderVisitor(ModuleAdapter module, ITextSelection selection) {
        this.module = module;
        this.selection = selection;
        this.stmtExprStack = new FastStack<SimpleNode>(20);
        this.extendNodeInSelection = null;
    }

    @Override
    public void traverse(SimpleNode node) throws Exception {
        if (node != null) {
            updateSelection(node);
            node.traverse(this);
        }
    }

    @Override
    protected Object unhandled_node(SimpleNode node) throws Exception {
        return null;
    }

    private void updateSelection(SimpleNode node) {
        extendSelection(node);
        if (node.beginLine <= selection.getEndLine() + 1) {
            updateStack(node);
            checkAndExtend(node, node);
        } else if (node.beginLine >= selection.getEndLine() + 1) {
            if (!stmtExprStack.isEmpty()) {
                this.extendNodeInSelection = stmtExprStack.peek();
            }
        }
    }

    protected SimpleNode visit(SimpleNode node) throws Exception {
        if (node == null) {
            return null;
        }

        if (!(node instanceof suiteType)) {
            updateSelection(node);
        }

        if (node instanceof suiteType) {
            visitSuiteType((suiteType) node);
        } else if (node instanceof excepthandlerType) {
            visitExceptionHandler((excepthandlerType) node);
        } else if (node instanceof decoratorsType) {
            visitDecoratorsType((decoratorsType) node);
        } else if (node instanceof keywordType) {
            visitKeywordType((keywordType) node);
        } else if (node instanceof argumentsType) {
            visitArgumentsType((argumentsType) node);
        } else if (node instanceof aliasType) {
            visitAliasType((aliasType) node);
        } else {
            node.accept(this);
        }

        if (isAnyInSelection(node.getSpecialsBefore()) || isAnyInSelection(node.getSpecialsAfter())) {
            this.extendNodeInSelection = node;
        }

        return node;
    }

    private boolean isAnyInSelection(List<Object> specials) {
        for (Object o : specials) {
            Str strNode = convertSpecialToStr(o);
            if (strNode != null) {
                if (module.isNodeInSelection(selection, strNode)) {
                    return true;
                }
            }

        }
        return false;
    }

    private Str convertSpecialToStr(Object o) {
        Str stringNode = null;
        if (o instanceof ISpecialStr) {
            ISpecialStr special = (ISpecialStr) o;
            stringNode = new Str(special.toString(), Str.SingleDouble, false, false, false);
            stringNode.beginLine = special.getBeginLine();
            stringNode.beginColumn = special.getBeginCol();
        }
        return stringNode;
    }

    private void extendSelection(SimpleNode node) {
        if (extendNodeInSelection != null && isExtendable(node) && node != extendNodeInSelection) {

            node = resolveExtendNode(node);

            this.selection = this.module.extendSelection(selection, extendNodeInSelection, node);
            this.extendNodeInSelection = null;
            this.stmtExprStack.clear();
        }
    }

    private boolean isExtendable(SimpleNode node) {
        return (node instanceof stmtType || node instanceof excepthandlerType || node instanceof suiteType);
    }

    private SimpleNode resolveExtendNode(SimpleNode node) {
        if (extendNodeInSelection instanceof exprType) {
            while (!(stmtExprStack.isEmpty()) && !(isExtendable(stmtExprStack.peek()))) {
                stmtExprStack.pop();
            }
            if (!(stmtExprStack.isEmpty())) {
                SimpleNode stmtBefore = stmtExprStack.peek();
                node = checkSpecials(stmtBefore, stmtBefore.getSpecialsAfter());
            }
        }
        node = checkSpecials(node, node.getSpecialsBefore());

        return node;
    }

    private SimpleNode checkSpecials(SimpleNode node, List<Object> specials) {
        if (specials.size() > 0) {
            for (Object o : specials) {
                if (o instanceof ISpecialStr) {
                    ISpecialStr str = (ISpecialStr) o;
                    if (str.getBeginLine() >= extendNodeInSelection.beginLine) {
                        return convertSpecialToStr(o);
                    }
                }
            }
        }
        return node;
    }

    private void checkAndExtend(SimpleNode extendNode, SimpleNode checkNode) {
        if (module.isNodeInSelection(selection, checkNode)) {
            extendNodeInSelection = extendNode;
        }
    }

    public Object visitCall(Call node) throws Exception {
        traverse(node);
        checkAndExtend(node, node.func);

        return null;

    }

    @Override
    public Object visitAttribute(Attribute node) throws Exception {
        visit(node.value);
        visit(node.attr);
        checkAndExtend(node, node.value);
        checkAndExtend(node, node.attr);
        return null;
    }

    @Override
    public Object visitFor(For node) throws Exception {
        visit(node.target);
        visit(node.iter);
        visit(node.body);
        visit(node.orelse);
        return null;
    }

    @Override
    public Object visitWhile(While node) throws Exception {
        visit(node.test);
        visit(node.body);
        visit(node.orelse);
        return null;
    }

    @Override
    public Object visitIf(If node) throws Exception {
        visit(node.test);
        visit(node.body);
        if (node.orelse != null) {
            if (this.extendNodeInSelection instanceof exprType) {
                extendSelection(node);
            }
            visit(node.orelse);
        }

        return null;
    }

    @Override
    public Object visitTryExcept(TryExcept node) throws Exception {
        visit(node.body);
        visit(node.handlers);
        visit(node.orelse);
        if (node.orelse != null) {
            if (this.extendNodeInSelection instanceof exprType) {
                extendSelection(node);
            }
            visit(node.orelse);
        }
        return null;
    }

    private Object visitSuiteType(suiteType node) throws Exception {
        visit(node.body);
        return null;
    }

    private Object visitExceptionHandler(excepthandlerType node) throws Exception {
        visit(node.type);
        visit(node.name);
        visit(node.body);
        return null;
    }

    private Object visitDecoratorsType(decoratorsType node) throws Exception {
        visit(node.func);
        visit(node.args);
        visit(node.keywords);
        visit(node.starargs);
        visit(node.kwargs);
        return null;
    }

    private Object visitKeywordType(keywordType node) throws Exception {
        visit(node.arg);
        visit(node.value);
        return null;
    }

    private Object visitArgumentsType(argumentsType node) throws Exception {
        visit(node.args);
        visit(node.defaults);
        visit(node.vararg);
        visit(node.kwarg);
        return null;
    }

    private Object visitAliasType(aliasType node) throws Exception {
        visit(node.name);
        visit(node.asname);
        return null;
    }

    @Override
    public Object visitFunctionDef(FunctionDef node) throws Exception {
        visit(node.name);
        visit(node.args);
        visit(node.body);
        extendLast(node);

        return null;
    }

    @Override
    public Object visitClassDef(ClassDef node) throws Exception {
        visit(node.name);
        visit(node.bases);
        visit(node.body);
        extendLast(node);

        return null;
    }

    @Override
    public Object visitModule(Module node) throws Exception {
        visit(node.body);
        extendLast(node);

        return null;
    }

    private void extendLast(SimpleNode node) {
        if (extendNodeInSelection != null) {
            selection = module.extendSelectionToEnd(selection, node);
        }
        if (!stmtExprStack.isEmpty()) {
            selection = module.extendSelection(selection, stmtExprStack.peek());
        }
    }

    private void visit(SimpleNode[] body) throws Exception {
        for (SimpleNode node : body) {
            visit(node);
        }
    }

    private void updateStack(SimpleNode node) {
        if (!stmtExprStack.isEmpty()) {
            if (node == stmtExprStack.peek()) {
                return;
            }
        }
        if (node instanceof stmtType || node instanceof exprType) {
            stmtExprStack.push(node);
        }
    }

    public ITextSelection getSelection() {
        return selection;
    }

}
