package org.jabref.gui.openoffice;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.jabref.gui.DialogService;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.openoffice.NoDocumentFoundException;
import org.jabref.model.openoffice.uno.CreationException;
import org.jabref.model.openoffice.uno.NoDocumentException;
import org.jabref.model.openoffice.uno.UnoCast;
import org.jabref.model.openoffice.uno.UnoTextDocument;
import org.jabref.model.openoffice.util.OOResult;
import org.jabref.model.openoffice.util.OOVoidResult;

import com.sun.star.bridge.XBridge;
import com.sun.star.bridge.XBridgeFactory;
import com.sun.star.comp.helper.BootstrapException;
import com.sun.star.container.NoSuchElementException;
import com.sun.star.container.XEnumeration;
import com.sun.star.container.XEnumerationAccess;
import com.sun.star.frame.XDesktop;
import com.sun.star.lang.DisposedException;
import com.sun.star.lang.WrappedTargetException;
import com.sun.star.lang.XComponent;
import com.sun.star.lang.XMultiComponentFactory;
import com.sun.star.text.XTextDocument;
import com.sun.star.uno.XComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.sun.star.uno.UnoRuntime.queryInterface;

/// Establish connection to a document opened in OpenOffice or LibreOffice.
public class OOBibBaseConnect {

    private static final Logger LOGGER = LoggerFactory.getLogger(OOBibBaseConnect.class);

    private final DialogService dialogService;
    private final XDesktop xDesktop;
    private final XComponentContext componentContext;

    /// Created when connected to a document.
    ///
    /// Cleared (to null) when we discover we lost the connection.
    private XTextDocument xTextDocument;

    public OOBibBaseConnect(Path loPath, DialogService dialogService)
            throws
            BootstrapException,
            CreationException, IOException, InterruptedException {

        this.dialogService = dialogService;
        this.componentContext = Bootstrap.bootstrap(loPath);
        this.xDesktop = createDesktop(componentContext);
    }

    private XDesktop createDesktop(XComponentContext context)
            throws
            CreationException {

        XMultiComponentFactory sem = context.getServiceManager();

        // Create the desktop, which is the root frame of the
        // hierarchy of frames that contain viewable components:
        Object desktop;
        try {
            desktop = sem.createInstanceWithContext("com.sun.star.frame.Desktop", context);
        } catch (com.sun.star.uno.Exception e) {
            throw new CreationException(e.getMessage());
        }
        return UnoCast.cast(XDesktop.class, desktop).get();
    }

    XComponentContext getComponentContext() {
        return componentContext;
    }

    /// Close any open office connection, if none exists does nothing
    public static void closeOfficeConnection() {
        try {
            // get the bridge factory from the local service manager
            XBridgeFactory bridgeFactory = queryInterface(XBridgeFactory.class,
                    Bootstrap.createSimpleServiceManager()
                             .createInstance("com.sun.star.bridge.BridgeFactory"));

            if (bridgeFactory != null) {
                for (XBridge bridge : bridgeFactory.getExistingBridges()) {
                    // dispose of this bridge after closing its connection
                    queryInterface(XComponent.class, bridge).dispose();
                }
            }
        } catch (com.sun.star.uno.Exception ex) {
            LOGGER.error("Exception disposing office process connection bridge", ex);
        }
    }

    private static List<XTextDocument> getTextDocuments(XDesktop desktop)
            throws
            NoSuchElementException,
            WrappedTargetException {

        List<XTextDocument> result = new ArrayList<>();

        XEnumerationAccess enumAccess = desktop.getComponents();
        XEnumeration compEnum = enumAccess.createEnumeration();

        while (compEnum.hasMoreElements()) {
            Object next = compEnum.nextElement();
            XComponent comp = UnoCast.cast(XComponent.class, next).get();
            Optional<XTextDocument> doc = UnoCast.cast(XTextDocument.class, comp);
            doc.ifPresent(result::add);
        }
        return result;
    }

    /// Run a dialog allowing the user to choose among the documents in `list`.
    ///
    /// @return Null if no document was selected. Otherwise the document selected.
    private static XTextDocument selectDocumentDialog(List<XTextDocument> list,
                                                      DialogService dialogService) {

        class DocumentTitleViewModel {

            private final XTextDocument xTextDocument;
            private final String description;

            public DocumentTitleViewModel(XTextDocument xTextDocument) {
                this.xTextDocument = xTextDocument;
                this.description = UnoTextDocument.getFrameTitle(xTextDocument).orElse("");
            }

            public XTextDocument getXtextDocument() {
                return xTextDocument;
            }

            @Override
            public String toString() {
                return description;
            }
        }

        List<DocumentTitleViewModel> viewModel = list.stream()
                                                     .map(DocumentTitleViewModel::new)
                                                     .collect(Collectors.toList());

        // This whole method is part of a background task when
        // auto-detecting instances, so we need to show dialog in FX
        // thread
        Optional<DocumentTitleViewModel> selectedDocument =
                dialogService
                        .showChoiceDialogAndWait(Localization.lang("Select document"),
                                Localization.lang("Found documents:"),
                                Localization.lang("Use selected document"),
                                viewModel);

        return selectedDocument
                .map(DocumentTitleViewModel::getXtextDocument)
                .orElse(null);
    }

    /// Choose a document to work with.
    ///
    /// Assumes we have already connected to LibreOffice or OpenOffice.
    ///
    /// If there is a single document to choose from, selects that. If there are more than one, shows selection dialog. If there are none, throws NoDocumentFoundException
    ///
    /// After successful selection connects to the selected document and extracts some frequently used parts (starting points for managing its content).
    ///
    /// Finally initializes this.xTextDocument with the selected document and parts extracted.
    public OOVoidResult<OOError> selectDocument(boolean autoSelectForSingle) {
        try {
            XTextDocument selected;
            List<XTextDocument> textDocumentList = getTextDocuments(this.xDesktop);
            if (textDocumentList.isEmpty()) {
                return OOVoidResult.error(OOError.from(new NoDocumentFoundException()));
            } else if ((textDocumentList.size() == 1) && autoSelectForSingle) {
                selected = textDocumentList.getFirst(); // Get the only one
            } else { // Bring up a dialog
                selected = OOBibBaseConnect.selectDocumentDialog(textDocumentList,
                        this.dialogService);
            }

            if (selected == null) {
                return OOVoidResult.ok();
            }

            this.xTextDocument = selected;
            return OOVoidResult.ok();
        } catch (DisposedException e) {
            return OOVoidResult.error(OOError.from(e).setTitle(Localization.lang("Problem connecting")));
        } catch (NoSuchElementException | WrappedTargetException e) {
            return OOVoidResult.error(OOError.fromMisc(e).setTitle(Localization.lang("Problem connecting")));
        }
    }

    /// Mark the current document as missing.
    private void forgetDocument() {
        this.xTextDocument = null;
    }

    /// A simple test for document availability.
    ///
    /// See also `isDocumentConnectionMissing` for a test actually attempting to use the connection.
    public boolean isConnectedToDocument() {
        return this.xTextDocument != null;
    }

    /// @return true if we are connected to a document
    public boolean isDocumentConnectionMissing() {
        XTextDocument doc = this.xTextDocument;

        if (doc == null) {
            return true;
        }

        if (UnoTextDocument.isDocumentConnectionMissing(doc)) {
            forgetDocument();
            return true;
        }
        return false;
    }

    /// Returns either a valid XTextDocument in OOResult or a NoDocumentException error
    public OOResult<XTextDocument, OOError> getXTextDocument() {
        if (isDocumentConnectionMissing()) {
            return OOResult.error(OOError.from(new NoDocumentException()));
        }
        return OOResult.ok(this.xTextDocument);
    }

    /// The title of the current document, or Optional.empty()
    public Optional<String> getCurrentDocumentTitle() {
        if (isDocumentConnectionMissing()) {
            return Optional.empty();
        } else {
            return UnoTextDocument.getFrameTitle(this.xTextDocument);
        }
    }
}
