package com.josesamuel.logviewer.view;

import com.android.ddmlib.logcat.LogCatHeader;
import com.android.ddmlib.logcat.LogCatMessage;
import com.android.tools.idea.actions.BrowserHelpAction;
import com.android.tools.idea.ddms.DeviceContext;
import com.android.tools.idea.logcat.*;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.diagnostic.logging.*;
import com.intellij.execution.impl.ConsoleViewImpl;
import com.intellij.execution.process.ProcessOutputTypes;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.icons.AllIcons;
import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.FoldRegion;
import com.intellij.openapi.editor.SelectionModel;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.SideBorder;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.UIUtil;
import com.josesamuel.logviewer.gist.GistCreator;
import com.josesamuel.logviewer.log.LogDataListener;
import com.josesamuel.logviewer.log.LogProcess;
import com.josesamuel.logviewer.log.LogSource;
import com.josesamuel.logviewer.log.LogSourceManager;
import com.josesamuel.logviewer.log.dnd.DnDHandler;
import com.josesamuel.logviewer.util.SingleTaskBackgroundExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.*;
import java.util.List;
import java.util.Timer;
import java.util.concurrent.ConcurrentHashMap;

/**
 * The main view of the logviewer
 *
 * @author js
 */
public abstract class LogView implements Disposable, GistCreator.GistListener, LogSourceManager.LogSourceManagerListener, LogDataListener {

    private static final String CONSOLE_VIEW_POPUP_MENU = "ConsoleView.PopupMenu";
    private static final String groupName = "LogViewer Folding";
    private static final long RESET_DELAY = 600;
    private final Project myProject;
    private final AndroidLogConsole myLogConsole;
    private LogSourceManager logSourceManager;
    private LogSourcePanel logSourcePanel;
    private JPanel myPanel;
    private JPanel editorPanel;
    private JSplitPane splitPane;
    private JCheckBox vCheckBox;
    private JCheckBox dCheckBox;
    private JCheckBox iCheckBox;
    private JList processList;
    private JTextArea textFilters;
    private JCheckBox wCheckBox;
    private JCheckBox eCheckBox;
    private JCheckBox aCheckBox;
    private JLabel processFilterTitle;
    private volatile LogSource myLogSource;
    private boolean filterOpen;
    private DefaultListModel<LogProcess> processListModel;
    private java.util.Set<LogProcess> processFilter;
    private java.util.Set<String> addFilters;
    private java.util.Set<String> removeFilters;
    private GistCreator gistCreator;
    private LogViewerFilterModel myLogFilterModel = new LogViewerFilterModel();
    private AndroidLogcatFormatter logFormatter;
    private int defaultCycleBufferSize;
    private Timer timer;
    private long textFilterUpdateCreateTime;
    private boolean liveLogPaused;

    /**
     * Initialize the views
     */
    public LogView(final Project project, @Nullable DeviceContext deviceContext, int defaultCycleBufferSize) {
        myProject = project;
        gistCreator = new GistCreator();
        this.defaultCycleBufferSize = defaultCycleBufferSize;
        logSourceManager = new LogSourceManager(project, deviceContext, this);

        processFilter = Collections.newSetFromMap(new ConcurrentHashMap<LogProcess, Boolean>());
        addFilters = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
        removeFilters = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());

        logFormatter = new AndroidLogcatFormatter(AndroidLogcatPreferences.getInstance(project));
        myLogConsole = new AndroidLogConsole(myProject, myLogFilterModel, logFormatter);
        timer = new Timer(true);

        //Update tasks
        Runnable processUpdateTask = () -> {
            processFilter.clear();
            for (Object selection : processList.getSelectedValuesList()) {
                processFilter.add((LogProcess) selection);
            }
            myLogConsole.refresh("Filtering process");
        };
        Runnable textUpdateTask = this::resetTextFilters;
        Runnable levelUpdateTask = () -> myLogConsole.refresh("Applying level filters");


        //initialize the process list
        processList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        processListModel = new DefaultListModel<LogProcess>();
        processList.setModel(processListModel);
        processList.setCellRenderer(new ClientCellRenderer());
        processList.addListSelectionListener(listSelectionEvent -> {
            if (!listSelectionEvent.getValueIsAdjusting()) {
                resetUpdateTimer(processUpdateTask);
            }
        });


        //set up filters
        textFilters.addKeyListener(new KeyAdapter() {
            @Override
            public void keyReleased(KeyEvent keyEvent) {
                super.keyReleased(keyEvent);
                resetUpdateTimer(textUpdateTask);
            }
        });

        //level checkboxes
        ItemListener levelChangeListener = itemEvent -> resetUpdateTimer(levelUpdateTask);

        vCheckBox.addItemListener(levelChangeListener);
        dCheckBox.addItemListener(levelChangeListener);
        iCheckBox.addItemListener(levelChangeListener);
        wCheckBox.addItemListener(levelChangeListener);
        eCheckBox.addItemListener(levelChangeListener);
        aCheckBox.addItemListener(levelChangeListener);


        filterOpen = false;
        splitPane.setDividerLocation(0);
        splitPane.setDividerSize(0);

        JComponent consoleComponent = myLogConsole.getComponent();

        final ConsoleView console = myLogConsole.getConsole();

        if (console != null) {
            final ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar("LogViewer",
                    getEditorActions(), false);
            toolbar.setTargetComponent(console.getComponent());

            final JComponent tbComp1 = toolbar.getComponent();
            myPanel.add(tbComp1, BorderLayout.WEST);
        }

        logSourcePanel = new LogSourcePanel(deviceContext);
        JPanel panel = logSourcePanel.getComponent();
        panel.setBorder(IdeBorderFactory.createBorder(SideBorder.BOTTOM));
        myPanel.add(panel, BorderLayout.NORTH);
        editorPanel.add(consoleComponent, BorderLayout.CENTER);
        Disposer.register(myProject, this);
        Disposer.register(this, myLogConsole);

        updateLogConsole();
        DnDHandler dnDHandler = new DnDHandler(logSourceManager);
        dnDHandler.addDndSupportForComponent(myPanel);
    }

    /**
     * Add the fold related actions to the popup
     */
    private void addFoldingMenu() {
        removeFoldingMenu();
        ActionManager actionManager = ActionManager.getInstance();
        DefaultActionGroup actionGroup = (DefaultActionGroup) actionManager.getAction(CONSOLE_VIEW_POPUP_MENU);
        if (actionGroup != null) {
            DefaultActionGroup foldGroup = new DefaultActionGroup("LogViewer Folding", true);
            foldGroup.add(createFoldTopToCurrentAction());
            foldGroup.add(createFoldSelectiontAction());
            foldGroup.add(createFoldRestAction());
            foldGroup.add(createClearFoldAction());
            actionGroup.add(foldGroup);
        }
    }

    /**
     * Remove custom folding menu
     */
    private void removeFoldingMenu() {
        ActionManager actionManager = ActionManager.getInstance();
        DefaultActionGroup actionGroup = (DefaultActionGroup) actionManager.getAction(CONSOLE_VIEW_POPUP_MENU);

        if (actionGroup != null) {
            for (AnAction action : actionGroup.getChildActionsOrStubs()) {
                if (action.getTemplatePresentation() != null
                        && action.getTemplatePresentation().getText() != null &&
                        action.getTemplatePresentation().getText().equals(groupName)) {
                    actionGroup.remove(action);
                }
            }
        }
    }

    /**
     * Updates the refresh timer
     */
    private void resetUpdateTimer(Runnable task) {
        textFilterUpdateCreateTime = System.currentTimeMillis();
        if (timer == null) {
            timer = new Timer(true);
        }
        timer.schedule(new FilterApplyTask(textFilterUpdateCreateTime, task), RESET_DELAY);
    }

    /**
     * Resets the text filters with the current data
     */
    private void resetTextFilters() {
        addFilters.clear();
        removeFilters.clear();
        StringTokenizer tokenizer = new StringTokenizer(textFilters.getText(), "\n");
        String token;
        while (tokenizer.hasMoreElements()) {
            token = tokenizer.nextToken().trim().toLowerCase();
            if (!token.isEmpty()) {
                if (token.startsWith("-")) {
                    if (token.length() > 1) {
                        removeFilters.add(token.substring(1));
                    }
                } else {
                    if (token.startsWith("\\-")) {
                        token = token.substring(1);
                    }
                    addFilters.add(token);
                }
            }
        }
        myLogConsole.refresh("Applying Text Filters");
    }

    /**
     * Returns the updated ActionGroup for the editor
     */
    private ActionGroup getEditorActions() {
        DefaultActionGroup editorActions = new DefaultActionGroup();
        editorActions.addSeparator();
        editorActions.add(new ToggleAction("Show/Hide Filters", "Configure Log Viewer Filters", AllIcons.General.Filter) {

            @Override
            public boolean isSelected(AnActionEvent anActionEvent) {
                return filterOpen;
            }

            @Override
            public void setSelected(AnActionEvent anActionEvent, boolean b) {
                filterOpen = b;
                if (filterOpen) {
                    splitPane.setDividerLocation(400);
                    splitPane.setDividerSize(3);
                } else {
                    splitPane.setDividerLocation(0);
                    splitPane.setDividerSize(0);
                }
            }
        });
        editorActions.add(new ToggleAction("Pause/Resume logs", "Pause or Resume live logs", AllIcons.Actions.Pause) {

            @Override
            public boolean isSelected(AnActionEvent anActionEvent) {
                return liveLogPaused;
            }

            @Override
            public void setSelected(AnActionEvent anActionEvent, boolean b) {
                liveLogPaused = b;
            }

            @Override
            public void update(@NotNull AnActionEvent e) {
                super.update(e);
                e.getPresentation().setEnabled(logSourceManager.isDeviceSourceSelected());
            }
        });

        editorActions.add(createGistAction());
        editorActions.add(new BrowserHelpAction("LogViewer", "https://josesamuel.com/logviewer/"));
        editorActions.addSeparator();
        editorActions.add(myLogConsole.getOrCreateActions());
        editorActions.add(new MyConfigureLogcatHeaderAction());
        return editorActions;
    }

    /**
     * Returns an action that creates a gist
     */
    private AnAction createGistAction() {
        return new AnAction("Share Log", "Share log using Gist", AllIcons.Actions.Share) {
            @Override
            public void actionPerformed(AnActionEvent anActionEvent) {
                gistCreator.createGist(myProject, myLogConsole.getSelectedText(true), LogView.this);
            }
        };
    }

    /**
     * Action to fold from top
     */
    private AnAction createFoldTopToCurrentAction() {
        return new AnAction("Fold from top") {
            @Override
            public void actionPerformed(AnActionEvent e) {
                ConsoleView console = myLogConsole.getConsole();
                Editor myEditor = console != null ? CommonDataKeys.EDITOR.getData((DataProvider) console) : null;
                if (myEditor != null) {
                    myEditor.getFoldingModel().runBatchFoldingOperation(() -> {
                        SelectionModel selectionModel = myEditor.getSelectionModel();
                        int start = 0;
                        int end = selectionModel.getSelectionStart();
                        FoldRegion foldRegion = myEditor.getFoldingModel().addFoldRegion(start, end,
                                "...");
                        if (foldRegion != null) {
                            foldRegion.setExpanded(false);
                        }
                    }, true);
                }
            }

            @Override
            public void update(AnActionEvent e) {
                ConsoleView console = myLogConsole.getConsole();
                Editor myEditor = console != null ? CommonDataKeys.EDITOR.getData((DataProvider) console) : null;
                e.getPresentation().setEnabled(e.getProject() == myProject && myEditor != null && myEditor.getSelectionModel().getSelectionStart() > 0);
            }
        };
    }

    /**
     * Action to fold selection
     */
    private AnAction createFoldSelectiontAction() {
        return new AnAction("Fold selection") {
            @Override
            public void actionPerformed(AnActionEvent e) {
                ConsoleView console = myLogConsole.getConsole();
                Editor myEditor = console != null ? CommonDataKeys.EDITOR.getData((DataProvider) console) : null;
                if (myEditor != null && myEditor.getSelectionModel().hasSelection()) {
                    myEditor.getFoldingModel().runBatchFoldingOperation(() -> {
                        SelectionModel selectionModel = myEditor.getSelectionModel();
                        int start = selectionModel.getSelectionStart();
                        int end = selectionModel.getSelectionEnd();
                        FoldRegion foldRegion = myEditor.getFoldingModel().addFoldRegion(start, end,
                                "...");
                        if (foldRegion != null) {
                            foldRegion.setExpanded(false);
                        }

                    }, true);
                }
            }

            @Override
            public void update(AnActionEvent e) {
                ConsoleView console = myLogConsole.getConsole();
                Editor myEditor = console != null ? CommonDataKeys.EDITOR.getData((DataProvider) console) : null;
                e.getPresentation().setEnabled(e.getProject() == myProject && myEditor != null && myEditor.getSelectionModel().hasSelection());
            }
        };
    }

    /**
     * Action to fold rest
     */
    private AnAction createFoldRestAction() {
        return new AnAction("Fold till end") {
            @Override
            public void actionPerformed(AnActionEvent e) {
                ConsoleView console = myLogConsole.getConsole();
                Editor myEditor = console != null ? CommonDataKeys.EDITOR.getData((DataProvider) console) : null;
                if (myEditor != null) {
                    myEditor.getFoldingModel().runBatchFoldingOperation(() -> {
                        SelectionModel selectionModel = myEditor.getSelectionModel();
                        int start = selectionModel.getSelectionStart();
                        int end = myEditor.getDocument().getTextLength();
                        FoldRegion foldRegion = myEditor.getFoldingModel().addFoldRegion(start, end,
                                "...");

                        if (foldRegion != null) {
                            foldRegion.setExpanded(false);
                        }

                    }, true);
                }
            }

            @Override
            public void update(AnActionEvent e) {
                ConsoleView console = myLogConsole.getConsole();
                Editor myEditor = console != null ? CommonDataKeys.EDITOR.getData((DataProvider) console) : null;
                e.getPresentation().setEnabled(e.getProject() == myProject && myEditor != null && myEditor.getSelectionModel().getSelectionStart() < myEditor.getDocument().getTextLength());
            }
        };
    }

    /**
     * Action to clear folds
     */
    private AnAction createClearFoldAction() {
        return new AnAction("Clear all folds") {
            @Override
            public void actionPerformed(AnActionEvent e) {
                ConsoleView console = myLogConsole.getConsole();
                Editor myEditor = console != null ? CommonDataKeys.EDITOR.getData((DataProvider) console) : null;
                if (myEditor != null) {
                    myEditor.getFoldingModel().runBatchFoldingOperation(() -> {
                        for (FoldRegion foldRegion : myEditor.getFoldingModel().getAllFoldRegions()) {
                            myEditor.getFoldingModel().removeFoldRegion(foldRegion);
                        }
                    }, true);
                }
            }

            @Override
            public void update(AnActionEvent e) {
                ConsoleView console = myLogConsole.getConsole();
                Editor myEditor = console != null ? CommonDataKeys.EDITOR.getData((DataProvider) console) : null;
                e.getPresentation().setEnabled(e.getProject() == myProject && myEditor != null && myEditor.getFoldingModel().getAllFoldRegions().length > 0);
            }
        };
    }


    protected abstract boolean isActive();

    /**
     * Shows the given message as nint
     */
    public void showHint(String message) {
        ConsoleView console = myLogConsole.getConsole();
        Editor myEditor = console != null ? CommonDataKeys.EDITOR.getData((DataProvider) console) : null;
        if (myEditor != null) {
            HintManager.getInstance().showInformationHint(myEditor, message);
        }
    }


    /**
     * Activate the view
     */
    public final void activate() {
        if (isActive()) {
            onSourceListChanged();
            updateLogConsole();
        }
        if (myLogConsole != null) {
            myLogConsole.activate();
        }
        addFoldingMenu();
    }

    /**
     * Deactivates the view
     */
    public void deactivate() {
        logSourceManager.clearFileSources();
        removeFoldingMenu();
    }

    /**
     * Update the log console. Unregisters any previous source, and register with current source
     */
    private void updateLogConsole() {
        LogSource logSource = logSourceManager.getSelectedSource();
        if (myLogSource != logSource) {
            liveLogPaused = false;
            processFilter.clear();
            processListModel.clear();
            if (myLogSource != null) {
                myLogSource.getLogProvider().unRegisterLogListener(this);
            }
            if (myLogConsole.getConsole() != null) {
                myLogConsole.clear();
            }
            myLogFilterModel.processingStarted();
            myLogSource = logSource;
            if (myLogSource != null) {
                myLogSource.getLogProvider().registerLogListener(this);
            }
            if (logSourceManager.isDeviceSourceSelected()) {
                processFilterTitle.setText("Process");
            } else {
                processFilterTitle.setText("Tags");
            }
        }
    }


    @NotNull
    public final JPanel getContentPanel() {
        return myPanel;
    }

    @Override
    public final void dispose() {
        logSourceManager.dispose();
    }

    /**
     * Check whether the given line can be shown based on current filters
     */
    private boolean canAccept(LogCatMessage line) {
        return canAcceptLevel(line) && canAcceptProcess(line) && canAcceptMessage(line);
    }

    /**
     * Check whether the  log level of the given message is acceptable
     */
    private boolean canAcceptLevel(LogCatMessage line) {
        switch (line.getHeader().getLogLevel()) {
            case ASSERT:
                return aCheckBox.isSelected();
            case DEBUG:
                return dCheckBox.isSelected();
            case ERROR:
                return eCheckBox.isSelected();
            case INFO:
                return iCheckBox.isSelected();
            case VERBOSE:
                return vCheckBox.isSelected();
            case WARN:
                return wCheckBox.isSelected();
        }
        return true;
    }

    /**
     * Check whether the  process of the given message is acceptable
     */
    private boolean canAcceptProcess(LogCatMessage line) {
        if (processFilter.isEmpty()) {
            return true;
        }
        boolean accept = true;
        if (logSourceManager.isDeviceSourceSelected()) {
            String appName = line.getAppName();
            if (appName != null) {
                accept = canAcceptProcessName(line.getAppName());
                if (!accept && appName.indexOf('.') == -1) {
                    accept = canAcceptProcessName(line.getTag());
                }
            }
            if (accept && line.getPid() != 0) {
                accept = canAcceptPid(line.getPid());
            }
        } else {
            if (line.getTag() != null) {
                accept = canAcceptProcessName(line.getTag());
            }
            if (accept && line.getPid() != 0) {
                accept = canAcceptPid(line.getPid());
            }
        }
        return accept;
    }

    /**
     * Check whether the  process name of the given message is acceptable
     */
    private boolean canAcceptProcessName(String processName) {
        for (LogProcess logProcess : processFilter) {
            if (logProcess.getProcessName().equalsIgnoreCase(processName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check whether the  process id of the given message is acceptable
     */
    private boolean canAcceptPid(int pid) {
        for (LogProcess client : processFilter) {
            if (client.getProcessID() == pid) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check whether the message of the given message is acceptable
     */
    private boolean canAcceptMessage(LogCatMessage line) {
        return (addFilters.isEmpty() || isInFilter(line, addFilters))
                && (removeFilters.isEmpty() || !isInFilter(line, removeFilters));
    }

    /**
     * Check whether the message of the given message is acceptable
     */
    private boolean canAcceptMessage(String line) {
        return (addFilters.isEmpty() || isInFilter(line, addFilters))
                && (removeFilters.isEmpty() || !isInFilter(line, removeFilters));
    }

    /**
     * Check whether given message matches with any filter in the given set
     */
    private boolean isInFilter(LogCatMessage line, Set<String> filter) {
        return isInFilter(line.getMessage(), filter) || isInFilter(line.getTag(), filter) || isInFilter(line.getAppName(), filter) || isInFilter("" + line.getPid(), filter);
    }

    private boolean isInFilter(String text, Set<String> filters) {
        if (text != null) {
            text = text.toLowerCase();
            for (String filter : filters) {
                if (text.contains(filter)) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public void onGistCreated(final String gistUrl) {
        UIUtil.invokeLaterIfNeeded(() -> BrowserUtil.browse(gistUrl));
    }

    @Override
    public void onGistFailed(String ex) {
        debug(ex);
    }

    @Override
    public void onSourceSelectionChanged() {
        UIUtil.invokeLaterIfNeeded(this::updateLogConsole);
    }

    @Override
    public void onSourceListChanged() {
        UIUtil.invokeLaterIfNeeded(() -> {
            if (!myProject.isDisposed() && logSourcePanel != null && logSourceManager != null) {
                logSourcePanel.updateDeviceCombo(logSourceManager.getLogSourceList(), logSourceManager.getSelectedSource());
                updateLogConsole();
            }
        });
    }

    @Override
    public void onLogLine(String log, LogProcess logProcess) {
        if (!liveLogPaused) {
            myLogConsole.addLogLine(log);
            if (!processListModel.contains(logProcess)) {
                processListModel.addElement(logProcess);
            }
        }
    }

    @Override
    public void onLogData(String log) {
        myLogConsole.addLogData(log);
        myLogConsole.refresh("Loading logs");
    }

    @Override
    public void onCleared() {
        myLogConsole.clear();
    }

    @Override
    public void debug(String log) {
        if (log != null) {
            UIUtil.invokeLaterIfNeeded(() -> {
                myLogConsole.addLogLine(log);
            });
        }
    }


    @Override
    public void onProcessList(Set<LogProcess> processList) {
        ApplicationManager.getApplication().executeOnPooledThread(() -> {
            final List<LogProcess> sortedList = new ArrayList<>(processList);
            sortedList.sort((l, r) -> {
                int c = l.getProcessName().compareTo(r.getProcessName());
                if (c == 0) {
                    c = l.getProcessID() < r.getProcessID() ? -1 : 1;
                }
                return c;
            });
            UIUtil.invokeLaterIfNeeded(() -> {
                processListModel.removeAllElements();

                for (LogProcess client : sortedList) {
                    processListModel.addElement(client);
                }
            });
        });

    }

    /**
     * console that shows the messages
     */
    final class AndroidLogConsole extends LogConsoleBase {
        private final RegexFilterComponent myRegexFilterComponent = new RegexFilterComponent("LOG_FILTER_HISTORY", 5);
        private final AndroidLogcatPreferences myPreferences;

        AndroidLogConsole(Project project, LogFilterModel logFilterModel, LogFormatter logFormatter) {
            super(project, null, "", false, logFilterModel, GlobalSearchScope.allScope(project), logFormatter);
            myPreferences = AndroidLogcatPreferences.getInstance(project);
            myRegexFilterComponent.setFilter(myPreferences.TOOL_WINDOW_CUSTOM_FILTER);
            myRegexFilterComponent.setIsRegex(myPreferences.TOOL_WINDOW_REGEXP_FILTER);
            myRegexFilterComponent.addRegexListener(filter -> {
                myPreferences.TOOL_WINDOW_CUSTOM_FILTER = filter.getFilter();
                myPreferences.TOOL_WINDOW_REGEXP_FILTER = filter.isRegex();
            });
        }

        @Override
        public boolean isActive() {
            return LogView.this.isActive();
        }

        @NotNull
        @Override
        protected Component getTextFilterComponent() {
            return myRegexFilterComponent;
        }

        void addLogLine(@NotNull String line) {
            super.addMessage(line);
            if (getOriginalDocument().length() > defaultCycleBufferSize) {
                if (getConsole() != null) {
                    ((ConsoleViewImpl) getConsole()).flushDeferredText();
                }
                getOriginalDocument().delete(0, defaultCycleBufferSize / 4);
                refresh("Clearing old logs");
            }
        }

        /**
         * Adds the bulk log data
         */
        void addLogData(String logData) {
            getOriginalDocument().append(logData).append("\n");
        }

        /**
         * Refreshes the log console in background
         */
        void refresh(final String task) {
            SingleTaskBackgroundExecutor.executeIfPossible(myProject, new SingleTaskBackgroundExecutor.BackgroundTask() {
                @Override
                public void run(ProgressIndicator progressIndicator) {
                    try {
                        UIUtil.invokeAndWaitIfNeeded((Runnable) () -> {
                            progressIndicator.setFraction(0);
                            doFilter(progressIndicator);
                        });
                    } catch (Throwable ex) {
                        debug("Exception " + ex.getMessage());
                    }
                }

                @Override
                public String getTaskName() {
                    return task;
                }
            });
        }


        /**
         * Filters the console
         */
        private synchronized void doFilter(ProgressIndicator progressIndicator) {
            final ConsoleView console = getConsole();
            String allLInes = getOriginalDocument().toString();
            final String[] lines = allLInes.split("\n");
            if (console != null) {
                console.clear();
            }
            myLogFilterModel.processingStarted();
            int size = lines.length;
            float current = 0;
            for (String line : lines) {
                printMessageToConsole(line);
                current++;
                progressIndicator.setFraction(current / size);
            }
            if (console != null) {
                ((ConsoleViewImpl) console).requestScrollingToEnd();
            }
        }

        /**
         * Prints the message to console
         */
        private void printMessageToConsole(String line) {
            final ConsoleView console = getConsole();
            final LogFilterModel.MyProcessingResult processingResult = myLogFilterModel.processLine(line);
            if (processingResult.isApplicable()) {
                final Key key = processingResult.getKey();
                if (key != null) {
                    ConsoleViewContentType type = ConsoleViewContentType.getConsoleViewType(key);
                    if (type != null) {
                        final String messagePrefix = processingResult.getMessagePrefix();
                        if (messagePrefix != null) {
                            String formattedPrefix = logFormatter.formatPrefix(messagePrefix);
                            if (console != null) {
                                console.print(formattedPrefix, type);
                            }
                        }
                        String formattedMessage = logFormatter.formatMessage(line);
                        if (console != null) {
                            console.print(formattedMessage + "\n", type);
                        }
                    }
                }
            }
        }


        /**
         * Returns the selected text if there is any selection. If not return all based on parameter
         *
         * @param defaultToAll If no selection, then this decides whether to return all text
         */
        String getSelectedText(boolean defaultToAll) {
            ConsoleView console = this.getConsole();
            Editor myEditor = console != null ? (Editor) CommonDataKeys.EDITOR.getData((DataProvider) console) : null;
            if (myEditor != null) {
                Document document = myEditor.getDocument();
                final SelectionModel selectionModel = myEditor.getSelectionModel();
                if (selectionModel.hasSelection()) {
                    return selectionModel.getSelectedText();
                } else if (defaultToAll) {
                    return document.getText();
                }
            }
            return null;
        }
    }

    /**
     * Action that performs the configuration
     */
    private final class MyConfigureLogcatHeaderAction extends AnAction {
        MyConfigureLogcatHeaderAction() {
            super("Configure Header", "Configure Header", AllIcons.General.GearPlain);
        }

        @Override
        public void actionPerformed(AnActionEvent e) {
            ConfigureLogcatFormatDialog dialog = new ConfigureLogcatFormatDialog(myProject);
            if (dialog.showAndGet()) {
                myLogConsole.refresh("Reloading logs");
            }
        }
    }

    /**
     * filter model
     */
    private class LogViewerFilterModel extends LogFilterModel {

        private final List<LogFilterListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
        private final StringBuilder myMessageSoFar = new StringBuilder();
        @Nullable
        private LogCatHeader myPrevHeader;
        private boolean myCustomApplicable = false;
        private List<? extends LogFilter> filters = new ArrayList();

        @Override
        public String getCustomFilter() {
            return "";
        }

        @Override
        public void addFilterListener(LogFilterListener listener) {
            this.myListeners.add(listener);
        }

        @Override
        public void removeFilterListener(LogFilterListener listener) {
            this.myListeners.remove(listener);
        }

        @Override
        public List<? extends LogFilter> getLogFilters() {
            return filters;
        }

        @Override
        public boolean isFilterSelected(LogFilter filter) {
            return false;
        }

        @Override
        public void selectFilter(LogFilter filter) {
        }

        public void processingStarted() {
            this.myPrevHeader = null;
            this.myCustomApplicable = false;
            this.myMessageSoFar.setLength(0);
        }

        private boolean isMessageApplicable(LogCatMessage message) {
            return canAccept(message);
        }

        @NotNull
        @Override
        public MyProcessingResult processLine(String line) {
            LogCatMessage message = null;
            String continuation = null;
            boolean validContinuation = false;
            try {
                message = AndroidLogcatFormatter.tryParseMessage(line);
                continuation = message == null ? AndroidLogcatFormatter.tryParseContinuation(line) : null;
                validContinuation = continuation != null && this.myPrevHeader != null;
            } catch (Exception ignored) {
            }

            if (message == null && !validContinuation) {
                return new MyProcessingResult(ProcessOutputTypes.STDOUT, canAcceptMessage(line), null);
            } else {
                if (message != null) {
                    this.myPrevHeader = message.getHeader();
                    this.myCustomApplicable = this.isMessageApplicable(message);
                    this.myMessageSoFar.setLength(0);
                }

                boolean isApplicable = this.myCustomApplicable;
                if (!isApplicable) {
                    this.myMessageSoFar.append(line);
                    this.myMessageSoFar.append('\n');
                }

                Key key = AndroidLogcatUtils.getProcessOutputType(this.myPrevHeader.getLogLevel());
                MyProcessingResult result = new MyProcessingResult(key, isApplicable, this.myMessageSoFar.toString());
                if (isApplicable) {
                    this.myMessageSoFar.setLength(0);
                }

                return result;
            }
        }
    }

    /**
     * A task that runs only if it is the current task.
     */
    class FilterApplyTask extends TimerTask {

        private long createTme;
        private Runnable task;

        FilterApplyTask(long createTme, Runnable task) {
            this.createTme = createTme;
            this.task = task;
        }

        @Override
        public void run() {
            try {
                if (this.createTme == textFilterUpdateCreateTime) {
                    UIUtil.invokeAndWaitIfNeeded((Runnable) () -> {
                        try {
                            task.run();
                        } catch (Exception ignored) {
                        }
                    });
                }
            } catch (Exception t) {
                debug(t.getMessage());
            }
        }
    }
}