#!/usr/bin/env python
"""
wal - Generate and change colorschemes on the fly.
Created by Dylan Araps
"""
import argparse
import os
import pathlib
import random
import re
import shutil
import subprocess
import sys


# Internal variables.
VERSION = "0.1"
COLOR_COUNT = 16
CACHE_DIR = pathlib.Path.home() / ".cache/wal/"


# pylint: disable=too-few-public-methods
class ColorType(object):
    """Store colors in various formats."""
    plain = []
    xrdb = []
    sequences = []
    shell = []
    scss = []
    css = [":root {"]
    putty = [
        "Windows Registry Editor Version 5.00",
        "[HKEY_CURRENT_USER\\Software\\SimonTatham\\PuTTY\\Sessions\\Wal]",
    ]


# pylint: disable=too-few-public-methods
class Args(object):
    """Store args."""
    notify = True


# ARGS {{{


def get_args():
    """Get the script arguments."""
    description = "wal - Generate colorschemes on the fly"
    arg = argparse.ArgumentParser(description=description)

    # Add the args.
    arg.add_argument("-c", action="store_true",
                     help="Delete all cached colorschemes.")

    arg.add_argument("-i", metavar="\"/path/to/img.jpg\"",
                     help="Which image or directory to use.")

    arg.add_argument("-n", action="store_true",
                     help="Skip setting the wallpaper.")

    arg.add_argument("-o", metavar="\"script_name\"",
                     help="External script to run after \"wal\".")

    arg.add_argument("-q", action="store_true",
                     help="Quiet mode, don\"t print anything and \
                           don't display notifications.")

    arg.add_argument("-r", action="store_true",
                     help="Reload current colorscheme.")

    arg.add_argument("-t", action="store_true",
                     help="Fix artifacts in VTE Terminals. \
                           (Termite, xfce4-terminal)")

    arg.add_argument("-v", action="store_true",
                     help="Print \"wal\" version.")

    return arg.parse_args()


def process_args(args):
    """Process args"""
    # If no args were passed.
    if not len(sys.argv) > 1:
        print("error: wal needs to be given arguments to run.")
        print("       Refer to \"wal -h\" for more info.")
        exit(1)

    # -q
    if args.q:
        sys.stdout = sys.stderr = open(os.devnull, "w")
        Args.notify = False

    # -c
    if args.c:
        shutil.rmtree(CACHE_DIR / "schemes")
        create_cache_dir()

    # -r
    if args.r:
        reload_colors(args.t)

    # -v
    if args.v:
        print(f"wal {VERSION}")
        exit(0)

    # -i
    if args.i:
        image = str(get_image(args.i))
        ColorType.plain = get_colors(image)

        if not args.n:
            set_wallpaper(image)

        # Set the colors.
        send_sequences(ColorType.plain, args.t)
        export_colors(ColorType.plain)

    # -o
    if args.o:
        subprocess.Popen(["nohup", args.o],
                         stdout=subprocess.DEVNULL,
                         stderr=subprocess.DEVNULL,
                         preexec_fn=os.setpgrp)


# }}}


# COLORSCHEME GENERATION {{{


def get_image(img):
    """Validate image input."""
    # Check if the user has Imagemagick installed.
    if not shutil.which("convert"):
        print("error: imagemagick not found, exiting...")
        print("error: wal requires imagemagick to function.")
        exit(1)

    image = pathlib.Path(img)

    if image.is_file():
        wal_img = image

    # Pick a random image from the directory.
    elif image.is_dir():
        file_types = (".png", ".jpg", ".jpeg", ".jpe", ".gif")

        # Get the filename of the current wallpaper.
        current_img = pathlib.Path(CACHE_DIR / "wal")

        if current_img.is_file():
            current_img = read_file(current_img)
            current_img = os.path.basename(current_img[0])

        # Get a list of images.
        images = [img for img in os.listdir(image)
                  if img.endswith(file_types) and
                  img != current_img]

        rand_img = random.choice(images)
        rand_img = pathlib.Path(image / rand_img)

        if rand_img.is_file():
            wal_img = rand_img

    else:
        print("error: No valid image file found.")
        exit(1)

    print("image: Using image", wal_img)
    return wal_img


def imagemagick(color_count, img):
    """Call Imagemagick to generate a scheme."""
    colors = subprocess.Popen(["convert", img, "+dither", "-colors",
                               str(color_count), "-unique-colors", "txt:-"],
                              stdout=subprocess.PIPE)

    return colors.stdout.readlines()


def gen_colors(img):
    """Generate a color palette using imagemagick."""
    # Generate initial scheme.
    raw_colors = imagemagick(COLOR_COUNT, img)

    # If imagemagick finds less than 16 colors, use a larger source number
    # of colors.
    index = 0
    while len(raw_colors) - 1 < COLOR_COUNT:
        index += 1
        raw_colors = imagemagick(COLOR_COUNT + index, img)

        print("colors: Imagemagick couldn't generate a", COLOR_COUNT,
              "color palette, trying a larger palette size",
              COLOR_COUNT + index)

    # Remove the first element, which isn"t a color.
    del raw_colors[0]

    # Create a list of hex colors.
    colors = [re.search("#.{6}", str(col)).group(0) for col in raw_colors]
    return colors


def get_colors(img):
    """Generate a colorscheme using imagemagick."""
    # Cache the wallpaper name.
    save_file(img, CACHE_DIR / "wal")

    # Cache the sequences file.
    cache_file = CACHE_DIR / "schemes" / img.replace("/", "_")
    cache_file = pathlib.Path(cache_file)

    if cache_file.is_file():
        colors = read_file(cache_file)
        print("colors: Found cached colorscheme.")

    else:
        print("colors: Generating a colorscheme...")
        notify("wal: Generating a colorscheme...")

        # Generate the colors.
        colors = gen_colors(img)
        colors = sort_colors(colors)

        # Cache the colorscheme.
        save_file("\n".join(colors), cache_file)

        print("colors: Generated colorscheme")
        notify("wal: Generation complete.")

    return colors


def sort_colors(colors):
    """Sort the generated colors."""
    sorted_colors = []
    sorted_colors.append(colors[0])
    sorted_colors.append(colors[9])
    sorted_colors.append(colors[10])
    sorted_colors.append(colors[11])
    sorted_colors.append(colors[12])
    sorted_colors.append(colors[13])
    sorted_colors.append(colors[14])
    sorted_colors.append(colors[15])
    sorted_colors.append(set_grey(colors))
    sorted_colors.append(colors[9])
    sorted_colors.append(colors[10])
    sorted_colors.append(colors[11])
    sorted_colors.append(colors[12])
    sorted_colors.append(colors[13])
    sorted_colors.append(colors[14])
    sorted_colors.append(colors[15])
    return sorted_colors


# }}}


# SEND SEQUENCES {{{


def set_special(index, color):
    """Build the escape sequence for special colors."""
    ColorType.sequences.append(f"\\033]{index};{color}\\007")

    if index == 10:
        ColorType.xrdb.append(f"URxvt*foreground: {color}")
        ColorType.xrdb.append(f"XTerm*foreground: {color}")

    elif index == 11:
        ColorType.xrdb.append(f"URxvt*background: {color}")
        ColorType.xrdb.append(f"XTerm*background: {color}")

    elif index == 12:
        ColorType.xrdb.append(f"URxvt*cursorColor: {color}")
        ColorType.xrdb.append(f"XTerm*cursorColor: {color}")


def set_color(index, color):
    """Build the escape sequence we need for each color."""
    ColorType.xrdb.append(f"*.color{index}: {color}")
    ColorType.xrdb.append(f"*color{index}: {color}")
    ColorType.sequences.append(f"\\033]4;{index};{color}\\007")
    ColorType.shell.append(f"color{index}='{color}'")
    ColorType.css.append(f"\t--color{index}: {color};")
    ColorType.scss.append(f"$color{index}: {color};")

    rgb = hex_to_rgb(color)
    ColorType.putty.append(f"\"Colour{index}\"=\"{rgb}\"")


def set_grey(colors):
    """Set a grey color based on brightness of color0"""
    return {
        0: "#666666",
        1: "#666666",
        2: "#757575",
        3: "#999999",
        4: "#999999",
        5: "#8a8a8a",
        6: "#a1a1a1",
        7: "#a1a1a1",
        8: "#a1a1a1",
        9: "#a1a1a1",
    }.get(int(colors[0][1]), colors[7])


def send_sequences(colors, vte):
    """Send colors to all open terminals."""
    set_special(10, colors[15])
    set_special(11, colors[0])
    set_special(12, colors[15])
    set_special(13, colors[15])
    set_special(14, colors[0])

    # This escape sequence doesn"t work in VTE terminals.
    if not vte:
        set_special(708, colors[0])

    # Create the sequences.
    # pylint: disable=W0106
    [set_color(num, color) for num, color in enumerate(colors)]

    # Set a blank color that isn"t affected by bold highlighting.
    set_color(66, colors[0])

    # Make the terminal interpret escape sequences.
    sequences = "".join(ColorType.sequences)
    sequences = fix_escape(sequences)

    # Get a list of terminals.
    terminals = [f"/dev/pts/{term}" for term in os.listdir("/dev/pts/")
                 if len(term) < 4]
    terminals.append(CACHE_DIR / "sequences")

    # Send the sequences to all open terminals.
    # pylint: disable=W0106
    [save_file(sequences, term) for term in terminals]

    print("colors: Set terminal colors")


# }}}


# WALLPAPER SETTING {{{


def get_desktop_env():
    """Identify the current running desktop environment."""
    desktop = os.getenv("XDG_CURRENT_DESKTOP")
    if desktop:
        return desktop

    desktop = os.getenv("DESKTOP_SESSION")
    if desktop:
        return desktop

    desktop = os.getenv("GNOME_DESKTOP_SESSION_ID")
    if desktop:
        return "GNOME"

    desktop = os.getenv("MATE_DESKTOP_SESSION_ID")
    if desktop:
        return "MATE"


def set_desktop_wallpaper(desktop, img):
    """Set the wallpaper for the desktop environment."""
    desktop = str(desktop).lower()

    if "xfce" in desktop or "xubuntu" in desktop:
        subprocess.Popen(["xfconf-query", "--channel", "xfce4-desktop",
                          "--property",
                          "/backdrop/screen0/monitor0/image-path",
                          "--set", img])

        # XFCE requires two commands since they differ between versions.
        subprocess.Popen(["xfconf-query", "--channel", "xfce4-desktop",
                          "--property",
                          "/backdrop/screen0/monitor0/workspace0/last-image",
                          "--set", img])

    elif "muffin" in desktop or "cinnamon" in desktop:
        subprocess.Popen(["gsettings", "set",
                          "org.cinnamon.desktop.background",
                          "picture-uri", "file:///" + img])

    elif "mate" in desktop:
        subprocess.Popen(["gsettings", "set", "org.mate.background",
                          "picture-filename", img])

    elif "gnome" in desktop:
        subprocess.Popen(["gsettings", "set",
                          "org.gnome.desktop.background",
                          "picture-uri", "file:///" + img])


def set_wallpaper(img):
    """Set the wallpaper."""
    desktop = get_desktop_env()

    if desktop:
        set_desktop_wallpaper(desktop, img)

    else:
        if shutil.which("feh"):
            subprocess.Popen(["feh", "--bg-fill", img])

        elif shutil.which("nitrogen"):
            subprocess.Popen(["nitrogen", "--set-zoom-fill", img])

        elif shutil.which("bgs"):
            subprocess.Popen(["bgs", img])

        elif shutil.which("hsetroot"):
            subprocess.Popen(["hsetroot", "-fill", img])

        elif shutil.which("habak"):
            subprocess.Popen(["habak", "-mS", img])

        else:
            print("error: No wallpaper setter found.")
            return

    print("wallpaper: Set the new wallpaper")
    return 0


# }}}


# EXPORT COLORS {{{


def save_colors(colors, export_file, message):
    """Export colors to var format."""
    colors = "\n".join(colors)
    save_file(f"{colors}\n", export_file)
    print(f"export: exported {message}.")


def export_rofi(colors):
    """Append rofi colors to the x_colors list."""
    ColorType.xrdb.append(f"rofi.color-window: {colors[0]}, "
                          f"{colors[0]}, {colors[10]}")
    ColorType.xrdb.append(f"rofi.color-normal: {colors[0]}, "
                          f"{colors[15]}, {colors[0]}, "
                          f"{colors[10]}, {colors[0]}")
    ColorType.xrdb.append(f"rofi.color-active: {colors[0]}, "
                          f"{colors[15]}, {colors[0]}, "
                          f"{colors[10]}, {colors[0]}")
    ColorType.xrdb.append(f"rofi.color-urgent: {colors[0]}, "
                          f"{colors[9]}, {colors[0]}, "
                          f"{colors[9]}, {colors[15]}")


def export_emacs(colors):
    """Set emacs colors."""
    ColorType.xrdb.append(f"emacs*background: {colors[0]}")
    ColorType.xrdb.append(f"emacs*foreground: {colors[15]}")


def export_xrdb(colors, export_file):
    """Export colors to xrdb."""
    save_colors(colors, export_file, "xrdb colors")

    # Merge the colors into the X db so new terminals use them.
    subprocess.call(["xrdb", "-merge", export_file])


def reload_i3():
    """Reload i3 colors."""
    if shutil.which("i3-msg"):
        subprocess.Popen(["i3-msg", "reload"],
                         stdout=subprocess.DEVNULL,
                         stderr=subprocess.DEVNULL,
                         preexec_fn=os.setpgrp)


def export_colors(colors):
    """Export colors in various formats."""
    save_colors(ColorType.plain, CACHE_DIR / "colors", "plain hex colors")
    save_colors(ColorType.shell, CACHE_DIR / "colors.sh", "shell variables")

    # Web based colors.
    ColorType.css.append("}")
    save_colors(ColorType.css, CACHE_DIR / "colors.css", "css variables")
    save_colors(ColorType.scss, CACHE_DIR / "colors.scss", "scss variables")

    # Text editor based colors.
    save_colors(ColorType.putty, CACHE_DIR / "colors-putty.reg", "putty theme")

    # X based colors.
    export_rofi(colors)
    export_emacs(colors)
    export_xrdb(ColorType.xrdb, CACHE_DIR / "xcolors")

    # i3 colors.
    reload_i3()


# }}}


# OTHER FUNCTIONS {{{


def reload_colors(vte):
    """Reload colors."""
    sequence_file = pathlib.Path(CACHE_DIR / "sequences")

    if sequence_file.is_file():
        with open(sequence_file, "r") as file:
            sequences = file.read()

        # If vte mode was used, remove the problem sequence.
        if vte:
            sequences = re.sub(r"\]708;\#.{6}", "", sequences)

        # Make the terminal interpret escape sequences.
        sequences = fix_escape(sequences)
        print(sequences, end="")

    exit(0)


def read_file(input_file):
    """Read colors from a file"""
    with open(input_file) as file:
        contents = file.readlines()

    # Strip newlines from each list element.
    contents = [x.strip() for x in contents]
    return contents


def save_file(colors, export_file):
    """Write the colors to the file."""
    with open(export_file, "w") as file:
        file.write(colors)


def create_cache_dir():
    """Alias to create the cache dir."""
    pathlib.Path(CACHE_DIR / "schemes").mkdir(parents=True, exist_ok=True)


def hex_to_rgb(color):
    """Convert a hex color to rgb."""
    red, green, blue = list(bytes.fromhex(color.strip("#")))
    return f"{red},{green},{blue}"


def fix_escape(string):
    """Decode a string."""
    return bytes(string, "utf-8").decode("unicode_escape")


def notify(msg):
    """Send arguements to notify-send."""
    if shutil.which("notify-send") and Args.notify:
        subprocess.Popen(["notify-send", msg],
                         stdout=subprocess.DEVNULL,
                         stderr=subprocess.DEVNULL,
                         preexec_fn=os.setpgrp)


# }}}


def main():
    """Main script function."""
    create_cache_dir()
    args = get_args()
    process_args(args)

    # This saves 10ms.
    # pylint: disable=W0212
    # os._exit(0)


main()
