package floobits;

import com.intellij.AppTopics;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileDocumentManagerListener;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileAdapter;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.VirtualFilePropertyEvent;
import com.intellij.openapi.vfs.newvfs.BulkFileListener;
import com.intellij.openapi.vfs.newvfs.events.*;
import com.intellij.util.messages.MessageBusConnection;
import floobits.common.EditorEventHandler;
import floobits.common.Ignore;
import floobits.common.interfaces.IFile;
import floobits.impl.ContextImpl;
import floobits.impl.DocImpl;
import floobits.impl.FactoryImpl;
import floobits.impl.FileImpl;
import floobits.utilities.Flog;
import floobits.utilities.IntelliUtils;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

public class Listener implements BulkFileListener, DocumentListener, SelectionListener, FileDocumentManagerListener, VisibleAreaListener, CaretListener {
    public final AtomicBoolean isListening = new AtomicBoolean(false);
    public final AtomicBoolean isSaving = new AtomicBoolean(false);
    private final ContextImpl context;
    private EditorEventHandler editorManager;
    private VirtualFileAdapter virtualFileAdapter;
    private MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect();
    private EditorEventMulticaster em = EditorFactory.getInstance().getEventMulticaster();
    private String oldRenamePath;
    private ArrayList<ArrayList<Integer>> ranges = new ArrayList<ArrayList<Integer>>();


    public Listener(ContextImpl context) {
        this.context = context;
    }

    public synchronized void start(final EditorEventHandler editorManager) {
        this.editorManager = editorManager;
        connection.subscribe(VirtualFileManager.VFS_CHANGES, this);
        connection.subscribe(AppTopics.FILE_DOCUMENT_SYNC, this);
        em.addDocumentListener(this);
        em.addSelectionListener(this);
        em.addCaretListener(this);
        em.addVisibleAreaListener(this);


        virtualFileAdapter = new VirtualFileAdapter() {
            public void beforePropertyChange(@NotNull final VirtualFilePropertyEvent event) {
                if (!event.getPropertyName().equals(VirtualFile.PROP_NAME)) {
                    return;
                }
                VirtualFile parent = event.getParent();
                if (parent == null) {
                    return;
                }
                String parentPath = parent.getPath();
                // XXX: pretty sure is this wrong.
                String newValue = parentPath + "/" + event.getNewValue().toString();
                String oldValue = parentPath + "/" + event.getOldValue().toString();
                editorManager.rename(oldValue, newValue);
            }
        };
        VirtualFileManager.getInstance().addVirtualFileListener(virtualFileAdapter);
    }

    public synchronized void shutdown() {
        if (connection != null) {
            connection.disconnect();
            connection = null;
        }
        if (em != null) {
            em.removeSelectionListener(this);
            em.removeDocumentListener(this);
            em.removeCaretListener(this);
            em.removeVisibleAreaListener(this);
            em = null;
        }
        if (virtualFileAdapter != null) {
            VirtualFileManager.getInstance().removeVirtualFileListener(virtualFileAdapter);
            virtualFileAdapter = null;
        }

    }

    @Override
    public void fileWithNoDocumentChanged(@NotNull VirtualFile file) {
       Flog.debug("%s change but has no document.", file.getPath());
    }

    @Override
    public void beforeDocumentSaving(@NotNull Document document) {
        if (isSaving.get()) {
            return;
        }
        FactoryImpl iFactory = (FactoryImpl) context.iFactory;
        String path = iFactory.getPathForDoc(document);
        if (path == null) {
            return;
        }
        editorManager.save(path);
    }


    @Override
    public void documentChanged(DocumentEvent event) {
        if (!isListening.get()) {
            return;
        }
        Document document = event.getDocument();
        Flog.debug("Document change: %s", document);
        VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
        if (virtualFile == null) {
            Flog.info("No virtual file for document %s", document);
            return;
        }
        editorManager.change(new FileImpl(virtualFile));
    }

    public void caretAdded(CaretEvent caretEvent) {
        // Not in use.
    }

    public void caretRemoved(CaretEvent caretEvent) {
        // Not in use.
    }

    @Override
    public void before(@NotNull List<? extends VFileEvent> events) {
        for (VFileEvent event : events) {
            if (event instanceof VFileDeleteEvent) {
                Flog.info("deleting a file %s", event.getPath());
                editorManager.deleteDirectory(IntelliUtils.getAllNestedFilePaths(event.getFile()));
                continue;
            }
            if (event instanceof VFilePropertyChangeEvent) {
                VFilePropertyChangeEvent propertyEvent = (VFilePropertyChangeEvent) event;
                if (!propertyEvent.getPropertyName().equals("name")) {
                    continue;
                }
                oldRenamePath = propertyEvent.getFile().getPath();
            }
        }
    }

    @Override
    public void after(@NotNull List<? extends VFileEvent> events) {
        for (VFileEvent event : events) {
            IFile virtualFile = new FileImpl(event.getFile());
            if (Ignore.isIgnoreFile(virtualFile) && !context.isIgnored(virtualFile)) {
                context.refreshIgnores();
                break;
            }
        }
        if (!isListening.get()) {
            return;
        }
        for (VFileEvent event : events) {
            Flog.debug(" after event type %s", event.getClass().getSimpleName());
            if (event instanceof VFilePropertyChangeEvent) {
                VFilePropertyChangeEvent propertyEvent = (VFilePropertyChangeEvent) event;
                if (!propertyEvent.getPropertyName().equals("name")) {
                    continue;
                }
                VirtualFile virtualFile = propertyEvent.getFile();
                renameAllNestedFiles(virtualFile,  oldRenamePath, virtualFile.getPath());
                oldRenamePath = null;
                continue;
            }
            if (event instanceof VFileMoveEvent) {
                Flog.info("move event %s", event);
                VirtualFile oldParent = ((VFileMoveEvent) event).getOldParent();
                VirtualFile newParent = ((VFileMoveEvent) event).getNewParent();
                renameAllNestedFiles(event.getFile(), oldParent.getPath(), newParent.getPath());
                continue;
            }

            if (event instanceof VFileCopyEvent) {
                // We get one copy event per file copied for copied directories, which makes this easy.
                Flog.info("Copying a file %s", event);
                VirtualFile newParent = ((VFileCopyEvent) event).getNewParent();
                String newChildName = ((VFileCopyEvent) event).getNewChildName();
                String path = event.getPath();
                VirtualFile[] children = newParent.getChildren();
                VirtualFile copiedFile = null;
                for (VirtualFile child : children) {
                    if (child.getName().equals(newChildName)) {
                        copiedFile = child;
                        break;
                    }
                }
                if (copiedFile == null) {
                    Flog.error("Couldn't find copied virtual file %s", path);
                    continue;
                }
                editorManager.createFile(new FileImpl(copiedFile));
                continue;
            }
            if (event instanceof VFileCreateEvent) {
                Flog.info("creating a file %s", event);
                ArrayList<IFile> createdFiles = IntelliUtils.getAllValidNestedFiles(context, event.getFile());
                for (final IFile createdFile : createdFiles) {
                    editorManager.createFile(createdFile);
                }
                continue;
            }
            if (event instanceof VFileContentChangeEvent) {
                ArrayList<IFile> changedFiles = IntelliUtils.getAllValidNestedFiles(context, event.getFile());
                for (IFile file : changedFiles) {
                    editorManager.change(file);
                }
            }
        }
    }

    private void renameAllNestedFiles(VirtualFile virtualFile, String oldPath, String newPath) {
        ArrayList<IFile> files = IntelliUtils.getAllValidNestedFiles(context, virtualFile);
        for (IFile file: files) {
            String newFilePath = file.getPath();
            String oldFilePath = newFilePath.replace(newPath, oldPath);
            editorManager.rename(oldFilePath, newFilePath);
        }
    }

    @Override
    public void unsavedDocumentsDropped() {
    }

    @Override
    public void beforeFileContentReload(VirtualFile file, @NotNull Document document) {
    }

    @Override
    public void fileContentReloaded(@NotNull VirtualFile file, @NotNull Document document) {
    }

    @Override
    public void fileContentLoaded(@NotNull VirtualFile file, @NotNull Document document) {
    }

    @Override
    public void beforeAllDocumentsSaving() {
        //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public void beforeDocumentChange(final DocumentEvent event) {
        if (!isListening.get()) {
            return;
        }
        if (!event.getDocument().isWritable()) {
            Flog.log("Document is not writable? %s", event.getDocument());
        }
        final VirtualFile file = FileDocumentManager.getInstance().getFile(event.getDocument());
        if (file == null)
            return;

        Document document = event.getDocument();
        editorManager.beforeChange(new DocImpl(context, document));
    }

    @Override
    public void caretPositionChanged(final CaretEvent event) {
        sendCaretPosition(event.getEditor(), false);
    }

    @Override
    public void visibleAreaChanged(final VisibleAreaEvent event) {
        sendCaretPosition(event.getEditor(), true);
    }

    private void sendCaretPosition(Editor editor, boolean following) {
        FactoryImpl iFactory = (FactoryImpl) context.iFactory;
        Document document = editor.getDocument();
        String path = iFactory.getPathForDoc(document);
        if (path == null) {
            return;
        }

        ArrayList<ArrayList<Integer>> rangesWithCaret = new ArrayList<ArrayList<Integer>>(ranges.size() + 1);
        for(ArrayList<Integer> item: ranges) {
            rangesWithCaret.add(item);
        }
        Integer offset = editor.getCaretModel().getOffset();
        rangesWithCaret.add(new ArrayList<Integer>(Arrays.asList(offset, offset)));
        editorManager.changeSelection(path, rangesWithCaret, !isListening.get() || following);
    }

    @Override
    public void selectionChanged(final SelectionEvent event) {
        Document document = event.getEditor().getDocument();
        FactoryImpl iFactory = (FactoryImpl) context.iFactory;
        String path = iFactory.getPathForDoc(document);
        if (path == null) {
            return;
        }

        TextRange[] textRanges = event.getNewRanges();
        ranges = new ArrayList<ArrayList<Integer>>();
        for(TextRange r : textRanges) {
            int start = r.getStartOffset();
            int end = r.getEndOffset();
            if (start == end) {
                //This signifies a selection was cleared. We don't want to store that as a range.
                continue;
            }
            ranges.add(new ArrayList<Integer>(Arrays.asList(start, end)));
        }
        editorManager.changeSelection(path, ranges, !isListening.get());
    }

}