import asyncio
from pathlib import Path
from winrt.windows.data.xml.dom import XmlDocument
from winrt.windows.foundation import IPropertyValue
from winrt.windows.ui.notifications import ToastNotificationManager, ToastNotification, NotificationData, ToastActivatedEventArgs, ToastDismissedEventArgs, ToastFailedEventArgs

DEFAULT_APP_ID = 'Python'

xml = """<toast activationType="protocol" launch="http:" scenario="{scenario}">
    <visual>
        <binding template="ToastGeneric"></binding>
    </visual>
</toast>"""

def set_attribute(document, xpath, name, value):
    attr = document.create_attribute(name)
    attr.value = value
    document.select_single_node(xpath).attributes.set_named_item(attr)

def add_text(msg, document):
    if isinstance(msg, str):
        msg = {'text': msg}
    binding = document.select_single_node('//binding')
    text = document.create_element('text')
    for k, v in msg.items():
        if k == 'text':
            text.inner_text = v
        else:
            text.set_attribute(k, v)
    binding.append_child(text)

def add_icon(icon, document):
    if isinstance(icon, str):
        icon = {'placement': 'appLogoOverride', 'hint-crop': 'circle', 'src': icon}
    binding = document.select_single_node('//binding')
    img = document.create_element('image')
    for k, v in icon.items():
        img.set_attribute(k, v)
    binding.append_child(img)

def add_image(img, document):
    if isinstance(img, str):
        img = {'src': img}
    binding = document.select_single_node('//binding')
    img_el = document.create_element('image')
    for k, v in img.items():
        img_el.set_attribute(k, v)
    binding.append_child(img_el)

def add_progress(prog, document):
    binding = document.select_single_node('//binding')
    prog_el = document.create_element('progress')
    for name in prog:
        prog_el.set_attribute(name, '{' + name + '}')
    binding.append_child(prog_el)

def add_audio(aud, document):
    if isinstance(aud, str):
        aud = {'src': aud}
    toast = document.select_single_node('/toast')
    audio = document.create_element('audio')
    for k, v in aud.items():
        audio.set_attribute(k, v)
    toast.append_child(audio)

def create_actions(document):
    toast = document.select_single_node('/toast')
    actions = document.create_element('actions')
    toast.append_child(actions)
    return actions

def add_button(button, document):
    if isinstance(button, str):
        button = {
            'activationType': 'protocol',
            'arguments': 'http:' + button,
            'content': button
        }
    actions = (document.select_single_node('//actions')
               or create_actions(document))
    action = document.create_element('action')
    for k, v in button.items():
        action.set_attribute(k, v)
    actions.append_child(action)

def add_input(id, document):
    if isinstance(id, str):
        id = {
            'id': id,
            'type': 'text',
            'placeHolderContent': id
        }
    actions = (document.select_single_node('//actions')
               or create_actions(document))
    input_el = document.create_element('input')
    for k, v in id.items():
        input_el.set_attribute(k, v)
    actions.append_child(input_el)

def add_selection(selection, document):
    if isinstance(selection, list):
        selection = {
            'input': {'id': 'selection', 'type': 'selection'},
            'selection': selection
        }
    actions = (document.select_single_node('//actions')
               or create_actions(document))
    input_el = document.create_element('input')

    for k, v in selection['input'].items():
        input_el.set_attribute(k, v)
    actions.append_child(input_el)
    
    for item in selection['selection']:
        item = item if isinstance(item, dict) else {'id': item, 'content': item}
        sel_el = document.create_element('selection')
        for k, v in item.items():
            sel_el.set_attribute(k, v)
        input_el.append_child(sel_el)

def result_wrapper(*args):
    global result
    result = args
    return result

def activated_args(_, event):
    e = ToastActivatedEventArgs._from(event)
    user_input = {}
    for name in e.user_input:
        value = IPropertyValue._from(e.user_input[name]).get_string()
        user_input[name] = value
    return {
        'arguments': e.arguments,
        'user_input': user_input
    }

async def play_sound(audio):
    from winrt.windows.media.core import MediaSource
    from winrt.windows.media.playback import MediaPlayer
    from winrt.windows.foundation import Uri
    from winrt.windows.storage import StorageFile

    source = (MediaSource.create_from_uri(Uri(audio))
              if audio.startswith('http')
              else MediaSource.create_from_storage_file(
                  await StorageFile.get_file_from_path_async(audio)))
    player = MediaPlayer()
    player.source = source
    player.play()
    await asyncio.sleep(7)

async def speak(text):
    from winrt.windows.media.speechsynthesis import SpeechSynthesizer
    from winrt.windows.media.core import MediaSource
    from winrt.windows.media.playback import MediaPlayer
    
    stream = await SpeechSynthesizer().synthesize_text_to_stream_async(text)
    player = MediaPlayer()
    player.source = MediaSource.create_from_stream(stream, stream.content_type)
    player.play()
    await asyncio.sleep(7)

async def recognize(ocr):
    from winrt.windows.media.ocr import OcrEngine
    from winrt.windows.graphics.imaging import BitmapDecoder
    from winrt.windows.globalization import Language
    from winrt.windows.storage import StorageFile, FileAccessMode

    if isinstance(ocr, str):
        ocr = {'ocr': ocr}

    file = await StorageFile.get_file_from_path_async(ocr['ocr'])
    stream = await file.open_async(FileAccessMode.READ)

    decoder = await BitmapDecoder.create_async(stream)
    bitmap = await decoder.get_software_bitmap_async()

    lang = ocr.get('lang')
    if lang and OcrEngine.is_language_supported(Language(lang)):
        engine = OcrEngine.try_create_from_language(Language(lang))
    else:
        engine = OcrEngine.try_create_from_user_profile_languages()

    result = await engine.recognize_async(bitmap)
    
    return {
        "text": result.text,
        "lines": [line.text for line in result.lines],
        "word_count": len(result.lines) if result.lines else 0
    }

def available_recognizer_languages():
    from winrt.windows.media.ocr import OcrEngine
    for language in OcrEngine.available_recognizer_languages():
        print(language.display_name, language.language_tag)
    print('Запусти от имени администратора командную строку и вбей следующие команды')
    print('Get-WindowsCapability -Online -Name "Language.OCR*"')
    print('Add-WindowsCapability -Online -Name "Language.OCR~~~en-US~0.0.1.0"')

def notify(title=None, body=None, on_click=print, icon=None, image=None, progress=None, audio=None,
           dialogue=None, duration=None, input=None, inputs=[], selection=None, selections=[],
           button=None, buttons=[], xml=xml, app_id=DEFAULT_APP_ID, scenario=None, tag=None, group=None):
    doc = XmlDocument()
    doc.load_xml(xml.format(scenario=scenario or 'default'))

    if isinstance(on_click, str):
        set_attribute(doc, '/toast', 'launch', on_click)
    if duration:
        set_attribute(doc, '/toast', 'duration', duration)

    for text in ([title] if title else []) + ([body] if body else []):
        add_text(text, doc)
    for item in inputs + ([input] if input else []):
        add_input(item, doc)
    for sel in selections + ([selection] if selection else []):
        add_selection(sel, doc)
    for btn in buttons + ([button] if button else []):
        add_button(btn, doc)

    if icon: 
        add_icon(icon, doc)
    if image: 
        add_image(image, doc)
    if progress: 
        add_progress(progress, doc)

    if audio:
        if isinstance(audio, str) and audio.startswith('ms'):
            add_audio(audio, doc)
        elif isinstance(audio, str) and Path(audio).is_file():
            add_audio(f'file:///{Path(audio).absolute().as_posix()}', doc)
        elif isinstance(audio, dict) and 'src' in audio and audio['src'].startswith('ms'):
            add_audio(audio, doc)
        else:
            add_audio({'silent': 'true'}, doc)
    elif dialogue:
        add_audio({'silent': 'true'}, doc)

    notification = ToastNotification(doc)
    
    if progress:
        data = NotificationData()
        data.values.update({k: str(v) for k, v in progress.items()})
        data.sequence_number = 1
        notification.data, notification.tag = data, 'my_tag'
    
    if tag: 
        notification.tag = tag
    if group: 
        notification.group = group

    try:
        notifier = (ToastNotificationManager.create_toast_notifier()
                    if app_id == DEFAULT_APP_ID
                    else ToastNotificationManager.create_toast_notifier_with_id(app_id))
    except Exception:
        notifier = ToastNotificationManager.create_toast_notifier_with_id(app_id)

    notifier.show(notification)
    return notification

async def toast_async(title=None, body=None, on_click=print, icon=None, image=None,
    progress=None, audio=None, dialogue=None, duration=None,
    input=None, inputs=[], selection=None, selections=[],
    button=None, buttons=[], xml=xml, app_id=DEFAULT_APP_ID,
    ocr=None, on_dismissed=print, on_failed=print,
    scenario=None, tag=None, group=None, timeout=30.0):

    if ocr:
        title = 'Результат OCR'
        body = (await recognize(ocr)).text
        src = ocr if isinstance(ocr, str) else ocr['ocr']
        image = {'placement': 'hero', 'src': src}

    notification = notify(title, body, on_click, icon, image, progress,
                       audio, dialogue, duration, input, inputs, selection,
                       selections, button, buttons, xml, app_id, scenario, tag, group)

    loop = asyncio.get_running_loop()
    futures = []
    tokens = {}

    if audio and isinstance(audio, str) and not audio.startswith('ms'):
        futures.append(loop.create_task(play_sound(audio)))
    if dialogue:
        futures.append(loop.create_task(speak(dialogue)))

    def add_handler(evt, cb):
        future = loop.create_future()
        token = getattr(notification, f'add_{evt}')(
            lambda *a: loop.call_soon_threadsafe(future.set_result, cb(*a)))
        futures.append(future)
        tokens[evt] = token
        return future

    add_handler('activated', lambda *a: on_click(activated_args(*a)))
    add_handler('dismissed', lambda _, e: on_dismissed(
                result_wrapper(ToastDismissedEventArgs._from(e).reason)))
    add_handler('failed', lambda _, e: on_failed(
                 result_wrapper(ToastFailedEventArgs._from(e).error_code)))

    try:
        done, pending = await asyncio.wait(futures, timeout=timeout,
                                           return_when=asyncio.FIRST_COMPLETED)
        for task in pending: task.cancel()
    finally:
        for evt, token in tokens.items():
            getattr(notification, f'remove_{evt}')(token)

def toast(*args, **kwargs):
    coro = toast_async(*args, **kwargs)
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        return asyncio.run(coro)
    else:
        task = loop.create_task(coro)
        future = loop.create_future()
        task.add_done_callback(lambda t: (
            future.set_exception(t.exception()) 
            if t.exception() else future.set_result(t.result())
        ))
        return future

async def atoast(*args, **kwargs):
    return await toast_async(*args, **kwargs)

def update_progress(progress, app_id=DEFAULT_APP_ID, tag='my_tag'):
    data = NotificationData()
    for name, value in progress.items():
        data.values[name] = str(value)
    data.sequence_number = 2
    if app_id == DEFAULT_APP_ID:
        try:
            notifier = ToastNotificationManager.create_toast_notifier()
        except Exception as e:
            notifier = ToastNotificationManager.create_toast_notifier(app_id)
    else:
        notifier = ToastNotificationManager.create_toast_notifier(app_id)
    return notifier.update(data, tag)

def clear_toast(app_id=DEFAULT_APP_ID, tag=None, group=None):
    history = ToastNotificationManager.history
    
    if tag and not group:
        raise AttributeError('для удаления уведомления необходимо указать значение группы')
    if not tag and not group:
        history.clear(app_id)
    elif tag and group:
        history.remove(tag, group, app_id)
    else:
        history.remove_group(group, app_id)