/*******************************************************************************
 * Copyright (c) 2005, 2007 IBM Corporation 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
 *

 *******************************************************************************/
package org.python.pydev.shared_interactive_console.console.ui;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.runtime.ListenerList;
import org.eclipse.debug.internal.ui.views.console.ProcessConsole;
import org.eclipse.debug.ui.console.IConsoleLineTracker;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.text.ITextHover;
import org.eclipse.jface.text.contentassist.ContentAssistant;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
import org.eclipse.jface.text.quickassist.QuickAssistAssistant;
import org.eclipse.jface.text.source.SourceViewerConfiguration;
import org.eclipse.swt.graphics.Color;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IViewReference;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.console.IConsole;
import org.eclipse.ui.console.IConsoleConstants;
import org.eclipse.ui.console.IConsoleDocumentPartitioner;
import org.eclipse.ui.console.IConsoleView;
import org.eclipse.ui.console.TextConsole;
import org.eclipse.ui.part.IPageBookViewPage;
import org.python.pydev.shared_core.callbacks.ICallback;
import org.python.pydev.shared_core.structure.Tuple;
import org.python.pydev.shared_core.utils.Reflection;
import org.python.pydev.shared_interactive_console.console.IScriptConsoleCommunication;
import org.python.pydev.shared_interactive_console.console.IScriptConsoleInterpreter;
import org.python.pydev.shared_interactive_console.console.InterpreterResponse;
import org.python.pydev.shared_interactive_console.console.ScriptConsoleHistory;
import org.python.pydev.shared_interactive_console.console.ScriptConsolePrompt;
import org.python.pydev.shared_interactive_console.console.ui.internal.ICommandHandler;
import org.python.pydev.shared_interactive_console.console.ui.internal.IHandleScriptAutoEditStrategy;
import org.python.pydev.shared_interactive_console.console.ui.internal.ScriptConsolePage;
import org.python.pydev.shared_interactive_console.console.ui.internal.ScriptConsoleSession;
import org.python.pydev.shared_interactive_console.console.ui.internal.ScriptConsoleViewer;
import org.python.pydev.shared_interactive_console.console.ui.internal.actions.AbstractHandleBackspaceAction;
import org.python.pydev.shared_ui.content_assist.AbstractCompletionProcessorWithCycling;

public abstract class ScriptConsole extends TextConsole implements ICommandHandler {

    protected ScriptConsolePage page;

    protected ScriptConsolePartitioner partitioner;

    protected IScriptConsoleInterpreter interpreter;

    protected ScriptConsoleSession session;

    protected ListenerList consoleListeners;

    protected ScriptConsolePrompt prompt;

    protected ScriptConsoleHistory history;

    private WeakReference<ScriptConsoleViewer> viewer;

    public static final String DEFAULT_CONSOLE_TYPE = "org.python.pydev.debug.newconsole.PydevConsole";

    public static final String SCRIPT_DEBUG_CONSOLE_IN_PROCESS_CONSOLE = "SCRIPT_DEBUG_CONSOLE_IN_PROCESS_CONSOLE";

    // Backward-compatibility
    public static ScriptConsole getActiveScriptConsole(String ignored) {
        return getActiveScriptConsole();
    }

    public static Iterable<IConsoleView> iterConsoles() {
        List<IConsoleView> consoles = new ArrayList<>();
        IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
        if (window != null) {
            IWorkbenchPage page = window.getActivePage();
            if (page != null) {

                List<IViewPart> consoleParts = getConsoleParts(page, false);
                if (consoleParts.size() == 0) {
                    consoleParts = getConsoleParts(page, true);
                }
                for (IViewPart iViewPart : consoleParts) {
                    if (iViewPart instanceof IConsoleView) {
                        consoles.add((IConsoleView) iViewPart);
                    }
                }
            }
        }
        return consoles;
    }

    /**
     * @return the currently active script console.
     */
    @SuppressWarnings("restriction")
    public static ScriptConsole getActiveScriptConsole() {
        IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
        if (window != null) {
            IWorkbenchPage page = window.getActivePage();
            if (page != null) {

                List<IViewPart> consoleParts = getConsoleParts(page, false);
                if (consoleParts.size() == 0) {
                    consoleParts = getConsoleParts(page, true);
                }

                if (consoleParts.size() > 0) {
                    IConsoleView view = null;
                    long lastChangeMillis = Long.MIN_VALUE;

                    if (consoleParts.size() == 1) {
                        view = (IConsoleView) consoleParts.get(0);
                    } else {
                        //more than 1 view available
                        for (int i = 0; i < consoleParts.size(); i++) {
                            IConsoleView temp = (IConsoleView) consoleParts.get(i);
                            IConsole console = temp.getConsole();
                            if (console instanceof ScriptConsole) {
                                ScriptConsole tempConsole = (ScriptConsole) console;
                                ScriptConsoleViewer viewer = tempConsole.getViewer();

                                long tempLastChangeMillis = viewer.getLastChangeMillis();
                                if (tempLastChangeMillis > lastChangeMillis) {
                                    lastChangeMillis = tempLastChangeMillis;
                                    view = temp;
                                }
                            }
                        }
                    }

                    if (view != null) {
                        IConsole console = view.getConsole();

                        if (console instanceof ScriptConsole) {
                            return (ScriptConsole) console;
                        } else {
                            if (console instanceof ProcessConsole) {
                                ProcessConsole processConsole = (ProcessConsole) console;
                                Object scriptConsole = processConsole
                                        .getAttribute(ScriptConsole.SCRIPT_DEBUG_CONSOLE_IN_PROCESS_CONSOLE);
                                if (scriptConsole instanceof ScriptConsole) {
                                    ScriptConsole scriptConsole2 = (ScriptConsole) scriptConsole;
                                    IScriptConsoleCommunication consoleCommunication = scriptConsole2.getInterpreter()
                                            .getConsoleCommunication();
                                    if (consoleCommunication.isConnected()) {
                                        return scriptConsole2;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

    /**
     * @param page the page where the console view is
     * @param restore whether we should try to restore it
     * @return a list with the parts containing the console
     */
    private static List<IViewPart> getConsoleParts(IWorkbenchPage page, boolean restore) {
        List<IViewPart> consoleParts = new ArrayList<IViewPart>();

        IViewReference[] viewReferences = page.getViewReferences();
        for (IViewReference ref : viewReferences) {
            if (ref.getId().equals(IConsoleConstants.ID_CONSOLE_VIEW)) {
                IViewPart part = ref.getView(restore);
                if (part != null) {
                    consoleParts.add(part);
                    if (restore) {
                        return consoleParts;
                    }
                }
            }
        }
        return consoleParts;
    }

    @Override
    protected IConsoleDocumentPartitioner getPartitioner() {
        return partitioner;
    }

    public ScriptConsole(String consoleName, String consoleType, IScriptConsoleInterpreter interpreterArg) {
        super(consoleName, consoleType, null, true);

        this.interpreter = interpreterArg;

        this.consoleListeners = new ListenerList(ListenerList.IDENTITY);
        this.prompt = createConsolePrompt();
        this.history = new ScriptConsoleHistory();

        this.session = new ScriptConsoleSession();
        addListener(this.session);

        partitioner = new ScriptConsolePartitioner();
        getDocument().setDocumentPartitioner(partitioner);
        partitioner.connect(getDocument());
    }

    /**
     * @return the assistant that should handle content assist requests (code completion)
     */
    protected abstract IContentAssistProcessor createConsoleCompletionProcessor(ContentAssistant pyContentAssistant);

    /**
     * @return the assistant that should handle quick assist requests (quick fixes)
     */
    protected abstract IQuickAssistProcessor createConsoleQuickAssistProcessor(QuickAssistAssistant quickAssist);

    /**
     * @return the text hover to be used in the console.
     */
    protected abstract ITextHover createHover();

    /**
     * @return the console prompt that should be used.
     */
    protected abstract ScriptConsolePrompt createConsolePrompt();

    public IScriptConsoleSession getSession() {
        return session;
    }

    public void addListener(IScriptConsoleListener listener) {
        consoleListeners.add(listener);
    }

    public void removeListener(IScriptConsoleListener listener) {
        consoleListeners.remove(listener);
    }

    protected void setInterpreter(IScriptConsoleInterpreter interpreter) {
        this.interpreter = interpreter;
    }

    public IScriptConsoleInterpreter getInterpreter() {
        return interpreter;
    }

    public ScriptConsolePrompt getPrompt() {
        return prompt;
    }

    public ScriptConsoleHistory getHistory() {
        return history;
    }

    /**
     * Creates the actual page to be shown to the user.
     */
    @Override
    public IPageBookViewPage createPage(IConsoleView view) {
        page = new ScriptConsolePage(this, view, createSourceViewerConfiguration());
        return page;
    }

    public abstract SourceViewerConfiguration createSourceViewerConfiguration();

    /**
     * Clears the console
     */
    @Override
    public void clearConsole() {
        page.clearConsolePage();
    }

    @Override
    public void setOnContentsReceivedCallback(ICallback<Object, Tuple<String, String>> onContentsReceived) {
        interpreter.setOnContentsReceivedCallback(onContentsReceived);
    }

    @Override
    public boolean isOnStateWhereCommandHandlingShouldStop(String commandLine) {
        final Object[] listeners = consoleListeners.getListeners();

        for (Object listener : listeners) {
            if (((IScriptConsoleListener) listener).isOnStateWhereCommandHandlingShouldStop(commandLine)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void beforeHandleCommand(String userInput, ICallback<Object, InterpreterResponse> onResponseReceived) {
        final Object[] listeners = consoleListeners.getListeners();

        //notify about the user request in the UI thread.
        for (Object listener : listeners) {
            ((IScriptConsoleListener) listener).userRequest(userInput, prompt);
        }
    }

    /**
     * Handles some command that the user entered
     *
     * @param userInput that's the command to be evaluated by the user.
     */
    @Override
    public void handleCommand(String userInput, final ICallback<Object, InterpreterResponse> onResponseReceived) {
        final Object[] listeners = consoleListeners.getListeners();

        //executes the user input in the interpreter
        if (interpreter != null) {
            interpreter.exec(userInput, new ICallback<Object, InterpreterResponse>() {

                @Override
                public Object call(final InterpreterResponse response) {
                    //sets the new mode
                    prompt.setMode(!response.more);
                    prompt.setNeedInput(response.need_input);

                    //notify about the console answer (not in the UI thread).
                    for (Object listener : listeners) {
                        ((IScriptConsoleListener) listener).interpreterResponse(response, prompt);
                    }
                    onResponseReceived.call(response);
                    return null;
                }
            });
        }

    }

    /**
     * Fetch the current completions for the content presented in the user's ipython console
     */
    @Override
    public ICompletionProposal[] getTabCompletions(String commandLine, int cursorPosition) {
        try {
            ICompletionProposal[] completions = interpreter.getCompletions(viewer.get(), commandLine, cursorPosition,
                    cursorPosition, AbstractCompletionProcessorWithCycling.SHOW_FOR_TAB_COMPLETIONS);
            return completions;
        } catch (Exception e) {
        }
        return new ICompletionProposal[0];
    }

    /**
     * Finishes the interpreter (and stops the communication)
     */
    public void terminate() {
        try {
            if (history != null) {
                history.close();
            }
            interpreter.close();
        } catch (Exception e) {
        }
        history = null;
        interpreter = null;
    }

    /**
     * Interrupts the interpreter
     */
    public void interrupt() {
        try {
            interpreter.interrupt();
            getViewer().discardCommandLine();
        } catch (Exception e) {
        }
    }

    public void setViewer(ScriptConsoleViewer scriptConsoleViewer) {
        this.viewer = new WeakReference<ScriptConsoleViewer>(scriptConsoleViewer);
    }

    public ScriptConsoleViewer getViewer() {
        if (this.viewer != null) {
            return this.viewer.get();
        }
        return null;
    }

    /**
     * Must be overridden to create a style provider for the console (configures colors)
     * @return a style provider.
     */
    public abstract IConsoleStyleProvider createStyleProvider();

    /**
     * @return a list of trackers that'll identify links in the console passed.
     */
    public abstract List<IConsoleLineTracker> createLineTrackers(final TextConsole console);

    /**
     * @return the commands that should be initially set in the prompt.
     */
    public abstract String getInitialCommands();

    /**
     * Used for backward compatibility because the setBackground/getBackground is not available for eclipse 3.2
     */
    private Color fPydevConsoleBackground;

    /**
     * Used for backward compatibility because the setBackground/getBackground is not available for eclipse 3.2
     */
    public Color getPydevConsoleBackground() {
        try {
            Color ret = (Color) Reflection.invoke(this, "getBackground");
            return ret;
        } catch (Throwable e) {
            //not available in eclipse 3.2
            return fPydevConsoleBackground;
        }
    }

    /**
     * Used for backward compatibility because the setBackground/getBackground is not available for eclipse 3.2
     */
    public void setPydevConsoleBackground(Color color) {
        try {
            Reflection.invoke(this, "setBackground", color);
        } catch (Throwable e) {
            //not available in eclipse 3.2
            fPydevConsoleBackground = color;
        }
    }

    public Object getInterpreterInfo() {
        return this.interpreter.getInterpreterInfo();
    }

    /**
     * @return
     */
    public abstract boolean getFocusOnStart();

    public abstract boolean getTabCompletionEnabled();

    /**
     * Enable/Disable linking of the debug console with the suspended frame.
     */
    public void linkWithDebugSelection(boolean isLinkedWithDebug) {
        this.interpreter.linkWithDebugSelection(isLinkedWithDebug);
    }

    public abstract AbstractHandleBackspaceAction getBackspaceAction();

    public abstract void createActions(IToolBarManager toolbarManager);

    public abstract IHandleScriptAutoEditStrategy getAutoEditStrategy();
}
