#!/usr/bin/env python3
# zx0 1.0: ZX0, Einar Saukas's optimal compressor of ZX Spectrum data (v2.2), in Python: packs and unpacks byte for
# byte as the original zx0 and dzx0 do.
# Copyright (c) 2026 Spectre (Optical Brothers), https://www.zxby.org. MIT License (see LICENSE); the program
# is a port of ZX0 and also under ZX0's BSD licence (see LICENSE-zx0).
#
#   zx0 [-f] [-c] [-b] [-q] [+N] INPUT [OUTPUT]   pack (OUTPUT: INPUT.zx0)
#   zx0 -d [-f] [-c] [-b] INPUT [OUTPUT]          unpack (OUTPUT: INPUT without .zx0)
#
# OUTPUT - is the standard output (for mc's viewer). See README.md.
#
# The format: a stream of bits and whole bytes; literals, copies from the last offset, copies from a new offset;
# lengths in interlaced Elias gamma code; the end is an offset MSB of 256. The original's copyright and licence:
# LICENSE (BSD 3-clause); this port keeps its algorithm and gives its blocks.

import heapq
import os
import re
import sys

MAX_OFFSET = 32640        # MAX_OFFSET_ZX0
QUICK_OFFSET = 2176       # MAX_OFFSET_ZX7: zx0 -q
INITIAL_OFFSET = 1
MAX_OUTPUT = 16 << 20     # unpacking: more than this is not a ZX0 file
BIG = 1 << 50             # no block
T = 64                    # the best lengths of a new offset's copy: a table up to T, range minima beyond
ZEROS = re.compile(b'\x00+')


class Error(Exception):
    pass


def offset_ceiling(index, limit):
    return limit if index > limit else INITIAL_OFFSET if index < INITIAL_OFFSET else index


# Packing. The original's optimize() looks at every offset at every index (up to 32640 a byte: half a minute for 40 KB)
# and keeps, for each offset, the cheapest way that ends with a copy from it (LM) and with literals after such a copy
# (LL); a block is (bits, index, offset, the block before), offset 0 for literals. This one does the same sums only
# where something changes and gives the same blocks, ties broken as there (the smallest offset):
# - an offset that does not match at an index only adds a literal: offsets are kept in groups by the index of their
#   LM, sorted by its bits; a heap holds each group's best;
# - a run of matches is seen where it starts (its end is found then) and where it ends; while it goes on, its copies
#   from the last offset are kept in groups by the run's start, and for copies from a new offset each class of
#   offsets (by the length of the MSB's code) gives its longest run;
# - an offset's own blocks (literals, copies from the last offset) are made only for a block that wins: from its
#   last block made, by its runs of matches in the data.

def optimize(data, skip, limit, progress=None):
    n = len(data)
    maxo = offset_ceiling(n - 1, limit)
    egb = [0] * (n + 3)  # elias_gamma_bits()
    for v in range(1, n + 3):
        egb[v] = 2 * v.bit_length() - 1
    egb1 = [x + 1 for x in egb]
    lit = [0] + [1 + egb[L] + 8 * L for L in range(1, n + 2)]  # L literals after a copy
    same = [1] * (n + 1)  # how many bytes from j are data[j]
    same[n] = 0
    for j in range(n - 2, -1, -1):
        if data[j] == data[j + 1]:
            same[j] = same[j + 1] + 1

    # per offset: LM's bits and index, the last block of its own made; the run of 2+ matches: start, end, LL's bits
    lmb = [BIG] * (maxo + 1)
    lmi = [0] * (maxo + 1)
    made = [None] * (maxo + 1)
    rs = [-1] * (maxo + 1)
    re_ = [-1] * (maxo + 1)
    llb = [BIG] * (maxo + 1)
    cls = [0] * (maxo + 1)
    cost = [0] * (maxo + 1)  # a new offset's copy but its length
    for o in range(1, maxo + 1):
        g = egb[((o - 1) >> 7) + 1]
        cost[o] = 8 + g
        cls[o] = g >> 1
    ncls = cls[maxo] + 1
    ccost = [8 + 2 * k + 1 for k in range(ncls)]
    cfirst = [1 + (((1 << k) - 1) << 7) for k in range(ncls)]
    cruns = [[] for _ in range(ncls)]   # runs of 2+ of each class in start order: start << 16 | offset
    chead = [0] * ncls
    ends_at = [[] for _ in range(n + 1)]
    opt = [None] * n
    optb = [0] * n
    CV = [BIG] * (T + 2)  # by length: the bits before a new offset's copy and of its length code; the best length
    BL = [0] * (T + 2)    # (the longest), the shortest as good
    ST = [0] * (T + 2)

    # groups: (bits << 16 | offset), sorted; heap entries: key << kb | offset << ib | index + base, ib bits enough for
    # any index + base (up to n + skip), so a file of any size
    lgroups, lptr, lheap = {}, {}, []
    agroups, aptr, aheap = {}, {}, []
    push, pop, replace = heapq.heappush, heapq.heappop, heapq.heapreplace
    ib = (n + skip).bit_length()
    kb = ib + 16
    mask = (1 << ib) - 1
    base = skip + 1
    lmb[1] = -1  # the fake block to start with
    lmi[1] = skip - 1
    made[1] = (-1, skip - 1, INITIAL_OFFSET, None)
    lgroups[skip - 1] = [(-1 << 16) | 1]
    lptr[skip - 1] = 0
    push(lheap, ((-1 - 8 * (skip - 1) + egb[1]) << kb) | (1 << ib) | (skip - 1 + base))

    def lm_block(o):
        """The LM block of o: from its last block made, each run of matches after it: the literals, then the copy."""
        b = made[o]
        t = lmi[o]
        if b[1] != t:
            j = max(b[1] + 1, skip + 1)
            z = (int.from_bytes(data[j:t + 1], 'big') ^ int.from_bytes(data[j - o:t + 1 - o], 'big')).to_bytes(
                t + 1 - j, 'big')
            for run in ZEROS.finditer(z):
                s = j + run.start()
                e = j + run.end() - 1
                x = b[0] + lit[s - 1 - b[1]]
                b = (x + egb1[e - s + 1], e, o, (x, s - 1, 0, b))
            made[o] = b
        return b

    # range minima of optb (bits << ib | position): the leftmost and the rightmost position of the minimum
    rmin_l, rmin_r = [[]], [[]]
    built = [0]

    def build(upto):
        for y in range(built[0], upto):
            v = optb[y] << ib
            rmin_l[0].append(v | y)
            rmin_r[0].append(v | (mask - y))
            k = 1
            while (1 << k) <= y + 1:
                if len(rmin_l) <= k:
                    rmin_l.append([])
                    rmin_r.append([])
                x = y - (1 << k) + 1
                h = x + (1 << (k - 1))
                a = rmin_l[k - 1]
                rmin_l[k].append(min(a[x], a[h]))
                a = rmin_r[k - 1]
                rmin_r[k].append(min(a[x], a[h]))
                k += 1
        built[0] = upto

    def best_length(i, m):
        """CV, BL, ST at i for a length m > T: the table up to T, then lengths 2^k + 1 .. 2^(k+1) by range minima."""
        if built[0] < i - 1:
            build(i - 1)
        dbest = CV[T]
        rbest, rbl, rst = BIG, 0, 0
        k = T.bit_length() - 1
        while (1 << k) + 1 <= m:
            lo = i - min(m, 1 << (k + 1))
            hi = i - (1 << k) - 1
            j = (hi - lo + 1).bit_length() - 1
            a = rmin_l[j]
            x = min(a[lo], a[hi - (1 << j) + 1])
            v = (x >> ib) + 2 * k + 1
            if v <= rbest:
                if v < rbest:
                    a = rmin_r[j]
                    y = min(a[lo], a[hi - (1 << j) + 1])
                    rst = i - (mask - (y & mask))
                rbest = v
                rbl = i - (x & mask)
            k += 1
        if rbest < dbest:
            return rbest, rbl, rst
        return dbest, (rbl if rbest == dbest else BL[T]), ST[T]

    # the window: positions by byte, by the pair (the byte before, the byte)
    wset = [set() for _ in range(256)]
    pset = {}
    preds = [[] for _ in range(256)]

    def window_add(p):
        wset[data[p]].add(p)
        if p:
            key = (data[p - 1] << 8) | data[p]
            s = pset.get(key)
            if s is None:
                s = pset[key] = set()
                preds[data[p]].append(data[p - 1])
            s.add(p)

    lo = max(skip - offset_ceiling(skip, limit), 0)
    for p in range(lo, skip):
        window_add(p)
    tick = n // 50 + 1
    next_tick = skip + tick
    grown = [0]

    def compact(i):
        """Groups keep the offsets that left them until the group is looked at: drops those (each offset is in one
        group at most), and the groups left empty."""
        for e in list(lgroups):
            g = [x for x in lgroups[e][lptr[e]:] if lmi[x & 0xFFFF] == e and rs[x & 0xFFFF] <= e]
            if g:
                lgroups[e] = g
                lptr[e] = 0
            else:
                del lgroups[e], lptr[e]
        lheap[:] = [(((g[0] >> 16) - 8 * e + egb[i - e]) << kb) | ((g[0] & 0xFFFF) << ib) | (e + base)
                    for e, g in lgroups.items()]
        heapq.heapify(lheap)
        for s in list(agroups):
            g = [x for x in agroups[s][aptr[s]:] if rs[x & 0xFFFF] == s and re_[x & 0xFFFF] >= i]
            if g:
                agroups[s] = g
                aptr[s] = 0
            else:
                del agroups[s], aptr[s]
        aheap[:] = [(((g[0] >> 16) + egb1[i + 1 - s]) << kb) | ((g[0] & 0xFFFF) << ib) | (s + base)
                    for s, g in agroups.items()]
        heapq.heapify(aheap)
        grown[0] = 0

    for i in range(skip, n):
        if progress and i >= next_tick:
            progress(i)
            next_tick += tick
        M = offset_ceiling(i, limit)
        c = data[i]
        im1 = i - 1
        ip1 = i + 1
        if i > skip:
            window_add(im1)
        while lo < i - M:
            wset[data[lo]].discard(lo)
            if lo:
                pset[(data[lo - 1] << 8) | data[lo]].discard(lo)
            lo += 1
        best = BIG << 16  # the best block so far: bits << 16 | offset
        if grown[0] > 4 * maxo + 65536:
            compact(i)

        # runs of 2+ going on: each class's longest; with those that end here, how far the CV table must go
        ends = ends_at[i]
        ends_at[i] = None
        need = 0
        fronts = []
        for k in range(ncls):
            q = cruns[k]
            h = chead[k]
            ln = len(q)
            while h < ln:
                x = q[h]
                o = x & 0xFFFF
                if rs[o] == x >> 16 and re_[o] >= i:
                    break
                h += 1
            if h > 4096 and h > ln >> 1:
                del q[:h]
                h = 0
                ln = len(q)
            chead[k] = h
            if h < ln:
                m = ip1 - (q[h] >> 16)
                fronts.append((k, m))
                if m > need:
                    need = m
        for o in ends:
            m = ip1 - rs[o]
            if m > need:
                need = m
        if need >= 2:
            BL[2] = 2
            CV[2] = optb[i - 2] + 1
            ST[2] = 2
            top = need if need < T else T
            k = 3
            while k <= top:
                v = optb[i - k] + egb[k - 1]
                u = CV[k - 1]
                if v < u:
                    BL[k] = k
                    ST[k] = k
                    CV[k] = v
                elif v == u:
                    BL[k] = k
                    ST[k] = ST[k - 1]
                    CV[k] = u
                else:
                    BL[k] = BL[k - 1]
                    ST[k] = ST[k - 1]
                    CV[k] = u
                k += 1

        # literals: the best group (its first offset still in literals, with the same LM)
        while lheap:
            x = lheap[0]
            e = (x & mask) - base
            g = lgroups[e]
            k = lptr[e]
            ln = len(g)
            while k < ln:
                o = g[k] & 0xFFFF
                if lmi[o] == e and (i == skip or data[i - o] != c):
                    break
                k += 1
            if k == ln:
                pop(lheap)
                del lgroups[e], lptr[e]
                continue
            lptr[e] = k
            y = (((g[k] >> 16) - 8 * e + egb[i - e]) << kb) | (o << ib) | (e + base)
            if y == x:
                v = (((g[k] >> 16) + lit[i - e]) << 16) | o
                if v < best:
                    best = v
                break
            replace(lheap, y)

        # runs of 2+ going on: copies from the last offset, the best group
        while aheap:
            x = aheap[0]
            s = (x & mask) - base
            g = agroups[s]
            k = aptr[s]
            ln = len(g)
            while k < ln:
                o = g[k] & 0xFFFF
                if rs[o] == s and re_[o] >= i:
                    break
                k += 1
            if k == ln:
                pop(aheap)
                del agroups[s], aptr[s]
                continue
            aptr[s] = k
            v = (g[k] >> 16) + egb1[ip1 - s]
            y = (v << kb) | (o << ib) | (s + base)
            if y == x:
                v = (v << 16) | o
                if v < best:
                    best = v
                break
            replace(aheap, y)

        # the matches that start a run here: one byte (it ends here), or two or more
        newlit = []
        newa = []
        if i != skip:
            if im1 == skip:
                starts = wset[c]
            else:
                cp = data[im1]
                ws = wset[c]
                pr = preds[c]
                if len(ws) < 8 * len(pr) + 32:
                    starts = ws.difference(pset.get((cp << 8) | c, ()))
                else:
                    starts = set()
                    for b in pr:
                        if b != cp:
                            starts.update(pset[(b << 8) | c])
                    if lo == 0 and data[0] == c:
                        starts.add(0)
            if starts:
                nxt = data[ip1] if ip1 < n else -1
                for p in starts:
                    o = i - p
                    x = lmb[o] + lit[im1 - lmi[o]]  # LL's bits (an offset new to the window has no LM)
                    if data[p + 1] == nxt:
                        e = ip1
                        while True:
                            q = e + 1
                            if q >= n or data[q] != data[q - o]:
                                break
                            r = same[q]
                            if r > 1:
                                r2 = same[q - o]
                                if r2 < r:
                                    r = r2
                            e += r
                            if r == 1:
                                q = e + 1
                                if q < n and data[q] == data[q - o]:
                                    step = 4
                                    while e + step < n and data[e + 1:e + 1 + step] == data[e + 1 - o:e + 1 - o + step]:
                                        e += step
                                        step <<= 1
                                    while step > 1:
                                        step >>= 1
                                        q = e + 1
                                        if e + step < n and data[q:q + step] == data[q - o:q - o + step]:
                                            e += step
                                    break
                        rs[o] = i
                        re_[o] = e
                        llb[o] = x
                        ends_at[e].append(o)
                        cruns[cls[o]].append((i << 16) | o)
                        if x < BIG:
                            newa.append((x << 16) | o)
                    elif x < BIG:
                        x += 2
                        lmb[o] = x
                        lmi[o] = i
                        newlit.append((x << 16) | o)
                if newlit:
                    x = min(newlit)
                    if x < best:
                        best = x
                if newa:
                    x = min(newa) + (2 << 16)
                    if x < best:
                        best = x

        # runs of 2+ going on: copies from a new offset; each class's longest run, the smallest offset as good
        bv = best >> 16
        bo = best & 0xFFFF
        for k, m in fronts:
            if m <= T:
                v = CV[m] + ccost[k]
                st = ST[m]
            else:
                v, _, st = best_length(i, m)
                v += ccost[k]
            if v < bv or (v == bv and cfirst[k] < bo):
                q = cruns[k]
                lim = ip1 - st
                o = 0xFFFF
                for h in range(chead[k], len(q)):
                    x = q[h]
                    s = x >> 16
                    if s > lim:
                        break
                    y = x & 0xFFFF
                    if y < o and rs[y] == s and re_[y] >= i:
                        o = y
                if v < bv or o < bo:
                    bv = v
                    bo = o
        v = bv
        o = bo

        # the block
        if i == skip or data[i - o] != c:
            blk = (v, i, 0, lm_block(o))
        elif rs[o] < i <= re_[o]:
            m = ip1 - rs[o]
            if llb[o] + egb1[m] == v:
                blk = (v, i, o, (llb[o], rs[o] - 1, 0, lm_block(o)))
            else:
                blk = (v, i, o, opt[i - (BL[m] if m <= T else best_length(i, m)[1])])
        elif rs[o] == i:
            blk = (v, i, o, (llb[o], im1, 0, lm_block(o)))
        else:
            blk = lm_block(o)
        opt[i] = blk
        optb[i] = v

        # runs of 2+ that end here: their LM
        for o in ends:
            m = ip1 - rs[o]
            a = llb[o] + egb1[m]
            if m <= T:
                cv, bl = CV[m], BL[m]
            else:
                cv, bl, _ = best_length(i, m)
            b = cv + cost[o]
            if a > b:
                a = b
                made[o] = (b, i, o, opt[i - bl])
            lmb[o] = a
            lmi[o] = i
            newlit.append((a << 16) | o)
        if newlit:
            grown[0] += len(newlit)
            newlit.sort()
            lgroups[i] = newlit
            lptr[i] = 0
            x = newlit[0]
            push(lheap, (((x >> 16) - 8 * i + 1) << kb) | ((x & 0xFFFF) << ib) | (i + base))
        if newa:
            grown[0] += len(newa)
            newa.sort()
            agroups[i] = newa
            aptr[i] = 0
            x = newa[0]
            push(aheap, (((x >> 16) + egb1[2]) << kb) | ((x & 0xFFFF) << ib) | (i + base))
    return opt[n - 1]


class Writer(object):
    """The original's output: bits into bytes, the first bit of a length into the offset's byte (backtrack). diff is
    the stream's bytes not written yet less the data's not read yet; delta, its largest."""

    def __init__(self, diff, backwards):
        self.out = bytearray()
        self.bit_index = 0
        self.bit_mask = 0
        self.backtrack = True
        self.backwards = backwards
        self.diff = diff
        self.delta = 0

    def read(self, n):
        self.diff += n
        if self.delta < self.diff:
            self.delta = self.diff

    def byte(self, value):
        self.out.append(value)
        self.diff -= 1

    def bit(self, value):
        if self.backtrack:
            if value:
                self.out[-1] |= 1
            self.backtrack = False
            return
        if not self.bit_mask:
            self.bit_mask = 128
            self.bit_index = len(self.out)
            self.byte(0)
        if value:
            self.out[self.bit_index] |= self.bit_mask
        self.bit_mask >>= 1

    def gamma(self, value, invert=False):
        i = 2
        while i <= value:
            i <<= 1
        i >>= 2
        while i:
            self.bit(self.backwards)
            self.bit(not (value & i) if invert else value & i)
            i >>= 1
        self.bit(not self.backwards)


def write_stream(best, data, skip, backwards, invert):
    """The original's compress(): the blocks into the stream. Returns the stream and the delta."""
    blocks = []
    b = best
    while b is not None:
        blocks.append(b)
        b = b[3]
    blocks.reverse()
    size = (best[0] + 25) // 8
    w = Writer(size - len(data) + skip, backwards)
    pos = skip
    last_offset = INITIAL_OFFSET
    for prev, b in zip(blocks, blocks[1:]):
        length = b[1] - prev[1]
        if b[2] == 0:  # literals
            w.bit(0)
            w.gamma(length)
            for k in range(length):
                w.byte(data[pos])
                pos += 1
                w.read(1)
        elif b[2] == last_offset:  # a copy from the last offset
            w.bit(0)
            w.gamma(length)
            pos += length
            w.read(length)
        else:  # a copy from a new offset
            w.bit(1)
            w.gamma((b[2] - 1) // 128 + 1, invert)
            w.byte(((b[2] - 1) % 128) << 1 if backwards else (127 - (b[2] - 1) % 128) << 1)
            w.backtrack = True
            w.gamma(length - 1)
            pos += length
            w.read(length)
            last_offset = b[2]
    w.bit(1)
    w.gamma(256, invert)
    if len(w.out) != size:
        raise Error('internal error: %d bytes written, %d counted' % (len(w.out), size))
    return bytes(w.out), w.delta


def pack(data, skip=0, backwards=False, classic=False, quick=False, progress=None):
    """zx0: data -> (stream, delta). skip: a prefix (backwards: a suffix) only referred to, not packed."""
    if backwards:
        data = data[::-1]
    best = optimize(data, skip, QUICK_OFFSET if quick else MAX_OFFSET, progress)
    stream, delta = write_stream(best, data, skip, backwards, not classic and not backwards)
    return (stream[::-1] if backwards else stream), delta


# Unpacking: dzx0's decompress(), and backwards as the Z80 dzx0_standard_back does.

def unpack(stream, classic=False, backwards=False):
    """A stream -> its data. Error if it is cut short, copies from before the data's start or has bytes after its
    end."""
    if backwards:
        stream = stream[::-1]
    size = len(stream)
    out = bytearray()
    st = [0, 0, 0, False, 0]  # the next byte, the bit mask, the bits' byte, backtrack, the last byte read
    more = 1 if backwards else 0  # the bit that goes on with a gamma code
    inverted = 0 if classic or backwards else 1

    def read_byte():
        if st[0] >= size:
            raise Error('cut short' if size else 'empty')
        st[4] = stream[st[0]]
        st[0] += 1
        return st[4]

    def read_bit():
        if st[3]:
            st[3] = False
            return st[4] & 1
        st[1] >>= 1
        if not st[1]:
            st[1] = 128
            st[2] = read_byte()
        return 1 if st[2] & st[1] else 0

    def gamma(inv=0):
        value = 1
        while read_bit() == more:
            value = value << 1 | (read_bit() ^ inv)
            if value > MAX_OUTPUT:
                raise Error('a length or an offset past %d' % MAX_OUTPUT)
        return value

    def copy(offset, length):
        if offset > len(out):
            raise Error('a copy from before the start')
        if len(out) + length > MAX_OUTPUT:
            raise Error('more than %d bytes' % MAX_OUTPUT)
        if offset >= length:
            out.extend(out[len(out) - offset:len(out) - offset + length])
        else:
            chunk = out[len(out) - offset:]
            out.extend((chunk * (length // offset + 1))[:length])

    last_offset = INITIAL_OFFSET
    state = 0  # 0: literals, 1: a copy from the last offset, 2: a copy from a new offset
    while True:
        if state == 0:
            length = gamma()
            if len(out) + length > MAX_OUTPUT:
                raise Error('more than %d bytes' % MAX_OUTPUT)
            for k in range(length):
                out.append(read_byte())
            state = 2 if read_bit() else 1
        elif state == 1:
            copy(last_offset, gamma())
            state = 2 if read_bit() else 0
        else:
            msb = gamma(inverted)
            if msb == 256:
                if st[0] != size:
                    raise Error('bytes after its end: %d' % (size - st[0]))
                break
            if backwards:
                last_offset = (msb - 1) * 128 + (read_byte() >> 1) + 1
            else:
                last_offset = msb * 128 - (read_byte() >> 1)
            st[3] = True
            copy(last_offset, gamma() + 1)
            state = 2 if read_bit() else 0
    return bytes(out[::-1]) if backwards else bytes(out)


# The command line, as zx0's and dzx0's.

USAGE = """usage: zx0 [-f] [-c] [-b] [-q] [+N] INPUT [OUTPUT]   pack (OUTPUT: INPUT.zx0)
       zx0 -d [-f] [-c] [-b] INPUT [OUTPUT]          unpack (OUTPUT: INPUT without .zx0)
  -f  overwrite OUTPUT       -c  the classic format (v1)       -b  backwards
  -q  quick: offsets up to 2176 only, a little longer      +N  the first N bytes (-b: the last) are only referred to
  OUTPUT - is the standard output
"""


def show(name):
    return name.encode('utf-8', 'surrogateescape').decode('utf-8', 'replace')


def say(text):
    """A line on the standard output, what its encoding lacks as \\x.. (as on the standard error): it can't fail."""
    code = getattr(sys.stdout, 'encoding', None) or 'utf-8'
    sys.stdout.write(text.encode(code, 'backslashreplace').decode(code) + '\n')
    sys.stdout.flush()


def main(argv):
    args = argv[1:]
    if args and args[0] in ('-h', '--help'):
        sys.stdout.write(USAGE)
        return 0
    flags = set()
    skip = 0
    k = 0
    while k < len(args) and args[k][:1] in ('-', '+'):
        a = args[k]
        if a in ('-d', '-f', '-c', '-b', '-q'):
            flags.add(a[1])
        elif re.match(r'\+[0-9]+$', a) and int(a[1:]) > 0:
            skip = int(a[1:])
        else:
            sys.stderr.write('zx0: %s: no such option\n%s' % (show(a), USAGE))
            return 2
        k += 1
    names = args[k:]
    if not 1 <= len(names) <= 2 or 'd' in flags and ('q' in flags or skip):
        sys.stderr.write(USAGE)
        return 2
    src = names[0]
    try:
        with open(src, 'rb') as f:
            data = f.read()
        if 'd' in flags:
            if len(names) == 2:
                dest = names[1]
            elif len(src) > 4 and src.endswith('.zx0'):
                dest = src[:-4]
            else:
                raise Error('%s: no .zx0 at the end of the name: give OUTPUT' % show(src))
        else:
            dest = names[1] if len(names) == 2 else src + '.zx0'
            if not data:
                raise Error('%s: empty' % show(src))
            if skip >= len(data):
                raise Error('%s: %d bytes, all skipped' % (show(src), len(data)))
        if dest != '-' and 'f' not in flags and os.path.exists(dest):
            raise Error('%s exists (-f to overwrite)' % show(dest))
        if 'd' in flags:
            try:
                out = unpack(data, 'c' in flags, 'b' in flags)
            except Error as e:
                kind = 'packed backwards' if 'b' in flags else 'of the classic format' if 'c' in flags else \
                    '(or one packed with -b, -c or +N)'
                raise Error('%s: not a ZX0 stream %s: %s' % (show(src), kind, e))
            note = '%s: %d -> %d bytes' % (show(dest), len(data), len(out))
        else:
            progress = None
            if dest != '-' and len(data) - skip >= 4096 and sys.stderr.isatty():
                def progress(i):
                    sys.stderr.write('\rzx0: %s %d%%' % (show(src), 100 * (i - skip) // (len(data) - skip)))
                    sys.stderr.flush()
            out, delta = pack(data, skip, 'b' in flags, 'c' in flags, 'q' in flags, progress)
            if progress:
                sys.stderr.write('\r%s\r' % (' ' * (len(show(src)) + 10)))
            note = '%s: %d -> %d bytes, delta %d' % (show(dest), len(data) - skip, len(out), delta)
        if dest == '-':
            sys.stdout.buffer.write(out)
            sys.stdout.flush()
        else:
            with open(dest, 'wb') as f:
                f.write(out)
            say(note)
    except (Error, OSError) as e:
        if isinstance(e, BrokenPipeError):  # the reader went away: no flush of the rest at exit, one line
            try:
                os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
            except (OSError, ValueError, AttributeError):
                pass
            e = 'the standard output: %s' % e.strerror
        elif isinstance(e, OSError) and e.filename is not None:
            e = '%s: %s' % (show(e.filename), e.strerror)
        sys.stderr.write('zx0: %s\n' % e)
        return 1
    except KeyboardInterrupt:
        sys.stderr.write('\nzx0: interrupted\n')
        return 1
    return 0


if __name__ == '__main__':
    sys.exit(main(sys.argv))
