import { show as monacoShow, destroy as monacoDestroy } from './monaco-editor.js';
import { isMobile } from './util.js';

export class CodeExplorer {
    constructor() {
        /** @type {Map<string, Element[]>} 复用最近一次 PJAX 返回的目录行,避免重复请求 */
        this.prefetchedDirectoryRows = new Map();
        this.init();
    }

    init() {
        this.container = document.querySelector('.code-explorer');
        if (!this.container) return;

        if (this.container.dataset.jsInitialized === 'true') {
            this.treeContainer = this.container.querySelector('.code-explorer__tree');
            this.syncTreeWithUrl().then(() => { });
            return;
        }

        this.treeContainer = this.container.querySelector('.code-explorer__tree');
        this.contentContainer = this.container.querySelector('.code-explorer__content');
        this.searchInput = this.container.querySelector('.code-explorer__search input');
        this.searchButton = this.container.querySelector('.code-explorer__search .btn-search');

        if (this.searchInput) {
            this.searchInput.addEventListener('keydown', (e) => {
                if (e.key === 'Enter') {
                    e.preventDefault();
                    this.handleSearch(this.searchInput.value).then(() => { });
                }
            });
        }

        if (this.searchButton && this.searchInput) {
            this.searchButton.addEventListener('click', async () => {
                await this.handleSearch(this.searchInput.value);
            });
        }

        this.container.addEventListener('click', async (e) => await this.handleClick(e));
        this._initResizeHandle();
        this.syncTreeWithUrl().then(() => { });
        this._renderCodeView();

        this.container.dataset.jsInitialized = 'true';
    }

    /** 渲染当前页面的代码视图(首次加载或由外部调用) */
    _renderCodeView() {
        const wrapper = document.querySelector('.code-view-wrapper');
        if (!wrapper) return;
        if (isMobile()) {
            if (typeof Prism !== 'undefined') {
                const block = wrapper.querySelector('.prism-fallback code');
                if (block) Prism.highlightElement(block);
            }
        } else {
            const host = wrapper.querySelector('.monaco-host');
            const code = wrapper.querySelector('.prism-fallback code')?.textContent ?? '';
            const lang = host?.dataset.language ?? '';
            if (host && code.trim()) {
                monacoShow(host, code, lang).catch(err => {
                    console.warn('Monaco 初始化失败,回退到 Prism:', err);
                    wrapper.classList.add('use-prism');
                    if (typeof Prism !== 'undefined') {
                        const block = wrapper.querySelector('.prism-fallback code');
                        if (block) Prism.highlightElement(block);
                    }
                });
            }
        }
    }

    async handleClick(e) {
        // 展开/折叠按钮
        const toggle = e.target.closest('.code-tree__toggle');
        if (toggle) {
            e.preventDefault();
            e.stopPropagation();
            await this.toggleNode(toggle.closest('.code-tree__node'));
            return;
        }

        // 点击 .code-tree__content 任意区域均触发导航(不仅限于 <a> 标签)
        const treeContent = e.target.closest('.code-tree__content');
        const link = treeContent
            ? treeContent.querySelector('.code-tree__label')
            : e.target.closest('a');

        if (!link?.href) return;

        let url;
        try { url = new URL(link.href); } catch { return; }

        if (url.origin !== window.location.origin) return;
        if (url.protocol !== 'http:' && url.protocol !== 'https:') return;
        if (!url.pathname.toLowerCase().startsWith('/code')) return;

        e.preventDefault();

        if (treeContent) {
            const node = treeContent.closest('.code-tree__node');
            this.selectNode(node);
            if (node.dataset.isFolder === 'true') {
                // 点击目录名称时不主动拉取子目录,避免和后续 PJAX 导航产生双请求。
                // 真正的数据填充在 PJAX 响应后复用返回内容完成。
                node.classList.add('is-expanded');
            }
        }

        this.loadContent(link.href);
    }

    loadContent(url) {
        const that = this;
        if ($.fn.pjax) {
            $.pjax({
                url: url,
                container: '.code-explorer__content',
                fragment: '.code-explorer__content',
                timeout: 5000,
                push: true,
                scrollTo: false,
            });

            $(document).one('pjax:success', function (event, data, status, xhr, options) {
                const targetUrl = new URL(url, window.location.origin).href;
                const requestUrl = new URL(options.url, window.location.origin).href;
                if (targetUrl !== requestUrl) return;

                that.updateTreeFromResponse(url, data);
                that.updateBreadcrumbs(data);

                const wrapper = document.querySelector('.code-view-wrapper');
                if (wrapper) {
                    if (isMobile()) {
                        if (typeof Prism !== 'undefined') {
                            const block = wrapper.querySelector('.prism-fallback code');
                            if (block) Prism.highlightElement(block);
                        }
                    } else {
                        const host = wrapper.querySelector('.monaco-host');
                        const code = wrapper.querySelector('.prism-fallback code')?.textContent ?? '';
                        const lang = host?.dataset.language ?? '';
                        if (host && code.trim()) {
                            monacoShow(host, code, lang).catch(err => {
                                console.warn('Monaco 渲染失败,回退到 Prism:', err);
                                wrapper.classList.add('use-prism');
                                if (typeof Prism !== 'undefined') {
                                    const block = wrapper.querySelector('.prism-fallback code');
                                    if (block) Prism.highlightElement(block);
                                }
                            });
                        }
                    }
                } else {
                    monacoDestroy();
                }

                window.scrollTo(0, 0);
            });
        } else {
            window.location.href = url;
        }
    }

    updateBreadcrumbs(htmlContent) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(htmlContent, 'text/html');
        const newBreadcrumbs = doc.querySelector('.code-explorer__breadcrumbs');

        if (newBreadcrumbs) {
            const currentBreadcrumbs = document.querySelector('.code-explorer__breadcrumbs');
            if (currentBreadcrumbs) {
                currentBreadcrumbs.innerHTML = newBreadcrumbs.innerHTML;
            }
        }
    }

    // 利用 PJAX 响应体更新树节点,避免二次 fetch
    updateTreeFromResponse(url, htmlContent) {
        const path = new URL(url, window.location.origin).pathname
            .replace(/^\/code\/?/i, '').replace(/^\//, '');

        const doc = new DOMParser().parseFromString(htmlContent, 'text/html');
        const fileRows = Array.from(doc.querySelectorAll('.row'))
            .filter(r => !r.classList.contains('prev'));

        // 缓存本次响应内容,供 syncTreeWithUrl -> expandNode -> loadDirectory 复用。
        this.prefetchedDirectoryRows.set(decodeURIComponent(path), fileRows);

        if (fileRows.length === 0) {
            this.syncTreeWithUrl().then(() => { });
            return;
        }

        this.findNodeByPath(path).then(node => {
            if (node && node.dataset.isFolder === 'true') {
                const childrenContainer = node.querySelector('.code-tree__children');
                if (childrenContainer && childrenContainer.dataset.loaded !== 'true') {
                    this.populateContainer(childrenContainer, fileRows);
                    node.classList.add('is-expanded');
                }
            }
            this.syncTreeWithUrl().then(() => { });
        });
    }

    async findNodeByPath(path) {
        if (!path) return null;

        const segments = path.split('/').filter(x => x);
        let parentContainer = this.treeContainer;
        let foundNode = null;

        for (const segment of segments) {
            const decoded = decodeURIComponent(segment);
            let node = null;
            for (const n of parentContainer.querySelectorAll(':scope > .code-tree__node')) {
                if (n.dataset.name === decoded) { node = n; break; }
            }
            if (!node) return null;
            foundNode = node;
            if (node.dataset.isFolder === 'true') {
                parentContainer = node.querySelector('.code-tree__children');
                if (parentContainer.dataset.loaded !== 'true') return null;
            }
        }
        return foundNode;
    }

    populateContainer(container, rows) {
        if (container.dataset.loaded === 'true') return;

        const fragment = document.createDocumentFragment();

        rows.forEach(row => {
            if (row.classList.contains('prev')) return;

            const link = row.querySelector('.header a');
            if (!link) return;

            const name = row.dataset.name || link.getAttribute('title') || link.innerText.trim();
            const href = link.getAttribute('href');
            const iconContainer = row.querySelector('.icon');
            const iconHtml = iconContainer ? iconContainer.innerHTML : '';
            const isFolder = row.dataset.isFolder === 'true';

            const node = this.createTreeNode(name, href, iconHtml, isFolder);
            fragment.appendChild(node);
        });

        container.appendChild(fragment);
        container.dataset.loaded = 'true';
    }

    async syncTreeWithUrl() {
        if (!this.treeContainer) return;

        const path = window.location.pathname.replace(/^\/code\/?/i, '').replace(/^\//, '');
        const segments = path.split('/').filter(x => x);

        if (this.treeContainer.children.length === 0) {
            await this.loadDirectory('', this.treeContainer);
        }

        let currentPath = '';
        let parentContainer = this.treeContainer;

        this.treeContainer.querySelectorAll('.code-tree__content').forEach(el => el.classList.remove('is-active'));

        for (const segment of segments) {
            currentPath += (currentPath ? '/' : '') + segment;

            const decoded = decodeURIComponent(segment);
            let node = null;
            for (const n of parentContainer.querySelectorAll(':scope > .code-tree__node')) {
                if (n.dataset.name === decoded) { node = n; break; }
            }

            if (!node) break;

            if (decodeURIComponent(currentPath) === decodeURIComponent(path)) {
                this.selectNode(node);
            }

            if (node.dataset.isFolder === 'true') {
                const children = node.querySelector('.code-tree__children');
                if (children && children.dataset.loaded === 'true') {
                    node.classList.add('is-expanded');
                } else {
                    await this.expandNode(node);
                }
                parentContainer = node.querySelector('.code-tree__children');
            }
        }
    }

    async loadDirectory(path, container) {
        if (container.dataset.loaded === 'true') return;

        const normalizedPath = decodeURIComponent((path || '').replace(/^\//, ''));
        const prefetchedRows = this.prefetchedDirectoryRows.get(normalizedPath);
        if (prefetchedRows) {
            this.populateContainer(container, prefetchedRows);
            this.prefetchedDirectoryRows.delete(normalizedPath);
            return;
        }

        const url = `/code/${path}`;
        try {
            const response = await fetch(url, {
                headers: { 'X-PJAX': 'true' }
            });
            const html = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');

            const rows = doc.querySelectorAll('.row');
            this.populateContainer(container, rows);

        } catch (e) {
            console.error('Failed to load directory', path, e);
        }
    }

    createTreeNode(name, href, iconHtml, isFolder) {
        const div = document.createElement('div');
        div.className = 'code-tree__node';
        div.dataset.name = name;
        div.dataset.isFolder = isFolder;

        const content = document.createElement('div');
        content.className = 'code-tree__content';

        // Toggle/Expand Icon
        const toggle = document.createElement('div');
        toggle.className = `code-tree__toggle ${isFolder ? '' : 'is-hidden'}`;
        toggle.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>';
        content.appendChild(toggle);

        // File/Folder Icon
        const icon = document.createElement('div');
        icon.className = 'code-tree__icon';
        icon.innerHTML = iconHtml;
        content.appendChild(icon);

        // Label
        const label = document.createElement('a');
        label.className = 'code-tree__label';
        label.href = href;
        label.innerText = name;
        content.appendChild(label);

        content.title = name;
        div.appendChild(content);

        if (isFolder) {
            const children = document.createElement('div');
            children.className = 'code-tree__children';
            div.appendChild(children);
        }

        return div;
    }

    async toggleNode(node) {
        if (node.dataset.isFolder !== 'true') return;

        if (node.classList.contains('is-expanded')) {
            node.classList.remove('is-expanded');
        } else {
            await this.expandNode(node);
        }
    }

    async expandNode(node) {
        if (!node || node.dataset.isFolder !== 'true') return;

        node.classList.add('is-expanded');

        const childrenContainer = node.querySelector('.code-tree__children');
        if (childrenContainer && childrenContainer.dataset.loaded !== 'true') {
            const link = node.querySelector('.code-tree__label');
            const href = link.getAttribute('href');
            const path = href.replace(/^\/code\/?/i, '').replace(/^\//, '');
            node.classList.add('is-loading');
            await this.loadDirectory(path, childrenContainer);
            node.classList.remove('is-loading');
        }
    }

    selectNode(node) {
        if (!node) return;
        this.treeContainer.querySelectorAll('.code-tree__content').forEach(el => el.classList.remove('is-active'));
        const content = node.querySelector('.code-tree__content');
        if (content) {
            content.classList.add('is-active');
            this._scrollNodeIntoView(content);
        }
    }

    /** 让当前选中树节点在可滚动容器中可见。 */
    _scrollNodeIntoView(contentEl) {
        if (!this.treeContainer || !contentEl) return;

        const containerRect = this.treeContainer.getBoundingClientRect();
        const nodeRect = contentEl.getBoundingClientRect();
        const overTop = nodeRect.top < containerRect.top;
        const overBottom = nodeRect.bottom > containerRect.bottom;

        if (overTop || overBottom) {
            // 选中节点尽量滚动到容器中部
            const containerCenter = this.treeContainer.clientHeight / 2;
            const nodeTopInContainer =
                this.treeContainer.scrollTop + (nodeRect.top - containerRect.top);
            const targetScrollTop =
                nodeTopInContainer - containerCenter + nodeRect.height / 2;
            const maxScrollTop = this.treeContainer.scrollHeight - this.treeContainer.clientHeight;
            const nextScrollTop = Math.max(0, Math.min(targetScrollTop, Math.max(0, maxScrollTop)));

            this.treeContainer.scrollTo({
                top: nextScrollTop,
                behavior: 'smooth',
            });
        }
    }

    _initResizeHandle() {
        const sidebar = this.container.querySelector('.code-explorer__sidebar');
        if (!sidebar) return;

        const handle = document.createElement('div');
        handle.className = 'code-explorer__resize-handle';
        sidebar.insertAdjacentElement('afterend', handle);

        let startX, startWidth;
        handle.addEventListener('mousedown', (e) => {
            startX = e.clientX;
            startWidth = sidebar.offsetWidth;
            handle.classList.add('is-resizing');
            document.body.style.cursor = 'col-resize';
            document.body.style.userSelect = 'none';

            const onMove = (e) => {
                const w = Math.min(Math.max(startWidth + e.clientX - startX, 160), 600);
                sidebar.style.width = w + 'px';
            };
            const onUp = () => {
                handle.classList.remove('is-resizing');
                document.body.style.cursor = '';
                document.body.style.userSelect = '';
                document.removeEventListener('mousemove', onMove);
                document.removeEventListener('mouseup', onUp);
            };

            document.addEventListener('mousemove', onMove);
            document.addEventListener('mouseup', onUp);
        });
    }

    async handleSearch(keyword) {
        keyword = keyword.trim();
        if (!keyword) {
            this.treeContainer.innerHTML = '';
            this.treeContainer.dataset.loaded = 'false';
            await this.loadDirectory('', this.treeContainer);
            return;
        }

        try {
            const response = await fetch(`/search/code?keyword=${encodeURIComponent(keyword)}`);
            if (!response.ok) return;
            this.renderSearchResults(await response.json(), keyword);
        } catch (e) {
            console.error('Search failed', e);
        }
    }

    renderSearchResults(data, keyword) {
        if (!data) return;
        this.treeContainer.innerHTML = '';

        const fragment = document.createDocumentFragment();

        const getIconHtml = (isFolder) => {
            if (isFolder) {
                return '<svg viewBox="0 0 24 24" width="18" height="18" fill="#F4BF50"><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>';
            } else {
                return '<svg viewBox="0 0 24 24" width="18" height="18" fill="#90A4AE"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>';
            }
        };

        const renderItems = (items, isFolder) => {
            if (!items) return;
            items.forEach(item => {
                const name = item.name;
                const pathList = item.currentPath || [];
                // Construct href
                const href = '/code/' + pathList.join('/');

                const node = this.createTreeNode(name, href, getIconHtml(isFolder), isFolder);
                const label = node.querySelector('.code-tree__label');
                if (keyword) {
                    label.innerHTML = name.replace(new RegExp(`(${keyword})`, 'gi'), '<mark>$1</mark>');
                }
                const cnt = node.querySelector('.code-tree__content');
                if (cnt) cnt.title = pathList.join('/');

                fragment.appendChild(node);
            });
        };

        renderItems(data.directories, true);
        renderItems(data.files, false);

        if (fragment.children.length === 0) {
            const empty = document.createElement('div');
            empty.className = 'text-center text-muted p-3';
            empty.innerText = '没有找到匹配项';
            fragment.appendChild(empty);
        }

        this.treeContainer.appendChild(fragment);
    }
}
评论加载中...