"use strict"

let modelsCache
let modelsOptions

/*
*** SEARCHABLE MODELS ***
Creates searchable dropdowns for SD, VAE, or HN models.
Also adds a reload models button (placed next to SD models, reloads everything including VAE and HN models).
More reload buttons may be added at strategic UI locations as needed.
Merely calling getModels() makes all the magic happen behind the scene to refresh the dropdowns.

HOW TO CREATE A MODEL DROPDOWN:
1) Create an input element. Make sure to add a data-path property, as this is how model dropdowns are identified in auto-save.js.
<input id="stable_diffusion_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />

2) Just declare one of these for your own dropdown (remember to change the element id, e.g. #stable_diffusion_models to your own input's id).
let stableDiffusionModelField = new ModelDropdown(document.querySelector('#stable_diffusion_model'), 'stable-diffusion')
let vaeModelField = new ModelDropdown(document.querySelector('#vae_model'), 'vae', 'None')
let hypernetworkModelField = new ModelDropdown(document.querySelector('#hypernetwork_model'), 'hypernetwork', 'None')

3) Model dropdowns will be refreshed automatically when the reload models button is invoked.
*/
class ModelDropdown
{
    modelFilter //= document.querySelector("#model-filter")
    modelFilterArrow //= document.querySelector("#model-filter-arrow")
    modelList //= document.querySelector("#model-list")
    modelResult //= document.querySelector("#model-result")
    modelNoResult //= document.querySelector("#model-no-result")
    
    currentSelection //= { elem: undefined, value: '', path: ''}
    highlightedModelEntry //= undefined
    activeModel //= undefined

    inputModels //= undefined
    modelKey //= undefined
    flatModelList //= []
    noneEntry //= ''
    modelFilterInitialized //= undefined

    /* MIMIC A REGULAR INPUT FIELD */
    get parentElement() {
        return this.modelFilter.parentElement
    }
    get parentNode() {
        return this.modelFilter.parentNode
    }
    get value() {
        return this.modelFilter.dataset.path
    }
    set value(path) {
        this.modelFilter.dataset.path = path
        this.selectEntry(path)
    }
    addEventListener(type, listener, options) {
        return this.modelFilter.addEventListener(type, listener, options)
    }
    dispatchEvent(event) {
        return this.modelFilter.dispatchEvent(event)
    }
    appendChild(option) {
        // do nothing
    }

    // remember 'this' - http://blog.niftysnippets.org/2008/04/you-must-remember-this.html
    bind(f, obj) {
        return function() {
            return f.apply(obj, arguments)
        }
    }

    /* SEARCHABLE INPUT */    
    constructor (input, modelKey, noneEntry = '') {
        this.modelFilter = input
        this.noneEntry = noneEntry
        this.modelKey = modelKey

        if (modelsOptions !== undefined) { // reuse models from cache (only useful for plugins, which are loaded after models)
            this.inputModels = modelsOptions[this.modelKey]
            this.populateModels()
        }
        document.addEventListener("refreshModels", this.bind(function(e) {
            // reload the models
            this.inputModels = modelsOptions[this.modelKey]
            this.populateModels()
        }, this))
    }

    saveCurrentSelection(elem, value, path) {
        this.currentSelection.elem = elem
        this.currentSelection.value = value
        this.currentSelection.path = path
        this.modelFilter.dataset.path = path
        this.modelFilter.value = value
        this.modelFilter.dispatchEvent(new Event('change'))
    }
    
    processClick(e) {
        e.preventDefault()
        if (e.srcElement.classList.contains('model-file')) {
            this.saveCurrentSelection(e.srcElement, e.srcElement.innerText, e.srcElement.dataset.path)
            this.hideModelList()
            this.modelFilter.focus()
            this.modelFilter.select()
        }
    }

    getPreviousVisibleSibling(elem) {
        let prevSibling = elem.previousElementSibling
        while (prevSibling && prevSibling.classList.contains('model-file')) {
            if (prevSibling.style.display == 'list-item') return prevSibling
            prevSibling = prevSibling.previousElementSibling
        }
        if (prevSibling && prevSibling.style.display == 'list-item') return prevSibling
    }

    getLastVisibleChild(elem) {
        let lastElementChild = elem.lastElementChild
        if (lastElementChild.style.display == 'list-item') return lastElementChild
        return this.getPreviousVisibleSibling(lastElementChild)
    }
    
    findPreviousSibling(elem) {
        // is there an immediate sibling?
        let prevSibling = this.getPreviousVisibleSibling(elem)
        if (prevSibling) {
            // if the previous sibling is a model file, just select it
            if (prevSibling.classList.contains('model-file')) return prevSibling
            
            // if the previous sibling is a model folder, select the last model file it contains
            if (prevSibling.classList.contains('model-folder')) return prevSibling.firstElementChild.lastElementChild
        }

        // no sibling model file and no sibling model folder. look for siblings around the parent element.
        prevSibling = this.getPreviousVisibleSibling(elem.parentElement.parentElement)
        if (prevSibling) {
            // if the previous entry is a model file, select it
            if (prevSibling.classList.contains('model-file')) return prevSibling

            // is there another model folder to jump to before the current one?
            if (prevSibling.classList.contains('model-folder')) return this.getLastVisibleChild(prevSibling.firstElementChild)
        }
    }
    
    getNextVisibleSibling(elem) {
        let nextSibling = elem.nextElementSibling
        while (nextSibling && nextSibling.classList.contains('model-file')) {
            if (nextSibling.style.display == 'list-item') return nextSibling
            nextSibling = nextSibling.nextElementSibling
        }
        if (nextSibling && nextSibling.style.display == 'list-item') return nextSibling
    }
    
    getFirstVisibleChild(elem) {
        let firstElementChild = elem.firstElementChild
        if (firstElementChild.style.display == 'list-item') return firstElementChild
        return this.getNextVisibleSibling(firstElementChild)
    }
    
    findNextSibling(elem) {
        // is there an immediate sibling?
        let nextSibling = this.getNextVisibleSibling(elem)
        if (nextSibling) {
            // if the next sibling is a model file, just select it
            if (nextSibling.classList.contains('model-file')) return nextSibling
            
            // if the next sibling is a model folder, select the first model file it contains
            if (nextSibling.classList.contains('model-folder')) return nextSibling.firstElementChild.firstElementChild
        }

        // no sibling model file and no sibling model folder. look for siblings around the parent element.
        nextSibling = this.getNextVisibleSibling(elem.parentElement.parentElement)
        if (nextSibling) {
            // if the next entry is a model file, select it
            if (nextSibling.classList.contains('model-file')) return nextSibling

            // is there another model folder to jump to after the current one?
            if (nextSibling.classList.contains('model-folder')) return this.getFirstVisibleChild(nextSibling.firstElementChild)
        }
    }
    
    selectModelEntry(elem) {
        if (elem) {
            if (this.highlightedModelEntry !== undefined) {
                this.highlightedModelEntry.classList.remove('selected')
            }
            this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path)
            elem.classList.add('selected')
            elem.scrollIntoView({block: 'nearest'})
            this.highlightedModelEntry = elem
        }
    }
    
    selectPreviousFile() {
        const elem = this.findPreviousSibling(this.highlightedModelEntry)
        if (elem) {
            this.selectModelEntry(elem)
        }
        else
        {
            //this.highlightedModelEntry.parentElement.parentElement.scrollIntoView({block: 'nearest'})
            this.highlightedModelEntry.closest('.model-list').scrollTop = 0
        }
        this.modelFilter.select()
    }
    
    selectNextFile() {
        this.selectModelEntry(this.findNextSibling(this.highlightedModelEntry))
        this.modelFilter.select()
    }
    
    selectFirstFile() {
        this.selectModelEntry(this.modelList.querySelector('.model-file'))
        this.highlightedModelEntry.scrollIntoView({block: 'nearest'})
        this.modelFilter.select()
    }
    
    selectLastFile() {
        const elems = this.modelList.querySelectorAll('.model-file:last-child')
        this.selectModelEntry(elems[elems.length -1])
        this.modelFilter.select()
    }

    resetSelection() {
        this.hideModelList()
        this.showAllEntries()
        this.modelFilter.value = this.currentSelection.value
        this.modelFilter.focus()
        this.modelFilter.select()
    }

    validEntrySelected() {
        return (this.modelNoResult.style.display === 'none')
    }
    
    processKey(e) {
        switch (e.key) {
            case 'Escape':
                e.preventDefault()
                this.resetSelection()
                break
            case 'Enter':
                e.preventDefault()
                if (this.validEntrySelected()) {
                    if (this.modelList.style.display != 'block') {
                        this.showModelList()
                    }
                    else
                    {
                        this.saveCurrentSelection(this.highlightedModelEntry, this.highlightedModelEntry.innerText, this.highlightedModelEntry.dataset.path)
                        this.hideModelList()
                        this.showAllEntries()
                    }
                    this.modelFilter.focus()
                }
                else
                {
                    this.resetSelection()
                }
                break
            case 'ArrowUp':
                e.preventDefault()
                if (this.validEntrySelected()) {
                    this.selectPreviousFile()
                }
                break
            case 'ArrowDown':
                e.preventDefault()
                if (this.validEntrySelected()) {
                    this.selectNextFile()
                }
                break
            case 'ArrowLeft':
                if (this.modelList.style.display != 'block') {
                    e.preventDefault()
                }
                break
            case 'ArrowRight':
                if (this.modelList.style.display != 'block') {
                    e.preventDefault()
                }
                break
            case 'PageUp':
                e.preventDefault()
                if (this.validEntrySelected()) {
                    this.selectPreviousFile()
                    this.selectPreviousFile()
                    this.selectPreviousFile()
                    this.selectPreviousFile()
                    this.selectPreviousFile()
                    this.selectPreviousFile()
                    this.selectPreviousFile()
                    this.selectPreviousFile()
                }
                break
            case 'PageDown':
                e.preventDefault()
                if (this.validEntrySelected()) {
                    this.selectNextFile()
                    this.selectNextFile()
                    this.selectNextFile()
                    this.selectNextFile()
                    this.selectNextFile()
                    this.selectNextFile()
                    this.selectNextFile()
                    this.selectNextFile()
                }
                break
            case 'Home':
                //if (this.modelList.style.display != 'block') {
                    e.preventDefault()
                    if (this.validEntrySelected()) {
                        this.selectFirstFile()
                    }
                //}
                break
            case 'End':
                //if (this.modelList.style.display != 'block') {
                    e.preventDefault()
                    if (this.validEntrySelected()) {
                        this.selectLastFile()
                    }
                //}
                break
            default:
                //console.log(e.key)
        }
    }
    
    modelListFocus() {
        this.selectEntry()
        this.showAllEntries()
    }
    
    showModelList() {
        this.modelList.style.display = 'block'
        this.selectEntry()
        this.showAllEntries()
        this.modelFilter.value = ''
        this.modelFilter.focus()
        this.modelFilter.style.cursor = 'auto'
    }
    
    hideModelList() {
        this.modelList.style.display = 'none'
        this.modelFilter.value = this.currentSelection.value
        this.modelFilter.style.cursor = ''
    }
    
    toggleModelList(e) {
        e.preventDefault()
        if (this.modelList.style.display != 'block') {
            this.showModelList()
        }
        else
        {
            this.hideModelList()
            this.modelFilter.select()
        }
    }
    
    selectEntry(path) {
        if (path !== undefined) {
            const entries = this.modelList.querySelectorAll('.model-file');

            for (let i = 0; i < entries.length; i++) {
                if (entries[i].dataset.path == path) {
                    this.saveCurrentSelection(entries[i], entries[i].innerText, entries[i].dataset.path)
                    this.highlightedModelEntry = entries[i]
                    entries[i].scrollIntoView({block: 'nearest'})
                    break
                }
            }
        }
        
        if (this.currentSelection.elem !== undefined) {
            // select the previous element
            if (this.highlightedModelEntry !== undefined && this.highlightedModelEntry != this.currentSelection.elem) {
                this.highlightedModelEntry.classList.remove('selected')
            }
            this.currentSelection.elem.classList.add('selected')
            this.highlightedModelEntry = this.currentSelection.elem
            this.currentSelection.elem.scrollIntoView({block: 'nearest'})
        }
        else
        {
            this.selectFirstFile()
        }
    }
    
    highlightModelAtPosition(e) {
        let elem = document.elementFromPoint(e.clientX, e.clientY)
        
        if (elem.classList.contains('model-file')) {
            this.highlightModel(elem)
        }
    }
    
    highlightModel(elem) {
        if (elem.classList.contains('model-file')) {
            if (this.highlightedModelEntry !== undefined && this.highlightedModelEntry != elem) {
                this.highlightedModelEntry.classList.remove('selected')
            }
            elem.classList.add('selected')
            this.highlightedModelEntry = elem
        }
    }
    
    showAllEntries() {
        this.modelList.querySelectorAll('li').forEach(function(li) {
            if (li.id !== 'model-no-result') {
                li.style.display = 'list-item'
            }
        })
        this.modelNoResult.style.display = 'none'
    }
    
    filterList(e) {
        const filter = this.modelFilter.value.toLowerCase()
        let found = false
        let showAllChildren = false
        
        this.modelList.querySelectorAll('li').forEach(function(li) {
            if (li.classList.contains('model-folder')) {
                showAllChildren = false
            }
            if (filter == '') {
                li.style.display = 'list-item'
                found = true
            } else if (showAllChildren || li.textContent.toLowerCase().match(filter)) {
                li.style.display = 'list-item'
                if (li.classList.contains('model-folder') && li.firstChild.textContent.toLowerCase().match(filter)) {
                    showAllChildren = true
                }
                found = true
            } else {
                li.style.display = 'none'
            }
        })
    
        if (found) {
            this.modelResult.style.display = 'list-item'
            this.modelNoResult.style.display = 'none'
            const elem = this.findNextSibling(this.modelList.querySelector('.model-file'))
            this.highlightModel(elem)
            elem.scrollIntoView({block: 'nearest'})
        }
        else
        {
            this.modelResult.style.display = 'none'
            this.modelNoResult.style.display = 'list-item'
        }
        this.modelList.style.display = 'block'
    }

    /* MODEL LOADER */
    getElementDimensions(element) {
        // Clone the element
        const clone = element.cloneNode(true)
        
        // Copy the styles of the original element to the cloned element
        const originalStyles = window.getComputedStyle(element)
        for (let i = 0; i < originalStyles.length; i++) {
            const property = originalStyles[i]
            clone.style[property] = originalStyles.getPropertyValue(property)
        }
        
        // Set its visibility to hidden and display to inline-block
        clone.style.visibility = "hidden"
        clone.style.display = "inline-block"
        
        // Put the cloned element next to the original element
        element.parentNode.insertBefore(clone, element.nextSibling)
        
        // Get its width and height
        const width = clone.offsetWidth
        const height = clone.offsetHeight
        
        // Remove it from the DOM
        clone.remove()
        
        // Return its width and height
        return { width, height }
    }
    
    flattenModelList(models, path) {
        models.forEach(entry => {
            if (Array.isArray(entry)) {
                this.flattenModelList(entry[1], path + '/' + entry[0])
            }
            else
            {
                this.flatModelList.push(path == '' ? entry : path + '/' + entry)
            }
        })
    }

    // sort models
    getFolder(model) {
        return model.substring(0, model.lastIndexOf('/') + 1)
    }
    
    sortModels(models) {
        // sort the models in alphabetical order, root folder models last
        models.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))

        /*
        // sort the models in alphabetical order, root folder models first
        models = models.sort()
        let found
        do {
            found = false
            for (let i = 0; i < models.length - 1; i++) {
                if (
                    (this.getFolder(models[i]) === this.getFolder(models[i+1]) && models[i].toLowerCase().localeCompare(models[i+1].toLowerCase()) > 0) // same folder, sort by alphabetical order
                    || (this.getFolder(models[i]).toLowerCase().localeCompare(this.getFolder(models[i+1]).toLowerCase()) > 0) // L1 folder > L2 folder
                ) {
                    [models[i], models[i+1]] = [models[i+1], models[i]]
                    found = true
                }
            }
        } while (found)
        */
    }

    populateModels() {
        this.activeModel = this.modelFilter.dataset.path
        
        this.currentSelection = { elem: undefined, value: '', path: ''}
        this.highlightedModelEntry = undefined
        this.flatModelList = []

        if(this.modelList !== undefined) {
            this.modelList.remove()
            this.modelFilterArrow.remove()
        }
        this.createDropdown()
    }

    createDropdown() {
        // prepare to sort the models
        this.flattenModelList(this.inputModels, '')
        this.sortModels(this.flatModelList)

        // create dropdown entries
        this.modelFilter.insertAdjacentHTML('afterend', this.parseModels(this.flatModelList))
        this.modelFilter.classList.add('model-selector')
        this.modelFilterArrow = document.querySelector(`#${this.modelFilter.id}-model-filter-arrow`)
        this.modelList = document.querySelector(`#${this.modelFilter.id}-model-list`)
        this.modelResult = document.querySelector(`#${this.modelFilter.id}-model-result`)
        this.modelNoResult = document.querySelector(`#${this.modelFilter.id}-model-no-result`)
        
        if (this.modelFilterInitialized !== true) {
            this.modelFilter.addEventListener('input', this.bind(this.filterList, this))
            this.modelFilter.addEventListener('focus', this.bind(this.modelListFocus, this))
            this.modelFilter.addEventListener('blur', this.bind(this.hideModelList, this))
            this.modelFilter.addEventListener('click', this.bind(this.showModelList, this))
            this.modelFilter.addEventListener('keydown', this.bind(this.processKey, this))

            this.modelFilterInitialized = true
        }
        this.modelFilterArrow.addEventListener('mousedown', this.bind(this.toggleModelList, this))
        this.modelList.addEventListener('mousemove', this.bind(this.highlightModelAtPosition, this))
        this.modelList.addEventListener('mousedown', this.bind(this.processClick, this))

        this.selectEntry(this.activeModel)
    }

    parseModels(models) {
        let html = `<i id="${this.modelFilter.id}-model-filter-arrow" class="model-selector-arrow fa-solid fa-angle-down"></i>
            <ul id="${this.modelFilter.id}-model-list" class="model-list">
                <li id="${this.modelFilter.id}-model-no-result" class="model-no-result">No result</li>
                <li id="${this.modelFilter.id}-model-result" class="model-result">
            <ul>
        `
        if (this.noneEntry != '') {
            html += `<li data-path='' class='model-file in-root-folder'>${this.noneEntry}</li>`
        }
        
        let currentFolder = ''
        models.forEach(entry => {
            const folder = entry.substring(0, 1) == '/' ? entry.substring(1, entry.lastIndexOf('/')) : ''
            if (folder !== '' && folder !== currentFolder) {
                if (currentFolder != '') {
                    html += '</ul></li>'
                }
                html += `<li class='model-folder'>/${folder}<ul>`
                currentFolder = folder
            }
            else if (folder == '' && currentFolder !== '') {
                currentFolder = ''
                html += '</ul></li>'
            }
            const modelName = entry.substring(entry.lastIndexOf('/') + 1)
            if (entry.substring(0, 1) == '/') {
                entry = entry.substring(1)
            }
            html += `<li data-path='${entry}' class='model-file${currentFolder == '' ? ' in-root-folder' : ''}'>${modelName}</li>`
        })
        if (currentFolder != '') {
            html += '</ul></li>'
        }
        
        html += `
                    </ul>
                </li>
            </ul>
            `
        return html
    }
}

/* (RE)LOAD THE MODELS */
async function getModels() {
    try {
        modelsCache = await SD.getModels()
        modelsOptions = modelsCache['options']
        if ("scan-error" in modelsCache) {
            // let previewPane = document.getElementById('tab-content-wrapper')
            let previewPane = document.getElementById('preview')
            previewPane.style.background="red"
            previewPane.style.textAlign="center"
            previewPane.innerHTML = '<H1>🔥Malware alert!🔥</H1><h2>The file <i>' + modelsCache['scan-error'] + '</i> in your <tt>models/stable-diffusion</tt> folder is probably malware infected.</h2><h2>Please delete this file from the folder before proceeding!</h2>After deleting the file, reload this page.<br><br><button onClick="window.location.reload();">Reload Page</button>'
            makeImageBtn.disabled = true
        }

        /* This code should no longer be needed. Commenting out for now, will cleanup later.
        const sd_model_setting_key = "stable_diffusion_model"
        const vae_model_setting_key = "vae_model"
        const hypernetwork_model_key = "hypernetwork_model"

        const stableDiffusionOptions = modelsOptions['stable-diffusion']
        const vaeOptions = modelsOptions['vae']
        const hypernetworkOptions = modelsOptions['hypernetwork']

        // TODO: set default for model here too
        SETTINGS[sd_model_setting_key].default = stableDiffusionOptions[0]
        if (getSetting(sd_model_setting_key) == '' || SETTINGS[sd_model_setting_key].value == '') {
            setSetting(sd_model_setting_key, stableDiffusionOptions[0])
        }
        */

        // notify ModelDropdown objects to refresh
        document.dispatchEvent(new Event('refreshModels'))
    } catch (e) {
        console.log('get models error', e)
    }
}

// reload models button
document.querySelector('#reload-models').addEventListener('click', getModels)