#!/usr/bin/env python
# -*- encoding: utf-8 -*-

# copy to ~/.gimp-2.8/plug-ins/
# dependencies: GIMP 2.8, python-imaging-tk, python-pyaudio

from gimpfu import register, main, pdb, PF_BOOL, PF_STRING, PF_RADIO, CLIP_TO_IMAGE
from PIL import Image, ImageTk
from Tkinter import Tk, Canvas, Button, Checkbutton, IntVar, Frame, LEFT, NW
from pysstv import __main__ as pysstv_main
from pysstv.examples.pyaudio_sstv import PyAudioSSTV
from pysstv.sstv import SSTV
from itertools import repeat
from threading import Thread
from Queue import Queue, Empty
from time import sleep
import gimp, os

MODULE_MAP = pysstv_main.build_module_map()

class AudioThread(Thread):
    def __init__(self, sstv, parent):
        Thread.__init__(self)
        self.pas = PyAudioSSTV(sstv)
        self.parent = parent

    def run(self):
        self.pas.execute()
        self.parent.audio_thread_ended()

    def stop(self):
        if self.pas is not None:
            self.pas.sampler = []
            self.pas = None


class Sine1750(SSTV):
    encode_line = None

    def gen_freq_bits(self):
        return repeat((1750, 1000))


class Transmitter(object):
    def __init__(self, sstv, root, progress, set_ptt_pin, ptt_state):
        def encode_line_hooked(line):
            progress.update_image(line)
            return self.original_encode_line(line)
        self.progress = progress
        self.sstv = sstv
        self.original_encode_line = sstv.encode_line
        sstv.encode_line = encode_line_hooked
        self.root = root
        self.tx_enabled = IntVar()
        self.audio_thread = None
        self.stopping = False
        self.set_ptt_pin = set_ptt_pin
        self.ptt_state = ptt_state

    def set_ptt(self, state):
        if self.set_ptt_pin is None:
            return
        if not state:
            sleep(0.2)
        self.set_ptt_pin(state != self.ptt_state)
        if state:
            sleep(0.2)

    def start_stop_tx(self):
        if self.tx_enabled.get():
            self.stopping = False
            self.audio_thread = AudioThread(self.sstv, self)
            self.set_ptt(True)
            self.audio_thread.start()
        else:
            self.stop()
            if self.progress is not None:
                self.progress.update_image()

    def stop(self):
        if self.audio_thread is not None:
            self.stopping = True
            self.audio_thread.stop()
            self.set_ptt(False)

    def audio_thread_ended(self):
        if not self.stopping:
            self.set_ptt(False)
            self.tx_enabled.set(0)

    def close(self):
        self.root.destroy()


class CanvasUpdater(Thread):
    def __init__(self, progress):
        Thread.__init__(self)
        self.progress = progress
        self.queue = Queue()
        self.should_run = True

    def stop(self):
        self.should_run = False

    def update_image(self, line=None):
        self.queue.put(line)

    def run(self):
        while self.should_run:
            try:
                self.progress.update_image(self.queue.get(timeout=0.5))
            except Empty:
                pass


class ProgressCanvas(Canvas):
    def __init__(self, master, image):
        self.height_ratio = 1
        width, height = image.size
        pixels = image.load()
        RED, GREEN, BLUE = range(3)
        self.colors = ['#{0:02x}{1:02x}{2:02x}'.format(
            contrast(sum(pixels[x, y][RED]   for x in xrange(width)) / width),
            contrast(sum(pixels[x, y][GREEN] for x in xrange(width)) / width),
            contrast(sum(pixels[x, y][BLUE]  for x in xrange(width)) / width))
            for y in xrange(height)]
        if height / float(width) > 1.5:
            width *= 2
        elif width < 200:
            width *= 2
            height *= 2
            self.height_ratio = 2
        if (width, height) != image.size:
            image = image.resize((width, height))
        Canvas.__init__(self, master, width=width, height=height)
        self.tk_img = ImageTk.PhotoImage(image)
        self.update_image()
        self.pack()

    def update_image(self, line=None):
        image = self.tk_img
        self.create_image(0, 0, anchor=NW, image=image)
        if line is not None:
            fill = self.colors[line]
            line *= self.height_ratio
            self.create_line(0, line, image.width(), line, fill=fill)


def contrast(value):
    if 80 < value < 160:
        return value + (80 if value < 120 else -80)
    else:
        return 255 - value

def transmit_current_image(image, drawable, mode, vox, fskid, ptt_port, ptt_pin, ptt_state):
    sstv = MODULE_MAP[mode]
    if ptt_port is not None:
        from serial import Serial
        set_ptt_pin = getattr(Serial(ptt_port), 'set' + ptt_pin)
        set_ptt_pin(ptt_state)
    else:
        set_ptt_pin = None
    pil_img = match_image_with_sstv_mode(image_gimp_to_pil(image), sstv)
    root = Tk()
    cu = CanvasUpdater(ProgressCanvas(root, pil_img))
    cu.start()
    tm = Transmitter(init_sstv(sstv, pil_img, vox, fskid), root, cu, set_ptt_pin, ptt_state)
    tm1750 = Transmitter(Sine1750(None, 44100, 16), None, None, set_ptt_pin, ptt_state)
    buttons = Frame(root)
    for text, tram in (('TX', tm), ('1750 Hz', tm1750)):
        Checkbutton(buttons, text=text, indicatoron=False, padx=5, pady=5,
                variable=tram.tx_enabled, command=tram.start_stop_tx).pack(side=LEFT)
    Button(buttons, text="Close", command=tm.close).pack(side=LEFT)
    buttons.pack()
    root.mainloop()
    for obj in (tm, tm1750, cu):
        obj.stop()

def image_gimp_to_pil(image):
    try:
        sandbox = image.duplicate()
        for layer in sandbox.layers:
            if not layer.visible:
                sandbox.remove_layer(layer)
        sandbox.merge_visible_layers(CLIP_TO_IMAGE)
        layer = sandbox.layers[0]
        if not layer.is_rgb:
            pdb.gimp_image_convert_rgb(sandbox)
        if layer.has_alpha:
            pdb.gimp_layer_flatten(layer)
        w, h = layer.width, layer.height
        pixels = layer.get_pixel_rgn(0, 0, w, h)[:, :] # all pixels
        return Image.frombuffer('RGB', (w, h), pixels, 'raw', 'RGB', 0, 0)
    finally:
        gimp.delete(sandbox)

def match_image_with_sstv_mode(image, mode):
    mode_size = mode.WIDTH, mode.HEIGHT
    if image.size != mode_size:
        image = image.resize(mode_size, Image.ANTIALIAS)
    if 'grayscale' in mode.__module__:
        image = image.convert('LA').convert('RGB')
    return image

def init_sstv(mode, image, vox, fskid):
    s = mode(image, 44100, 16)
    s.vox_enabled = vox
    if fskid:
        s.add_fskid_text(fskid)
    return s

def get_serial_ports():
    try:
        if os.name == 'nt':
            from serial.tools.list_ports_windows import comports
        elif os.name == 'posix':
            from serial.tools.list_ports_posix import comports
        else:
            raise ImportError("Sorry: no implementation for your"
            "platform ('%s') available" % (os.name,))
    except ImportError:
        yield "(couldn't import PySerial)", None
    else:
        yield "(disabled)", None
        for port, desc, _ in comports():
            yield '{0} ({1})'.format(port, desc.strip()), port


register(
        "pysstv_for_gimp",
        "PySSTV for GIMP",
        "Transmits the current image using PySSTV",
        "Andras Veres-Szentkiralyi",
        "Andras Veres-Szentkiralyi",
        "November 2013",
        "<Image>/PySSTV/Transmit...",
        "*",
        [
            (PF_RADIO, "mode", "SSTV mode", "MartinM1",
                tuple((n, n) for n in sorted(MODULE_MAP.iterkeys()))),
            (PF_BOOL, "vox", "Include VOX tones", True),
            (PF_STRING, "fskid", "FSK ID", ""),
            (PF_RADIO, "ptt_port", "PTT port", None,
                tuple(get_serial_ports())),
            (PF_RADIO, "ptt_pin", "PTT pin", "RTS",
                tuple((n, n) for n in ("RTS", "DTR"))),
            (PF_BOOL, "ptt_state", "PTT inversion", False),
        ],
        [],
        transmit_current_image
        )

main()
