/**
 * -------------------------------------------------------------------------
 *
 * The frontend scripts to serve the built-in constructor.
 *
 * -------------------------------------------------------------------------
 *
 * @package    MimimiFramework
 * @subpackage Examples / Static Pages only
 * @license    GPL-2.0
 *             https://opensource.org/license/gpl-2-0/
 * @copyright  2022 MiMiMi Community
 *             https://mimimi.software/
 *
 * -------------------------------------------------------------------------
 */

    let constructorMainBlock      = null,
        constructorActiveBlock    = null,
        constructorModalDialog    = null,
        constructorResultBlock    = null,
        constructorResultWrapping = false;

    const CONSTRUCTOR_ACTIVE_BLOCK_CLASSNAME     = 'fragment-active',
          CONSTRUCTOR_BLOCK_HOVER_CLASSNAME      = 'fragment-hover',
          CONSTRUCTOR_TEXT_HOVER_CLASSNAME       = 'text-hover',
          CONSTRUCTOR_DROP_HOVER_CLASSNAME       = 'drop-hover',
          CONSTRUCTOR_DROP_HOVER_AFTER_CLASSNAME = 'drop-hover-after',
          CONSTRUCTOR_WHO_AM_I_ATTRIBUTE         = 'data-who-am-i';

    /**
     * ---------------------------------------------------------------------
     *
     * To check if the construction area is currently inert.
     *
     * ---------------------------------------------------------------------
     */

    const isThisInert = ( ) => {
        return constructorModalDialog && constructorModalDialog.open;
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To check if this block is a SECTION-like node.
     *
     * ---------------------------------------------------------------------
     */

    const isThisSection = ( target ) => {
        const name = target.tagName;
        return name == 'MAIN'
            || name == 'ARTICLE'
            || name == 'SECTION'
            || name == 'ASIDE';
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To check if this block is a text.
     *
     * ---------------------------------------------------------------------
     */

    const isThisEditable = ( target ) => {
        const text = target.innerText;
        if ( text != '' ) {
            const name = target.tagName;
            return name == 'h1'
                || name == 'H2'
                || name == 'H3'
                || name == 'H4'
                || name == 'H5'
                || name == 'H6'
                || name == 'P'
                || name == 'FIGCAPTION'
                || name == 'PRE'
                || target.childElementCount == 0;
        }
        return false;
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To make sure that the active block exists.
     *
     * ---------------------------------------------------------------------
     */

    const needActiveBlock = ( ) => {
        if ( constructorMainBlock ) {
            if ( ! constructorActiveBlock ) {
                constructorActiveBlock = constructorMainBlock;
            }
            drawBlockMark ( constructorActiveBlock, CONSTRUCTOR_ACTIVE_BLOCK_CLASSNAME );
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To select a block above the currently active block.
     *
     * ---------------------------------------------------------------------
     */

    const selectBlockAbove = ( ) => {
        needActiveBlock ( );
        if ( constructorActiveBlock ) {
            if ( constructorActiveBlock != constructorMainBlock ) {
                const node = constructorActiveBlock.previousElementSibling;
                constructorActiveBlock = node ? node
                                              : constructorActiveBlock.parentNode;
            }
            constructorActiveBlock.scrollIntoView ({ behavior: 'smooth', block: 'start' });
        }
        drawBlockMark ( constructorActiveBlock, CONSTRUCTOR_ACTIVE_BLOCK_CLASSNAME );
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To select a block below the currently active block.
     *
     * ---------------------------------------------------------------------
     */

    const selectBlockBelow = ( ) => {
        needActiveBlock ( );
        if ( constructorActiveBlock ) {
            let node    = null,
                isFirst = true,
                initial = constructorActiveBlock;
            do {
                if ( isFirst ) {
                    if ( isThisSection ( constructorActiveBlock ) ) {
                        node = constructorActiveBlock.firstElementChild;
                    }
                }
                if ( ! node ) {
                    node = constructorActiveBlock.nextElementSibling;
                    isFirst = false;
                }
                constructorActiveBlock = node ? node
                                              : constructorActiveBlock.parentNode;
            } while ( ! node
                     && constructorActiveBlock != constructorMainBlock );
            if ( ! node ) {
                constructorActiveBlock = initial;
            }
            constructorActiveBlock.scrollIntoView ({ behavior: 'smooth', block: 'start' });
        }
        drawBlockMark ( constructorActiveBlock, CONSTRUCTOR_ACTIVE_BLOCK_CLASSNAME );
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To move the active block in front of the block above it.
     *
     * ---------------------------------------------------------------------
     */

    const moveBlockAbove = ( ) => {
        needActiveBlock ( );
        if ( constructorActiveBlock ) {
            const me = constructorActiveBlock;
            selectBlockAbove ( );
            if ( me != constructorActiveBlock ) {
                if ( constructorActiveBlock != constructorMainBlock ) {
                    const html = me.outerHTML;
                    insertHtml ( html, constructorActiveBlock, true, false );
                    selectBlockAbove ( );
                    me.remove ( );
                }
            }
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To move the active block after of the block below it.
     *
     * ---------------------------------------------------------------------
     */

    const moveBlockBelow = ( ) => {
        needActiveBlock ( );
        if ( constructorActiveBlock ) {
            const me = constructorActiveBlock;
            selectBlockBelow ( );
            if ( me != constructorActiveBlock ) {
                if ( me != constructorMainBlock ) {
                    const html = me.outerHTML;
                    insertHtml ( html, constructorActiveBlock, false );
                    selectBlockBelow ( );
                    me.remove ( );
                }
            }
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To delete the active block.
     *
     * ---------------------------------------------------------------------
     */

    const deleteActiveBlock = ( ) => {
        needActiveBlock ( );
        if ( constructorActiveBlock ) {
            drawBlockMark ( constructorActiveBlock, CONSTRUCTOR_ACTIVE_BLOCK_CLASSNAME );
            setTimeout ( ( ) => {
                if ( constructorActiveBlock == constructorMainBlock ) {
                    if ( window.confirm ( 'Are you sure you want to clear the entire page?' ) ) {
                        constructorActiveBlock.innerHTML = '';
                    }
                    return;
                }
                if ( isThisSection ( constructorActiveBlock ) ) {
                    if ( constructorActiveBlock.hasChildNodes ( ) ) {
                        if ( window.confirm ( 'Are you sure you want to clear this multi-fragment block?' ) ) {
                            constructorActiveBlock.innerHTML = '';
                        }
                        return;
                    }
                }
                if ( window.confirm ( 'Are you sure you want to delete this block?' ) ) {
                    const prev = constructorActiveBlock;
                    selectBlockBelow ( );
                    if ( prev == constructorActiveBlock ) {
                        selectBlockAbove ( );
                    }
                    prev.remove ( );
                }
            }, 10 );
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To edit the active block as HTML Source.
     *
     * ---------------------------------------------------------------------
     */

    const editActiveBlock = ( ) => {
        needActiveBlock ( );
        if ( constructorActiveBlock ) {
            drawBlockMark ( constructorActiveBlock, CONSTRUCTOR_ACTIVE_BLOCK_CLASSNAME );
            if ( constructorModalDialog ) {
                setTimeout ( ( ) => {
                    const textarea = constructorModalDialog.querySelector ( 'textarea' );
                    if ( textarea ) {
                        constructorResultWrapping = constructorActiveBlock != constructorMainBlock;
                        textarea.value            = getRawHtml ( constructorActiveBlock, constructorResultWrapping );
                        constructorResultBlock    = constructorActiveBlock;
                        constructorModalDialog.showModal ( );
                    }
                }, 10 );
            }
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To edit a text block as HTML Source.
     *
     * ---------------------------------------------------------------------
     */

    const editTextBlock = ( target ) => {
        if ( constructorModalDialog ) {
            setTimeout ( ( ) => {
                const textarea = constructorModalDialog.querySelector ( 'textarea' );
                if ( textarea ) {
                    constructorResultWrapping = false;
                    textarea.value            = getRawHtml ( target, constructorResultWrapping );
                    constructorResultBlock    = target;
                    constructorModalDialog.showModal ( );
                }
            }, 10 );
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To accept changes made by an admin after editing a block in the modal
     * dialog.
     *
     * ---------------------------------------------------------------------
     */

    const updateConstructionBlock = ( btn ) => {
        event.preventDefault ( );
        if ( constructorResultBlock ) {
            if ( constructorModalDialog ) {
                const textarea = constructorModalDialog.querySelector ( 'textarea' );
                if ( textarea ) {
                    const html = prepareHtml ( textarea.value );
                    textarea.value = '';
                    if ( constructorResultBlock == constructorMainBlock
                    || ! constructorResultWrapping ) {
                        constructorResultBlock.innerHTML = html;
                    } else {
                        constructorActiveBlock = constructorResultBlock;
                        insertHtml ( html, constructorActiveBlock, true, false );
                        selectBlockAbove ( );
                        constructorResultBlock.remove ( );
                    }
                }
                constructorModalDialog.close ( );
            }
            constructorResultBlock    = null;
            constructorResultWrapping = false;
            needActiveBlock ( );
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To insert the HTML fragment before or after a target block.
     *
     * ---------------------------------------------------------------------
     */

    const insertHtml = ( html, target, before, into = true ) => {
        const node = document.createElement ( 'div' );
        if ( into
        &&   isThisSection ( target ) ) {
            if ( before ) target.insertBefore ( node, target.firstChild );
            else          target.appendChild  ( node                    );
        } else {
            if ( before ) target.parentNode.insertBefore ( node, target                    );
            else          target.parentNode.insertBefore ( node, target.nextElementSibling );
        }
        node.outerHTML = html;
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To convert raw HTML code to prepared one. Please note that the RegExp
     * patterns used below are the same as those in the helperEchoEditableBody()
     * routine declared in the "static.pages.only/Helper/Helper.php" module.
     * These patterns are used to prevent certain parts of the content from
     * being automatically executed or shown while it is being edited.
     *
     * ---------------------------------------------------------------------
     */

    const prepareHtml = ( raw ) => {
        let html,
            next = raw.replace ( /<!--.*?-->/g, '' )
                      .replace ( /<!--.*$/,     '' )
                      .replace ( /<\?.*?\?>/g,  '' )
                      .replace ( /<\?.*$/,      '' )
                      .replace ( /(<\/?)(safe_)?(!doctype|html|head|body|script|noscript|template|meta|link|iframe|form|math|audio|video|object|embed|applet|frame|noframes)([\s=\/>])/gi, '$1safe_$2$3$4' )
                      .replace ( /<\/([a-z][^\s=\/>]*)[^>]*?>/gi, '</$1>' );
        do {
            html = next;
            next = html.replace ( /(<[a-z][^>]*?[\s="'\/])((on[a-z0-9_]*|href)[\s=\/>])/gi, '$1safe_$2' );
        } while ( next != html );
        return html;
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To convert prepared HTML code to raw. Please note that the RegExp
     * patterns used below are some inversion of the patterns from the
     * function prepareHtml() declared above.
     *
     * ---------------------------------------------------------------------
     */

    const getRawHtml = ( targetOrHtml, withWrapper = false ) => {
        drawTextMark   ( null                                     );
        drawBlockMark  ( null, CONSTRUCTOR_BLOCK_HOVER_CLASSNAME  );
        drawBlockMark  ( null, CONSTRUCTOR_ACTIVE_BLOCK_CLASSNAME );
        drawDropMark   ( null                                     );
        drawWhoAmIMark ( null                                     );
        const html = typeof targetOrHtml == 'object' ? withWrapper ? targetOrHtml.outerHTML
                                                                   : targetOrHtml.innerHTML.replace ( /(^\s+|\s+$)/g, '' )
                                                     : targetOrHtml;
        return html.replace ( /(<\/?)safe_/gi,                                        '$1'   )
                   .replace ( /<\/(!?doctype|meta|link|object|embed|applet)>/gi,      ''     )
                   .replace ( /(<[a-z][^>]*?[\s="'\/])safe_((on[a-z0-9_]*|href)[\s=\/>])/gi, '$1$2' );
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To define the drag's data.
     *
     * ---------------------------------------------------------------------
     */

    const dragstartFragment = ( event ) => {
        let template = event.target.nextElementSibling;
        while ( template && template.tagName != 'TEXTAREA' ) {
            template = template.nextElementSibling;
        }
        if ( template ) {
            const html = prepareHtml ( template.value  );
                  event.dataTransfer.setData ( 'text/html', html );
                  event.dataTransfer.dropEffect = 'copy';
        } else {
            event.preventDefault ( );
            alert ( 'Sorry, no dragging content for this fragment now!' );
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To initialize a sidebar containing draggable fragments.
     *
     * ---------------------------------------------------------------------
     */

    const initFragments = ( ) => {
        const fragments = document.querySelectorAll ( '.form .topbar + .aside li > a' );
        for ( let i = 0; i < fragments.length; i++ ) {
            fragments[ i ].removeAttribute ( 'href' );
            fragments[ i ].setAttribute ( 'draggable', 'true' );
            fragments[ i ].addEventListener ( 'dragstart', dragstartFragment );
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To draw the drop target marker.
     *
     * ---------------------------------------------------------------------
     */

    const drawDropMark = ( target, classname = '' ) => {
        const prev1 = CONSTRUCTOR_DROP_HOVER_CLASSNAME,
              prev2 = CONSTRUCTOR_DROP_HOVER_AFTER_CLASSNAME,
              nodes     = document.querySelectorAll ( '.' + prev1 + ', ' +
                                                      '.' + prev2 );
        for ( let i = 0; i < nodes.length; i++ ) {
            nodes[ i ].classList.remove ( prev1 );
            nodes[ i ].classList.remove ( prev2 );
            if ( nodes[ i ].classList.length == 0 ) {
                nodes[ i ].removeAttribute ( 'class' );
            }
        }
        if ( target ) {
            if ( classname ) {
                drawWhoAmIMark ( target, target.tagName );
                target.classList.add ( classname );
            }
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To draw the TEXT marker.
     *
     * ---------------------------------------------------------------------
     */

    const drawTextMark = ( target, classname = '' ) => {
        const prev  = CONSTRUCTOR_TEXT_HOVER_CLASSNAME,
              nodes = document.querySelectorAll ( '.' + prev );
        for ( let i = 0; i < nodes.length; i++ ) {
            nodes[ i ].classList.remove ( prev );
            if ( nodes[ i ].classList.length == 0 ) {
                nodes[ i ].removeAttribute ( 'class' );
            }
        }
        if ( target ) {
            if ( classname ) {
                target.classList.add ( classname );
            }
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To draw the mouse target marker.
     *
     * ---------------------------------------------------------------------
     */

    const drawBlockMark = ( target, classname ) => {
        if ( classname ) {
            const nodes = document.querySelectorAll ( '.' + classname );
            for ( let i = 0; i < nodes.length; i++ ) {
                nodes[ i ].classList.remove ( classname );
                if ( nodes[ i ].classList.length == 0 ) {
                    nodes[ i ].removeAttribute ( 'class' );
                }
            }
            if ( target ) {
                drawWhoAmIMark ( target, target.tagName );
                target.classList.add ( classname );
            }
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To draw the WhoAmI marker.
     *
     * ---------------------------------------------------------------------
     */

    const drawWhoAmIMark = ( target, value = '' ) => {
        const nodes = document.querySelectorAll ( '*[' + CONSTRUCTOR_WHO_AM_I_ATTRIBUTE + ']' );
        for ( let i = 0; i < nodes.length; i++ ) {
            nodes[ i ].removeAttribute ( CONSTRUCTOR_WHO_AM_I_ATTRIBUTE );
        }
        if ( target ) {
            if ( value ) {
                target.setAttribute ( CONSTRUCTOR_WHO_AM_I_ATTRIBUTE, value );
            }
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To find the drop's container.
     *
     * ---------------------------------------------------------------------
     */

    const getDropContainer = ( target ) => {
        if ( ! isThisSection ( target ) ) {
            while ( target.parentNode
               && ! isThisSection ( target.parentNode ) ) {
                target = target.parentNode;
            }
        }
        return target;
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To compute the mouse Y offset over the drop's container.
     *
     * ---------------------------------------------------------------------
     */

    const computeDropContainerY = ( container, event ) => {
        let y      = event.offsetY,
            target = event.target;
        while ( container && target && container != target ) {
            y += target.offsetTop;
            target = target.parentNode;
        }
        return y;
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To define the drop's data while the mouse is still moving.
     *
     * ---------------------------------------------------------------------
     */

    const dragoverConstruction = ( event ) => {
        event.preventDefault ( );
        event.dataTransfer.dropEffect = 'copyMove';
        const container = getDropContainer ( event.target );
        if ( container ) {
            const half   = container.clientHeight / 2,
                  y      = computeDropContainerY ( container, event ),
                  before = y <= half;
            drawDropMark ( container, before ? CONSTRUCTOR_DROP_HOVER_CLASSNAME
                                             : CONSTRUCTOR_DROP_HOVER_AFTER_CLASSNAME );
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To define the drop action when the mouse is released.
     *
     * ---------------------------------------------------------------------
     */

    const dropConstruction = ( event ) => {
        event.preventDefault ( );
        drawDropMark ( null );
        const container = getDropContainer ( event.target );
        if ( container ) {
            const html   = event.dataTransfer.getData ( 'text/html' ),
                  half   = container.clientHeight / 2,
                  y      = computeDropContainerY ( container, event ),
                  before = y <= half;
            insertHtml ( html, container, before );
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To dismiss the drop action when the mouse leaves the construction area.
     *
     * ---------------------------------------------------------------------
     */

    const dragleaveConstruction = ( event ) => {
        event.preventDefault ( );
        drawDropMark ( null );
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To highlight a block when moving the mouse over the construction area.
     *
     * ---------------------------------------------------------------------
     */

    const mouseoverConstruction = ( event ) => {
        event.preventDefault ( );
        const container = getDropContainer ( event.target );
        if ( container ) {
            const text = isThisEditable ( event.target ) ? event.target
                                                         : null;
            drawTextMark  ( text,      CONSTRUCTOR_TEXT_HOVER_CLASSNAME  );
            drawBlockMark ( container, CONSTRUCTOR_BLOCK_HOVER_CLASSNAME );
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To highlight an active block when the mouse Main Button is clicked.
     * It also handles the case when the active block has been double-clicked
     * to edit it.
     *
     * ---------------------------------------------------------------------
     */

    const mousedownConstruction = ( event ) => {
        event.preventDefault ( );
        if ( event.button == 0 ) {
            if ( event.ctrlKey ) {
                if ( isThisEditable ( event.target ) ) {
                    editTextBlock ( event.target );
                }
            } else {
                const container = getDropContainer ( event.target );
                if ( container ) {
                    drawTextMark  ( null                                          );
                    drawBlockMark ( null,      CONSTRUCTOR_BLOCK_HOVER_CLASSNAME  );
                    drawBlockMark ( container, CONSTRUCTOR_ACTIVE_BLOCK_CLASSNAME );
                    constructorActiveBlock = container;
                    if ( event.detail == 2 ) {
                        editActiveBlock ( );
                    }
                }
            }
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To cancel a block lighting when the mouse leaves the construction area.
     *
     * ---------------------------------------------------------------------
     */

    const mouseleaveConstruction = ( event ) => {
        event.preventDefault ( );
        drawTextMark  ( null                                    );
        drawBlockMark ( null, CONSTRUCTOR_BLOCK_HOVER_CLASSNAME );
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To handle a key pressed in the construction area.
     *
     * ---------------------------------------------------------------------
     */

    const keyupConstruction = ( event ) => {
        if ( ! isThisInert ( ) ) {
            if ( event.shiftKey ) {
                switch ( event.code ) {
                    case 'ArrowUp':
                         event.preventDefault ( );
                         selectBlockAbove ( );
                         break;
                    case 'ArrowDown':
                         event.preventDefault ( );
                         selectBlockBelow ( );
                         break;
                }
            } else if ( event.ctrlKey ) {
                switch ( event.code ) {
                    case 'ArrowUp':
                         event.preventDefault ( );
                         moveBlockAbove ( );
                         break;
                    case 'ArrowDown':
                         event.preventDefault ( );
                         moveBlockBelow ( );
                         break;
                    case 'NumpadDecimal':
                    case 'Delete':
                         event.preventDefault ( );
                         deleteActiveBlock ( );
                         break;
                    case 'NumpadEnter':
                    case 'Enter':
                         event.preventDefault ( );
                         editActiveBlock ( );
                         break;
                }
            }
        }
    };

    /**
     * ---------------------------------------------------------------------
     *
     * To initialize a sidebar containing a construction.
     *
     * ---------------------------------------------------------------------
     */

    const initConstruction = ( ) => {
        constructorMainBlock = document.querySelector ( '.form .topbar + .aside + .left' );
        if ( constructorMainBlock ) {
            constructorModalDialog = document.querySelector ( '.form .topbar + .aside .modal-window' );
            constructorMainBlock.addEventListener  ( 'dragover',   dragoverConstruction   );
            constructorMainBlock.addEventListener  ( 'drop',       dropConstruction       );
            constructorMainBlock.addEventListener  ( 'dragleave',  dragleaveConstruction  );
            constructorMainBlock.addEventListener  ( 'mouseover',  mouseoverConstruction  );
            constructorMainBlock.addEventListener  ( 'mousedown',  mousedownConstruction  );
            constructorMainBlock.addEventListener  ( 'mouseup',    mousedownConstruction  );
            constructorMainBlock.addEventListener  ( 'mouseleave', mouseleaveConstruction );
            document.body.addEventListener         ( 'keyup',      keyupConstruction      );
        }
    };

    setTimeout ( ( ) => { initFragments    ( );
                          initConstruction ( ); }, 100 );
