# Written by Jeremy Karst 2025
# This software is licensed under the MIT License with Additional Terms.
# See LICENSE.md for more information.

# Built-in Dependencies
import base64
import time
import datetime

# External Dependencies
from cryptography.hazmat.primitives import hashes
import qrcode
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.moduledrawers.pil import CircleModuleDrawer
from qrcode.image.styles.colormasks import RadialGradiantColorMask, SolidFillColorMask
from PIL import Image, ImageDraw, ImageFont

def hash_text(text: str) -> str:
    message = text.encode('utf-8')
    
    hash_class = hashes.Hash(hashes.SHA256())
    
    hash_class.update(message)
    signature = hash_class.finalize()

    first_half = signature[:len(signature)//2]
    second_half = signature[len(signature)//2:]

    xored = bytes(a ^ b for a, b in zip(first_half, second_half))

    b64bytes = base64.b64encode(xored).replace(b'+', b'-').replace(b'/', b'_').replace(b'=', b'')
    b64str = b64bytes.decode('utf-8')

    return b64str

def generate_signature_image(sig_name: str, sig_time: int, sig_timezone: datetime.timezone, signature_url: str, sig_font_path: str, note_font_path: str, padding: int = 100, interspacing: int = 60, invert_colors: bool = False, gradient: bool = True):
    start_time = time.perf_counter()
    
    # Create a datetime object from the sig_time and sig_timezone
    sig_time = datetime.datetime.fromtimestamp(sig_time, sig_timezone)
    datetime_time = time.perf_counter()

    note_text = f"Digitally Signed by\n{sig_name}\n{sig_time.strftime('%Y-%m-%d %I:%M %p')}"

    # QR code generation timing
    # qr_start = time.perf_counter()
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=8,
        border=0,
    )
    qr.add_data(signature_url)
    if invert_colors:
        if gradient:
            qr_img = qr.make_image(fit=True, image_factory=StyledPilImage, module_drawer=CircleModuleDrawer(), color_mask=RadialGradiantColorMask(back_color=(0, 0, 0, 128), center_color=(245, 245, 245, 255), edge_color=(250, 250, 250, 255)))
        else:
            qr_img = qr.make_image(fit=True, image_factory=StyledPilImage, module_drawer=CircleModuleDrawer(), color_mask=SolidFillColorMask(back_color=(0, 0, 0, 128), front_color=(250, 250, 250, 255)))
    else:
        if gradient:
            qr_img = qr.make_image(fit=True, image_factory=StyledPilImage, module_drawer=CircleModuleDrawer(), color_mask=RadialGradiantColorMask(back_color=(255, 255, 255, 128), center_color=(10, 10, 150, 255), edge_color=(5, 5, 20, 255)))
        else:
            qr_img = qr.make_image(fit=True, image_factory=StyledPilImage, module_drawer=CircleModuleDrawer())
    qr_size = qr_img.pixel_size
    # qr_time = time.perf_counter()

    # Text measurement timing
    # text_start = time.perf_counter()
    test_image = Image.new("RGBA", (2000, 1000), (0, 0, 0, 0))
    sig_font = ImageFont.truetype(sig_font_path, 350)
    sans_font = ImageFont.truetype(note_font_path, 80)
    draw = ImageDraw.Draw(test_image)
    sig_bbox = draw.textbbox((0, 0), sig_name, font=sig_font, anchor="ls") # (left, top, right, bottom)
    note_bbox = draw.multiline_textbbox((0, 0), note_text, font=sans_font, anchor="ls") # (left, top, right, bottom)
    # text_measure_time = time.perf_counter()

    sig_width = sig_bbox[2] - sig_bbox[0]
    sig_height = sig_bbox[3] - sig_bbox[1]
    note_width = note_bbox[2] - note_bbox[0]
    note_height = note_bbox[3] - note_bbox[1]

    total_image_width = padding + sig_width + interspacing + note_width + interspacing + qr_size + padding
    total_image_height = padding + max(sig_height, note_height) + padding
    v_anchor = total_image_height - sig_bbox[3] - padding

    if invert_colors:
        sig_color = "white"
        note_color = "white"
        background_color = (255, 255, 255, 0)
    else:
        sig_color = "black"
        note_color = "black"
        background_color = (0, 0, 0, 0)

    image = Image.new("RGBA", (total_image_width, total_image_height), background_color)
    draw = ImageDraw.Draw(image)
    draw.text((padding - sig_bbox[0], v_anchor), sig_name, font=sig_font, fill=sig_color, anchor="ls")
    draw.text((padding + sig_width + interspacing - note_bbox[0], v_anchor - note_bbox[3]), note_text, font=sans_font, fill=note_color, anchor="ls")
    image.paste(qr_img._img, (padding + sig_width + interspacing + note_width + interspacing, v_anchor - qr_size))

    # final_time = time.perf_counter()
    
    # # Print timing information
    # print(f"Signature Image Generation Timing:")
    # print(f"  DateTime processing: {(datetime_time - start_time)*1000:.2f}ms")
    # print(f"  QR code generation: {(qr_time - qr_start)*1000:.2f}ms")
    # print(f"  Text measurement: {(text_measure_time - text_start)*1000:.2f}ms")
    # print(f"  Final image composition: {(final_time - text_measure_time)*1000:.2f}ms")
    # print(f"  Total time: {(final_time - start_time)*1000:.2f}ms")
    
    return image


if __name__ == "__main__":
    sig_name = "Yöùr Ñamé Herê"
    sig_font_path = "./web_content/fonts/HerrVonMuellerhoff.ttf"
    note_font_path = "./web_content/fonts/Steelfish.ttf"

    timestamp = int(time.time())
    timezone = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
    sig_text = f"{sig_name}{timestamp}"

    hash_id = hash_text(sig_text)

    signature_data = f"osg.jkdev.org/v/{hash_id}"

    image = generate_signature_image(sig_name, timestamp, timezone, signature_data, sig_font_path, note_font_path, invert_colors=True, gradient=True)

    image.save(f"Signature {hash_id} {timestamp}.png", optimize=True, compress_level=9)