//#region notify
// Вызывает указанную fn даже если нет чего показать в уведомлении
function notify_answer( answer, fn = null ) {
    let notify_html = [];
    let max_timeout = 0;
    if ( answer ) {
        const keys = Object.keys( answer );
        keys.forEach( function( key ) {
            if ( answer[key] && answer[key]["notify_html"] ) {
                const notify = {
                    "notify_timeout": 5000,
                    "notify_html": answer[key]["notify_html"],
                    "notify_class": key,
                }
                if ( answer[key]["notify_time"] ) {
                    notify.notify_timeout = answer[key]["notify_time"];
                }
                notify_html.push( notify );

                // Обновляем максимальное время таймаута
                if (notify.notify_timeout > max_timeout) {
                    max_timeout = notify.notify_timeout;
                }
            }
        } );
    }
    // Только эти классы попадут в лог
    // стили есть еще для: notify
    const notifyClasses = ["result", "error", "warning"];
    notify_html.forEach( function( el ) {
        if ( notifyClasses.includes( el.notify_class ) ) {
            // Запись в лог
            const hist = document.body.querySelector( ":scope > .log .hist" );
            const date = new Date().toLocaleString( undefined, { hour12: false } );
            const at_end = hist.scrollHeight - hist.scrollTop === hist.clientHeight;
            hist.insertAdjacentHTML(
                "beforeend",
                `<div class="${el.notify_class}">
                    <div class=marker></div>
                    <div class=date>${date}</div>
                    <div class=message>${el.notify_html}</div>
                </div>`
            );
            log_marker_rm_control( hist.lastElementChild, hist );

            // Прокрутить вниз если докручено до низу
            if ( at_end ) {
                hist.scrollTop = hist.scrollHeight;
            }

            // Сохранить чтобы восстанавливать после перезагрузки страницы
            localStorage.setItem( "log", hist.innerHTML );

            // Огонек на кнопке лога
            const button = document.querySelector( "body > header .menu .button.log" );
            button.classList.remove( ... notifyClasses );
            setTimeout( function() {
                button.classList.add( el.notify_class );
            }, 1000 );
        }
        // Всплывающее уведомление
        if ( el.notify_timeout > 0 ) {
            var bulb = document.createElement( "div" );
            bulb.innerHTML = el.notify_html;
            bulb.classList.add( "info", el.notify_class );
            document.querySelector( ".notify" ).appendChild( bulb );
            var h = bulb.offsetHeight;
            // Чтобы анимировать схлопывание
            bulb.setAttribute( "style", `height:${h}px` );
                setTimeout( function() {
                    bulb.classList.add( "timeout" );
                }, el.notify_timeout );
        }
    } );
    // Вызвать функцию
    setTimeout( function() {
        if ( fn ) {
            // Возвращаем answer потому что может пригодиться
            fn( answer );
        }
    }, max_timeout );
}

// Используется в notify_answer, поэтому глобальна
function log_marker_rm_control( marker, hist ) {
    marker.addEventListener( "dblclick", function ( event ) {
        event.target.parentElement.remove();
        localStorage.setItem( "log", hist.innerHTML );
    } );
}
//#endregion

// Шаблон admin добавляет глобальную js-переменную
// в которой содержится текущая локаль и переводы,
// загруженные из файлов .cms/lang/...
// Поэтому ими можно пользоваться и на стороне админки.
// module: "admin.mod.php" or "pages.mod.php" etc
function _( str, module ) {
    if ( cms.locale && cms.lang && cms.lang[module] && cms.lang[module][cms.locale] && cms.lang[module][cms.locale][str] ) {
        return cms.lang[module][cms.locale][str];
    } else {
        api( { fn: "no_translation", str: str, module: module } );
        return str;
    }
}

//#region api
// Передаваемые на сервер данные упаковываются как положено
// и можно передавать даже массивы.
// Массивы вложенные в массивы воспринимаются как объекты
// и кодируются в JSON.
// После того, как сервер вернет ответ, вызывается функция result_fn
// И ответ передается ей в качестве параметра.
// Ошибки возвращенные с сервера, не считаются ошибками в контексте данной функции.
// Ошибки для данной функции - это сетевые ошибки или невозможность распарсить json.
// Только в этом случае вызывается error_fn - она необходима для повторов запросов
// или чего-то еще, если не отработал API.
function api( data, result_fn, error_fn, ajax_progress, ajax_error, ajax_abort ) {
    const ajax = new XMLHttpRequest();

    // addEventListener("progress") работает на ajax.upload для загрузки и на ajax для скачивания.
    if ( ajax_progress ) { ajax.upload.addEventListener( "progress", ajax_progress ); }
    if ( ajax_error    ) { ajax.addEventListener( "error",    ajax_error ); }
    if ( ajax_abort    ) { ajax.addEventListener( "abort",    ajax_abort ); }
        
    ajax.addEventListener( "load", function( event ) {
        let answer;
        try {
            answer = JSON.parse( event.target.responseText );
        } catch ( error ) {
            // Ошибка парсинга json - прокидываем в error_fn чтобы разобралась.
            // Ошибка 413 (слишком большой файл при аплоаде) тоже приведет сюда.
            if ( error_fn ) {
                error_fn( event );
            } else {
                console.table( data );
                console.table( {
                    type: typeof event.target.responseText,
                    value: event.target.responseText,
                } );
                console.table( error );
                console.log( "-----------------------------------------------" );
            }
            return;
        }
        if ( result_fn ) {
            result_fn( answer );
        }
    } );

    ajax.open( "POST", cms.api, cms.async_api );

    // Проверка на FormData
    if ( data instanceof FormData ) {
        ajax.send( data );  // FormData отправляется напрямую
    } else {
        const formData = new FormData();
        buildFormData( formData, data );
        ajax.send( formData );
    }

    // По умолчанию запросы отправляются асинхронно,
    // но если нужно дождаться ответа и затем изменить полученные данные,
    // то перед вызовом обновляющих функций нужно дописать строчку
    // cms.async_api = false;
    cms.async_api = true;
}

function buildFormData( formData, data, parentKey ) {
    if ( data && typeof data === 'object' && ! ( data instanceof Date ) && ! ( data instanceof File ) ) {
        if ( data instanceof Array ) {
            // Обработка обычного массива
            data.forEach( function ( item, index ) {
                buildFormData( formData, item, parentKey ? `${parentKey}[${index}]` : index );
            } );
        } else {
            // Обработка объекта
            Object.keys( data ).forEach( function ( key ) {
                buildFormData( formData, data[key], parentKey ? `${parentKey}[${key}]` : key );
            } );
        }
    } else {
        const value = data == null ? '' : data;
        // Для обычных данных и файлов
        formData.append( parentKey, value );
    }
}
//#endregion api

//#region Codemirror
// Создает и подключает Codemirror к элементу
function codemirror_connect( textarea, options = {} ) {

    // Видимо еще не загрузился
    if ( typeof CodeMirror !== "function" ) {
        return false;
    }

    var default_options = {
        mode: "application/x-httpd-php",
        styleActiveLine:   true,
        lineNumbers:       true,
        lineWrapping:      true,
        autoCloseBrackets: true,
        smartIndent:       true,
        indentUnit:        4,
        tabSize:           4,
        matchBrackets:     true,
        foldGutter: true,
        gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
        autoCloseTags: {
            whenClosing: true,
            whenOpening: true,
            indentTags:  [ "div", "ul", "ol", "script", "style" ],
        },
        phrases: {
            "Search:":                              _( "codemirror_search", "admin.mod.php" ),
            "(Use /re/ syntax for regexp search)" : _( "codemirror_re", "admin.mod.php" ),
            "Replace all:":                         _( "codemirror_replace_all", "admin.mod.php" ),
            "With:":                                _( "codemirror_replace_with", "admin.mod.php" ),
            "Replace:":                             _( "codemirror_replace_replace", "admin.mod.php" ),
            "Replace?":                             _( "codemirror_replace_confirm", "admin.mod.php" ),
            "Yes":                                  _( "codemirror_yes", "admin.mod.php" ),
            "No":                                   _( "codemirror_no", "admin.mod.php" ),
            "All":                                  _( "codemirror_all", "admin.mod.php" ),
            "Stop":                                 _( "codemirror_stop", "admin.mod.php" ),
        },
        extraKeys: {
            "Ctrl-Space": "autocomplete",
            "Ctrl-D": false,
            "Ctrl-U": false,
        }
    }

    options = Object.assign( default_options, options );
    var cm = CodeMirror.fromTextArea( textarea, options );

    if ( cm.showHint ) {
        cm.on( "keydown", function( editor, event ) {
            if ( event.ctrlKey == true ) { return } // Ctrl+S call Hint
            var isAlphaKey = /^[a-zA-Z]$/.test( event.key );
            if ( cm.state.completionActive && isAlphaKey ) {
                return;
            }

            // Prevent autocompletion in string literals or comments
            var cursor = cm.getCursor();
            var token = cm.getTokenAt( cursor );
            if ( token.type === "string" || token.type === "comment" ) {
                return;
            }
            
            var lineBeforeCursor = cm.doc.getLine( cursor.line );
            if ( typeof lineBeforeCursor !== "string" ) {
                return;
            }
            lineBeforeCursor = lineBeforeCursor.substring( 0, cursor.ch );

            // disable autoclose tag before text
            var charAfterCursor  = cm.doc.getLine( cursor.line );
            charAfterCursor = charAfterCursor.substring( cursor.ch, cursor.ch + 1 );
            cm.options.autoCloseTags.dontCloseTags = null;
            if ( charAfterCursor.match( /\S/ ) && charAfterCursor != "<" ) {
                if ( lineBeforeCursor.match( /<[^>]+$/ ) ) {
                    var tag = lineBeforeCursor.match( /<(\w+)\b[^>]*$/ );
                    if ( tag ) {
                        tag = tag[1];
                        cm.options.autoCloseTags.dontCloseTags = [tag];
                    }
                }
            }
            
            var m = CodeMirror.innerMode( cm.getMode(), token.state );
            var innerMode = m.mode.name;
            var shouldAutocomplete;
            if ( innerMode === "html" || innerMode === "xml" ) {
                shouldAutocomplete = event.key === "<" ||
                    event.key === "/" && token.type === "tag" ||
                    isAlphaKey && token.type === "tag" ||
                    isAlphaKey && token.type === "attribute" ||
                    token.string === "=" && token.state.htmlState && token.state.htmlState.tagName;
            } else if ( innerMode === "css" ) {
                shouldAutocomplete = isAlphaKey ||
                    event.key === ":" ||
                    event.key === " " && /:\s+$/.test( lineBeforeCursor );
            } else if ( innerMode === "javascript" ) {
                shouldAutocomplete = isAlphaKey || event.key === ".";
            } else if ( innerMode === "clike" && cm.options.mode === "php" ) {
                shouldAutocomplete = token.type === "keyword" || token.type === "variable";
            }
            if ( shouldAutocomplete ) {
                cm.showHint( { completeSingle: false } );
            }
        } );
    }

    // Replace < with &lt; in code context
    // https://stackoverflow.com/a/36388061/20443861
    cm.on( "inputRead", function( cm, event ) {
        if ( event.origin == "paste" ) {
            if ( cm.paste_context == "code" ) {
                var text = event.text.join( "\n" ); // pasted string
                // Заменять только два && потому что иногда нужно копировать строки
                // и одиночный & постоянно заменяется в &amp;
                // А даже если замены одиночного не произойдет, то отобразится все правильно.
                var new_text = text.replaceAll( /&&/g, "&amp;&amp;" );
                new_text = new_text.replaceAll( /</g, "&lt;" );
                //new_text = new_text.replaceAll( /&&/g, "&amp;&amp;" );
                cm.execCommand( "undo" );
                cm.replaceSelection( new_text );
            }
        }
    } );
    cm.on( "paste", async function( editor, event ) {
        var cursor = cm.getCursor();
        var token = cm.getTokenAt( cursor );
        if ( token && token.state && token.state.html && token.state.html.htmlState && token.state.html.htmlState.context && token.state.html.htmlState.context.tagName == "code" ) {
            cm.paste_context = "code";
        } else {
            cm.paste_context = "";
        }
    } );

    if ( options["cursor_save_to"] ) {
        [ "cursorActivity", "scroll" ].forEach( function( event ) {
            cm.on( event, function() {
                let cursor = cm.getCursor();
                let scroll = cm.getScrollInfo();
                localStorage.setItem( options["cursor_save_to"], JSON.stringify( { 
                    line:cursor.line,
                    ch:cursor.ch,
                    left: scroll.left,
                    top: scroll.top
                } ) );
            } );
        } );
    }

    cm.restore_cursor = function () {
        if ( options["cursor_save_to"] ) {
            // Нужно дать время отработать всем скриптам редактора.
            // Иначе курсор становится на букве и глюки с его перемещением.
            setTimeout( function() {
                cm.refresh();
                // restore scroll and cursor position
                let cursor = localStorage.getItem( options["cursor_save_to"] );
                if ( cursor ) {
                    cursor = JSON.parse( cursor );
                    cm.scrollTo( cursor.left, cursor.top );
                    cm.setCursor( { line:cursor.line, ch:cursor.ch } );
                    cm.scrollIntoView( { line:cursor.line, ch:cursor.ch } );
                }
            }, 100 );
        }
    }

    return cm;

}

// element - родительский элемент к которому будет добавлен обработчик keydown
// combo - комбинация клавишь типа CtrlAltShiftKeyS
// target - элемент, которому будем посылать событие click
// event - имя события, "click"
function bind_key( element, combo, target, event ) {
    if ( ! element.binded_keys ) element.binded_keys = {};
    element.binded_keys[combo] = { target: target, event: event };
    element.addEventListener( "keydown", key_router );
}
// Эта функция привязывается к родительскому элементу,
// определяет какая комбинация была нажата (CtrlAltShiftKeyS)
// и отправляет указанному объекту событие "click"
function key_router( e ) {
    var combo;
    if ( e.ctrlKey )
        combo  = "Ctrl";
    if ( e.altKey )
        combo += "Alt";
    if ( e.shiftKey )
        combo += "Shift";
    if ( e.code )
        combo += e.code;
    else {
        var key_map = __( "key_map", "admin.mod.php" );
        if ( typeof key_map === "object" && key_map[e.key] )
            combo += key_map[e.key];
        else
            combo += e.key;
    }
    if ( typeof this.binded_keys[combo] == "object" ) {
        e.preventDefault();
        event = new Event( this.binded_keys[combo]["event"] );
        this.binded_keys[combo]["target"].dispatchEvent( event );
    }
}
//#endregion Codemirror

function short_title( str ) {
    // чтобы работать с символами, а не с кодовыми точками
    let s = Array.from( str );
    if ( s.length <= 25 ) return str;
    return s.slice(0, 10).join("") + " ... " + s.slice(-10).join("");
}

// const search_options = {
//     widget: pages_sec.querySelector( ":scope > .header .search.widget" ),
//     api_fn: "get_pages_list",
//     answer_fn: set_pages_list,
// }
// const search = search_widget_controls( search_options ); //: { widget,search_run_fn }
function search_widget_controls( options ) {
    const input = options.widget.querySelector( `input` );
    input.addEventListener( "keydown", function( event ) {
        if ( event.code === "Enter" ) {
            // сохраняем событие целиком чтобы ниже считать Shift, Ctrl, Alt
            input.setAttribute( "data-ctrl", event.ctrlKey );
            input.setAttribute( "data-shift", event.shiftKey );
            input.setAttribute( "data-alt", event.altKey );
            input.setAttribute( "data-offset", 0 );
            search();
        } else {
            input.setAttribute( "data-ctrl", false );
            input.setAttribute( "data-shift", false );
            input.setAttribute( "data-alt", false );
            input.setAttribute( "data-offset", 0 );
        }
    } );
    
    const search_btn = options.widget.querySelector( `.send` );
    search_btn.addEventListener( "click", function( event ) {
        // клик по кнопке не дает Ctrl, Shift, Alt, запишем это
        input.setAttribute( "data-ctrl", false );
        input.setAttribute( "data-shift", false );
        input.setAttribute( "data-alt", false );
        input.setAttribute( "data-offset", 0 );
        search();
        input.focus();
    } );

    const reset_btn = options.widget.querySelector( `.reset` );
    reset_btn.addEventListener( "click", function( event ) {
        input.value = "";
        search_btn.click();
    } );

    function search() {
        const data = {
            fn: options.api_fn,
            search: input.value,
            Ctrl: input.getAttribute( "data-ctrl" ),
            Shift: input.getAttribute( "data-shift" ),
            Alt: input.getAttribute( "data-alt" ),
            offset: input.getAttribute( "data-offset" ),
        };
        api( data, options.answer_fn );
    }

    return {
        widget: options.widget,
       search_run_fn: search,
    }
}

// const pager_options = {
//     footer: pages_sec.querySelector( ":scope > .footer" ),
//     cookie: "pages_pager",
//     api_fn: "get_pages_list",
//     answer_fn: set_pages_list,
//     search: { search_el,search_run_fn },
//     loaded: 0,
//     count: 0,
// }
// create_pager( pager_options );
function create_pager( options ) {
    const pager_el  = options.footer.querySelector( ".pager" );
    const count_el  = options.footer.querySelector( ".count" );
    const loaded_el = options.footer.querySelector( ".loaded" );

    if ( options.loaded ) loaded_el.value = options.loaded;
    if ( options.count ) count_el.innerText = options.count;

    let offset_el;
    if ( options.search ) {
        offset_el = options.search.widget.querySelector( `input` );
    } else {
        offset_el = loaded_el;
    }
    const offset = offset_el.getAttribute( "data-offset" );

    let count  = parseInt( count_el.innerText );
    if ( count === 0 ) { count++; }
    const per_page = parseInt( get_cookie( options.cookie ) ) || 0;

    let buttons = 0;
    if ( per_page > 0 ) {
        buttons = Math.ceil( count / per_page );
    }
    pager_el.innerHTML = "";

    if ( buttons > 1 ) {
        for ( let i = 1; i <= buttons; i++ ) {
            const button = document.createElement( "div" );
            button.classList.add( "button" );
            button.setAttribute( "data-offset", ( i - 1 ) * per_page );
            const span = document.createElement( "span" );
            span.innerText = i;
            button.appendChild( span );
            pager_el.appendChild( button );
        }

        pager_el.querySelector( `[data-offset="${offset}"]` ).classList.add( "active" );
        pager_el.childNodes.forEach( function( el ) {
            // Клики по кнопкам пейджера
            el.addEventListener( "click", function( e ) {
                el.classList.add( "active" );
                const offset = el.getAttribute( "data-offset" );
                // Если передан search, то нужно это учитывать
                if ( options.search ) {
                    const input = options.search.widget.querySelector( `input` );
                    input.setAttribute( "data-offset", offset );
                    options.search.search_run_fn();
                } else {
                    api( { fn: options.api_fn, offset: offset }, options.answer_fn );
                }
            } );
        } );

        // Прокрутка при перетаскивании мышкой
        pager_el.onmousedown = function( event ) {
            const startX = event.pageX;
            const scrollLeft = pager_el.scrollLeft;

            document.onmousemove = function( moveEvent ) {
                const walk = moveEvent.pageX - startX;
                pager_el.scrollLeft = scrollLeft - walk;
            };

            document.onmouseup = function() {
                document.onmousemove = null;
                document.onmouseup = null;
            };
        };
        
        // Отключить стандартное перетаскивание браузера
        pager_el.ondragstart = function() { return false; };
    }

    if ( ! loaded_el.keydown_connected ) {
        loaded_el.addEventListener( "keydown", function( event ) {
            if ( event.code === "Enter" ) {
                const count = loaded_el.value;
                set_cookie_expires( options.cookie, count );
                // Если передан search, то нужно это учитывать
                if ( options.search ) {
                    const input = options.search.widget.querySelector( `input` );
                    input.setAttribute( "data-offset", 0 );
                    options.search.search_run_fn();
                } else {
                    api( { fn: options.api_fn, offset: 0 }, options.answer_fn );
                }
            }
        } );
        loaded_el.keydown_connected = true;
    }
}

function copy_to_clipboard( text ) {
    const tmp = document.createElement( "textarea" );
    tmp.value = text;
    document.body.appendChild( tmp );
    tmp.select();
    const r = document.execCommand( "copy" );
    tmp.remove();
    return r;
}

//#region listeners
// Проверить как это работает, описать и может быть пользоваться.
const listeners = new WeakMap();

function addListener(element, event, handler) {
  if (!listeners.has(element)) {
    listeners.set(element, new Map());
  }
  
  const elementListeners = listeners.get(element);
  
  if (!elementListeners.has(event)) {
    elementListeners.set(event, new Set());
  }
  
  const eventHandlers = elementListeners.get(event);
  
  if (!eventHandlers.has(handler)) {
    eventHandlers.add(handler);
    element.addEventListener(event, handler);
  }
}

function removeListener(element, event, handler) {
  const elementListeners = listeners.get(element);
  
  if (elementListeners && elementListeners.has(event)) {
    const eventHandlers = elementListeners.get(event);
    
    if (eventHandlers.has(handler)) {
      eventHandlers.delete(handler);
      element.removeEventListener(event, handler);
    }
  }
}
//#endregion listeners