#!/usr/bin/env python3

import png
import time
import signal
import os
import sys
import threading
from datetime import datetime
import argparse

# Application Defaults
CONFIG_FILE = "/etc/plasma/plasma.conf"
PIPE_FILE = "/tmp/plasma"
PATTERNS = "/etc/plasma/"
FPS = 30
BRIGHTNESS = 1.0
LIGHTS = 10  # Actual number of pixels is 4x this number
DEBUG = False

# Log & PID files
PID_FILE = "/var/run/plasma.pid"
LOG_FILE = "/var/log/plasma.log"
ERR_FILE = "/var/log/plasma.err"


stopped = threading.Event()


class FIFO():
    def __init__(self, filename):
        self.filename = filename
        try:
            os.mkfifo(self.filename)
        except OSError:
            pass
        print("Opening...")
        self.fifo = os.open(self.filename, os.O_RDONLY | os.O_NONBLOCK)
        print("Open...")

    def readline(self, timeout=1):
        t_start = time.time()
        try:
            buf = os.read(self.fifo, 1)
        except BlockingIOError:
            return None

        if len(buf) == 0:
            return None

        while time.time() - t_start < timeout:
            try:
                c = os.read(self.fifo, 1)
                if c == b"\n":
                    return buf
                if len(c) == 1:
                    buf += c
            except BlockingIOError as err:
                continue
        return None

    def __enter__(self):
        return self

    def __exit__(self, e_type, e_value, traceback):
        os.close(self.fifo)
        os.remove(self.filename)


def main():
    args = get_args()

    if args.daemonize:
        fork()

    from plasma import auto

    plasma = auto(f"GPIO:14:15:pixel_count={LIGHTS * 4}", CONFIG_FILE)

    log("Starting Plasma in the {daemon} with framerate {fps}fps".format(
        daemon='background' if args.daemonize else 'foreground',
        fps=args.fps))

    log("Plasma input pipe: {}".format(PIPE_FILE))

    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)

    with FIFO(PIPE_FILE) as fifo:
        r, g, b = 0, 0, 0
        pattern, pattern_w, pattern_h, pattern_meta = load_pattern("default")
        alpha = pattern_meta['alpha']
        channels = 4 if alpha else 3

        while not stopped.wait(1.0 / args.fps):
            delta = time.time() * 60
            command = fifo.readline()
            if command is not None:
                command = command.decode('utf-8').strip()

                if command == "stop":
                    stopped.set()
                    log('Received user command "stop". Stopping.')
                    break

                rgb = command.split(' ')
                if len(rgb) == 3:
                    try:
                        r, g, b = [min(255, int(c)) for c in rgb]
                        pattern, pattern_w, pattern_h, pattern_meta = None, 0, 0, None
                    except ValueError:
                        log("Invalid colour: {}".format(command))
                elif len(rgb) == 2 and rgb[0] == "fps":
                    try:
                        args.fps = int(rgb[1])
                        log("Framerate set to: {}fps".format(rgb[1]))
                    except ValueError:
                        log("Invalid framerate: {}".format(rgb[1]))
                elif len(rgb) == 2 and rgb[0] == "brightness":
                    try:
                        args.brightness = float(rgb[1])
                        log("Brightness set to: {}".format(args.brightness))
                    except ValueError:
                        log("Invalid brightness {}".format(rgb[1]))
                else:
                    pattern, pattern_w, pattern_h, pattern_meta = load_pattern(command)
                    alpha = pattern_meta['alpha']
                    channels = 4 if alpha else 3

            if pattern is not None:
                offset_y = int(delta % pattern_h)
                row = pattern[offset_y]
                for x in range(plasma.get_pixel_count()):
                    offset_x = (x * channels) % (pattern_w * channels)
                    r, g, b = row[offset_x:offset_x + 3]
                    plasma.set_pixel(x, r, g, b, brightness=args.brightness)
            else:
                plasma.set_all(r, g, b, brightness=args.brightness)

            plasma.show()


def load_pattern(pattern_name):
    pattern_file = os.path.join(PATTERNS, "{}.png".format(pattern_name))
    if os.path.isfile(pattern_file):
        r = png.Reader(file=open(pattern_file, 'rb'))
        pattern_w, pattern_h, pattern, pattern_meta = r.read()
        pattern = list(pattern)
        log("Loaded pattern file: {}".format(pattern_file))
        return pattern, pattern_w, pattern_h, pattern_meta
    else:
        log("Invalid pattern file: {}".format(pattern_file))
        return None, 0, 0, None


def get_args():
    parser = argparse.ArgumentParser()
    parser.add_argument("-d", "--daemonize", action="store_true", default=False,
                      help="run plasma as a daemon")
    parser.add_argument("-f", "--fps", type=int, default=FPS,
                      help="set plasma LED update framerate")
    parser.add_argument("-b", "--brightness", type=float, default=BRIGHTNESS,
                      help="set plasma LED brightness")
    parser.add_argument("-c", "--config", type=str, default=CONFIG_FILE,
                      help="path to plasma config file")
    return parser.parse_known_args()[0]


def fork():
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)

    except OSError as e:
        print("Fork #1 failed: {} ({})".format(e.errno, e.strerror))
        sys.exit(1)

    os.chdir("/")
    os.setsid()
    os.umask(0)

    try:
        pid = os.fork()
        if pid > 0:
            with open(PID_FILE, 'w') as f:
                f.write(str(pid))
            sys.exit(0)

    except OSError as e:
        print("Fork #2 failed: {} ({})".format(e.errno, e.strerror))
        sys.exit(1)

    si = open("/dev/null", 'r')
    so = open(LOG_FILE, 'a+')
    se = open(ERR_FILE, 'a+')

    os.dup2(si.fileno(), sys.stdin.fileno())
    os.dup2(so.fileno(), sys.stdout.fileno())
    os.dup2(se.fileno(), sys.stderr.fileno())

    return pid


def log(msg):
    sys.stdout.write(str(datetime.now()))
    sys.stdout.write(": ")
    sys.stdout.write(msg)
    sys.stdout.write("\n")
    sys.stdout.flush()


def signal_handler(sig, frame):
    log("Received SIGNAL {}. Stopping.".format(sig))
    stopped.set()


if __name__ == "__main__":
    main()
