#!/usr/bin/env python3
import socket
import optparse
import os
import json
import traceback
import time
import sys
import io
import subprocess
import logging

msg = None

def player_metadata():
    for k in range(20):
        try:
            time.sleep(1.0)
            return subprocess.check_output(['playerctl', 'metadata']).decode('utf-8').strip()
        except:
            continue

def memory_stats():
    with io.BytesIO(subprocess.check_output('free', shell=True)) as f:
        t1 = f.read().decode('utf-8').splitlines()
        mem_total = int(t1[1].strip().split()[1])
        mem_used = int(t1[1].strip().split()[2])

    return dict(
        mem_total=mem_total,
        mem_used=mem_used,
    )

def eternal_oom(memory_limit=None):
    import signal
    import re
    import time
    import pprint
    self_pid = os.getpid()
    if memory_limit is None:
        memory_limit = 3 * 1024 * 1024
    assert isinstance(memory_limit, int) \
        and memory_limit < memory_stats()['mem_total'] * 0.8 \
        and memory_limit > 512 * 1024

    def pandas_data_frame(lines, groups_regex, header_regex, extra_columns):
        header = re.compile(header_regex).search(lines[0]).groups()
        rows = [
            re.compile(groups_regex).search(row).groups()
            for row in lines[1:]
        ]
        columns = {
            column: []
            for column in header
        }
        for row in rows:
            for value, column in zip(row, header):
                columns[column].append(value)
        for column, transformation in extra_columns.items():
            columns[column] = [
                transformation(
                    {
                        k : v[index]
                        for k, v in columns.items()
                    }
                )
                for index in range(len(rows))
            ]

        return columns

    def pandas_merge(left, right, on):
        index = {}
        input_data_frames = [
            ('left', left),
            ('right', right),
        ]
        for index_name, data_frame in input_data_frames:
            current_index = {}
            for row_index, value in enumerate(data_frame[on]):
                if not value in current_index:
                    current_index[value] = []
                current_index[value].append(row_index)

            index[index_name] = current_index

        merged_data_frame = dict(
            header=[
                column + '_x'
                for column in left
            ] + [
                column + '_y'
                for column in right
            ],
            columns={},
        )

        for column in merged_data_frame['header']:
            merged_data_frame['columns'][column] = []

        common_values = {
            left_value
            for left_value in index['left']
            if left_value in index['right']
        }
        common_rows = sorted(
            [
                dict(
                    left_row_index=index['left'][value][0],
                    right_row_index=index['right'][value][0],
                )
                for value in common_values
            ],
            key=lambda x: x['left_row_index'],
        )
        for common_row in common_rows:
            row = sum([
                [
                    values[
                        common_row['%s_row_index' % index_name]
                    ]
                    for column, values in data_frame.items()
                ]
                for index_name, data_frame in input_data_frames
            ], [])
            for column, value in zip(merged_data_frame['header'], row):
                merged_data_frame['columns'][column].append(value)

        return merged_data_frame['columns']

    def pandas_sort_values(data_frame, by, ascending):
        assert len(by) == 1
        assert ascending is False
        t1 = [
            o['row_index']
            for o in sorted(
                [
                    dict(
                        row_index=row_index,
                        value=value
                    )
                    for row_index, value in enumerate(data_frame[by[0]])
                ],
                key=lambda x: x['value']
            )[::-1]
        ]
        return {
            column : [
                values[row_index]
                for row_index in t1
            ]
            for column, values in data_frame.items()
        }

    def pandas_filter_values(data_frame, condition):
        shape = [
            len(data_frame),
        ]
        if shape[0] > 0:
            shape.append(
                len(list(data_frame.values())[0])
            )
        t1 = [
            row_index
            for row_index in range(shape[1])
            if condition(
                {
                    column : values[row_index]
                    for column, values in data_frame.items()
                }
            )
        ]
        return {
            column : [
                values[row_index]
                for row_index in t1
            ]
            for column, values in data_frame.items()
        }

    def pandas_row(data_frame, row_index):
        return {
            column : values[row_index]
            for column, values in data_frame.items()
        }

    while True:
        with io.BytesIO(subprocess.check_output('ps -e -o pid,rss,user', shell=True)) as f:
            t1 = pandas_data_frame(
                f.read().decode('utf-8').splitlines(),
                r'^\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s*$',
                r'^\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s*$',
                dict(
                    PID=lambda row: int(row['PID']),
                    RSS=lambda row: int(row['RSS']),
                ),
            )

        mem_used = memory_stats()['mem_used']
        t5 = subprocess.check_output('ps -e -o pid,args', shell=True).decode('utf-8').splitlines()
        t6 = pandas_data_frame(
            t5,
            r'^\s*(\d+)\s(.*)$',
            r'^\s+(\w+)\s+(\w+)\s*$',
            dict(
                PID=lambda row: int(row['PID'])
            ),
        )
        t7 = pandas_merge(t1, t6, on='PID')
        t8 = pandas_sort_values(t7, by=['RSS_x'], ascending=False)
        t9 = pandas_filter_values(
            t8,
            lambda row: row['PID_x'] != self_pid and not 'freelancer' in row['COMMAND_y']
        )
        t4 = lambda : os.kill(t9['PID_x'][0], signal.SIGKILL)
        t10 = lambda : mem_used > memory_limit
        if t10():
            pprint.pprint([
                'Killing',
                pandas_row(t9, 0),
                mem_used,
            ])
            t4()
        time.sleep(1)

def resilient_vlc(stream=None):
    if stream is None:
        streams_path = os.path.join(
            os.environ['CACHE_PATH'],
            'resilient-vlc-streams.json'
        )

        if os.path.exists(streams_path):
            with io.open(
                streams_path,
                'r'
            ) as f:
                stream = json.load(f)
        else:
            raise RuntimeError(
                'not found, %s' % streams_path
            )

    if isinstance(stream, str):
        stream = [stream]

    if len(stream) == 0:
        raise RuntimeError('no streams')

    import subprocess
    import time
    while True:
        print('new start')
        with subprocess.Popen([
            'cvlc', '--verbose', '2', *stream,
        ], stderr=subprocess.PIPE) as p:
            while p.returncode is None:
                t1 = p.stderr.readline().decode('utf-8')
                if len(t1) > 0:
                    print(t1)
                if not all([
                    o in t1
                    for o in [
                        'prefetch stream error',
                        'terror',
                        'main interface error',
                    ]
                ]) and any([
                    o in t1
                    for o in [
                        'pulse audio output debug: underflow'
                    ]
                ]):
                    print('shit')
                    p.kill()
                    while True:
                        try:
                            t2 = p.wait(timeout=1)
                            print(t2)
                            break
                        except:
                            print('shit')
                            pass
        time.sleep(1.0)

def eternal_firefox(
    tabs=None,
    profile=None,
    group_name=None,
    window_position=None,
    debug=None,
):
    import os
    import datetime
    import pprint
    import subprocess
    import time
    if debug is None:
        debug = False
    if tabs is None:
        raise RuntimeError('no tabs provided')
    if profile is None:
        raise RuntimeError('no profile provided')
    if group_name is None:
        raise RuntimeError('no group provided')
    if window_position is None:
        #window_position = '1,600,0,600,540'
        raise RuntimeError('no window-position provided')
    while True:
        os.system(r'''date''')
        with subprocess.Popen([
            'firefox',
            '-P', profile,
            *tabs,
        ]) as p:
            try:
                if debug:
                    assert subprocess.check_call(['notify-send', '%s:Starting' % group_name]) == 0

                #t3 = ''
                for k in range(300):
                    t1 = subprocess.check_output(r'''
                        swaymsg -t get_tree | jq -r '..|try select(.pid== %d)'
                    ''' % p.pid, shell=True).decode('utf-8')
                    if len(t1) > 10:
                        break
                    #time.sleep(0.1)
                    #t1 = subprocess.check_output(['wmctrl', '-p', '-l']).decode('utf-8')
                    #t4 = [o for o in t1.splitlines() if str(p.pid) in o]
                    #if len(t4) == 1:
                    #    t3 = t4[0]
                    #    break

                #if t3 == '':
                #    raise RuntimeError

                #t2 = t3.split()[0]
                #assert os.system('wmctrl -i -r %s -e %s' % (t2, window_position)) == 0
                #assert os.system('wmctrl -i -r %s -b add,below' % t2) == 0
                def reposition():
                    t1 = lambda s: \
                        s \
                        .replace('{{PID}}', str(p.pid)) \
                        .replace('{{X}}', str(window_position[1])) \
                        .replace('{{Y}}', str(window_position[2])) \
                        .replace('{{W}}', str(window_position[3])) \
                        .replace('{{H}}', str(window_position[4])) \
                        .replace('{{WORKSPACE}}', str(window_position[0]))

                    assert os.system(t1(r'''
                        swaymsg '[pid="{{PID}}"] move window to workspace {{WORKSPACE}}'
                    ''')) == 0

                    if window_position[1] != '' and window_position[2] != '':
                        assert os.system(t1(r'''
                            swaymsg '[pid="{{PID}}"] floating enable' \
                            swaymsg '[pid="{{PID}}"] resize set width {{W}}px height {{H}}px' && \
                            swaymsg '[pid="{{PID}}"] move absolute position {{X}}px {{Y}}px'
                        ''')) == 0
                    else:
                        assert os.system(t1(r'''
                            swaymsg '[pid="{{PID}}"] floating disable'
                        ''')) == 0


                if False:
                    for tab in tabs[1:]:
                        time.sleep(10)
                        assert subprocess.check_call([
                            'firefox',
                            '-P', profile,
                            '--new-tab',
                            tab,
                        ]) == 0


                reposition()

                if debug:
                    assert subprocess.check_call(['notify-send', '%s:Started' % group_name]) == 0

                start = datetime.datetime.now()
                is_to_restart = lambda : (datetime.datetime.now() - start).total_seconds() >= 900 * 4
                polling_count = 0

                while not is_to_restart():
                    if polling_count == 0:
                        reposition()

                    if not p.poll() is None:
                        break
                    time.sleep(10)
                    polling_count += 1

                if debug:
                    assert subprocess.check_call(['notify-send', '%s:Closing' % group_name]) == 0

                #assert os.system('wmctrl -i -c %s' % t2) == 0
                assert os.system(r'''
                    swaymsg '[pid="%d"] kill'
                ''' % (p.pid,)) == 0

            except KeyboardInterrupt:
                assert os.system(r'''
                    swaymsg '[pid="%d"] kill'
                ''' % (p.pid,)) == 0
                break
            except:
                import traceback
                import pprint
                pprint.pprint(traceback.format_exc())
            finally:
                try:
                    p.wait(20)
                except subprocess.TimeoutExpired:
                    pprint.pprint([p.pid, '20 seconds timeout', 'kill'])
                    p.kill()
                if debug:
                    assert subprocess.check_call(['notify-send', '%s:Closed' % group_name]) == 0

def resilient_ethernet(ip_addr, ethernet_device):
    subprocess.check_call(
        r'''
            sudo sh -c '\
while true; \
do ping -c 3 -w 3 -W 1 {{IP_ADDR}} || (\
    ip link set {{ETHERNET_DEVICE}} down; \
    ip link set {{ETHERNET_DEVICE}} up; \
    sleep 4; true;\
); \
sleep 10; clear; date; \
done'
        '''.replace(
            '{{IP_ADDR}}',
            ip_addr
        ).replace(
            '{{ETHERNET_DEVICE}}}',
            ethernet_device
        ),
        shell=True
    )

def http_server(argv):
    assert isinstance(argv, list) and all([isinstance(o, str) for o in argv])
    parser = optparse.OptionParser()
    parser.add_option(
        '--public',
        dest='public',
        action='store_true',
        default=False,
    )
    parser.add_option(
        '--port',
        dest='port',
        type='int',
        default=80,
    )
    parser.add_option(
        '--host',
        dest='host',
        type='str',
        default='127.0.0.1',
    )
    options, args = parser.parse_args(argv)

    assert options.port >= 1

    try:
        assert not socket.inet_aton(options.host) is None
        subprocess.check_call([
            'ping', '-w', '1',
            options.host
        ])
    except:
        raise RuntimeError('invalid ip address %s' % options.host)


    index_section = 'autoindex on;'
    if len(sys.argv) == 3 and sys.argv[2] == '--public':
        location_section = 'location / {%s}' % index_section
    else:
        token = os.urandom(16).hex()
        print(
            'access url is http://%s:%d/%s/' % (
                options.host,
                options.port,
                token,
            )
        )
        location_section = (
            'location / {'
            'deny all;'
            '}'
            'location /%s/ {'
            'alias /app/;'
            '%s'
            '}'
        ) % (token, index_section)
    subprocess.check_call(
        r'''
            sudo docker run \
                -p %s:%d:80 \
                -u root \
                -it --entrypoint=/bin/bash \
                -v $PWD:/app:ro \
                nginx:latest \
                -c 'echo "server{listen 80; charset UTF-8; root /app; %s}" > /etc/nginx/conf.d/default.conf; nginx -g "daemon off;"'
        ''' % (
            options.host,
            options.port,
            location_section,
        ),
        shell=True)

def player_v1(folder_url, item_id):
    import sys
    import urllib.parse
    import re
    import subprocess
    import os
    import tqdm
    t4 = folder_url
    t1 = subprocess.check_output(['curl', '-s', t4]).decode('utf-8')
    t2 = re.compile(r"href=\"(.*\.mp3)\"");
    t3 = [o.group(1) for o in t2.finditer(t1)];
    t5 = ['%s/%s' % (t4, o) for o in t3]
    t6 = item_id
    t9 = range(t6, len(t5))
    with tqdm.tqdm(
        total=len(t5),
    ) as progress_bar:
        progress_bar.update(t6)
        for k in t9:
            t7 = t5[k]
            t9 = urllib.parse.unquote(os.path.split(t7)[1])
            progress_bar.set_description('%03d %s' % (k, t9))
            with subprocess.Popen(['ffprobe', '-hide_banner', '-i', t7], stderr=subprocess.PIPE, stdout=subprocess.PIPE) as p:
                p.wait()
                assert p.returncode == 0
                t8 = p.stderr.read().decode('utf-8')
            #print(t8)
            with subprocess.Popen(['ffplay', '-hide_banner', '-nodisp', '-autoexit', '-loop', '1', t7], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) as p:
                p.wait()
                assert p.returncode == 0
            progress_bar.update(1)

def status():
    return ' | '.join([
        subprocess.check_output(o, shell=True).decode('utf-8').strip()
        for o in [
            r'''
                free -h | \
                    grep -P Mem: | grep -Po '[\w\.\d]+' | tail -n +2 | head -n 3 | xargs echo -n;
            ''',
            r'''
                sensors | \
                    grep -Po '[\\\+\\\-\\\w][^\\\s]+C ' | head -n 5 | xargs echo -n
            ''',
            r'''
                ssh nartes@pizcool3070 free -h | \
                    grep -P Mem: | grep -Po '[\w\.\d]+' | tail -n +2 | head -n 3 | xargs echo -n;
            ''',
            r'''
                ssh nartes@pizcool3070 sensors | \
                    grep -Po '[\\\+\\\-\.0-9]+\s+C ' | head -n 1
            ''',
            r'''
                date +'%Y-%m-%d %l:%M:%S %p';
            ''',
        ]
    ]).replace('\n\r', '')


try:
    if sys.argv[1] == 'media-play-pause':
        subprocess.check_call(['playerctl', 'play-pause'])
        msg = player_metadata()
    elif sys.argv[1] == 'media-next':
        subprocess.check_call(['playerctl', 'next'])
        msg = player_metadata()
    elif sys.argv[1] == 'media-prev':
        subprocess.check_call(['playerctl', 'previous'])
        msg = player_metadata()
    elif sys.argv[1] == 'media-lower-volume':
        subprocess.check_call([
            'pactl',
            'set-sink-volume',
            '@DEFAULT_SINK@',
            '-5%'
        ])
        msg = subprocess.check_output([
            'pactl',
            'get-sink-volume',
            '@DEFAULT_SINK@'
        ]).decode('utf-8').strip()
    elif sys.argv[1] == 'media-raise-volume':
        subprocess.check_call([
            'pactl',
            'set-sink-volume',
            '@DEFAULT_SINK@',
            '+5%'
        ])
        msg = subprocess.check_output([
            'pactl',
            'get-sink-volume',
            '@DEFAULT_SINK@'
        ]).decode('utf-8').strip()
    elif sys.argv[1] == 'status':
        sys.stdout.write(status())
        sys.stdout.flush()
    elif sys.argv[1] == 'http-server':
        http_server(sys.argv[2:])
    elif sys.argv[1] == 'wl-screenshot':
        subprocess.check_call(r'''
            grim -g "$(slurp)" - | wl-copy
        ''', shell=True)
    elif sys.argv[1] == 'eternal-oom':
        eternal_oom(
            memory_limit=json.loads(sys.argv[2]),
        )
    elif sys.argv[1] == 'resilient-vlc':
        resilient_vlc(sys.argv[2:])
    elif sys.argv[1] == 'eternal-firefox':
        eternal_firefox(
            profile=sys.argv[2],
            group_name=sys.argv[3],
            window_position=json.loads(sys.argv[4]),
            debug=json.loads(sys.argv[5]),
            tabs=sys.argv[6:],
        )
    elif sys.argv[1] == 'resilient-ethernet':
        resilient_ethernet(
            ip_addr=sys.argv[2],
            ethernet_device=sys.argv[3],
        )
    elif sys.argv[1] == 'player':
        player_v1(
            folder_url=sys.argv[2],
            item_id=int(sys.argv[3]),
        )
    elif sys.argv[1] == 'desktop-services':
        assert all([
            env_name in os.environ
            for env_name in [
                'GTK_IM_MODULE',
                'XMODIFIERS',
                'QT_IM_MODULE',
                'I3SOCK',
                'SWAYSOCK',
                'WAYLAND_DISPLAY',
            ]
        ])
        services = []
        try:
            services.extend([
                subprocess.Popen(['ibus-daemon']),
                subprocess.Popen(r'''
                    swayidle -w \
                        timeout 300 'swaymsg "output * dpms off"' \
                        resume 'swaymsg "output * dpms on"'
                ''', shell=True),
            ])
            for o in services:
                o.wait()
        finally:
            for o in services:
                try:
                    o.terminate(timeout=10)
                except:
                    logging.error('killed %s' % str(o.__dict__))
                    o.kill()

    else:
        raise NotImplementedError
except SystemExit:
    pass
except:
    msg = 'not implemented\n%s' % traceback.format_exc()
    logging.error(msg)

if not msg is None:
    subprocess.check_call([
        'notify-send',
        'commands',
        msg[-128:]
    ])