#!/usr/bin/env python3

import glob
import gzip
import os
import subprocess
import threading
import time

# Where logs and sampled data will wind up, and where the sequences are read.
# Do note that the sequences variable is supposed to be a tuple, because you
# could have multiple sets of sequences.
logdir    =  "/tmp/rdcost/logs"
ofdir     =  "/tmp/rdcost/data"
sequences = ("/opt/test_seqs/custom_seqs/*/*.yuv",)

# Note that n_kvazaars * len(dest_qps) has to be less than the max number of
# fd's that a process can have (check it out: ulimit -a, likely 1024)
smt_threads   = 8 # Kinda lazy, but just match this to your cpu
n_kvz_threads = 1 # How many threads each kvz instance is running?
n_kvazaars    = smt_threads // n_kvz_threads

# You likely will not need to change anything below this line
kvz_srcdir    = lambda path: os.path.join(
                                 os.path.dirname(
                                     os.path.dirname(
                                         os.path.realpath(__file__)
                                     )
                                 ), "src", path)


dest_qps      = tuple(range(51))
base_qps      = tuple(range(12, 43))

kvzargs       = [kvz_srcdir("kvazaar"), "--threads", str(n_kvz_threads), "--preset=ultrafast", "--fastrd-sampling", "--fast-residual-cost=0"]
kvzenv        = {"LD_LIBRARY_PATH": kvz_srcdir(".libs/")}

class MultiPipeGZOutManager:
    pipe_fn_template  = "%02i.txt"
    gzout_fn_template = "%02i.txt.gz"

    def __init__(self, odpath, dest_qps):
        self.odpath = odpath
        self.dest_qps = dest_qps

        self.pipe_fns  = []
        self.gzout_fns = []
        for qp in dest_qps:
            pipe_fn  = os.path.join(self.odpath, self.pipe_fn_template % qp)
            gzout_fn = os.path.join(self.odpath, self.gzout_fn_template % qp)

            self.pipe_fns.append(pipe_fn)
            self.gzout_fns.append(gzout_fn)

    def __enter__(self):
        os.makedirs(self.odpath, exist_ok=True)
        for pipe_fn in self.pipe_fns:
            try:
                os.unlink(pipe_fn)
            except FileNotFoundError:
                pass
            os.mkfifo(pipe_fn)
        return self

    def __exit__(self, *_):
        for pipe_fn in self.pipe_fns:
            os.unlink(pipe_fn)

    def items(self):
        for pipe_fn, gzout_fn in zip(self.pipe_fns, self.gzout_fns):
            yield (pipe_fn, gzout_fn)

class MTSafeIterable:
    def __init__(self, iterable):
        self.lock = threading.Lock()
        self.iterable = iterable

    def __iter__(self):
        return self

    def __next__(self):
        with self.lock:
            return next(self.iterable)

def combinations(xi, yi):
    for x in xi:
        for y in yi:
            yield (x, y)

def chain(lol):
    for l in lol:
        for i in l:
            yield i

# Would've used Popen with gzip, but "gzip [fifo]" with an unconnected fifo
# will detect the situation and not block, but just consider it an empty
# file. Don't like it when tools outsmart their user..
def do_gzip(in_fn, out_fn):
    BLOCK_SZ = 65536
    PRINT_MULT = 1024
    with open(in_fn, "rb") as inf, gzip.open(out_fn, "wb") as outf:
        num_read = 0
        print_next_thres = BLOCK_SZ * PRINT_MULT
        while True:
            block = inf.read(BLOCK_SZ)
            num_read += len(block)
            if (num_read >= print_next_thres):
                print("    read     %8i MB from %s" % (num_read / (1024 * 1024), in_fn))
                print_next_thres += BLOCK_SZ * PRINT_MULT

            if (len(block) == 0):
                break
            outf.write(block)

        print("    finished %8i MB from %s" % (num_read / (1024 * 1024), in_fn))

def run_job(job):
    ifpath, qp = job
    ifname = os.path.basename(ifpath)

    jobname  = "%s-qp%i" % (ifname, qp)
    hevcname = "%s.hevc" % jobname
    logname  = "%s.log"  % jobname
    odname   = jobname

    hevcpath = os.path.join("/tmp", hevcname)
    logpath  = os.path.join(logdir, logname)
    odpath   = os.path.join(ofdir,  odname)

    my_kvzargs = kvzargs + ["-i",              ifpath,
                            "--qp",            str(qp),
                            "-o",              hevcpath,
                            "--fastrd-outdir", odpath]

    with open(logpath, "w") as lf:
        with MultiPipeGZOutManager(odpath, dest_qps) as pipes_and_outputs:
            gzip_threads = []
            for pipe_fn, out_fn in pipes_and_outputs.items():
                gzip_thread = threading.Thread(target=do_gzip, args=(pipe_fn, out_fn))
                gzip_thread.start()
                gzip_threads.append(gzip_thread)

            kvz = subprocess.Popen(my_kvzargs, env=kvzenv, stderr=lf)
            kvz.wait()

def threadfunc(joblist):
    for job in joblist:
        run_job(job)

def main():
    assert(isinstance(sequences, tuple))
    for d in (logdir, ofdir):
        os.makedirs(d, exist_ok=True)

    jobs = combinations(chain(map(glob.glob, sequences)), base_qps)
    joblist = MTSafeIterable(jobs)

    threads = [threading.Thread(target=threadfunc, args=(joblist,)) for _ in range(n_kvazaars)]
    for thread in threads:
        thread.start()

    for thread in threads:
        thread.join()

if (__name__ == "__main__"):
    main()
