#!/usr/bin/env python3


# Copyright (c) 2024-2025, Anders Andersen, UiT The Arctic University
# of Norway. All rights reserved.

# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:

# - Redistributions of source code must retain the above copyright
#   notice, this list of conditions and the following disclaimer.

# - Redistributions in binary form must reproduce the above copyright
#   notice, this list of conditions and the following disclaimer in
#   the documentation and/or other materials provided with the
#   distribution.

# - Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived
#   from this software without specific prior written permission.

# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.


#
# Modules, help values and help functions
#

# Import standard modules
import sys, os, json

# File path mainipulation from standard module pathlib
from pathlib import Path

# Install the 'webinteract' module with 'pip'
import webinteract

# You should also install either 'keyring' or 'onepw' with 'pip' (to
# store account password in a safe way)

# If used interactivly, Qt and the module PySide6 have to be installed

# Current version of program
version = "1.26"

# Valid file types (from mobileprint web page)
acceptable_file_types = [
    "All files (*)",
    "Portable Document Format (*.pdf)",
    "Microsoft Word document (*.doc)",
    "Microsoft Word document (*.docx)",
    "Microsoft Word document (*.docm)",
    "Microsoft Word template (*.dot)",
    "Microsoft Word template (*.dotx)",
    "Microsoft Word template (*.dotm)",
    "Rich Text Format (*.rtf)",
    "Microsoft Excel workbook (*.xls)",
    "Microsoft Excel workbook (*.xlsx)",
    "Microsoft Excel workbook (*.xlsm)",
    "Microsoft Excel workbook (*.xlsb)",
    "Microsoft Excel template (*.xltx)",
    "Microsoft Excel template (*.xltm)",
    "Comma Separated Values (*.csv)",
    "Microsoft PowerPoint slideshow (*.ppt)",
    "Microsoft PowerPoint slideshow (*.pptx)",
    "Microsoft PowerPoint slideshow (*.pps)",
    "Microsoft PowerPoint slideshow (*.ppsx)",
    "Microsoft PowerPoint template (*.pot)",
    "Microsoft PowerPoint template (*.potx)",
    "OpenDocument presentation (*.odp)",
    "OpenDocument spreadsheet (*.ods)",
    "OpenDocument document (*.odt)",
    "OpenDocument Text template (*.ott)",
    "Hypertext Markup Language (*.html)",
    "Hypertext Markup Language (*.htm)",
    "MIME Hypertext Markup Language (*.mhtml)",
    "Hypertext Markup Language (*.xhtml)",
    "Joint Photographic Expert Group image (*.jpg)",
    "Joint Photographic Expert Group image (*.jpeg)",
    "Portable Network Graphics (*.png)",
    "Bitmap image (*.bmp)",
    "Graphical Interchange Format (*.gif)",
    "Tag Image File Format (*.tif)",
    "Tag Image File Format (*.tiff)",
    "Icon images (*.ico)",
    "Windows Metafile (*.wmf)",
    "Enhanced Metafile (*.emf)",
    "Scalable Vector Graphics (*.svg)",
    "Text format (*.txt)",
    "Extensible Markup Language (*.xml)",
    "XSL Formatting Objects (*.fo)",
    "XML Paper Specification (*.xps)",
    "Electronic Publication format (*.epub)"]

# A backup wia script if the `uitprint.wia` file is not found
wia_script = """
#WIASCRIPT
visit(url)
fill(account, "name", "username")
fill(pw, "name", "password")
click("id", "log-in")
verify(is_text_present("Upload job"), True, \
       'The "Upload job" button is not present')
click_link("partial_text", "Upload job")
verify(is_present("id", "tap-area", wait_time=20), True, \
       'The "Drop file area" is not present')
attach_file(path, "id", "add-file")
verify(is_text_present("unsupported file type"), False, \
       'The file type is not supported')
verify(is_present("id", "uploading-widget", wait_time=20), True, \
       'The "Uploading widget" element is not present')
check("xpath", "//input[@class='duplex']", 1)
click("id", "upload")
verify(is_text_present("Job uploaded", wait_time=20), True, \
       'The "Job uploaded" text is not shown')
"""

# Messages (errors)
def announce(msg, error=True, terminate=True, wait=False):
    if error:
        out=sys.stderr
        err=1
    else:
        out=sys.stdout
        err=0
    print(msg, file=out)
    if wait:
        dummy = input("Press return ")
    if terminate:
        sys.exit(err)


#
# Read configuration from the config file
#

# Config file name (in home directory)
confile = ".uitprintconfig.json"

# The valid str content of the config file
confcontent = ["account", "service", "url", "driver"]

# Some default values
config = {
    "service": "UiTprint",
    "url": "https://uit.no/mobilprint"}

# Read the config file and handle different errors
estatus = ""
try:
    cfp = Path.home().joinpath(confile)
    with open(cfp) as configfile:
        config.update(json.load(configfile))
except FileNotFoundError:
    pass # config file is optional
except json.decoder.JSONDecodeError:
    announce(f'Unable to parse JSON config file "{str(cfp)}"\n')
except Exception as err:
    announce(f'Error reading "{str(cfp)}": {err}\n')

# Add placeholders for missing values
for conf_key in confcontent:
    if not conf_key in config:
        config[conf_key] = ""

        
#
# Parse and verify the arguments
#
        
# Create argument parser
import argparse
parser = argparse.ArgumentParser(
    description='Print a file using UiT print service.')
parser.add_argument(
    "file", nargs='?', type=argparse.FileType('r'),
    help="file to be printed")
parser.add_argument(
    "-V", "--version", action="version",
    version=f"%(prog)s " + version)
parser.add_argument(
    "-s", "--service", default=config["service"],
    help="service name used fecthing the password from the keychain " + \
    f'(default "{config["service"]}")')
parser.add_argument(
    "-a", "--account",
    help="account name used fecthing the password from the keychain")
if ("account" in config) and config["account"]:
    parser.set_defaults(account=config["account"])
parser.add_argument(
    "-u", "--url", default=config["url"],
    help="the address of the UiT print service " + \
    f'(default "{config["url"]}")')
parser.add_argument(
    "--one-password", action="store_true", default=False,
    help="get password (and account) from 1Password")
parser.add_argument(
    "--driver", default=config["driver"],
    help=f'the web driver (default "{config["driver"]}")')
parser.add_argument(
    "-d", "--duplex", action="store_true", default=True,
    help="duplex printing")
parser.add_argument(
    "-i", "--interactive", action="store_true", default=False,
    help="run program interactive (with gui)")
parser.add_argument(
    "--visible", action="store_true", default=False,
    help="show the browser window")

# Parse arguments
args = parser.parse_args()

# Verify arguments
if not args.interactive:
    if not args.file:
        announce(f"""
>>> File missing: if not interactiv, the "file" argument is needed:
""", terminate=False)
        parser.print_help()
        announce("")

        
#
# Grab other information needed (password)
#

# Get the password (and maybe account) from 1Password
if args.one_password:
    try:
        import onepw
    except ModuleNotFoundError:
        announce(f"""
    >>> Error: Install module 'onepw' when 1Password is used:
          pip install onepw
    """)
    op = onepw.OnePW()
    try:
        args.pw = op.get(args.service, field="password")
    except onepw.OnePWError:
        announce(f"""
        >>> Error: Unable to get password from 1Password ({args.service})
    """)

    # If not provided, get account (username) from 1Password
    if not "account" in args or not args.account:
        try:
            args.account = op.get(args.service, field="username")
        except onepw.OnePWError:
            announce(f"""
            >>> Error: Unable to get account from 1Password ({args.service})
            """)

    # If not provided, get also the url from 1Password
    if not "url" in args or not args.url:
        try:
            urls = op.get(args.service, info="urls")
        except onepw.OnePWError:
            announce(f"""
            >>> Error: Unable to get account from 1Password ({args.service})
            """)
        if urls:
            args.url = list(urls.values())[0]
        else:
            announce(f"""
            >>> Error: Unable to get url from 1Password ({args.service})
            """)
        
# Get password from keyring
elif "account" in args and args.account:
    try:
        import keyring
    except ModuleNotFoundError:
        announce(f"""
    >>> Error: Install module 'keyring' to be able to fetch password:
          pip install keyring
    """)
    args.pw = keyring.get_password(args.service, args.account)
    if not args.pw:
        announce(f"""
>>> Error: No password found in keychain for {args.account} ({args.service}).
>>> Add password for {args.account} ({args.service}) with this command:
      keyring set {args.service} {args.account}
""")

# Account missing
else:
    announce(f"""
    >>> Error: Unable to get password: no account (username) given
    """)
    

#
# Get the file name (path)
#
    
# Interactive file dialog?
if args.interactive:

    # We use Qt for the GUI part
    from PySide6.QtWidgets import QApplication, QFileDialog, QMainWindow

    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            args.path, selected_filter = QFileDialog.getOpenFileName(
                self, dir=os.path.expanduser('~'),
                selectedFilter=acceptable_file_types[0],
                filter=(";;").join(acceptable_file_types))

    app = QApplication(sys.argv)
    window = MainWindow()

# Passed by argument
else:
    # Set file path
    args.path = os.path.realpath(args.file.name)

# Verify that we have a path to a file
if not args.path:
    announce("""
>>> No file name given (no file to print)
""")


#
# Interact with the web page
#

# Create named arguments
kw = {"headless": (not args.visible)}
if args.driver:
    kw["driver"] = args.driver
    
# Open browser
try:
    web = webinteract.WebInteraction(**kw)
except Exception as err:
    announce(f"""
>>> Error: Could be a problem with the web driver. Try another using the
    '--driver' argument:

       --driver "chrome"
       --driver "firefox"
       --driver "edge"

    See https://blog.pg12.org/web-page-interaction-in-python for more
    information.

>>> Error message:
      {err}\n""")
    
# Add `url`, `account`, `pw`, `path` and `duplex` to the namespace of the script
ns = {k: vars(args)[k] for k in ('url', 'account', 'pw', 'path', 'duplex')}
web.update(ns)

# Try to locate `wia` script (file) first
wia_file_path = os.path.dirname(os.path.abspath(__file__)) + "/uitprint.wia"
if os.path.isfile(wia_file_path) and os.access(wia_file_path, os.R_OK):
    wia_file = open(wia_file_path)
else:
    from io import StringIO
    wia_file = StringIO(wia_script)

# Perform web interaction actions (the script)
try:
    web(wia_file)
except webinteract.WebInteractionError as err:
    announce(f"""
>>> Web interaction error:
    {err}
""")
except Exception as err:
    announce(f"""
>>> Other error:
    {err}
""")
else:
    web.quit()