#!/usr/bin/env python3
# Copyright (C) 2001 Colin Phipps <cphipps@doomworld.com>
# Copyright (C) 2008, 2013 Simon Howard
# Parts copyright (C) 1999 by id Software (http://www.idsoftware.com/)
#
# SPDX-License-Identifier: GPL-2.0+
#
# Takes PLAYPAL as input (filename is the only parameter)
# Produces a light graduated COLORMAP on stdout
# O(n^2)
#
# This was originally a Perl script by Colin Phipps; it was converted
# to Python and now is a more generic tool for generating all kinds of
# COLORMAP effects.
#

import os
import sys
import struct

# Parameters affecting colormap generation:

# "Darkness" is this color, which is usually black, but can be
# overridden (RGB 0-255):
dark_color = (0, 0, 0)

# Color to tint the colormap (RGB 0.0-1.0):
tint_color = (255, 255, 255)

# Fractional balance between tint and normal color. 0 is no tint applied,
# 1.0 is full tint.
tint_frac = 0

# Fudge factor to adjust brightness when calculating 'tinted' version
# of colors. Larger values are brighter but may cause color clipping.
# A value of 0.33 is a straight-average of the RGB channels. Maximum
# sensible value is 1.0, though it can be overdriven for fancy
# brightness effects.
tint_bright = 0.5


def read_palette(filename):
    """Read palette from file and return a list of tuples containing
       RGB values."""
    f = open(filename, "rb")

    colors = []

    for i in range(256):
        data = f.read(3)

        color = struct.unpack("BBB", data)
        colors.append(color)

    return colors


# Return closest palette entry to the given RGB triple


def search_palette(palette, target):
    """Search the given palette and find the nearest matching
       color to the given color, returning an index into the
       palette of the color that best matches."""
    best_diff = None
    best_index = None

    def square(x):
        return x * x

    for i in range(len(palette)):
        color = palette[i]

        diff = (
            square(target[0] - color[0])
            + square(target[1] - color[1])
            + square(target[2] - color[2])
        )

        if best_index is None or diff < best_diff:
            best_diff = diff
            best_index = i

    return best_index


def generate_colormap(colors, palette):
    """Given a list of colors, translate these into indexes into
       the given palette, finding the nearest color where an exact
       match cannot be found."""
    result = []

    for color in colors:
        index = search_palette(palette, color)
        result.append(index)

    return result


def tint_colors(colors, tint, bright=0.5):
    """Given a list of colors, tint them a particular color."""

    result = []
    for c in colors:
        # I've experimented with different methods of calculating
        # intensity, but this seems to work the best. This is basically
        # doing an average of the full channels, but a straight
        # average causes the picture to get darker - eg. (0,0,255)
        # maps to (87,87,87). So we have a controllable brightness
        # factor that allows the brightness to be adjusted.
        intensity = min((c[0] + c[1] + c[2]) * bright, 255) / 255.0
        result.append(
            (tint[0] * intensity, tint[1] * intensity, tint[2] * intensity)
        )

    return result


def blend_colors(colors1, colors2, factor=0.5):
    """Blend the two given lists of colors, with 'factor' controlling
       the mix between the two. factor=0 is exactly colors1, while
       factor=1 is exactly colors2. Returns a list of blended colors."""
    result = []

    for index, c1 in enumerate(colors1):
        c2 = colors2[index]

        result.append(
            (
                c2[0] * factor + c1[0] * (1 - factor),
                c2[1] * factor + c1[1] * (1 - factor),
                c2[2] * factor + c1[2] * (1 - factor),
            )
        )

    return result


def invert_colors(colors):
    """Given a list of colors, translate them to inverted monochrome."""
    result = []

    for color in colors:
        # Formula comes from ITU-R recommendation BT.709
        gray = round(color[0] * 0.2126 + color[1] * 0.7152 + color[2] * 0.0722)
        inverse = 255 - gray

        result.append((inverse, inverse, inverse))

    return result


def solid_color_list(color):
    """Generate a 256-entry palette where all entries are the
       same color."""
    return [color] * 256


def output_colormap(colormap):
    """Output the given palette to stdout."""
    for c in colormap:
        x = struct.pack("B", c)
        os.write(sys.stdout.fileno(), x)


def print_palette(colors):
    for y in range(16):
        for x in range(16):
            color = colors[y * 16 + x]

            print("#%02x%02x%02x" % color)

        print()


def parse_color_code(s):
    """Parse a color code in HTML color code format, into an RGB
       3-tuple value."""
    if not s.startswith("#") or len(s) != 7:
        raise Exception("Not in HTML color code form: %s" % s)
    return (int(s[1:3], 16), int(s[3:5], 16), int(s[5:7], 16))


def set_parameter(name, value):
    """Set configuration value, from command line parameters."""
    global dark_color, tint_color, tint_frac, tint_bright

    if name == "dark_color":
        dark_color = parse_color_code(value)
    elif name == "tint_color":
        tint_color = parse_color_code(value)
    elif name == "tint_pct":
        tint_frac = int(value) / 100.0
    elif name == "tint_bright":
        tint_bright = float(value)
    else:
        raise Exception("Unknown parameter: '%s'" % name)


# Parse command line.

playpal_filename = None

for arg in sys.argv[1:]:
    if arg.startswith("--") and "=" in arg:
        key, val = arg[2:].split("=", 2)
        set_parameter(key, val)
    else:
        playpal_filename = arg

if playpal_filename is None:
    print("Usage: %s playpal.lmp > output-file.lmp" % sys.argv[0])
    sys.exit(1)

palette = read_palette(playpal_filename)
colors = palette

# Apply tint, if enabled.
# The tint is intentionally applied *before* the darkening effect is
# applied. This allows us to darken to a different color than the tint
# color, if so desired.
if tint_frac > 0:
    colors = blend_colors(
        palette, tint_colors(colors, tint_color, tint_bright), tint_frac
    )

# Generate colormaps for different darkness levels, by blending between
# the default colors and a palette where every entry is the "dark" color.
dark = solid_color_list(dark_color)

for i in range(32):
    darken_factor = (32 - i) / 32.0
    darkened_colors = blend_colors(dark, colors, darken_factor)
    output_colormap(generate_colormap(darkened_colors, palette))

# Inverse color map for invulnerability effect.
inverse_colors = invert_colors(palette)
output_colormap(generate_colormap(inverse_colors, palette))

# Last colormap is all black, and is actually unused in Vanilla Doom
# (it was mistakenly included by the dcolors.c utility). It's
# strictly unneeded, though some utilities (SLADE) do not detect a
# lump as a COLORMAP unless it is the right length.
output_colormap(generate_colormap(dark, palette))
