import { createElement, createRender, onDestroy } from "../../lib/skeleton/index.js";
import { animate, slideYIn } from "../../lib/animate.js";
import rxjs, { effect, preventDefault } from "../../lib/rx.js";
import assert from "../../lib/assert.js";
import { loadCSS } from "../../helpers/loader.js";
import { qs } from "../../lib/dom.js";
import { ApplicationError } from "../../lib/error.js";
import { createLoader } from "../../components/loader.js";
import t from "../../locales/index.js";
import ctrlError from "../ctrl_error.js";

import { currentPath, sort, isMobile } from "./helper.js";
import { createThing } from "./thing.js";
import { clearSelection, addSelection, getSelection$, isSelected } from "./state_selection.js";
import { getState$ } from "./state_config.js";
import { ls, search } from "./model_files.js";
import { getPermission } from "./model_acl.js";

const ICONS = {
    EMPTY_FILES: "[encoded data, 12920 characters]",
    EMPTY_SEARCH: "[encoded data, 10272 characters]",
};

const VIRTUAL_SCROLL_MINIMUM_TRIGGER = 100;

export const files$ = new rxjs.BehaviorSubject(null);

export default async function(render) {
    const $page = createElement(`
        <div class="component_filesystem container">
            <div data-target="dragselect" style="display:none;"></div>
            <div data-target="header" style="text-align:center;"></div>

            <div class="ifscroll-before"></div>
            <div data-target="list" class="list"></div>
            <div class="ifscroll-after"></div>
            <br>
        </div>
    `);
    render($page);
    onDestroy(() => files$.next(null));

    // feature: virtual scrolling
    const path = currentPath();
    const $header = qs($page, `[data-target="header"]`);
    const $list = qs($page, `[data-target="list"]`);
    const removeLoader = createLoader($header);
    const $listBefore = qs($page, ".ifscroll-before");
    const $listAfter = qs($page, ".ifscroll-after");
    const refreshScreen$ = rxjs.merge(
        // case1: trigger the first display
        rxjs.of(null),
        // case2: height change => redraw screen
        rxjs.fromEvent(window, "resize").pipe( // height change = always redraw
            rxjs.startWith(null),
            rxjs.map(() => document.body.clientHeight),
            rxjs.distinctUntilChanged(),
            rxjs.skip(1),
        ),
        // case3: width change => redraw if grid size change
        rxjs.fromEvent(window, "resize").pipe(
            rxjs.startWith(null),
            rxjs.map(() => {
                if ($list.getAttribute("data-type") === "grid") {
                    return gridSize($list.clientWidth, document.body.clientWidth);
                }
                return 0;
            }),
            rxjs.distinctUntilChanged(),
            rxjs.skip(1),
        ),
    );

    let count = 0;
    effect(ls(path).pipe(
        rxjs.switchMap(({ files, ...rest }) => getState$().pipe(rxjs.switchMap((state) => {
            $header.innerHTML = "";
            $list.innerHTML = "";
            if (state.search) {
                const removeLoader = createLoader($header);
                $listBefore.setAttribute("style", "");
                $listAfter.setAttribute("style", "");
                return rxjs.timer(state.search ? 450 : 0).pipe(
                    rxjs.switchMap(() => search(state.search).pipe(
                        rxjs.map(({ files }) => ({
                            files, ...state, ...rest,
                        })),
                    )),
                    rxjs.finalize(() => effect(rxjs.of(null).pipe(removeLoader))),
                );
            }
            return rxjs.of({ files, ...state, ...rest });
        }))),
        rxjs.mergeMap((obj) => getPermission(path).pipe(
            rxjs.map((permissions) => ({ ...obj, permissions })),
        )),
        rxjs.mergeMap(({ show_hidden, files, ...rest }) => {
            if (show_hidden === false) files = files.filter(({ name }) => name[0] !== ".");
            files = sort(files, rest["sort"], rest["order"]);
            return rxjs.of({ ...rest, files });
        }),
        rxjs.map((data) => ({ ...data, count: count++ })),
        removeLoader,
        rxjs.mergeMap(({ files, search, ...rest }) => {
            files$.next(files);
            if (files.length === 0) {
                renderEmpty(createRender(qs($page, `[data-target="header"]`)), search ? ICONS.EMPTY_SEARCH : ICONS.EMPTY_FILES);
                return rxjs.EMPTY;
            }
            return rxjs.of({ ...rest, files, search });
        }),
        rxjs.mergeMap((obj) => refreshScreen$.pipe(rxjs.mapTo(obj))),
        rxjs.mergeMap(({ files, view, search, count, permissions }) => { // STEP1: setup the list of files
            $list.closest(".scroll-y").scrollTop = 0;
            let FILE_HEIGHT, COLUMN_PER_ROW;
            switch (view) {
            case "grid":
                FILE_HEIGHT = 160;
                COLUMN_PER_ROW = gridSize($list.clientWidth, document.body.clientWidth);
                $list.style.gridTemplateColumns = `repeat(${COLUMN_PER_ROW}, 1fr)`;
                $list.setAttribute("data-type", "grid");
                break;
            case "list":
                FILE_HEIGHT = 47;
                COLUMN_PER_ROW = 1;
                $list.style.gridTemplateColumns = `repeat(1, 1fr)`;
                $list.setAttribute("data-type", "list");
                break;
            default:
                throw new Error("Not Implemented");
            }
            const BLOCK_SIZE = Math.ceil(document.body.clientHeight / FILE_HEIGHT) + 1;

            let size = files.length;
            if (size > VIRTUAL_SCROLL_MINIMUM_TRIGGER) {
                size = Math.min(files.length, BLOCK_SIZE * COLUMN_PER_ROW);
            }
            const $fs = document.createDocumentFragment();
            for (let i = 0; i < size; i++) {
                const file = files[i];
                $fs.appendChild(createThing({
                    ...file,
                    ...createLink(file, currentPath()),
                    view,
                    search,
                    n: i,
                    permissions,
                }));
            }
            if (count === 0) animate(
                $list,
                { time: 200, keyframes: slideYIn(isMobile ? 10 : 5) },
            );
            $list.replaceChildren($fs);

            /// ///////////////////////////////////
            // CASE 1: virtual scroll isn't enabled
            if (files.length <= VIRTUAL_SCROLL_MINIMUM_TRIGGER) {
                return rxjs.of({ virtual: false });
            }

            /// ///////////////////////////////////
            // CASE 2: with virtual scroll
            const height = (Math.ceil(files.length / COLUMN_PER_ROW) - BLOCK_SIZE) * FILE_HEIGHT;
            if (height > 33554400) {
                console.log(`maximum CSS height reached, requested height ${height} is too large`);
            }
            const setHeight = (size) => {
                if (size < 0 || size > height) throw new ApplicationError(
                    "INTERNAL ERROR",
                    `assertion on size failed: size[${size}] height[${height}]`
                );
                $listBefore.style.height = `${size}px`;
                $listAfter.style.height = `${height - size}px`;
            };
            setHeight(0);
            const top = ($node) => $node.getBoundingClientRect().top;
            return rxjs.of({
                virtual: true,
                files,
                search,
                path,
                view,
                permissions,
                currentState: 0,
                $list,
                setHeight,
                FILE_HEIGHT,
                BLOCK_SIZE,
                COLUMN_PER_ROW,
                MARGIN: top($list) - top($list.closest(".scroll-y")),
            });
        }),
        rxjs.switchMap(({
            files, path, view, search, permissions,
            BLOCK_SIZE, COLUMN_PER_ROW, FILE_HEIGHT,
            MARGIN,
            currentState,
            setHeight,
            $list,
            virtual,
        }) => (
            virtual
                ? rxjs.fromEvent($page.closest(".scroll-y"), "scroll", { passive: true })
                : rxjs.EMPTY
        ).pipe(
            rxjs.map((e) => {
                // 0-------------0-----------1-----------2-----------3 ....
                //    [padding]     $block1     $block2     $block3    ....
                const nextState = Math.floor((e.target.scrollTop - MARGIN) / FILE_HEIGHT);
                return Math.max(nextState, 0);
            }),
            rxjs.distinctUntilChanged(),
            rxjs.debounce(() => new rxjs.Observable((observer) => {
                const id = requestAnimationFrame(() => observer.next());
                return () => cancelAnimationFrame(id);
            })),
            rxjs.tap((nextState) => {
                // STEP1: calculate the virtual scroll paramameters
                let diff = nextState - currentState;
                const diffSgn = Math.sign(diff);
                if (Math.abs(diff) > BLOCK_SIZE) { // diff is bound by BLOCK_SIZE
                    // we can't be moving more than what is on the screen
                    diff = diffSgn * BLOCK_SIZE;
                }
                let fileStart = nextState * COLUMN_PER_ROW;
                if (diffSgn > 0) { // => scroll down
                    fileStart += BLOCK_SIZE * COLUMN_PER_ROW;
                    fileStart -= Math.min(diff, BLOCK_SIZE) * COLUMN_PER_ROW;
                }
                let fileEnd = fileStart + diffSgn * diff * COLUMN_PER_ROW;
                if (fileStart >= files.length) { // occur when BLOCK_SIZE is larger than its absolute minimum
                    return;
                }
                else if (fileEnd > files.length) {
                    // occur when files.length isn't a multiple of COLUMN_PER_ROW and
                    // we've scrolled to the bottom of the list already
                    nextState = Math.ceil(files.length / COLUMN_PER_ROW) - BLOCK_SIZE;
                    fileEnd = files.length - 1;
                    for (let i=0; i<COLUMN_PER_ROW; i++) {
                        // add some padding to fileEnd to balance the list to the
                        // nearest COLUMN_PER_ROW
                        fileEnd += 1;
                        if (fileEnd % COLUMN_PER_ROW === 0) {
                            break;
                        }
                    }
                }

                // STEP2: create the new elements
                const $fs = document.createDocumentFragment();
                let n = 0;
                for (let i = fileStart; i < fileEnd; i++) {
                    const file = files[i];
                    if (file === undefined) $fs.appendChild(createThing({
                        type: "hidden",
                    }));
                    else $fs.appendChild(createThing({
                        ...file,
                        ...createLink(file, path),
                        search,
                        view,
                        permissions,
                        n: i,
                    }));
                    n += 1;
                }

                // STEP3: update the DOM
                if (diffSgn > 0) { // scroll down
                    $list.appendChild($fs);
                    for (let i = 0; i < n; i++) $list.firstChild.remove();
                } else { // scroll up
                    $list.insertBefore($fs, $list.firstChild);
                    for (let i = 0; i < n; i++) $list.lastChild.remove();
                }
                setHeight(nextState * FILE_HEIGHT);
                currentState = nextState;
            }),
        )),
        rxjs.catchError(ctrlError()),
    ));

    // feature: keyboard selection
    effect(rxjs.fromEvent(window, "keydown").pipe(
        rxjs.filter((e) => e.keyCode === 27),
        rxjs.tap(() => clearSelection()),
    ));
    effect(rxjs.fromEvent(window, "keydown").pipe(
        rxjs.filter((e) => e.key === "a" &&
                    (e.ctrlKey || e.metaKey) &&
                    (files$.value || []).length > 0 &&
                    assert.type(document.activeElement, HTMLElement).tagName !== "INPUT"),
        preventDefault(),
        rxjs.tap(() => {
            clearSelection();
            if (!Array.isArray(files$.value) || files$.value.length === 0) return;
            const path = currentPath();
            const el0 = files$.value[0];
            const elm1 = files$.value.slice(-1)[0];
            addSelection({
                n: 0,
                path: path + el0.name + (el0.type === "directory" ? "/" : ""),
                shift: false,
                files: [],
            });
            if (elm1) addSelection({
                n: (files$.value || []).length - 1,
                path: path + elm1.name + (elm1.type === "directory" ? "/" : ""),
                shift: true,
                files: (files$.value || []),
            });
        }),
    ));
    effect(getSelection$().pipe(rxjs.tap(() => {
        for (const $thing of $page.querySelectorAll(".component_thing")) {
            let checked = isSelected(parseInt(assert.truthy($thing.getAttribute("data-n"))));
            if ($thing.getAttribute("data-selectable") === "false") checked = false;
            $thing.classList.add(checked ? "selected" : "not-selected");
            $thing.classList.remove(checked ? "not-selected" : "selected");
            qs(assert.type($thing, HTMLElement), `input[type="checkbox"]`).checked = checked;
        };
    })));

    // feature: mouse drag selection
    const $dragContainer = assert.type($page.closest(".component_page_filespage"), HTMLElement);
    const $dragselect = qs($page, `[data-target="dragselect"]`);
    const dragmove = (x, y, w, h) => {
        $dragselect.style.left = `${x}px`;
        $dragselect.style.top = `${y}px`;
        $dragselect.style.width = `${w}px`;
        $dragselect.style.height = `${h}px`;
        if ((w+1) * (h+1) < 50) {
            $dragselect.style.display = "none";
            return false;
        }
        $dragselect.style.display = "block";
        return true;
    };
    if (isMobile === false) effect(rxjs.fromEvent($dragContainer, "mousedown").pipe(
        rxjs.filter((e) => {
            if (e.target.nodeName === "INPUT") return false;
            else if (e.target.closest("button")) return false;
            else if (e.buttons !== 1) return false;
            return !e.target.closest(`[draggable="true"]`);
        }),
        rxjs.tap((e) => e.preventDefault()),
        rxjs.map((e) => ({
            start: [e.clientX, e.clientY],
            state: [...$page.querySelectorAll(".component_thing")].map(($file) => {
                const bounds = $file.getBoundingClientRect();
                const $checkbox = qs(assert.type($file, HTMLElement), ".component_checkbox");
                return {
                    $checkbox,
                    checked: () => $checkbox.firstElementChild.checked,
                    bounds: {
                        x: bounds.x,
                        y: bounds.y,
                        w: bounds.width,
                        h: bounds.height,
                    },
                };
            }),
        })),
        rxjs.mergeMap(({ start, state }) => rxjs.fromEvent(document, "mousemove").pipe(
            rxjs.takeUntil(rxjs.merge(
                rxjs.fromEvent(window, "mouseup"),
                rxjs.fromEvent(window, "keydown").pipe(rxjs.filter(({ key }) => key === "Escape")),
            )),
            rxjs.finalize(() => dragmove(0, 0, 0, 0)),
            rxjs.map((e) => ({ start, end: [e.clientX, e.clientY], state })),
        )),
        rxjs.map(({ start, end, state }) => ({
            state,
            obj: {
                x: Math.min(start[0], end[0]),
                y: Math.min(start[1], end[1]),
                w: Math.abs(start[0] - end[0]),
                h: Math.abs(start[1] - end[1]),
            },
        })),
        rxjs.filter(({ obj }) => dragmove(obj.x, obj.y, obj.w, obj.h)),
        rxjs.tap(({ obj, state }) => {
            for (let i=0; i<state.length; i++) {
                const { bounds, $checkbox, checked } = state[i];
                const collision = !(
                    obj.x + obj.w < bounds.x ||
                        obj.x > bounds.x + bounds.w ||
                        obj.y + obj.h < bounds.y ||
                        obj.y > bounds.y + bounds.h
                );
                if ((collision && !checked()) || (!collision && checked())) {
                    $checkbox.click();
                }
            }
        }),
    ));

    // feature: remove long touch popup on mobile
    const disableLongTouch = (e) => {
        if (isMobile === false) return;
        e.preventDefault();
    };
    document.addEventListener("contextmenu", disableLongTouch);
    onDestroy(() => document.removeEventListener("contextmenu", disableLongTouch));
}

function renderEmpty(render, base64Icon) {
    const $page = createElement(`
        <div class="empty no-select">
            <p class="empty_image">
                <img class="component_icon" draggable="false" src="data:image/svg+xml;base64,${base64Icon}" alt="empty_folder">
            </p>
            <p class="label">${t("There is nothing here")}</p>
        </div>
    `);
    animate(render($page), { time: 250, keyframes: slideYIn(5) });
}

export function init() {
    return Promise.all([
        loadCSS(import.meta.url, "./ctrl_filesystem.css"),
        loadCSS(import.meta.url, "./thing.css"),
        loadCSS(import.meta.url, "./modal.css"),
    ]);
}

export function createLink(file, currentPath) {
    let path = file.path;
    if (!path) path = currentPath + file.name + (file.type === "directory" ? "/" : "");
    let link = file.type === "directory" ? "files" + path : "view" + path;
    link = encodeURIComponent(link).replaceAll("%2F", "/");
    return {
        path,
        link,
    };
}

function gridSize(size, windowSize) {
    const DESIRED_FILE_WIDTH_ON_LARGE_SCREEN = 210;
    if (windowSize > 1100) return Math.max(
        4,
        Math.floor(size / DESIRED_FILE_WIDTH_ON_LARGE_SCREEN),
    );
    else if (size > 750) return 4;
    else if (size > 520) return 3;
    else if (size > 300) return 2;
    return 1;
}
