#!/usr/bin/env python3
# encoding: utf-8
"""
add-on_manager.py -- a command-line client for the Wesnoth add-on server

This tool is mainly intendended for user-made content authors and maintainers.
It can be used to manage the WML content on the Wesnoth add-on server.
Available functions include listing, downloading, uploading, and deleting
add-ons.
"""

import sys, os.path, re, time, glob, shutil
from subprocess import Popen
import wesnoth.wmlparser3 as wmlparser
from wesnoth.campaignserver_client import CampaignClient

# This is the validation code for the -u arguments. It checks if the input path is valid
def valid_file_path(path):
    if os.path.isdir(path) or os.path.isfile(path):
        return path
    else:
        sys.stderr.write("No such file or directory: %s\n" % path)
        sys.exit(1)
    return None

if __name__ == "__main__":
    import argparse

    argumentparser = argparse.ArgumentParser()
    argumentparser.add_argument("-a", "--address", help="specify server address",
        default="add-ons.wesnoth.org")
    argumentparser.add_argument("--html",
        help="Output a HTML overview into the given directory.",)
    argumentparser.add_argument("-p", "--port",
        help="specify server port or BfW version (%s)" % " or ".join(
        [x[1] for x in CampaignClient.portmap]),
        default=CampaignClient.portmap[0][0])
    argumentparser.add_argument("-l", "--list", help="list available add-ons",
        action="store_true",)
    argumentparser.add_argument("-w", "--wml",
        help="when listing add-ons, list the raw wml",
        action="store_true",)
    argumentparser.add_argument("-C", "--color",
        help="use colored WML output",
        action="store_true",)
    argumentparser.add_argument("-c", "--campaigns-dir",
        help="directory where add-ons are stored",
        default=".")
    argumentparser.add_argument("-d", "--download",
        help="download the named add-on; " +
        "name may be a Python regexp matched against all add-on names " +
        "(specify the path where to put it with -c, " +
        "current directory will be used by default)")
    argumentparser.add_argument("-T", "--type",
        help="Type of addons to download, e.g. 'era' or 'campaign'.")
    argumentparser.add_argument("-t", "--tar",
        help="When used together with --download, create tarballs of any " +
        "downloaded addons and put into the specified directory.")
    argumentparser.add_argument("--pbl", help="override standard PBL location")
    argumentparser.add_argument("--pbl-key", action='append', nargs=2,
        metavar=("KEY", "VALUE"),
        help="When uploading, override KEY with VALUE in _server.pbl. " +
        "No changes are written to disk, only the upload is affected. " +
        "This option only makes sense with --upload.")
    argumentparser.add_argument("-u", "--upload",
        help="Upload an add-on. " +
        "UPLOAD should be either the name of an add-on subdirectory," +
        "(in which case the client looks for _server.pbl beneath it) " +
        "or a path to the .pbl file (in which case the name of the " +
        "add-on subdirectory is the name of the path with .pbl removed)", type=valid_file_path)
    argumentparser.add_argument("-s", "--status",
        help="Display the status of addons installed in the given " +
        "directory.")
    argumentparser.add_argument("-f", "--update",
        help="Update all installed add-ons in the given directory. " +
        "This works by comparing the _info.cfg file in each addon directory " +
        "with the version on the server.")
    argumentparser.add_argument("-V", "--verbose",
        help="be even more verbose for everything",
        action="store_true",)
    argumentparser.add_argument("-r", "--remove", nargs=2,
        metavar=("ADD-ON", "PASSPHRASE"),
        help="remove the named add-on from the server")
    argumentparser.add_argument("-R", "--raw-download",
        action="store_true",
        help="download as a binary WML packet")
    argumentparser.add_argument("--url", help="When used with --html, " +
        "a download link will be added for each campaign, with the given " +
        "base URL.")
    argumentparser.add_argument("--datadir", help="When used with --html, " +
        "specifies the Wesnoth data dir where add-on icons will be copied " +
        "from.")
    argumentparser.add_argument("-U", "--unpack",
        help="unpack the file UNPACK as a binary WML packet " +
        "(specify the add-on path with -c)")
    argumentparser.add_argument("--change-passphrase", nargs=3,
        metavar=("ADD-ON","OLD","NEW"),
        help="Change the passphrase for ADD-ON from OLD to NEW")
    args = argumentparser.parse_args()

    port = args.port
    if not args.port.isdigit():
        for (portnum, version) in CampaignClient.portmap:
            if args.port == version:
                port = portnum
                break
        else:
            sys.stderr.write("Unknown BfW version %s\n" % args.port)
            sys.exit(1)

    address = args.address
    if not ":" in address:
        address += ":" + str(port)

    def get(name, title, version, type, uploads, dependencies, cdir):
        mythread = cs.get_campaign_raw_async(name)

        pcounter = 0
        while not mythread.event.isSet():
            mythread.event.wait(1)
            if pcounter != cs.counter:
                print("%s: %d/%d" % (name, cs.counter, cs.length))
                pcounter = cs.counter

        if args.raw_download:
            with open(name, "wb") as f:
                f.write(mythread.data)
        else:
            decoded = cs.decode(mythread.data)
            dirname = os.path.join(cdir, name)
            oldcfg_path = os.path.join(cdir, name + ".cfg")

            # Try to remove old campaign in case it exists.
            shutil.rmtree(dirname, True)
            try: os.remove(oldcfg_path)
            except OSError: pass

            print("Unpacking %s..." % name)
            cs.unpackdir(decoded, cdir, verbose=args.verbose)


            info = os.path.join(dirname, "_info.cfg")
            try:
                f = open(info, "w")
                infowml = """#
# File automatically generated by Wesnoth to keep track
# of version information on installed add-ons. DO NOT EDIT!
#
[info]
\tdependencies="%s"
\ttitle="%s"
\ttype="%s"
\tuploads=%s
\tversion="%s"
[/info]
"""
                f.write(infowml %
                    (dependencies, title, type, uploads, version))
                f.close()
            except OSError:
                pass
            print_messages(decoded)

            if args.tar:
                try: os.mkdir(args.tar)
                except OSError: pass
                tarname = args.tar + "/" + name + ".tar.bz2"
                if os.path.isfile(oldcfg_path):
                    oldcfg = name + ".cfg"
                    if args.verbose:
                        sys.stderr.write("Creating tarball with command: tar " +
                            "cjf %(tarname)s -C %(cdir)s %(name)s %(oldcfg)s\n" %
                            locals())
                    Popen(["tar", "cjf", tarname, "-C", cdir, name, oldcfg])
                else:
                    if args.verbose:
                        sys.stderr.write("Creating tarball with command: tar " +
                            "cjf %(tarname)s -C %(cdir)s %(name)s\n" % locals())
                    Popen(["tar", "cjf", tarname, "-C", cdir, name])

    def print_messages(data):
        for message in data.get_all(tag = "message") + data.get_all(tag = "error"):
            print(message.get_text_val("message"))

    def parse_wml_file(name):
        p = wmlparser.Parser()
        p.parse_file(name)
        return p.root

    def parse_wml_text(text):
        p = wmlparser.Parser()
        p.parse_text(text)
        return p.root

    def get_info(name):
        """
        Get info for a locally installed add-on. It expects a direct path
        to the _info.cfg file.
        """
        if not os.path.exists(name):
            return None, None

        info = parse_wml_file(name)
        uploads = info.get_all(tag = "info")[0].get_text_val("uploads", "")
        version = info.get_all(tag = "info")[0].get_text_val("version", "")
        return uploads, version

    def fixup(column):
        return column.replace("\n", "\\n").replace("\t", "\\t")

    campaign_list = None

    if args.list:
        cs = CampaignClient(address)
        campaign_list = data = cs.list_campaigns()
        if data:
            campaigns = data.get_all(tag = "campaigns")[0]
            if args.wml:
                for campaign in campaigns.get_all(tag = "campaign"):
                    print(campaign.debug())
            else:
                columns = [["type", "name", "title", "author",
                    "version", "uploads", "downloads",
                    "size", "timestamp", "translate"]]
                for campaign in campaigns.get_all(tag = "campaign"):
                    column = [
                        campaign.get_text_val("type", "?"),
                        campaign.get_text_val("name", "?"),
                        campaign.get_text_val("title", "?"),
                        campaign.get_text_val("author", "?"),
                        campaign.get_text_val("version", "?"),
                        campaign.get_text_val("uploads", "?"),
                        campaign.get_text_val("downloads", "?"),
                        campaign.get_text_val("size", "?"),
                        time.ctime(int(campaign.get_text_val("timestamp", "0"))),
                        campaign.get_text_val("translate", "?")]
                    columns.append(column)
                for c in columns:
                    print(*map(fixup, c), sep="\t", end="\n")
            print_messages(data)
        else:
            sys.stderr.write("Could not connect.\n")

    elif args.download:
        cs = CampaignClient(address)
        fetchlist = []
        campaign_list = data = cs.list_campaigns()
        if data:
            campaigns = data.get_all(tag = "campaigns")[0]
            for campaign in campaigns.get_all(tag = "campaign"):
                name = campaign.get_text_val("name", "?")
                title = campaign.get_text_val("title")
                type = campaign.get_text_val("type", "")
                version = campaign.get_text_val("version", "")
                uploads = campaign.get_text_val("uploads", "")
                dependencies = campaign.get_text_val("dependencies", "")
                if re.escape(args.download).replace("\\_", "_") == args.download:
                    if name == args.download:
                        fetchlist.append((name, title, version, type, uploads, dependencies))
                elif not args.type or args.type == type:
                    if re.search(args.download, name):
                        fetchlist.append((name, title, version, type, uploads, dependencies))

        for name, title, version, type, uploads, dependencies in fetchlist:
            info = os.path.join(args.campaigns_dir, name, "_info.cfg")
            local_uploads, local_version = get_info(info)
            if uploads != local_uploads:
                # The uploads > local_uploads likely means a server reset
                if version != local_version or uploads > local_uploads:
                    get(name, title, version, type, uploads, dependencies, args.campaigns_dir)
                else:
                    print("Not downloading", name, \
                        "as the version already is", local_version, \
                        "(The add-on got re-uploaded.)")
            else:
                if args.verbose:
                    print("Not downloading", name, \
                        "because it is already up-to-date.")

    elif args.unpack:
        cs = CampaignClient(address)
        with open(args.unpack, "rb") as f:
            data = f.read()
        decoded = cs.decode(data)
        print("Unpacking %s..." % args.unpack)
        cs.unpackdir(decoded, args.campaigns_dir,  verbose=True)
    elif args.remove:
        cs = CampaignClient(address)
        data = cs.delete_campaign(*args.remove)
        print_messages(data)

    elif args.change_passphrase:
        cs = CampaignClient(address)
        data = cs.change_passphrase(*args.change_passphrase)
        print_messages(data)

    elif args.upload:
        cs = CampaignClient(address)
        if os.path.isdir(args.upload):
            # else basename returns an empty string
            args.upload = args.upload.rstrip("/")

            # New style with _server.pbl
            pbl_file_name = os.path.join(args.upload, "_server.pbl")
            name = os.path.basename(os.path.realpath(args.upload))
            wmldir = args.upload
            cfgfile = None # _main.cfg will be uploaded with the rest
            ignfile = os.path.join(args.upload, "_server.ign")
        else:
            # Old style with external .pbl file
            pbl_file_name = args.upload
            name = os.path.basename(args.upload)
            name = os.path.splitext(name)[0]
            wmldir = os.path.join(os.path.dirname(args.upload), name)
            cfgfile = args.upload.replace(".pbl", ".cfg")
            ignfile = args.upload.replace(".pbl", ".ign")

        if args.pbl:
            pbl_file_name = args.pbl

        with open(pbl_file_name, 'r') as pbl_file:
            pbl_text = pbl_file.read()

        for key_value_pair in args.pbl_key or []:
            key = key_value_pair[0]
            value = key_value_pair[1]
            if not re.match("^[a-zA-Z]+[_a-zA-Z]*$", key):
                raise ValueError("non-standard --pbl-key " + key)
            pbl_text = pbl_text + '\n' + key + '="' + value.replace('"', '""') + '"'

        pbl = parse_wml_text(pbl_text)
        if os.path.exists(ignfile):
            ign = open(ignfile).readlines()
            # strip line endings and whitespace
            ign = [i.strip() for i in ign if i.strip()]
        else:
            ign = [
                ".*",
                ".*/",
                "#*#",
                "*~",
                "*-bak",
                "*.swp",
                "*.pbl",
                "*.ign",
                "_info.cfg",
                "*.exe",
                "*.bat",
                "*.cmd",
                "*.com",
                "*.scr",
                "*.sh",
                "*.js",
                "*.vbs",
                "*.o",
                "*.ini",
                "Thumbs.db",
                "*.wesnoth",
                "*.project",
                "__MACOSX/"]

        mythread = cs.put_campaign_async(name, cfgfile, wmldir, ign, pbl)

        pcounter = 0
        while not mythread.event.isSet():
            mythread.event.wait(1)
            if cs.counter != pcounter:
                print("%d/%d" % (cs.counter, cs.length))
                pcounter = cs.counter

        print_messages(mythread.data)

    elif args.update or args.status:
        if args.status:
            cdir = args.status
        else:
            cdir = args.update
        dirs = glob.glob(os.path.join(cdir, "*"))
        dirs = [x for x in dirs if os.path.isdir(x)]
        cs = CampaignClient(address)
        campaign_list = data = cs.list_campaigns()
        if not data:
            sys.stderr.write("Could not connect to the add-on server.\n")
            sys.exit(-1)
        campaigns = {}
        for c in data.get_all(tag = "campaigns")[0].get_all(tag = "campaign"):
            name = c.get_text_val("name")
            campaigns[name] = c
        for d in dirs:
            dirname = os.path.basename(d)
            if dirname in campaigns:
                info = os.path.join(d, "_info.cfg")
                stitle = campaigns[dirname].get_text_val("title", "")
                sversion = campaigns[dirname].get_text_val("version", "")
                stype = campaigns[dirname].get_text_val("type", "")
                srev = campaigns[dirname].get_text_val("uploads", "")
                sdeps = campaigns[dirname].get_text_val("dependencies", "")
                if os.path.exists(info):
                    lrev, lversion = get_info(info)
                    if not srev:
                        sys.stdout.write(" ? " + dirname + " - has no " +
                            "version info on the server.\n")
                    elif srev == lrev:
                        sys.stdout.write("   " + dirname +
                            " - is up to date.\n")
                    elif sversion == lversion:
                        sys.stdout.write(" # " + dirname + " - is version " +
                            sversion + (" but you have revision %s not %s." +
                            " (The add-on got re-uploaded.)\n") %
                            (lrev, srev))
                        if srev > lrev: # server reset?
                            if args.update:
                                get(dirname, stitle, sversion, stype, srev, sdeps, cdir)
                    else:
                        sys.stdout.write(" * " + dirname + " - you have " +
                            "revision " + lrev + " but revision " + srev +
                            " is available.\n")
                        if args.update: get(dirname, stitle, sversion, stype, srev, sdeps, cdir)
                else:
                    sys.stdout.write(" ? " + dirname +
                        " - is installed but has no " +
                        "version info.\n")
                    if args.update: get(dirname, stitle, sversion, stype, srev, sdeps, cdir)
            else:
                sys.stdout.write(" - %s - is installed but not on server.\n" %
                    dirname)
    elif args.html:
        pass
    else:
        argumentparser.print_help()

    if args.html:
        if not campaign_list:
            cs = CampaignClient(address)
            campaign_list = cs.list_campaigns()
            del cs
        if campaign_list:
            import addon_manager.html
            addon_manager.html.output(args.html, args.url, args.datadir,
                campaign_list)
        else:
            sys.stderr.write("Could not retrieve campaign list " +
                "for HTML output.\n")
