import os
import math
import time
import requests
import cloudscraper
import tkinter as tk
from io import BytesIO
from tkinter import font
from pathlib import Path
from functools import wraps
from bs4 import BeautifulSoup
from decord import VideoReader
from PIL.ImageFont import FreeTypeFont
from PIL import Image, ImageTk, ImageFont, ImageEnhance

__author__ = "Jochem van Dolder"
__license__ = "MIT"

SERVER_URL = os.getenv("SERVER_URL")

DEFAULT_APP = os.getenv("DEFAULT_APP")

BG_COL = os.getenv("BG_COL")
D_GREY = os.getenv("D_GREY")
N_GREY = os.getenv("N_GREY")
L_GREY = os.getenv("L_GREY")
EMB_BG = os.getenv("EMB_BG")

DEFAULT_CHAR = os.getenv("DEFAULT_CHAR")
DEFAULT_URL = os.getenv("DEFAULT_URL")
DEFAULT_COLOR = os.getenv("DEFAULT_COLOR")

FONT_FAM = os.getenv("FONT_FAM")

EMB_IMG_H = int(os.getenv("EMB_IMG_H"))

MIN_FRAME_DURATION_MS = int(os.getenv("MIN_FRAME_DURATION_MS"))
MAX_FRAME_DURATION_MS = int(os.getenv("MAX_FRAME_DURATION_MS"))
DEFAULT_FRAME_DURATION_MS = int(os.getenv("DEFAULT_FRAME_DURATION_MS"))
MAX_RAM_MB = int(os.getenv("MAX_RAM_MB"))

MAX_URL_RECURSION = int(os.getenv("MAX_URL_RECURSION"))

ENTRY_HEIGHT = int(os.getenv("ENTRY_HEIGHT"))
ENTRY_COUNT = int(os.getenv("ENTRY_COUNT"))
HISTORY_WIDTH = int(os.getenv("HISTORY_WIDTH"))

NUM_COLORS = int(os.getenv("NUM_COLORS"))
SCRAPER_TIMEOUT_MS = int(os.getenv("SCRAPER_TIMEOUT_MS"))

EMOJI_FOLDER = Path(__file__.rsplit("\\", 1)[0]).parent / "Assets" / "Emojis"
FONTS_FOLDER = Path(__file__.rsplit("\\", 1)[0]).parent / "Assets" / "Fonts"
GLOB_ASSETS_FOLDER = Path(__file__.rsplit("\\", 1)[0]).parent / "Assets"

NUMBERS_DICT = {
    0: "zero",
    1: "one",
    2: "two",
    3: "three",
    4: "four",
    5: "five",
    6: "six",
    7: "seven",
    8: "eight",
    9: "nine",
}

### --- GLOBAL CLOUDSCRAPER INSTANCE --- ###

scraper = cloudscraper.create_scraper()

### --- DECORATORS --- ###

def timer(func):
    @wraps(func)
    def inner(*args, **kwargs):
        tic = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.perf_counter() - tic}s")
        return result
    return inner

def ui_decorator(func):
    @wraps(func)
    def inner(*args, **kwargs):
        self:default_app = args[0]
        result = func(*args, **kwargs)
        self.raise_tags()
        return result
    return inner

def ui_update(func):
    @wraps(func)
    def inner(*args, **kwargs):
        self:default_app = args[0]
        result = func(*args, **kwargs)
        self.ui.update()
        return result
    return inner
  
### --- GLOBAL FUNCTIONS --- ###

def stop_splash():
    open("ready.flag", "w").close()
    time.sleep(0.2)
    os.remove("ready.flag")

def parse_pil(img:Image.Image):
    frames, durations = [], []
    if getattr(img, "is_animated", False):
        for i in range(img.n_frames):
            img.seek(i)
            frames.append(img.convert("RGBA"))
            durations.append(int(img.info.get("duration", DEFAULT_FRAME_DURATION_MS)))
        return frames, durations
    else:
        return [img.convert("RGBA")], [0]

def get_frames_from_url(url, depth=0):
    if depth > MAX_URL_RECURSION: return
    try:
        headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
        response = requests.get(url, headers=headers)
        return parse_pil(Image.open(BytesIO(response.content)))
    except: pass
    try:
        response = scraper.get(url)
        return parse_pil(Image.open(BytesIO(response.content)))
    except: pass
    try:
        response = scraper.get(url)
        if response.status_code != 200:
            response = requests.get(url)
        if response.status_code != 200: return
        soup = BeautifulSoup(response.text, "html.parser")
        for img in soup.find_all("img"):
            if not img['src'].startswith('http'): continue
            ret = get_frames_from_url(img['src'], depth+1)
            if not ret: continue
            return ret
    except: pass
    print(f"{url} did not resolve to an image")

def get_frames_from_file(path):
    ext = os.path.splitext(path)[1].lower()
    image_exts = {".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".webp", ".gif"}
    video_exts = {".mp4", ".mov", ".avi", ".mkv", ".webm"}
    if not ((ext in image_exts) or (ext in video_exts)): return
    if ext not in video_exts: return parse_pil(Image.open(path))
    frames, durations = [], []
    max_ram_byte = MAX_RAM_MB*10**6
    dur_acc = 0
    eff_min_dur = MIN_FRAME_DURATION_MS
    vr = VideoReader(path)
    fps = vr.get_avg_fps()
    height, width = vr[0].shape[:2]
    frame_count = len(vr)
    vid_length = frame_count/fps
    if frame_count*width*height*4 > max_ram_byte:
        eff_min_dur = int((1000*vid_length*width*height*4)/max_ram_byte)
    if eff_min_dur > MAX_FRAME_DURATION_MS:
        eff_min_dur = MAX_FRAME_DURATION_MS
        scale = math.sqrt((max_ram_byte*eff_min_dur)/(1000*vid_length*width*height*4))
        width = int(width*scale)
        height = int(height*scale)
        if height < EMB_IMG_H: return print("Video is too big\nplease make it smaller")
    if fps <= 0: fps = 30
    frame_duration = int(1000 / fps)
    indices = []
    for i in range(len(vr)):
        dur_acc += frame_duration
        if dur_acc < eff_min_dur: continue
        indices.append(i)
        durations.append(dur_acc//eff_min_dur*eff_min_dur)
        dur_acc = dur_acc%eff_min_dur
    vr = VideoReader(path, width=width, height=height)
    batch = vr.get_batch(indices).asnumpy()
    frames = [Image.fromarray(frame).convert("RGBA") for frame in batch]
    return frames, durations

def inline_write(self:tk.Text, raw_text:str, justify:str="", width=None):
    def _set_text_width(text_parts:list[list[str,str|ImageTk.PhotoImage]]):
        verbose = False
        normal_font = font.nametofont(self.cget("font"))
        bold_font = font.nametofont(self.tag_cget("bold", "font"))
        
        out_parts:list[list[str,str|ImageTk.PhotoImage]] = []
        width_left = width
        for part in text_parts:
            text_font = bold_font if part[0] == "bold" else normal_font
            if part[0] != "emoji":
                if '\n' in part[1]:
                    width_left = width
                    if verbose: print(f"newlines found in '{part[1].replace('\n','\\n')}'")
                    first = True
                    for text in part[1].split('\n'):
                        if verbose: print(f"sub-part '{text}' split from existing newline")
                        if not first: out_parts.append("\n")
                        out_parts.append([part[0], text])
                        first = False
                    continue
            size = text_font.measure(part[1]) if (part[0] in ("text", "bold")) else part[1].width()
            if size < width_left:
                if verbose: print(f"part '{part[1]}' < left {size}<{width_left}")
                width_left -= size
                out_parts.append(part)
                continue
            if (size < width) or (part[0] == "emoji"):
                if verbose: print(f"part '{part[1]}' < width {size}<{width}")
                width_left = width - size
                out_parts.append("\n")
                out_parts.append([part[0], part[1] if part[0] == "emoji" else part[1].lstrip()])
                continue
            if verbose: print(f"'{part[1]}' > width {size}>{width}")
            words = part[1].split(' ')
            line = ''
            for word in words:
                word += ' '
                word_size = text_font.measure(word)
                if word_size < width_left:
                    if verbose: print(f"word '{word}' < left {word_size}<{width_left}")
                    width_left -= word_size
                    line += word
                    continue
                if word_size < width:
                    if verbose: print(f"word '{word}' < width {word_size}<{width}")
                    width_left = width - word_size
                    out_parts.append([part[0], line])
                    out_parts.append("\n")
                    line = word
                    continue
                if verbose: print(f"word '{word}' > width {word_size}>{width}")
                i = 0
                while word_size > width:
                    if text_font.measure(word[:i]) < width:
                        i += 1
                        continue
                    if verbose: print(f"word '{word[:i-1]}' chunk")
                    out_parts.append("\n")
                    out_parts.append([part[0], word[:i-1]])
                    word = word[i:]
                    i = 0
                    word_size = text_font.measure(word)
                if word:
                    if verbose: print(f"word '{word}' last chunk")
                    width_left = width - word_size
                    out_parts.append("\n")
                    line = word
            if line:
                if verbose: print(f"line '{line}' last line")
                out_parts.append([part[0], line])
        return out_parts
    def _get_emoji_parts(part):
        emoji_text = raw_text[part[1]:part[2]][1:-1]
        i = 0
        while i < len(emoji_text) and not emoji_text[i:].isdigit():
            i += 1
        if emoji_text[i:] == '': return emoji_text, 18
        emoji_size = int(emoji_text[i:])
        emoji_text = emoji_text[:i]
        return emoji_text, emoji_size
    
    self.delete("1.0", "end")
    text_parts:list[tuple[str,int,int]] = []
    section_start_index = 0
    section_type = "text"
    text_type = "text"
    for i in range(len(raw_text)):
        if (section_type == "emoji") and (raw_text[i] == ' '):
            section_type = text_type
        
        if raw_text[i] == ':':
            text_parts.append((section_type, section_start_index, i+1 if section_type == "emoji" else i))
            section_start_index = i if section_type != "emoji" else i+1
            section_type = text_type if section_type == "emoji" else "emoji"
            
        if (raw_text[i] == '*') and (section_type != "emoji"):
            text_parts.append((section_type, section_start_index, i))
            section_start_index = i+1
            text_type = "text" if section_type == "bold" else "bold"
            section_type = text_type
    
    if (section_type == "emoji") and (text_parts[-1][0] == "bold"):
        text_parts[-1] = ("text",text_parts[-1][1]-1,text_parts[-1][2])
    text_parts.append(("text", section_start_index, len(raw_text)))
    if (section_type == "bold"):
        text_parts[-1] = ("text",text_parts[-1][1]-1,text_parts[-1][2])
    
    out_parts:list[list[str,str|ImageTk.PhotoImage]] = []
    text_type = "text"
    # [print(f"{part[0]} '{raw_text[part[1]:part[2]]}'" + "" if part[0] != "emoji" else self.emoji_dict.keys()) for part in text_parts]
    for part in text_parts:
        if not out_parts:
            emoji_text, emoji_size = _get_emoji_parts(part)
            if (part[0] != "emoji") or (emoji_text not in self.emoji_dict.keys()):
                out_parts.append([part[0] if part[0] != "emoji" else "text", raw_text[part[1]:part[2]]])
                text_type = out_parts[-1][0]
            else:
                emoji = self.emoji_dict[emoji_text]
                if not (emoji_text, emoji_size) in self.emoji_dict_TK.keys():
                    self.emoji_dict_TK[(emoji_text, emoji_size)] = ImageTk.PhotoImage(emoji.resize((emoji_size,int((emoji.height/emoji.width)*emoji_size))))
                out_parts.append(["emoji", self.emoji_dict_TK[(emoji_text, emoji_size)]])
            continue
        
        if part[0] == "emoji":
            emoji_text, emoji_size = _get_emoji_parts(part)
            if emoji_text in self.emoji_dict.keys():
                emoji = self.emoji_dict[emoji_text]
                if not (emoji_text, emoji_size) in self.emoji_dict_TK.keys():
                    self.emoji_dict_TK[(emoji_text, emoji_size)] = ImageTk.PhotoImage(emoji.resize((emoji_size,int((emoji.height/emoji.width)*emoji_size))))
                out_parts.append(["emoji", self.emoji_dict_TK[(emoji_text, emoji_size)]])
            else:
                out_parts.append([text_type, raw_text[part[1]:part[2]]])
            continue
        
        if part[0] == out_parts[-1][0]:
            out_parts[-1][1] += raw_text[part[1]:part[2]]
            continue
        
        out_parts.append([part[0], raw_text[part[1]:part[2]]])
        text_type = out_parts[-1][0]

    if width: out_parts = _set_text_width(out_parts)
    for part in out_parts:
        if part == "\n": 
            self.insert("end", part)
            continue
        if not part[1]: continue
        if part[0] == "text":
            self.insert("end", part[1], justify)
        if part[0] == "bold":
            self.insert("end", part[1], (justify, "bold"))
        if part[0] == "emoji":
            self.image_create("end", image=part[1])

EMOJI_DICT = {}
def get_emojis():
    for emoji in os.scandir(EMOJI_FOLDER):
        temp = Image.open(emoji.path)
        EMOJI_DICT[emoji.name.split('.')[0]] = temp
        # EMOJI_DICT[emoji.name.split('.')[0]] = ImageTk.PhotoImage(temp.resize((18,int((temp.height/temp.width)*18))))
    
    tk.Text.write = inline_write
    tk.Text.emoji_dict = EMOJI_DICT
    tk.Text.emoji_dict_TK = {}

FONTS = {}
def get_fonts():
    for i in range(20):
        FONTS[f"{i+1}"]  = font.Font(family=FONT_FAM, size=i+1)
        FONTS[f"{i+1}b"] = font.Font(family=FONT_FAM, size=i+1, weight="bold")
    for i in range(20):
        FONTS[f"pil_{i+1}"]  = ImageFont.truetype(FONTS_FOLDER / f"{FONT_FAM.upper()}_REGULAR.TTF", i+1)
        FONTS[f"pil_{i+1}b"] = ImageFont.truetype(FONTS_FOLDER / f"{FONT_FAM.upper()}_BOLD.TTF", i+1)

class default_app():
    def __init__(self, *args, **kwargs):
        self.ui:tk.Canvas = kwargs["ui"]
        self.bindings:tk.StringVar = kwargs["bindings"]
        self.bindings.set("│")
        
        self.ui_size = (self.ui.winfo_width(),self.ui.winfo_height())
        self.ui_cntr = (self.ui_size[0]/2,self.ui_size[1]/2)
        
        self.Embed_corner_NW = Image.open(GLOB_ASSETS_FOLDER / "Embed_corner(5x5).png")
        self.Embed_corner_SW:Image.Image = self.Embed_corner_NW.rotate(90, expand=True)
        self.Embed_corner_SE:Image.Image = self.Embed_corner_SW.rotate(90, expand=True)
        self.Embed_corner_NE:Image.Image = self.Embed_corner_SE.rotate(90, expand=True)
        
        self.Embed_edge_T = Image.open(GLOB_ASSETS_FOLDER / "Embed_edge(1x5).png")
        self.Embed_edge_L:Image.Image = self.Embed_edge_T.rotate(90, expand=True)
        self.Embed_edge_B:Image.Image = self.Embed_edge_L.rotate(90, expand=True)
        self.Embed_edge_R:Image.Image = self.Embed_edge_B.rotate(90, expand=True)
    
    def build_ui(self):
        self.create_textbox(100,100,*self.ui_cntr,anchor="center", text="No build_ui method found, please write one in your app class. :drag_n_drop: :paste:", tags=["no build_ui"])
    
### --- HELPER FUNCS --- ###
    
    def raise_tags(self):
        self.ui.tag_raise("ui_overlay")
    
    def remove_tag_bind(self, tags:list[str], bindings:list[str]=[]):
        for tag in tags:
            if self.ui.find_withtag(tag):
                for binding in bindings:
                    self.ui.tag_unbind(tag, binding)
                self.ui.delete(tag)

    def recolor_keep_alpha(self, img:Image.Image, rgb:tuple[int,int,int], alpha:Image.Image=None):
        r, g, b = rgb
        if alpha == None:
            alpha = img.getchannel("A")
        recolored = Image.new("RGBA", img.size, (r, g, b, 255))
        recolored.putalpha(alpha)
        return recolored

    def recolor_strip(self, img:Image.Image, color, width=4):
        color = self.hex_to_rgb(color)
        img_edit = img.copy()
        pixels = img_edit.load()
        w, h = img_edit.size
        for x in range(min(width, w)):
            for y in range(h):
                r, g, b, a = pixels[x, y]
                if (y == 0) or (y == h-1):
                    cr, cg, cb = color
                    m_color = tuple((int(((width-x)*cr+x*r)/width),int(((width-x)*cg+x*g)/width),int(((width-x)*cb+x*b)/width)))
                    pixels[x, y] = (*m_color, a)
                else:
                    pixels[x, y] = (*color, a)  # preserve alpha
        return img_edit
    
    def get_corners(self, item_id:str|int):
        bbox = self.ui.bbox(item_id)
        if bbox == None: return
        return ((bbox[0],bbox[1]),(bbox[0],bbox[3]),(bbox[2],bbox[3]),(bbox[2],bbox[1]),(int((bbox[0]+bbox[2])/2),int((bbox[1]+bbox[3])/2)))

    def hex_to_rgb(self, hex_color:str):
        hex_color = hex_color.lstrip("#")
        return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
    
    def rgb_to_hex(self, rgb_color:tuple[int,int,int]):
        return "#{:02X}{:02X}{:02X}".format(*rgb_color)

    def register_binding(self, binding:str|list, comment:str=None):
        if isinstance(binding, list):
            [self.register_binding(_binding, comment) for _binding in binding]
            return
        if not hasattr(self, "_bindings_cache"):
            self._bindings_cache = {}
        if binding in self._bindings_cache.keys():
            if self._bindings_cache[binding] == comment: return
        if comment == None: 
            if binding in self._bindings_cache.keys():
                self._bindings_cache.pop(binding)
            else:
                return
        else: 
            written = comment in self._bindings_cache.values()
            self._bindings_cache[binding] = comment
            if written: return
        self.bindings.set("| " + " | ".join(set([self._bindings_cache[key] for key in sorted(self._bindings_cache.keys())])))
    
    def create_widget_img(self, im_w:int, im_h:int):
        key = (im_w, im_h)

        if not hasattr(self, "_widget_cache"):
            self._widget_cache = {}

        if key not in self._widget_cache:
            edge_T = self.Embed_edge_T.resize((im_w-10, 5), Image.NEAREST)
            edge_B = self.Embed_edge_B.resize((im_w-10, 5), Image.NEAREST)
            edge_L = self.Embed_edge_L.resize((5, im_h-10), Image.NEAREST)
            edge_R = self.Embed_edge_R.resize((5, im_h-10), Image.NEAREST)
            
            img = Image.new("RGBA", (im_w, im_h), EMB_BG)

            img.paste(self.Embed_corner_NW, (0, 0))
            img.paste(self.Embed_corner_SW, (0, im_h - 5))
            img.paste(self.Embed_corner_SE, (im_w - 5, im_h - 5))
            img.paste(self.Embed_corner_NE, (im_w - 5, 0))

            img.paste(edge_T, (5, 0))
            img.paste(edge_B, (5, im_h - 5))
            img.paste(edge_L, (0, 5))
            img.paste(edge_R, (im_w - 5, 5))

            self._widget_cache[key] = img
            
        return self._widget_cache[key]

    def clear_buttons(self, tag:str=None):
        self.remove_tag_bind([f"{tag}_button" if tag != None else 'Button'], ["<ButtonRelease-1>","<Enter>","<Leave>"])
    
    def create_button(self, w:int, h:int, x:int, y:int, anchor:str, tags:list[str]|str, 
                      text='', click_func=None, txt_col:str=L_GREY, active:bool=True, color:str='', font:FreeTypeFont=None):
        if isinstance(tags, str):
            tags = [tags]
        def _on_leave_enter(event, pressed=False):
            key = (w, h, active, color, text, txt_col, id(font), pressed)
            if key not in self.button_dict:
                key_temp = (*key[:-1], False)
                widget_img = self.create_widget_img(self.button_dict[key_temp].width(),self.button_dict[key_temp].height())
                if color: widget_img = self.recolor_strip(widget_img, color)
                if text: widget_img = self.text_on_img(widget_img, text, font=font, txt_col=txt_col, edge_spacing=4)
                if pressed:widget_img = ImageEnhance.Brightness(widget_img).enhance(1.276)
                self.button_dict[key] = ImageTk.PhotoImage(widget_img)
            if pressed:
                self.ui.tag_bind(f"{tags}_button", "<ButtonRelease-1>", lambda event: _on_click(event))
            else:
                self.ui.tag_unbind(f"{tags}_button", "<ButtonRelease-1>")
            self.ui.itemconfig(f"{tags}_img", image=self.button_dict[key])
        def _on_click(event):
            click_func(event)
        
        self.clear_buttons(tags[0])
        
        if not hasattr(self, "button_dict"):
            self.button_dict:dict[str,ImageTk.PhotoImage] = {}
        
        if not font: font = FONTS["pil_12b"]
        key = (w, h, active, color, text, txt_col, id(font), False)
        if key not in self.button_dict:
            widget_img = self.create_widget_img(w,h)
            if not active:
                widget_img = ImageEnhance.Brightness(widget_img).enhance(0.783)
                txt_col = N_GREY
            if color: widget_img = self.recolor_strip(widget_img, color)
            if text: widget_img = self.text_on_img(widget_img, text, font=font, txt_col=txt_col, edge_spacing=4)
            self.button_dict[key] = ImageTk.PhotoImage(widget_img)
        self.ui.create_image(x, y, image=self.button_dict[key], anchor=anchor, 
                             tags=(tags, f"{tags}_img", f"{tags}_button", "Button"))

        if (not active) or (not click_func): return
        self.ui.tag_bind(f"{tags}_button", "<Enter>", lambda event: _on_leave_enter(event, True))
        self.ui.tag_bind(f"{tags}_button", "<Leave>", lambda event: _on_leave_enter(event, False))

    def clear_textboxes(self, tag:str=None):
        self.remove_tag_bind([f"{tag}_textbox" if tag != None else 'Textbox'])
    
    def create_textbox(self, w:int, h:int, x:int, y:int, anchor:str, tags:list[str]|str, text:str, 
                       state:str="disabled", txt_col:str=L_GREY, bg_col:str=BG_COL, font:font.Font=None, justify:str=""):
        if isinstance(tags, str):
            tags = [tags]
        self.clear_textboxes(tags[0])
        if not hasattr(self, "textbox_dict"):
            self.textbox_dict:dict[str,tk.Text] = {}
        if not font: font = FONTS["10"]
        key = tags[0]
        if key not in self.textbox_dict:
            self.textbox_dict[key] = tk.Text(highlightthickness=0, height=0, font=font, bg=bg_col, fg=txt_col, relief="flat", state=state)
            self.textbox_dict[key].tag_configure("center", justify="center")
            self.textbox_dict[key].tag_configure("bold", font=FONTS[f"{next(k for k,v in FONTS.items() if v==font)}b"])
            
        self.textbox_dict[key].config(state="normal")
        self.textbox_dict[key].write(raw_text=text, justify=justify, width=w-4)
        self.textbox_dict[key].config(state=state)
        self.ui.create_window(x, y, width=w, height=h, anchor=anchor, window=self.textbox_dict[key],
                              tags=(*tags, f"{tags[0]}_textbox", "Textbox"))
        
        display_lines = self.textbox_dict[key].count("1.0","end","displaylines")[0]
        self.textbox_dict[key].config(height=display_lines+1)
        
        text_info = self.textbox_dict[key].dlineinfo("end-1c")
        self.ui.itemconfig(f"{tags[0]}_textbox", height=text_info[1]+text_info[3]+4)

    def text_on_img(self, Background:Image.Image, text:str, 
                    coords:tuple[int,int]=None, anchor:str='center', font:FreeTypeFont=None, txt_col:str=L_GREY, edge_spacing=None):
        if font == None:
            font = FONTS["pil_10b"]
        if coords == None:
            coords = (Background.size[0]/2, Background.size[1]/2)
        if not hasattr(self, "_text_img_cache "):
            self._text_img_cache  = {}
        key = (text, id(font))
        if key not in self._text_img_cache:
            m = font.getmask(text)
            self._text_img_cache[key] = Image.frombytes("L", m.size, bytes(m))
        text_img = self._text_img_cache[key]
        text_tmp = Image.new("RGBA", Background.size, (0, 0, 0, 0))
        if edge_spacing == None: edge_spacing = int((text_tmp.size[1]-text_img.size[1])/2)
        if text_img.size[0] > text_tmp.size[0]-2*edge_spacing:
            text_img = Image.Image.crop(text_img, [0,0,text_tmp.size[0]-2*edge_spacing,text_img.size[1]])
        if anchor != 'nw':
            if anchor == 'center':
                coords = (max(edge_spacing,int(coords[0] - text_img.size[0]/2)), 
                          int(coords[1] - text_img.size[1]/2))
            if anchor == 'sw':
                coords = (max(edge_spacing,int(coords[0])), 
                          int(coords[1] - text_img.size[1]))
            if anchor == 'se':
                coords = (max(edge_spacing,int(coords[0] - text_img.size[0])), 
                          int(coords[1] - text_img.size[1]))
            if anchor == 'ne':
                coords = (max(edge_spacing,int(coords[0] - text_img.size[0])), 
                          int(coords[1]))
        text_tmp.paste(self.recolor_keep_alpha(text_img, self.hex_to_rgb(txt_col), text_img), coords)
        return Image.alpha_composite(Background, text_tmp)