package floobits.impl;

import com.intellij.notification.NotificationType;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ContentIterator;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import floobits.Listener;
import floobits.common.*;
import floobits.common.interfaces.IContext;
import floobits.common.interfaces.IFile;
import floobits.common.protocol.FlooUser;
import floobits.common.protocol.handlers.FlooHandler;
import floobits.dialogs.*;
import floobits.utilities.Colors;
import floobits.utilities.Flog;
import floobits.utilities.IntelliUtils;
import floobits.windows.FloobitsWindowManager;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * I am the link between a project and floobits
 */
public class ContextImpl extends IContext {

    static public class BalloonState {
        public Image smallGravatar;
        public Image largeGravatar;
        public int lineNumber;
        public Balloon balloon;
    }

    private Listener listener = new Listener(this);
    public ConcurrentHashMap<String, BalloonState> gravatars = new ConcurrentHashMap<String, BalloonState>();
    public Project project;
    public FloobitsWindowManager floobitsWindowManager;
    private ExecutorService pool;

    public ContextImpl(Project project) {
        super();
        this.project = project;
        this.iFactory = new FactoryImpl(this, editor);
    }

    public void statusMessage(String message, NotificationType notificationType) {
        Flog.statusMessage(message, notificationType, project);
    }

    @Override
    public void loadFloobitsWindow() {
        floobitsWindowManager = new FloobitsWindowManager(this);
    }

    @Override public void flashMessage(final String message) {
        Flog.flashMessage(message, project);
    }


    @Override public void warnMessage(String message) {
        Flog.log(message);
        statusMessage(message, NotificationType.WARNING);
        chatStatusMessage(message);
    }

    @Override public void statusMessage(String message) {
        Flog.log(message);
        if (floobitsWindowManager != null && !floobitsWindowManager.isOpen()) {
            //Only show a status message when chat is not open.
            statusMessage(message, NotificationType.INFORMATION);
        }
        chatStatusMessage(message);
    }

    @Override public void errorMessage(String message) {
        Flog.error(message);
        statusMessage(message, NotificationType.ERROR);
        chatErrorMessage(message);
    }

    @Override
    public boolean confirmDialog(String message) {
        int answer = JOptionPane.showConfirmDialog(null, message);
        return answer == JOptionPane.YES_OPTION;
    }

    @Override public void chatStatusMessage(String message) {
        if (floobitsWindowManager != null) {
            floobitsWindowManager.statusMessage(message);
        }
    }

    @Override public void chatErrorMessage(String message) {
        if (floobitsWindowManager != null) {
            floobitsWindowManager.errorMessage(message);
        }
    }

    @Override
    public Object getActualContext() {
        return project;
    }

    @Override
    protected void shareProjectDialog(String name, List<String> orgs, final String host, final boolean _private_, final String projectPath) {
        final ContextImpl context = this;
        ShareProjectDialog shareProjectDialog = new ShareProjectDialog(name, orgs, project,
                new RunLater<ShareProjectDialog>() {
                    @Override
                    public void run(ShareProjectDialog dialog) {
                        if (API.createWorkspace(host, dialog.getOrgName(), dialog.getWorkspaceName(), context, _private_)) {
                            FlooUrl url = new FlooUrl(host, dialog.getOrgName(), dialog.getWorkspaceName(), Constants.defaultPort, true);
                            joinWorkspace(url, projectPath, true, null);
                        }
                    }
                },
                new RunLater<ShareProjectDialog>() {
                    @Override
                    public void run(ShareProjectDialog dialog) {
                        FileChooserDescriptor descriptor = new FileChooserDescriptor(false, true, false, false, false, false);
                        descriptor.setTitle("Select folder to upload");
                        descriptor.setDescription("NOTE: You cannot choose a folder outside of your project.");
                        VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(new File(projectPath));
                        VirtualFile[] vFiles = FileChooser.chooseFiles(descriptor, null, virtualFile);
                        if (vFiles.length < 1) {
                            Flog.warn("No directory selected for picking files to upload in share project.");
                            return;
                        }
                        if (API.createWorkspace(host, dialog.getOrgName(), dialog.getWorkspaceName(), context, _private_)) {
                            FlooUrl url = new FlooUrl(host, dialog.getOrgName(), dialog.getWorkspaceName(), Constants.defaultPort, true);
                            String filePath = vFiles[0].getCanonicalPath();
                            if (filePath == null) {
                                Flog.warn("Upload for picked directory in share project has a null path");
                                return;
                            }
                            IFile dirToAdd = iFactory.findFileByIoFile(new File(filePath));
                            joinWorkspace(url, projectPath, true, dirToAdd);
                        }
                    }
                });
        shareProjectDialog.createCenterPanel();
        shareProjectDialog.show();
    }

    @Override
    public void followUser() {
        final FlooHandler flooHandler = this.getFlooHandler();
        if (flooHandler == null) {
            return;
        }
        HashMap<String, Boolean> usersToChoose = new HashMap<String, Boolean>();
        String me = flooHandler.state.username;
        for (FlooUser user : flooHandler.state.users.values()) {
            if (user.username.equals(me)) {
                continue;
            }
            if (user.client.equals("flootty")) {
                continue;
            }
            if (Arrays.asList(user.perms).indexOf("highlight") == -1) {
                continue;
            }
            usersToChoose.put(user.username, flooHandler.state.followedUsers.contains(user.username));
        }
        FollowUserDialog followUserDialog = new FollowUserDialog(usersToChoose, project, new RunLater<FollowUserDialog>() {
            @Override
            public void run(FollowUserDialog dialog) {
                getFlooHandler().state.setFollowedUsers(dialog.getFollowedUsers());
            }
        });
        followUserDialog.createCenterPanel();
        followUserDialog.show();
    }

    @Override
    public void updateFollowing() {
        if (floobitsWindowManager != null) {
            floobitsWindowManager.updateUserList();
        }
    }

    @Override
    public void connected() {
        editor.reset();
        if (pool != null) {
            Flog.info("Pool wasn't null when creating a new one.");
        }
        pool = Executors.newFixedThreadPool(5);
    }

    @Override
    public void removeUser(FlooUser user) {
        statusMessage(String.format("%s left the workspace.", user.username));
        if (floobitsWindowManager != null) {
            floobitsWindowManager.removeUser(user);
        }
        iFactory.removeHighlightsForUser(user.user_id);
    }

    @Override
    public synchronized void shutdown() {
        super.shutdown();
        if (floobitsWindowManager != null) {
            floobitsWindowManager.clearUsers();
        }
        try {
            listener.shutdown();
        } catch (Throwable e) {
            Flog.error(e);
        }
        listener = new Listener(this);
        if (pool != null) {
            pool.shutdownNow();
            pool = null;
        }
    }

    public void setListener(boolean b) {
        listener.isListening.set(b);
    }

    public void setSaving(boolean b) {
        listener.isSaving.set(b);
    }

    @Override
    public void mainThread(Runnable runnable) {
        ApplicationManager.getApplication().invokeLater(runnable);
    }

    @Override
    public void readThread(final Runnable runnable) {
        final ContextImpl context = this;
        mainThread(new Runnable() {
            @Override
            public void run() {
                try {
                    ApplicationManager.getApplication().runReadAction(runnable);
                } catch (Throwable throwable) {
                    API.uploadCrash(context, throwable);
                }
            }
        });
    }

    @Override
    public void writeThread(final Runnable runnable) {
        final long l = System.currentTimeMillis();
        final ContextImpl context = this;
        mainThread(new Runnable() {
            @Override
            public void run() {
                CommandProcessor.getInstance().executeCommand(context.project, new Runnable() {
                    @Override
                    public void run() {
                        ApplicationManager.getApplication().runWriteAction(new Runnable() {
                            @Override
                            public void run() {
                                long time = System.currentTimeMillis() - l;
                                if (time > 200) {
                                    Flog.log("spent %s getting lock", time);
                                }
                                try {
                                    runnable.run();
                                } catch (Throwable throwable) {
                                    API.uploadCrash(context, throwable);
                                }
                            }
                        });
                    }
                }, "Floobits", null);
            }
        });
    }

    @Override
    public void dialog(String title, String body, RunLater<Boolean> runLater) {
        DialogBuilder.build(title, body, runLater);
    }

    @Override
    public void dialogDisconnect(int _tooMuch, int _howMany) {
        NumberFormat numberFormat = NumberFormat.getNumberInstance();
        String howMany = numberFormat.format(_howMany);
        String tooMuch = numberFormat.format(_tooMuch);
        String notice = String.format("You have too many directories that are over %s MB to upload with Floobits.", tooMuch);
        DisconnectNoticeDialog disconnectNoticeDialog = new DisconnectNoticeDialog(new Runnable() {
            @Override
            public void run() {
                shutdown();
            }
        }, String.format("%s We limit it to %d and you have %s big directories.", notice, Constants.TOO_MANY_BIG_DIRS, howMany));
        disconnectNoticeDialog.createCenterPanel();
        disconnectNoticeDialog.show();
    }

    @Override
    public void dialogPermsRequest(final String username, final RunLater<String> runLater) {
        final ContextImpl context = this;
        mainThread(new Runnable() {
            @Override
            public void run() {
                HandleRequestPermsRequestDialog d = new HandleRequestPermsRequestDialog(username, context, runLater);
                d.createCenterPanel();
                d.show();
            }
        });
    }

    @Override
    public boolean dialogTooBig(HashMap<String, Integer> bigStuff) {
        HandleTooBigDialog handleTooBigDialog = new HandleTooBigDialog(bigStuff);
        handleTooBigDialog.createCenterPanel();
        handleTooBigDialog.show();
        return handleTooBigDialog.getExitCode() == 0;
    }

    @Override
    public void dialogResolveConflicts(final Runnable stompLocal, final Runnable stompRemote, final boolean readOnly,
                                       final Runnable flee, final String[] conflictedPathsArray,
                                       final String[] connections) {
        ProjectRootManager prm = ProjectRootManager.getInstance(project);
        final AtomicInteger count = new AtomicInteger();
        count.set(0);
        for (VirtualFile file : prm.getContentRoots()) {
            VfsUtil.iterateChildrenRecursively(file, null, new ContentIterator() {
                @Override
                public boolean processFile(VirtualFile file) {
                    FileImpl fileImpl = new FileImpl(file);
                    if (fileImpl.isValid()) {
                        count.getAndIncrement();
                    }
                    return true;
                }
            });
        }
        mainThread(new Runnable() {
            @Override
            public void run() {
                FlooHandler flooHandler = getFlooHandler();
                if (flooHandler == null) {
                    return;
                }
                ResolveConflictsDialog dialog = new ResolveConflictsDialog(stompLocal, stompRemote, readOnly, flee,
                        conflictedPathsArray, connections, flooHandler.state.numBufs(), count.get());
                dialog.createCenterPanel();
                dialog.show();
            }
        });
    }

    @Override
    protected String selectAccount(String[] keys) {
        SelectAccount selectAccount = new SelectAccount(project, keys);
        selectAccount.show();
        int exitCode = selectAccount.getExitCode();
        if (exitCode != DialogWrapper.OK_EXIT_CODE) {
            return null;
        }
        return selectAccount.getAccount();
    }


    @Override
    public void chat(String username, String msg, Date messageDate) {
        if (floobitsWindowManager == null) {
            return;
        }
        if (!floobitsWindowManager.isOpen()) {
            statusMessage(String.format("%s: %s", username, msg));
        }
        floobitsWindowManager.chatMessage(username, msg, messageDate);
    }

    @Override
    public void openFloobitsWindow() {
        if (floobitsWindowManager == null || floobitsWindowManager.isOpen()) {
            return;
        }
        floobitsWindowManager.openFloobitsWindow();
        floobitsWindowManager.updateUserList();
    }

    @Override
    public void closeFloobitsWindow() {
        if (floobitsWindowManager == null) {
            return;
        }
        floobitsWindowManager.closeFloobitsWindow();
    }

    @Override
    public void toggleFloobitsWindow() {
        if (floobitsWindowManager == null) {
            return;
        }
        floobitsWindowManager.toggleFloobitsWindow();
        if (floobitsWindowManager.isOpen()) {
            floobitsWindowManager.updateUserList();
        }
    }

    @Override
    public void setupFloobitsWindow() {
        if (floobitsWindowManager != null) {
            floobitsWindowManager.clearUsers();
        }
        openFloobitsWindow();
    }

    @Override
    public void listenToEditor(EditorEventHandler editorEventHandler) {
        listener.start(editorEventHandler);
    }

    @Override
    public boolean isAccountAutoGenerated() {
        return IntelliUtils.isAutoGenerated();
    }

    @Override
    public void notifyCompleteSignUp() {
        String url = IntelliUtils.getCompleteSignUpURL(project);
        if (url == null) {
            Flog.log("notifyCompleteSignUp: No pinocchio URL");
            return;
        }
        Flog.info("Complete signup url is:", url);
        chatStatusMessage(String.format("Your account was auto-created. Please %s.",
                Utils.getLinkHTML(url, "click here to complete sign up")));
    }

    @Override
    public void addUser(final FlooUser user) {
        if (pool == null) {
            Flog.info("Pool is null cannot add user.");
            return;
        }

        if (user.color != null) {
            Colors.color_map.put(user.username, user.color);
        }

        statusMessage(String.format("%s joined the workspace on %s (%s).", user.username, user.platform, user.client));
        Flog.info("Adding gravatar for user %s.", user);
        pool.execute(new Runnable() {
            @Override
            public void run() {
                Image img;
                URL url;
                try {
                    url = new URL(user.gravatar);
                } catch (MalformedURLException e) {
                    Flog.info("Could not create url for gravatar %s.", user.gravatar);
                    return;
                }
                try {
                    URLConnection con = url.openConnection();
                    con.setConnectTimeout(10000);
                    con.setReadTimeout(10000);
                    InputStream in = con.getInputStream();
                    img = ImageIO.read(in);
                } catch (IOException e) {
                    Flog.info("Could not load gravatar from network.");
                    return;
                }
                ContextImpl.BalloonState balloonState = new ContextImpl.BalloonState();
                balloonState.largeGravatar = img.getScaledInstance(75, 75, Image.SCALE_SMOOTH);
                balloonState.smallGravatar = img.getScaledInstance(30, 30, Image.SCALE_SMOOTH);
                gravatars.put(user.gravatar, balloonState);
                FlooHandler handler = getFlooHandler();
                if (handler == null) {
                    return;
                }
                if (floobitsWindowManager != null) {
                    floobitsWindowManager.updateUserList();
                }
            }
        });
        if (floobitsWindowManager != null) {
            floobitsWindowManager.addUser(user);
        }
    }
}