#! /opt/openoffice4/program/python
# -*- coding: utf-8 -*-
# OpenOffice mobile presentation controller, as Python
#   by imacat <imacat@mail.imacat.idv.tw>, 2014-02-28

#  Copyright (c) 2016 imacat.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

"""Controls OpenOffice presentation with mobile devices.

"""

from __future__ import print_function

import BaseHTTPServer
import argparse
import os
import os.path
import ssl
import sys
import time


def append_uno_path():
    """Append the path of the uno module to the import path."""

    for p in sys.path:
        if os.path.exists(os.path.join(p, "uno.py")):
            return
    # For uno.py on MacOS
    candidate = "/Applications/OpenOffice.app/Contents/MacOS"
    if os.path.exists(os.path.join(candidate, "uno.py")):
        sys.path.append(candidate)
        return
    # Find uno.py for MS-Windows
    candidate = sys.executable
    while candidate != os.path.dirname(candidate):
        candidate = os.path.dirname(candidate)
        if os.path.exists(os.path.join(candidate, "uno.py")):
            sys.path.append(candidate)
            return


append_uno_path()
import uno
from com.sun.star.beans import PropertyValue
from com.sun.star.lang import DisposedException
from com.sun.star.connection import NoConnectException


def main():
    """The main program."""
    global args

    # Parses the arguments
    parse_args()

    run(args.doc_file, args.www_port, args.use_ssl)


def parse_args():
    """Parse the arguments."""
    global args

    parser = argparse.ArgumentParser(
        description=("Synchronize the local Basic scripts"
                     " with OpenOffice/LibreOffice Basic."))
    parser.add_argument(
        "doc_file", metavar="PRESENTATION", type=presentation_doc,
        help="The presentation document to play.")
    parser.add_argument(
        "-w", "--www-port", metavar="N", type=int, default=53177,
        help=("The TCP port for the web presentation controller "
              "(default: %(default)s)"))
    parser.add_argument(
        "-p", "--port", metavar="N", type=int, default=2002,
        help=("The TCP port to communicate with "
              "OpenOffice/LibreOffice with (default: %(default)s)"))
    parser.add_argument(
        "-v", "--version", action="version", version="%(prog)s 1.0")
    args = parser.parse_args()

    # Obtain the absolute path
    args.doc_file = os.path.abspath(args.doc_file)

    # Check whether we are using SSL.
    # TODO: Changed to an option or find the certificates automatically
    doc_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
    key_file = os.path.join(doc_dir, "server.key.pem")
    crt_file = os.path.join(doc_dir, "server.crt.pem")
    args.use_ssl = False
    if os.path.isfile(key_file) and os.path.isfile(crt_file):
        args.use_ssl = (key_file, crt_file)

    return


def presentation_doc(doc_file):
    """Check the supplied presentation document argument.

    Arguments:
        doc_file: The supplied presentation document argument.

    Returns:
        The supplied presentation document argument.
    """
    s = doc_file.lower()
    if not (s.endswith(".odp")
            or s.endswith(".sxi")
            or s.endswith(".ppt")
            or s.endswith(".pptx")):
        raise argparse.ArgumentTypeError(
            "%s: Not a presentation document" % doc_file)
    path = os.path.abspath(doc_file)
    if not os.path.exists(path):
        raise argparse.ArgumentTypeError(
            "%s: File does not exit" % doc_file)
    if not os.path.isfile(path):
        raise argparse.ArgumentTypeError(
            "%s: Not a file" % doc_file)
    return doc_file


def run(doc, port, use_ssl):
    """
    Start the presentation and run the web presentation controller.
    """
    global oo

    print("Listen on port %d" % port)
    oo = PresentationController(doc)
    oo.check_valid()
    server_address = ("", port)
    httpd = BaseHTTPServer.HTTPServer(server_address, MyHTTPRequestHandler)
    if use_ssl is not False:
        httpd.socket = ssl.wrap_socket(
            httpd.socket,
            keyfile=use_ssl[0],
            certfile=use_ssl[1],
            server_side=True)
    httpd.serve_forever()


class PresentationController:
    """The OpenOffice mobile presentation controller."""
    WIN_DETACHED_PROCESS = 0x00000008

    def __init__(self, doc_file):
        """Initialize the object."""
        self.file = doc_file
        self.port = 2002
        self.bootstrap_context = None
        self.service_manager = None
        self.desktop = None
        self.doc = None

    def check_valid(self):
        """
        Check the validity of the connection and the opened
        document.
        """
        try:
            presentation = self.doc.getPresentation()
        except (AttributeError, DisposedException):
            try:
                self.bootstrap_context.getServiceManager()
            except (AttributeError, DisposedException):
                self.__connect()
            self.open()
            presentation = self.doc.getPresentation()
        if not presentation.isRunning():
            presentation.start()

    def open(self):
        """Open an office document."""
        file_content_provider = self.service_manager.createInstance(
            "com.sun.star.ucb.FileContentProvider")
        url = file_content_provider.getFileURLFromSystemPath("", self.file)
        enum = self.desktop.getComponents().createEnumeration()
        while enum.hasMoreElements():
            component = enum.nextElement()
            if component.supportsService(
                    "com.sun.star.presentation.PresentationDocument"):
                if component.getURL() == url:
                    self.doc = component
                    return
        prop1 = PropertyValue()
        prop1.Name = "ReadOnly"
        prop1.Value = True
        prop2 = PropertyValue()
        prop2.Name = "MacroExecutionMode"
        # com.sun.star.document.MacroExecMode.ALWAYS_EXECUTE
        prop2.Value = 2
        self.doc = self.desktop.loadComponentFromURL(
            url, "_default", 0, (prop2,))
        if not self.doc.supportsService(
                "com.sun.star.presentation.PresentationDocument"):
            sys.stderr.write(self.file + ": not a presentation document.\n")
            sys.exit(1)
        return

    def goto_next_slide(self):
        """Go to the next slide."""
        self.doc.getPresentation().getController().gotoNextSlide()
        return

    def goto_prev_slide(self):
        """Go to the previous slide."""
        self.doc.getPresentation().getController().gotoPreviousSlide()
        return

    def __connect(self):
        """Connect to the running OpenOffice/LibreOffice process.

        Run OpenOffice/LibreOffice in server listening mode if it is
        not running yet.
        """
        # Obtain the local context
        local_context = uno.getComponentContext()
        # Obtain the local service manager
        local_service_manager = local_context.getServiceManager()
        # Obtain the URL resolver
        url_resolver = local_service_manager.createInstanceWithContext(
            "com.sun.star.bridge.UnoUrlResolver", local_context)
        # Obtain the context
        url = ("uno:socket,host=localhost,port=%d;"
               "urp;StarOffice.ComponentContext") % self.port
        while True:
            try:
                self.bootstrap_context = url_resolver.resolve(url)
            except NoConnectException:
                self.__start_oo()
            else:
                break
        # Obtain the service manager
        self.service_manager = self.bootstrap_context.getServiceManager()
        # Obtain the desktop service
        self.desktop = self.service_manager.createInstanceWithContext(
            "com.sun.star.frame.Desktop", self.bootstrap_context)

    def __start_oo(self):
        """Start OpenOffice/LibreOffice in server listening mode."""
        # For MS-Windows, which does not have fork()
        if os.name == "nt":
            from subprocess import Popen
            soffice = os.path.join(
                os.path.dirname(uno.__file__), "soffice.exe")
            Popen([soffice,
                   "-accept=socket,host=localhost,port=%d;urp;" %
                   self.port],
                  close_fds=True, creationflags=self.WIN_DETACHED_PROCESS)
            time.sleep(2)
            return

        # For POSIX systems, including Linux and MacOSX
        try:
            pid = os.fork()
        except OSError:
            print("Failed to fork().", file=sys.stderr)
            sys.exit(1)
        if pid != 0:
            time.sleep(2)
            return
        os.setsid()
        soffice = self.__find_posix_soffice()
        if soffice is None:
            print("Failed to find the "
                  "OpenOffice/LibreOffice installation.",
                  file=sys.stderr)
            sys.exit(1)
        param = "-accept=socket,host=localhost,port=%d;urp;" % \
            self.port
        # LibreOffice on POSIX systems uses --accept instead of
        # -accept now.
        if self.__is_soffice_lo(soffice):
            param = "-" + param
        try:
            os.execl(soffice, soffice, param)
        except OSError:
            print("%s: Failed to run the"
                  " OpenOffice/LibreOffice server." % soffice,
                  file=sys.stderr)
            sys.exit(1)

    @staticmethod
    def __find_posix_soffice():
        """Find soffice on POSIX systems (Linux or MacOSX).

        Returns:
            The found soffice executable, or None if not found.
        """
        # Check soffice in the same directory of uno.py
        # This works for Linux OpenOffice/LibreOffice local
        # installation, and OpenOffice on MacOSX.
        soffice = os.path.join(
            os.path.dirname(uno.__file__), "soffice")
        if os.path.exists(soffice):
            return soffice

        # Now we have LibreOffice on MacOSX and Linux
        # OpenOffice/LibreOffice vendor installation.

        # LibreOffice on MacOSX.
        soffice = "/Applications/LibreOffice.app/Contents/MacOS/soffice"
        if os.path.exists(soffice):
            return soffice

        # Linux OpenOffice/LibreOffice vendor installation.
        soffice = "/usr/bin/soffice"
        if os.path.exists(soffice):
            return soffice

        # Not found
        return None

    @staticmethod
    def __is_soffice_lo(soffice):
        """Check whether the soffice executable is LibreOffice.

        LibreOffice on POSIX systems accepts "--accept" instead of
        "-accept" now.

        Returns:
            True if soffice is LibreOffice, or False otherwise.
        """
        # This works for most cases.
        if soffice.lower().find("libreoffice") != -1:
            return True

        # Check the symbolic link at /usr/bin/soffice
        if soffice == "/usr/bin/soffice" and os.path.islink(soffice):
            if os.readlink(soffice).lower().find("libreoffice") != -1:
                return True

        # Not found
        return False


class MyHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    """The local HTTP request handler."""

    def do_GET(self):
        """Handle the GET requests."""
        oo.check_valid()
        html = """<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
    "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.5" />
<style type="text/css">
.pocket input {
    font-size: 8em;
}
</style>
<title>OpenOffice Presentation Controller</title>
</head>
<body>

<div class="pocket">
<form action="/next" method="POST">
<input type="submit" value="▲" />
</form>
</div>

<div class="pocket">
<form action="/prev" method="POST">
<input type="submit" value="▼" />
</form>
</div>

</body>"""
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=UTF-8")
        self.send_header("Content-Length", len(html))
        self.end_headers()
        self.wfile.write(html)

    def do_POST(self):
        """Handle the POST requests."""
        oo.check_valid()
        if self.path == "/next":
            oo.goto_next_slide()
        elif self.path == "/prev":
            oo.goto_prev_slide()
        html = """<?xml version="1.0" encoding="US-ASCII" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
    "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=US-ASCII" />
<meta name="viewport" content="width=device-width, initial-scale=1.5" />
<title>303 See Others</title>
</head>
<body>
<h1>303 See Others</h1>
<p>Please follow <a href="/">this location</a>.</p>
</body>
</html>"""
        self.send_response(303)
        self.send_header("Location", "/")
        self.send_header("Content-Type", "text/html; charset=US-ASCII")
        self.send_header("Content-Length", len(html))
        self.end_headers()
        self.wfile.write(html)


if __name__ == "__main__":
    main()
    """
    if len(sys.argv) != 2:
        sys.stderr.write("Please specify the presentation document.\n")
        sys.exit(1)
    doc_file = os.path.abspath(sys.argv[1])
    s = doc_file.lower()
    if not (s.endswith(".odp")
            or s.endswith(".sxi")
            or s.endswith(".ppt")
            or s.endswith(".pptx")):
        sys.stderr.write(doc_file + ": not a presentation document.\n")
        sys.exit(1)
    if not os.path.exists(doc_file):
        sys.stderr.write(doc_file + ": file does not exist.\n")
        sys.exit(1)
    if not os.path.isfile(doc_file):
        sys.stderr.write(doc_file + ": file does not exist.\n")
        sys.exit(1)
    my_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
    key_file = os.path.join(my_dir, "server.key.pem")
    crt_file = os.path.join(my_dir, "server.crt.pem")
    use_ssl = False
    if os.path.isfile(key_file) and os.path.isfile(crt_file):
        use_ssl = (key_file, crt_file)
    oo = PresentationController(doc_file)
    run()
    """
