#!/usr/bin/env python3
"""
Read only, Amiga LZX[1] archiver Virtual filesystem executive for Midnight
Commander.

Tested against python 3.8, unlzx[1] 1.1 and mc 4.8.22

[1] ftp://us.aminet.net/pub/aminet/misc/unix/unlzx.c.gz.readme

Changelog:
    1.3 Code cleanup
    1.2 Use python3
    1.1 Moved common code into extfslib library
    1.0 Initial release

Author: Roman 'gryf' Dobosz <gryf73@gmail.com>
Date: 2023-10-20
Version: 1.2
Licence: BSD
"""
import os
import re
import shutil
import subprocess
import sys
import tempfile

import extfslib


class ULzx(extfslib.Archive):
    """LZX archive handle. Provides interface to MC's extfs subsystem"""
    LINE_PAT = re.compile(r"^\s+(?P<size>\d+)\s+"
                          r"((n/a)|\d+)\s"
                          r"(?P<time>\d{2}:\d{2}:\d{2})\s+"
                          r"(?P<date>\d+-[a-z]{3}-\d{4})\s"
                          r"(?P<perms>[h-][s-][p-][a-][r-][w-][e-][d-])\s"
                          r"\"(?P<fpath>.*)\"")
    ARCHIVER = "unlzx"
    CMDS = {"list": "-v",
            "read": "-x"}
    DATETIME = "%02d-%02d-%s %02d:%02d"

    def _get_date(self, time, date):
        """Return MM-DD-YYYY hh:mm formatted date out of time and date
        strings"""
        month_list = ["jan", "fe", "mar", "apr", "may", "jun", "jul",
                      "aug", "sep", "oct", "nov", "dec"]
        day, month, year = date.split("-")
        month = month_list.index(month) + 1
        hours, minutes, dummy = time.split(":")
        return self.DATETIME % (month, int(day), year, int(hours),
                                int(minutes))

    def _map_name(self, name):
        if name.startswith(" "):
            new_name = "".join(["~", name[1:]])
            return new_name
        return name

    def _get_dir(self):
        """Prepare archive file listing"""
        contents = []

        out = subprocess.run([self.ARCHIVER, self.CMDS['list'], self._arch],
                             capture_output=True, encoding="latin-1")
        if out.stderr:
            sys.stderr.write(out.stderr)

        for line in out.stdout.split("\n"):
            match = self.LINE_PAT.match(line)
            if not match:
                continue

            match_entry = match.groupdict()
            entry = {}
            for key in match_entry:
                entry[key] = match_entry[key]
            del match_entry

            entry['datetime'] = self._get_date(entry['time'], entry['date'])
            entry['display_name'] = self._map_name(entry['fpath'])
            entry['perms'] = "-rw-r--r--"  # lzx doesn't store empty dirs
            entry['uid'] = str(self._uid)
            entry['gid'] = str(self._gid)
            contents.append(entry)

        return contents

    def list(self):
        """Output contents of the archive to stdout"""
        for entry in self._contents:
            sys.stdout.write(self.ITEM.decode('utf-8') % entry)
        return 0

    def run(self, dst):
        """Execute file out of archive"""
        fdesc, tmp_file = tempfile.mkstemp()
        os.close(fdesc)
        result = 0

        if self.copyout(dst, tmp_file) != 0:
            result = 1

        os.chmod(tmp_file, int("700", 8))

        with open(os.devnull, "w") as fnull:
            result = subprocess.run([tmp_file], stderr=fnull)
        os.unlink(tmp_file)

        return result.returncode

    def copyout(self, src, dst):
        """Unfortunately, to copy one file out entire LZX archive have to be
        extracted. For small archives is not a problem, but in relatively big
        one it could be a performance issue."""
        tmp_dir = tempfile.mkdtemp()
        src = [e['display_name'] for e in self._contents
               if e['display_name'] == src]
        if not src:
            raise IOError("No such file or directory")

        src = src[0].encode('latin-1')

        current_dir = os.path.abspath(os.curdir)
        os.chdir(tmp_dir)

        with open(os.devnull, "w") as fnull:
            result = subprocess.run([self.ARCHIVER, self.CMDS['read'],
                                     os.path.join(current_dir, self._arch)],
                                    stdout=fnull, stderr=fnull)
        if result.returncode == 0:
            # use subprocess, as shutil.copy2 will complain about mixing
            # strings with bytes
            subprocess.run(['cp', src, dst])

        shutil.rmtree(tmp_dir)
        os.chdir(current_dir)

        return result.returncode


if __name__ == "__main__":
    sys.exit(extfslib.parse_args(ULzx))
