"""Callback functions & State info for Sewing Pattern Configurator """

# NOTE: NiceGUI reference: https://nicegui.io/

import asyncio
import shutil
import traceback
from argparse import Namespace
# Async execution of regular functions
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from pathlib import Path
from typing import List, Tuple
from uuid import uuid4

import numpy as np
import yaml
from nicegui import ui, app, events

# Custom
from .gui_pattern import GUIPattern

icon_github = """
    <svg viewbox="0 0 98 96" xmlns="http://www.w3.org/2000/svg">
    <path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 
    21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 
    0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 
    4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 
    0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 
    12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 
    13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 
    2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/>
    </svg>
    """
icon_arxiv = """<svg id="primary_logo_-_single_color_-_white" data-name="primary logo - single color - white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 246.978 110.119"><path d="M492.976,269.5l24.36-29.89c1.492-1.989,2.2-3.03,1.492-4.723a5.142,5.142,0,0,0-4.481-3.161h0a4.024,4.024,0,0,0-3.008,1.108L485.2,261.094Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M526.273,325.341,493.91,287.058l-.972,1.033-7.789-9.214-7.743-9.357-4.695,5.076a4.769,4.769,0,0,0,.015,6.53L520.512,332.2a3.913,3.913,0,0,0,3.137,1.192,4.394,4.394,0,0,0,4.027-2.818C528.4,328.844,527.6,327.133,526.273,325.341Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M479.215,288.087l6.052,6.485L458.714,322.7a2.98,2.98,0,0,1-2.275,1.194,3.449,3.449,0,0,1-3.241-2.144c-.513-1.231.166-3.15,1.122-4.168l.023-.024.021-.026,24.851-29.448m-.047-1.882-25.76,30.524c-1.286,1.372-2.084,3.777-1.365,5.5a4.705,4.705,0,0,0,4.4,2.914,4.191,4.191,0,0,0,3.161-1.563l27.382-29.007-7.814-8.372Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M427.571,255.154c1.859,0,3.1,1.24,3.985,3.453,1.062-2.213,2.568-3.453,4.694-3.453h14.878a4.062,4.062,0,0,1,4.074,4.074v7.828c0,2.656-1.327,4.074-4.074,4.074-2.656,0-4.074-1.418-4.074-4.074V263.3H436.515a2.411,2.411,0,0,0-2.656,2.745v27.188h10.007c2.658,0,4.074,1.329,4.074,4.074s-1.416,4.074-4.074,4.074h-26.39c-2.659,0-3.986-1.328-3.986-4.074s1.327-4.074,3.986-4.074h8.236V263.3h-7.263c-2.656,0-3.985-1.329-3.985-4.074,0-2.658,1.329-4.074,3.985-4.074Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M539.233,255.154c2.656,0,4.074,1.416,4.074,4.074v34.007h10.1c2.746,0,4.074,1.329,4.074,4.074s-1.328,4.074-4.074,4.074H524.8c-2.656,0-4.074-1.328-4.074-4.074s1.418-4.074,4.074-4.074h10.362V263.3h-8.533c-2.744,0-4.073-1.329-4.073-4.074,0-2.658,1.329-4.074,4.073-4.074Zm4.22-17.615a5.859,5.859,0,1,1-5.819-5.819A5.9,5.9,0,0,1,543.453,237.539Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M605.143,259.228a4.589,4.589,0,0,1-.267,1.594L590,298.9a3.722,3.722,0,0,1-3.721,2.48h-5.933a3.689,3.689,0,0,1-3.808-2.48l-15.055-38.081a3.23,3.23,0,0,1-.355-1.594,4.084,4.084,0,0,1,4.164-4.074,3.8,3.8,0,0,1,3.718,2.656l14.348,36.134,13.9-36.134a3.8,3.8,0,0,1,3.72-2.656A4.084,4.084,0,0,1,605.143,259.228Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M390.61,255.154c5.018,0,8.206,3.312,8.206,8.4v37.831H363.308a4.813,4.813,0,0,1-5.143-4.929V283.427a8.256,8.256,0,0,1,7-8.148l25.507-3.572v-8.4H362.306a4.014,4.014,0,0,1-4.141-4.074c0-2.87,2.143-4.074,4.355-4.074Zm.059,38.081V279.942l-24.354,3.4v9.9Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M448.538,224.52h.077c1,.024,2.236,1.245,2.589,1.669l.023.028.024.026,46.664,50.433a3.173,3.173,0,0,1-.034,4.336l-4.893,5.2-6.876-8.134L446.652,230.4c-1.508-2.166-1.617-2.836-1.191-3.858a3.353,3.353,0,0,1,3.077-2.02m0-1.25a4.606,4.606,0,0,0-4.231,2.789c-.705,1.692-.2,2.88,1.349,5.1l39.493,47.722,7.789,9.214,5.853-6.221a4.417,4.417,0,0,0,.042-6.042L452.169,225.4s-1.713-2.08-3.524-2.124Z" transform="translate(-358.165 -223.27)" fill="#fff"/></svg>"""


# # Dracula color theme
# theme_colors = Namespace(
#     primary='#BD93F9',   # Light purple (primary accent in Dracula theme)
#     secondary='#FF79C6', # Pink
#     accent='#FFB86C',    # Orange
#     dark='#282A36',      # Dracula background (dark)
#     positive='#50FA7B',  # Green
#     negative='#FF5555',  # Red
#     info='#8BE9FD',      # Cyan (info color)
#     warning='#F1FA8C'    # Yellow
# )

theme_colors = Namespace(
    primary='#5C5D71',   
    secondary='#333333',
    accent='#a82c64',
    dark='#4d1f48',
    positive='#22ba38',
    negative='#f50000',
    info='#31CCEC',
    warning='#9333ea'
)


messages: List[Tuple[str, str, str, str, str]] = []


@ui.refreshable
def chat_messages(own_id: str) -> None:
    if messages:
        for user_id, avatar, text, stamp, img_url in messages:
            if img_url:
                with ui.chat_message(text=text, stamp=stamp, avatar=avatar, sent=own_id==user_id).classes('w-full'):
                    ui.image(img_url).classes('w-64')
            else:
                ui.chat_message(text=text, stamp=stamp, avatar=avatar, sent=own_id==user_id).classes('w-full')
    else:
        ui.label('').classes('mx-auto my-36')
    ui.run_javascript('window.scrollTo(0, document.body.scrollHeight)')


# State of GUI
class GUIState:
    """State of GUI-related objects
    
        NOTE: "#" is used as a separator in GUI keys to avoid confusion with
            symbols that can be (typically) used in body/design parameter names 
            ('_', '-', etc.) 

    """
    def __init__(self) -> None:
        self.window = None

        # Pattern
        self.pattern_state = GUIPattern()

        # Pattern display constants
        self.canvas_aspect_ratio = 1500. / 900   # Millimiter paper
        self.w_rel_body_size = 0.5  # Body size as fraction of horisontal canvas axis
        self.h_rel_body_size = 0.95
        self.background_body_scale = 1 / 171.99   # Inverse of the mean_all body height from GGG
        self.background_body_canvas_center = 0.273  # Fraction of the canvas (millimiter paper)
        self.w_canvas_pad, self.h_canvas_pad = 0.011, 0.04
        self.body_outline_classes = ''   # Application of pattern&body scaling when it overflows

        # Paths setup
        # Static images for GUI
        self.path_static_img = '/img'
        app.add_static_files(self.path_static_img, './assets/img')
        
        # 3D updates
        self.path_static_3d = '/geo'
        self.garm_3d_filename = f'garm_3d_{self.pattern_state.id}.glb'
        self.local_path_3d = Path('./tmp_gui/garm_3d')
        self.local_path_3d.mkdir(parents=True, exist_ok=True)
        app.add_static_files(self.path_static_3d, self.local_path_3d)
        app.add_static_files('/body', './assets/bodies')
        #model api
        self.baseurl = None
        self.api = None
        self.model = None
        self.text_model = None
        # Elements
        self.ui_design_subtabs = {}
        self.ui_pattern_display = None
        self._async_executor = ThreadPoolExecutor(1)  

        self.pattern_state.reload_garment()
        self.stylings()
        self.layout()

    def release(self):
        """Clean-up after the sesssion"""
        self.pattern_state.release()
        (self.local_path_3d / self.garm_3d_filename).unlink(missing_ok=True)

    # Initial definitions
    def stylings(self):
        """Theme definition"""
        # Theme
        # Here: https://quasar.dev/style/theme-builder
        ui.colors(
            primary=theme_colors.primary,  
            secondary=theme_colors.secondary,
            accent=theme_colors.accent,
            dark=theme_colors.dark,
            positive=theme_colors.positive,
            negative=theme_colors.negative,
            info=theme_colors.info,
            warning=theme_colors.warning
        )

    # SECTION Top level layout        
    def layout(self):
        """Overall page layout"""

        # as % of viewport width/height
        self.h_header = 5
        self.h_params_content = 88
        self.h_garment_display = 74 
        self.w_garment_display = 65
        self.w_splitter_design = 32
        self.scene_base_resoltion = (1024, 800)

        # Helpers
        self.def_pattern_waiting()
        # TODOLOW One dialog for both? 
        self.def_design_file_dialog()
        self.def_body_file_dialog()

        # Configurator GUI
        with ui.row(wrap=False).classes(f'w-full h-[{self.h_params_content}dvh] p-0 m-0 '): 
            # Tabs
            self.def_param_tabs_layout()
            
            # Pattern visual
            self.view_tabs_layout()

        # Overall wrapping
        # NOTE: https://nicegui.io/documentation/section_pages_routing#page_layout
        with ui.header(elevated=True, fixed=False).classes(f'h-[{self.h_header}vh] items-center justify-end py-0 px-4 m-0'):
            ui.label('Design2GarmentCode').classes('mr-auto').style('font-size: 150%; font-weight: 400')
            ui.button(
                'Project', 
                on_click=lambda: ui.navigate.to('https://style3d.github.io/design2garmentcode/', new_tab=True)
                ).props('flat color=white')
            with ui.link(target='http://arxiv.org/abs/2412.08603', new_tab=True):
                ui.html(icon_arxiv).classes('w-16 bg-transparent')
            with ui.link(target='https://github.com/Style3D/SXDGarmentCode', new_tab=True):
                ui.html(icon_github).classes('w-8 bg-transparent')
        # NOTE No ui.left_drawer(), no ui.right_drawer()
        with ui.footer(fixed=False, elevated=True).classes('items-center justify-center p-0 m-0'): 
            # https://www.termsfeed.com/blog/sample-copyright-notices/
            ui.link(
                '© 2025 Style3D Research', 
                'https://www.linctex.com/', 
                new_tab=True
            ).classes('text-white')

    def view_tabs_layout(self):
        """2D/3D view tabs"""
        with ui.column(wrap=False).classes(f'h-[{self.h_params_content}vh] w-full items-center'):
            with ui.tabs() as tabs: 
                self.ui_2d_tab = ui.tab('Sewing Pattern')
                self.ui_3d_tab = ui.tab('3D view')
            with ui.tab_panels(tabs, value=self.ui_2d_tab, animated=True).classes('w-full h-full items-center'):  
                with ui.tab_panel(self.ui_2d_tab).classes('w-full h-full items-center justify-center p-0 m-0'):
                    self.def_pattern_display()
                with ui.tab_panel(self.ui_3d_tab).classes('w-full h-full items-center p-0 m-0'):
                    self.def_3d_scene()

            ui.button('Download Current Garment', on_click=lambda: self.state_download()).classes('justify-self-end')

    
    def def_param_tabs_layout(self):
        """Layout of tabs with parameters"""
        with ui.column(wrap=False).classes(f'h-[{self.h_params_content}vh]'):
            with ui.tabs().classes('w-full').props('dense') as tabs:
                self.ui_lmm_tab = ui.tab('Parse Design').props('icon=chat').classes('text-sm')
                self.ui_design_tab = ui.tab('Design Parameters').props('icon=checkroom').classes('text-sm')
                self.ui_body_tab = ui.tab('Body Measurement').props('icon=accessibility_new').classes('text-sm')
                
            with ui.tab_panels(tabs, value=self.ui_design_tab, animated=True).classes('w-full h-full items-center'):
                with ui.tab_panel(self.ui_lmm_tab).classes('w-full h-full items-center p-0 m-0'):
                    self.def_lmm_tab()
                with ui.tab_panel(self.ui_design_tab).classes('w-full h-full items-center p-0 m-0'):
                    self.def_design_tab()
                with ui.tab_panel(self.ui_body_tab).classes('w-full h-full items-center p-0 m-0'):
                    self.def_body_tab()
                    
                    
    def def_lmm_tab(self):
        self.own_id = str(uuid4())
        self.chat_msg = ''
        self.chat_img_url = ''

        def toggle_inputs():
            if 'hidden' in self.api_input_fields.classes:
                self.api_input_fields.classes.remove('hidden')  # display
            else:
                self.api_input_fields.classes.append('hidden')  # hidden
            self.api_input_fields.update()  # Update the UI
        def submit_api_data():
            self.baseurl = self.baseurl_input.value
            self.api = self.api_input.value
            self.model = self.model_input.value
            self.text_model = self.text_model_input.value
            toggle_inputs()
        with ui.column().classes('w-full h-full'):
            with ui.element('div').classes('w-full h-full overflow-auto'):
                chat_messages(self.own_id)

            with ui.row().classes('w-full mt-2'):
                self.chat_input = ui.input(
                    placeholder='Describe your design or upload a reference image...').props('autofocus').classes('flex-grow').on('keydown.enter', self.handle_chat_input)
                ui.button('Send', on_click=self.handle_chat_input).props('icon=send flat').classes('mt-2')
                with ui.column().classes('hidden').style('margin-left: auto;') as self.api_input_fields:
                    self.baseurl_input = ui.input(placeholder='Base URL').classes('w-full')
                    self.api_input = ui.input(placeholder='API Key').classes('w-full')
                    self.model_input = ui.input(placeholder='Model Name').classes('w-full')
                    self.text_model_input = ui.input(placeholder='Text Model Name').classes('w-full')
                    ui.button('Submit', on_click=submit_api_data).classes('icon=send')

                ui.button('open/close input baseurl api model', on_click=toggle_inputs).classes('icon=send')

                with self.chat_input.add_slot('prepend'):
                    ui.icon('add_photo_alternate').on('click', self.open_image_upload_dialog).classes('cursor-pointer').style('margin-left: 8px;')

        with ui.dialog() as self.image_upload_dialog:
            with ui.card():
                ui.upload(on_upload=self.handle_image_upload).props('accept="image/*"').classes('w-full')
                ui.button('Close', on_click=self.image_upload_dialog.close).classes('mt-2')


    async def handle_chat_input(self, e=None):
        self.chat_msg = self.chat_input.value.strip()
        
        if self.chat_msg:
            timestamp = datetime.now().strftime('%H:%M')
            messages.append((self.own_id, f'{self.path_static_img}/artist.png', self.chat_msg, timestamp, self.chat_img_url))
            chat_messages.refresh(self.own_id)
    
            await self.parse_design(self.chat_msg, self.chat_img_url,api_key=self.api, base_url=self.baseurl, model=self.model,text_model=self.text_model)
            
        self.chat_input.value = ''
        self.chat_img_url = ''


    def open_image_upload_dialog(self):
        self.image_upload_dialog.open()


    async def handle_image_upload(self, e: events.UploadEventArguments):
        image_name = e.name
        image_path = f'{self.path_static_img}/{image_name}'
        with open(f'./assets/img/{image_name}', 'wb') as f:
            f.write(e.content.read())
        
        self.chat_img_url = image_path
        self.image_upload_dialog.close()
        self.chat_msg = self.chat_input.value.strip()
        
        timestamp = datetime.now().strftime('%H:%M')
        messages.append((self.own_id, f'{self.path_static_img}/artist.png', self.chat_msg, timestamp, self.chat_img_url))
        chat_messages.refresh(self.own_id)
        await self.parse_design(self.chat_msg, f'./assets/img/{image_name}',api_key=self.api, base_url=self.baseurl, model=self.model,text_model=self.text_model)
        
        self.chat_input.value = ''
        self.chat_img_url = ''
                

    def def_body_tab(self):
    
        # Set of buttons
        with ui.row():
            ui.button('Upload', on_click=self.ui_body_dialog.open)  
        
        self.ui_active_body_refs = {}
        self.ui_passive_body_refs = {}
        with ui.scroll_area().classes('w-full h-full p-0 m-0'): # NOTE: p-0 m-0 gap-0 dont' seem to have effect
            body = self.pattern_state.body_params
            for param in body:
                param_name = param.replace('_', ' ').capitalize()
                elem = ui.number(
                        label=param_name, 
                        value=str(body[param]), 
                        format='%.2f',
                        precision=2,
                        step=0.5,
                ).classes('text-[0.85rem]')

                if param[0] == '_':  # Info elements for calculatable parameters
                    elem.disable()
                    self.ui_passive_body_refs[param] = elem
                else:   # active elements accepting input
                    # NOTE: e.sender == UI object, e.value == new value
                    elem.on_value_change(lambda e, dic=body, param=param: self.update_pattern_ui_state(
                        dic, param, e.value, body_param=True
                    ))
                    self.ui_active_body_refs[param] = elem

    def def_flat_design_subtab(self, ui_elems, design_params, use_collapsible=False):
        """Group of design parameters"""
        for param in design_params: 
            param_name = param.replace('_', ' ').capitalize()
            if 'v' not in design_params[param]:
                ui_elems[param] = {}
                if use_collapsible:
                    with ui.expansion().classes('w-full p-0 m-0') as expansion: 
                        with expansion.add_slot('header'):
                            ui.label(f'{param_name}').classes('text-base self-center w-full h-full p-0 m-0')
                        with ui.row().classes('w-full h-full p-0 m-0'):  # Ensures correct application of style classes for children
                            self.def_flat_design_subtab(ui_elems[param], design_params[param])
                else:
                    with ui.card().classes('w-full shadow-md border m-0 rounded-md'): 
                        ui.label(f'{param_name}').classes('text-base self-center w-full h-full p-0 m-0')
                        self.def_flat_design_subtab(ui_elems[param], design_params[param])
            else:
                # Leaf value
                p_type = design_params[param]['type']
                val = design_params[param]['v']
                p_range = design_params[param]['range']
                if 'select' in p_type:
                    values = design_params[param]['range']
                    if 'null' in p_type and None not in values: 
                        values.append(None)  # NOTE: Displayable value
                    ui.label(param_name).classes('p-0 m-0 mt-2 text-stone-500 text-[0.85rem]') 
                    ui_elems[param] = ui.select(
                        values, value=val,
                        on_change=lambda e, dic=design_params, param=param: self.update_pattern_ui_state(dic, param, e.value)
                    ).classes('w-full') 
                elif p_type == 'bool':
                    ui_elems[param] = ui.switch(
                        param_name, value=val, 
                        on_change=lambda e, dic=design_params, param=param: self.update_pattern_ui_state(dic, param, e.value)
                    ).classes('text-stone-500')
                elif p_type == 'float' or p_type == 'int':
                    ui.label(param_name).classes('p-0 m-0 mt-2 text-stone-500 text-[0.85rem]')
                    ui_elems[param] = ui.slider(
                        value=val, 
                        min=p_range[0], 
                        max=p_range[1], 
                        step=0.025 if p_type == 'float' else 1,
                    ).props('snap label').classes('w-full')  \
                        .on('update:model-value', 
                            lambda e, dic=design_params, param=param: self.update_pattern_ui_state(dic, param, e.args),
                            throttle=0.5, leading_events=False)

                    # NOTE Events control: https://nicegui.io/documentation/slider#throttle_events_with_leading_and_trailing_options
                elif 'file' in p_type:
                    print(f'GUI::NotImplementedERROR::{param}::'
                          '"file" parameter type is not yet supported in Web GarmentCode. '
                          'Creation of corresponding UI element skipped'
                    )
                else:
                    print(f'GUI::WARNING::Unknown parameter type: {p_type}')
                    ui_elems[param] = ui.input(label=param_name, value=val, placeholder='Type the value',
                        validation={'Input too long': lambda value: len(value) < 20},
                        on_change=lambda e, dic=design_params, param=param: self.update_pattern_ui_state(dic, param, e.value)
                    ).classes('w-full')
                
    def def_design_tab(self):
        # Set of buttons
        with ui.row():
            ui.button('Random', on_click=self.random)
            ui.button('Default', on_click=self.default)
            ui.button('Upload', on_click=self.ui_design_dialog.open)  
    
        # Design parameters
        design_params = self.pattern_state.design_params
        self.ui_design_refs = {}
        if self.pattern_state.is_design_sectioned():
            # Use tabs to represent top-level sections
            with ui.splitter(value=self.w_splitter_design).classes('w-full h-full p-0 m-0') as splitter:
                with splitter.before:
                    with ui.tabs().props('vertical').classes('w-full h-full') as tabs:
                        for param in design_params:
                            # Tab
                            self.ui_design_subtabs[param] = ui.tab(param)
                            self.ui_design_refs[param] = {}

                with splitter.after:
                    with ui.tab_panels(tabs, value=self.ui_design_subtabs['meta']).props('vertical').classes('w-full h-full p-0 m-0'):
                        for param, tab_elem in self.ui_design_subtabs.items():
                            with ui.tab_panel(tab_elem).classes('w-full h-full p-0 m-0').style('gap: 0px'): 
                                with ui.scroll_area().classes('w-full h-full p-0 m-0').style('gap: 0px'):
                                    self.def_flat_design_subtab(
                                        self.ui_design_refs[param],
                                        design_params[param],
                                        use_collapsible=(param == 'left')
                                    )
        else:
            # Simplified display of designs
            with ui.scroll_area().classes('w-full h-full p-0 m-0'):
                self.def_flat_design_subtab(
                    self.ui_design_refs,
                    design_params,
                    use_collapsible=True
                )
                            
    # !SECTION
    # SECTION -- Pattern visuals
    def def_pattern_display(self):
        """Prepare pattern display area"""
        with ui.column().classes('h-full p-0 m-0'):
            with ui.row().classes('w-full p-0 m-0 justify-between'):
                switch = ui.switch(
                    'Body Silhouette', value=True, 
                ).props('dense left-label').classes('text-stone-800')

                self.ui_self_intersect = ui.label(
                    'WARNING: Garment panels are self-intersecting!'
                ).classes('font-semibold text-purple-600 border-purple-600 border py-0 px-1.5 rounded-md') \
                .bind_visibility(self.pattern_state, 'is_self_intersecting')

            with ui.image(
                    f'{self.path_static_img}/millimiter_paper_1500_900.png'
                ).classes(f'aspect-[{self.canvas_aspect_ratio}] h-[95%] p-0 m-0')  as self.ui_pattern_bg:  
                # NOTE: Positioning: https://github.com/zauberzeug/nicegui/discussions/957 
                with ui.row().classes('w-full h-full p-0 m-0 bg-transparent relative top-[0%] left-[0%]'):
                    self.body_outline_classes = 'bg-transparent h-full absolute top-[0%] left-[0%] p-0 m-0'
                    try:
                        self.ui_body_outline = ui.image(f'{self.path_static_img}/ggg_outline_{self.pattern_state.body_id}.svg').classes(replace=self.body_outline_classes)
                        switch.bind_value(self.ui_body_outline, 'visible')
                    except Exception as e:
                        print('GUI::WARNING::Body silhouette not found, using mean_all', str(e))
                        self.ui_body_outline = ui.image(f'{self.path_static_img}/ggg_outline_mean_all.svg').classes(self.body_outline_classes)
                        switch.bind_value(self.ui_body_outline, 'visible')
                    
                # NOTE: ui.row allows for correct classes application (e.g. no padding on svg pattern)
                with ui.row().classes('w-full h-full p-0 m-0 bg-transparent relative'):
                    # Automatically updates from source
                    self.ui_pattern_display = ui.interactive_image(
                        ''
                    ).classes('bg-transparent p-0 m-0')                    

    # !SECTION
    # SECTION 3D view
    def create_lights(self, scene:ui.scene, intensity=30.0):
        light_positions = np.array([
            [1.60614, 1.23701, 1.5341,],
            [1.31844, -2.52238, 1.92831],
            [-2.80522, 2.34624, 1.2594],
            [0.160261, 3.52215, 1.81789],
            [-2.65752, -1.26328, 1.41194]
        ])
        light_colors = [
            '#ffffff',
            '#ffffff',
            '#ffffff',
            '#ffffff',
            '#ffffff'
        ]
        z_dirs = np.arctan2(light_positions[:, 1], light_positions[:, 0])

        # Add lights to the scene
        for i in range(len(light_positions)):
            scene.spot_light(
                color=light_colors[i], intensity=intensity,
                angle=np.pi,
                ).rotate(0., 0., -z_dirs[i]).move(light_positions[i][0], light_positions[i][1], light_positions[i][2])

    def create_camera(self, cam_location, fov, scale=1.):
        camera = ui.scene.perspective_camera(fov=fov)
        camera.x = cam_location[0] * scale
        camera.y = cam_location[1] * scale
        camera.z = cam_location[2] * scale

        # direction
        camera.look_at_x = 0
        camera.look_at_y = 0
        camera.look_at_z = cam_location[2] * scale * 2/3

        return camera

    def def_3d_scene(self):
        y_fov = 30   # Degrees == np.pi / 6. rad FOV
        camera_location = [0, -4.15, 1.25] 
        bg_color='#ffffff'

        def body_visibility(value):
            self.ui_body_3d.visible(value)

        with ui.row().classes('w-full p-0 m-0 justify-between items-center'):
            self.ui_body_3d_switch = ui.switch(
                'Body Silhouette', 
                value=True, 
                on_change=lambda e: body_visibility(e.value) 
            ).props('dense left-label').classes('text-stone-800')

            ui.button('Drape current design', on_click=lambda: self.update_3d_scene())

            ui.label(
                'INFO: it takes a few minutes'
            ).classes(f'font-semibold text-[{theme_colors.primary}] border-[{theme_colors.primary}] '
                    'border py-0 px-1.5 rounded-md')

        camera = self.create_camera(camera_location, y_fov)
        with ui.scene(
            width=self.scene_base_resoltion[0], 
            height=self.scene_base_resoltion[1], 
            camera=camera, 
            grid=False, 
            background_color=bg_color   
            ).classes(f'w-[{self.w_garment_display}vw] h-[90%] p-0 m-0') as self.ui_3d_scene:
            # Lights setup
            self.create_lights(self.ui_3d_scene, intensity=60.)
            # NOTE: texture is there, just needs a better setup
            self.ui_garment_3d = None
            # TODOLOW Update body model to a correct shape
            try:
                self.ui_body_3d = self.ui_3d_scene.stl(
                    f'/body/{self.pattern_state.body_id}.stl'
                ).rotate(np.pi / 2, 0., 0.).material(color='#000000')
            except Exception as e:
                print(str(e))
                self.ui_body_3d = self.ui_3d_scene.stl(
                        '/body/mean_all.stl' 
                    ).rotate(np.pi / 2, 0., 0.).material(color='#000000')

    # !SECTION
    # SECTION -- Other UI details
    def def_pattern_waiting(self):
        """Define the waiting splashcreen with spinner 
            (e.g. waiting for a pattern to update)"""
        
        # NOTE: the screen darkens because of the shadow
        with ui.dialog(value=False).props(
            'persistent maximized'
        ) as self.spin_dialog, ui.card().classes('bg-transparent'):
            # Styles https://quasar.dev/vue-components/spinners
            ui.spinner('hearts', size='15em').classes('fixed-center')   # NOTE: 'dots' 'ball' 

    def def_body_file_dialog(self):
        """ Dialog for loading parameter files (body)
        """
        async def handle_upload(e: events.UploadEventArguments):
            param_dict = yaml.safe_load(e.content.read())['body']

            self.toggle_param_update_events(self.ui_active_body_refs)

            self.pattern_state.body_id = e.name.split('.')[0]
            self.pattern_state.set_new_body_params(param_dict)
            self.update_body_params_ui_state(self.ui_active_body_refs)            
            await self.update_pattern_ui_state()
            
            if self.ui_body_3d is not None: self.ui_body_3d.delete()                    
            try:
                print(f'INFO::Body silhouette update ggg_outline_{self.pattern_state.body_id}.svg')
                self.ui_body_3d = self.ui_3d_scene.stl(
                    f'/body/{self.pattern_state.body_id}.stl'
                ).rotate(np.pi / 2, 0., 0.).material(color='#000000')
            except Exception as e:
                print('GUI::WARNING::3D Body mesh not found, using mean_all', str(e))
                self.ui_body_3d = self.ui_3d_scene.stl(
                        '/body/mean_all.stl' 
                    ).rotate(np.pi / 2, 0., 0.).material(color='#000000')
                
            try:
                print(f'INFO::Body silhouette update ggg_outline_{self.pattern_state.body_id}.svg')
                print('*** img_url: ', self.ui_body_outline.source)
                self.ui_body_outline.set_source(f'{self.path_static_img}/ggg_outline_{self.pattern_state.body_id}.svg')
                self.ui_body_outline.update()
            except Exception as e:
                print('GUI::WARNING::Body silhouette not found, using mean_all', str(e))

            self.toggle_param_update_events(self.ui_active_body_refs)
            
            ui.notify(f'Successfully applied {e.name}')
            self.ui_body_dialog.close()

        with ui.dialog() as self.ui_body_dialog, ui.card().classes('items-center'):
            # NOTE: https://www.reddit.com/r/nicegui/comments/1393i2f/file_upload_with_restricted_types/
            ui.upload(
                label='Body parameters .yaml or .json',  
                on_upload=handle_upload
            ).classes('max-w-full').props('accept=".yaml,.json"')  

            ui.button('Close without upload', on_click=self.ui_body_dialog.close)

    def def_design_file_dialog(self):
        """ Dialog for loading parameter files (design)
        """
        async def handle_upload(e: events.UploadEventArguments):
            param_dict = yaml.safe_load(e.content.read())['design']

            self.toggle_param_update_events(self.ui_design_refs)  # Don't react to value updates

            self.pattern_state.set_new_design(param_dict)
            self.update_design_params_ui_state(self.ui_design_refs, self.pattern_state.design_params)
            await self.update_pattern_ui_state()

            self.toggle_param_update_events(self.ui_design_refs)  # Re-enable reaction to value updates

            ui.notify(f'Successfully applied {e.name}')
            self.ui_design_dialog.close()

        with ui.dialog() as self.ui_design_dialog, ui.card().classes('items-center'):
            # NOTE: https://www.reddit.com/r/nicegui/comments/1393i2f/file_upload_with_restricted_types/
            ui.upload(
                label='Design parameters .yaml or .json',  
                on_upload=handle_upload
            ).classes('max-w-full').props('accept=".yaml,.json"')  

            ui.button('Close without upload', on_click=self.ui_design_dialog.close)

    # !SECTION
    # SECTION -- Event callbacks
    async def update_pattern_ui_state(self, param_dict=None, param=None, new_value=None, body_param=False):
        """UI was updated -- update the state of the pattern parameters and visuals"""
        # NOTE: Fixing to the "same value" issue in lambdas 
        # https://github.com/zauberzeug/nicegui/wiki/FAQs#why-have-all-my-elements-the-same-value
   
        print('INFO::Updating pattern...')

        # Update the values
        if param_dict is not None:
            if body_param:
                param_dict[param] = new_value
            else:
                param_dict[param]['v'] = new_value
                self.pattern_state.is_in_3D = False   # Design param changes -> 3D model is not synced with the param
 
        try:
            if not self.pattern_state.is_slow_design(): 
                # Quick update
                self._sync_update_state()
                return

            # Display waiting spinner untill getting the result
            # NOTE Splashscreen solution to block users from modifying params while updating
            # https://github.com/zauberzeug/nicegui/discussions/1988

            self.spin_dialog.open()   
            # NOTE: Using threads for async call 
            # https://stackoverflow.com/questions/49822552/python-asyncio-typeerror-object-dict-cant-be-used-in-await-expression
            self.loop = asyncio.get_event_loop()
            await self.loop.run_in_executor(self._async_executor, self._sync_update_state)
            
            self.spin_dialog.close()

        except KeyboardInterrupt as e:
            raise e
        except BaseException as e:
            traceback.print_exc()
            print(e)
            self.spin_dialog.close()  # If open
            ui.notify(
                'Failed to generate pattern correctly. Try different parameter values',
                type='negative',
                close_button=True,
                position='center'
            )

    def _sync_update_state(self):
        # Update derivative body values (just in case)
        # TODOLOW only do that on body value updates
        self.pattern_state.body_params.eval_dependencies()
        self.update_body_params_ui_state(self.ui_passive_body_refs) # Display evaluated dependencies

        # Update the garment
        # Sync left-right for easier editing
        self.pattern_state.sync_left(with_check=True)

        # NOTE This is the slow part 
        self.pattern_state.reload_garment()

        # TODOLOW the pattern is floating around when collars are added.. 
        # Update display
        if self.ui_pattern_display is not None:

            if self.pattern_state.svg_filename:
                # Re-align the canvas and body with the new pattern
                p_bbox_size = self.pattern_state.svg_bbox_size
                p_bbox = self.pattern_state.svg_bbox

                # Margin calculations w.r.t. canvas size
                # s.t. the pattern scales correctly
                w_shift = abs(p_bbox[0])  # Body feet location in width direction w.r.t top-left corner of the pattern
                m_top = (1. - abs(p_bbox[2]) * self.background_body_scale) * self.h_rel_body_size + (1. - self.h_rel_body_size) / 2 
                m_left = self.background_body_canvas_center - w_shift * self.background_body_scale * self.w_rel_body_size
                m_right = 1 - m_left - p_bbox_size[0] * self.background_body_scale * self.w_rel_body_size
                m_bottom = 1 - m_top - p_bbox_size[1] * self.background_body_scale * self.h_rel_body_size

                # Canvas padding adjustment
                m_top -= self.h_canvas_pad
                m_left -= self.w_canvas_pad
                m_right += self.w_canvas_pad  # preserve evaluated width
                m_bottom -= self.h_canvas_pad

                # New placement
                if m_top < 0 or m_bottom < 0 or m_left < 0 or m_right < 0:
                    # Calculate the fraction
                    scale_margin = 1.2
                    y_top_scale = abs(min(m_top * scale_margin, 0.)) + 1.
                    y_bot_scale = 1. + abs(min(m_bottom * scale_margin, 0.))
                    x_left_scale = abs(min(m_left * scale_margin, 0.)) + 1.
                    x_right_scale = abs(min(m_right * scale_margin, 0.)) + 1.
                    scale = min(1. / y_top_scale, 1. / y_bot_scale, 1. / x_left_scale, 1. / x_right_scale)

                    # Rescale the body
                    self.ui_body_outline.classes(
                        replace=self.body_outline_classes + f' origin-center scale-[{scale}]'
                    )

                    # Recalculate positioning & width
                    body_center = 0.5 - self.background_body_canvas_center
                    m_top = (1. - abs(p_bbox[2]) * self.background_body_scale) * self.h_rel_body_size * scale + (1. - self.h_rel_body_size * scale) / 2 
                    m_left = (0.5 - body_center * scale) - w_shift * self.background_body_scale * self.w_rel_body_size * scale
                    m_right = 1 - m_left - p_bbox_size[0] * self.background_body_scale * self.w_rel_body_size * scale

                    # Canvas padding adjustment
                    # TODOLOW For some reason top adjustment is not needed here: m_top -= self.h_canvas_pad * scale
                    m_left -= self.w_canvas_pad * scale
                    m_right += self.w_canvas_pad * scale

                else:  # Display normally 
                    # Remove body transforms if any were applied
                    self.ui_body_outline.classes(replace=self.body_outline_classes)

                # New pattern image
                self.ui_pattern_display.set_source(
                    str(self.pattern_state.svg_path()) if self.pattern_state.svg_filename else '')
                self.ui_pattern_display.classes(
                        replace=f"""bg-transparent p-0 m-0
                                absolute 
                                left-[{m_left * 100}%]
                                top-[{m_top * 100}%] 
                                w-[{(1. - m_right - m_left) * 100}%]
                                height-auto
                        """)  
                    
            else:
                # Restore default state
                self.ui_pattern_display.set_source('')
                self.ui_body_outline.classes(replace=self.body_outline_classes)

    def update_design_params_ui_state(self, ui_elems, design_params):
        """Sync ui params with the current state of the design params"""
        for param in design_params: 
            if 'v' not in design_params[param]:
                self.update_design_params_ui_state(ui_elems[param], design_params[param])
            else:
                ui_elems[param].value = design_params[param]['v']

    def toggle_param_update_events(self, ui_elems):
        """Enable/disable event handling on the ui elements related to GarmentCode parameters"""
        for param in ui_elems:
            if isinstance(ui_elems[param], dict):
                self.toggle_param_update_events(ui_elems[param])
            else:
                if ui_elems[param].is_ignoring_events:  # -> disabled
                    ui_elems[param].enable()
                else:
                    ui_elems[param].disable()

    def update_body_params_ui_state(self, ui_body_refs):
        """Sync ui params with the current state of the body params"""
        for param in ui_body_refs: 
            ui_body_refs[param].value = self.pattern_state.body_params[param]


    async def update_3d_scene(self):
        """According the whatever pattern current state"""

        print('INFO::Updating 3D...')

        # Cleanup 
        if self.ui_garment_3d is not None:
            self.ui_garment_3d.delete()
            self.ui_garment_3d = None
        
        if not self.pattern_state.svg_filename:
            print('INFO::Current garment is empty, skipped 3D update')
            ui.notify('Current garment is empty. Chose a design to start simulating!')
            self.ui_body_3d.visible(True)
            self.ui_body_3d_switch.set_value(True)
            return

        try:
            # Display waiting spinner untill getting the result
            # NOTE Splashscreen solution to block users from modifying params while updating
            # https://github.com/zauberzeug/nicegui/discussions/1988

            self.spin_dialog.open()
            # NOTE: Using threads for async call 
            # https://stackoverflow.com/questions/49822552/python-asyncio-typeerror-object-dict-cant-be-used-in-await-expression
            self.loop = asyncio.get_event_loop()
            await self.loop.run_in_executor(self._async_executor, self._sync_update_3d)

            # Update ui
            # https://github.com/zauberzeug/nicegui/discussions/1269
            with self.ui_3d_scene:
                # NOTE: material is defined in the glb file
                self.ui_garment_3d = self.ui_3d_scene.gltf(
                            f'geo/{self.garm_3d_filename}', 
                        ).scale(0.01).rotate(np.pi / 2, 0., 0.)
            
            # Show the result! =)
            self.spin_dialog.close()

        except KeyboardInterrupt as e:
            raise e
        except BaseException as e:
            traceback.print_exc()
            print(e)
            self.ui_3d_scene.set_visibility(True)
            self.spin_dialog.close()  # If open
            ui.notify(
                'Failed to generate 3D model correctly. Try different parameter values',
                type='negative',
                close_button=True,
                position='center'
            )
    
    def _sync_update_3d(self):
        """Update 3d model"""

        # Run simulation
        path, filename = self.pattern_state.drape_3d()

        # NOTE: The files will be available publically at the static point
        # However, we cannot do much about it, since it won't be available for the interface otherwise
        shutil.copy2(path / filename, self.local_path_3d / self.garm_3d_filename)

    # Design buttons updates
    async def design_sample(self):
        """Run design sampling"""
        self.loop = asyncio.get_event_loop()
        await self.loop.run_in_executor(self._async_executor, self.pattern_state.sample_design)

    async def random(self):
        # Sampling could be slow, so add spin always
        self.spin_dialog.open() 

        self.toggle_param_update_events(self.ui_design_refs)  # Don't react to value updates

        await self.design_sample()
        self.update_design_params_ui_state(self.ui_design_refs, self.pattern_state.design_params)
        await self.update_pattern_ui_state()

        self.toggle_param_update_events(self.ui_design_refs)  # Re-do reaction to value updates

        self.spin_dialog.close() 

    async def default(self):
        self.toggle_param_update_events(self.ui_design_refs)

        self.pattern_state.restore_design(False)
        self.update_design_params_ui_state(self.ui_design_refs, self.pattern_state.design_params)
        await self.update_pattern_ui_state()

        self.toggle_param_update_events(self.ui_design_refs)

    async def parse_design(self, text_prompt='', img_url='',api_key=None, base_url=None, model=None,text_model=None):
        """Parse design from text or image"""

        def _sync_parse_design():
            response = self.pattern_state.parse_chat(text_prompt, img_url,api_key, base_url, model,text_model)
            return response

        self.spin_dialog.open()
        self.toggle_param_update_events(self.ui_design_refs)

        self.loop = asyncio.get_event_loop()
        response = await self.loop.run_in_executor(self._async_executor, _sync_parse_design)

        # response = self.pattern_state.parse_chat(text_prompt, img_url)
        print('GPT response: ', response)

        self.update_design_params_ui_state(self.ui_design_refs, self.pattern_state.design_params)
        await self.update_pattern_ui_state()
        self.toggle_param_update_events(self.ui_design_refs)
        self.spin_dialog.close()

        # !SECTION

    def state_download(self):
        """Download current state of a garment"""
        archive_path = self.pattern_state.save()
        ui.download(archive_path, f'Configured_design_{datetime.now().strftime("%y%m%d-%H-%M-%S")}.zip')
