const defaultTag = "div";
const fragmentFasterHeuristic = /[-+,> ]/;
const selectorParse = /(?:\s*([-+ ,<>]))?\s*(\.|!\.?|#)?([-\w$]+)?(?:\[([^\]=]+)=?['"]?([^\]'"]*)['"]?\])?/g;
const insertTextNode = (element, text) => {
    element.appendChild(document.createTextNode(text));
};
const isElement = (node) => {
    return node !== null && node.nodeType === Node.ELEMENT_NODE;
};
const isRecord = (obj) => {
    return typeof obj === "object" && !obj.nodeType;
};
const isElementArgument = (argument) => {
    return argument.nodeType === Node.ELEMENT_NODE;
};
const createInitialState = (topReferenceElement) => {
    return {
        fragment: null,
        returnValue: null,
        lastSelectorArg: false,
        nextSibling: null,
        referenceElement: topReferenceElement,
        current: null,
        topRef: topReferenceElement,
    };
};
const canInsertCurrent = (state) => {
    return Boolean(state.current) && state.current !== state.referenceElement;
};
const canUseFragmentRoot = (state) => {
    return state.referenceElement === state.topRef;
};
const canCreateFragment = (state, argument) => {
    return !state.fragment && fragmentFasterHeuristic.test(argument);
};
const createFragment = (state) => {
    state.fragment = document.createDocumentFragment();
    return true;
};
const ensureFragmentAvailable = (state, argument) => {
    return (Boolean(state.fragment) ||
        (canCreateFragment(state, argument) && createFragment(state)));
};
const shouldUseFragment = (state, argument) => {
    return canUseFragmentRoot(state) && ensureFragmentAvailable(state, argument);
};
const getInsertContainer = (state, argument) => {
    if (shouldUseFragment(state, argument) && state.fragment) {
        return state.fragment;
    }
    return state.referenceElement;
};
const insertLastElement = (state, argument) => {
    if (!canInsertCurrent(state)) {
        return;
    }
    const container = getInsertContainer(state, argument);
    container.insertBefore(state.current, state.nextSibling);
};
const setReturnValueFromState = (state) => {
    if (state.current) {
        state.referenceElement = state.current;
        state.returnValue = state.current;
        return;
    }
    if (isElement(state.referenceElement)) {
        state.returnValue = state.referenceElement;
    }
};
const appendTextForPreviousSelector = (state, argument) => {
    state.lastSelectorArg = false;
    if (!state.current) {
        return;
    }
    insertTextNode(state.current, argument);
};
const applyAttributeRecord = (state, attributes) => {
    if (!state.current) {
        return;
    }
    Object.entries(attributes).forEach(([key, value]) => {
        state.current?.setAttribute(key, value);
    });
};
const handleElementArgument = (state, element, insertLast) => {
    state.current = element;
    insertLast("");
    state.referenceElement = state.current;
    state.nextSibling = null;
};
const handleRecordArgument = (state, argument) => {
    if (isRecord(argument)) {
        applyAttributeRecord(state, argument);
    }
};
const handleObjectArgument = (state, argument, insertLast) => {
    state.lastSelectorArg = false;
    if (isElementArgument(argument)) {
        handleElementArgument(state, argument, insertLast);
        return;
    }
    handleRecordArgument(state, argument);
};
const markSelectorStart = (state, index) => {
    if (index === 0) {
        state.topRef = null;
    }
    state.lastSelectorArg = true;
};
const consumeInjectedArgument = (context) => {
    context.index += 1;
    return String(context.args[context.index]);
};
const getTagFromToken = (prefix, value) => {
    return !prefix && value ? value : null;
};
const shouldCreateElement = (state, tag, prefix, attrName) => {
    return Boolean(tag || (!state.current && (prefix || attrName)));
};
const resolveInjectedValue = (context, value) => {
    return value === "$" ? consumeInjectedArgument(context) : value;
};
const insertInjectedText = (context) => {
    insertTextNode(context.state.referenceElement, consumeInjectedArgument(context));
};
const createCurrentElementFromTag = (context, tag) => {
    if (tag === "$") {
        insertInjectedText(context);
        return;
    }
    context.state.current = document.createElement(tag || defaultTag);
};
const ensureCurrentElement = (context, prefix, value, attrName) => {
    const tag = getTagFromToken(prefix, value);
    if (!shouldCreateElement(context.state, tag, prefix, attrName)) {
        return;
    }
    createCurrentElementFromTag(context, tag);
};
const getRemovedClassCandidate = (currentClassName, value) => {
    if (!currentClassName) {
        return "";
    }
    return (" " + currentClassName + " ").replace(" " + value + " ", " ");
};
const buildAddedClassName = (currentClassName, value) => {
    if (!currentClassName) {
        return value || "";
    }
    return (getRemovedClassCandidate(currentClassName, value) + value).substring(1);
};
const addClassName = (current, value) => {
    current.className = buildAddedClassName(current.className || "", value);
};
const removeClassName = (current, value) => {
    const currentClassName = current.className || "";
    const removed = getRemovedClassCandidate(currentClassName, value);
    const normalized = removed.substring(1, removed.length - 1);
    if (normalized !== currentClassName) {
        current.className = normalized;
    }
};
const clearCurrentContents = (current) => {
    current.innerHTML = "";
};
const setCurrentId = (current, value) => {
    current.id = value || "";
};
const directPrefixHandlers = {
    "#": setCurrentId,
    ".": addClassName,
};
const getFallbackPrefixHandler = (argument) => {
    return argument === "!" ? clearCurrentContents : removeClassName;
};
const getPrefixHandler = (prefix, argument) => {
    return directPrefixHandlers[prefix] || getFallbackPrefixHandler(argument);
};
const applyPrefix = (context, prefix, value) => {
    const current = context.state.current;
    if (!current) {
        return;
    }
    getPrefixHandler(prefix, context.argument)(current, resolveInjectedValue(context, value));
};
const applyStyleAttribute = (current, _attrName, value) => {
    current.style.cssText = value || "";
};
const removeNamedAttribute = (current, attrName) => {
    current.removeAttribute(attrName.substring(1));
};
const normalizeAttributeValue = (attrName, value) => {
    return value === "" ? attrName : value || "";
};
const setNamedAttribute = (current, attrName, value) => {
    current.setAttribute(attrName, normalizeAttributeValue(attrName, value));
};
const getAttributeHandler = (attrName) => {
    if (attrName === "style") {
        return applyStyleAttribute;
    }
    return attrName.charAt(0) === "!" ? removeNamedAttribute : setNamedAttribute;
};
const applyAttribute = (context, attrName, attrValue) => {
    const current = context.state.current;
    if (!current) {
        return;
    }
    getAttributeHandler(attrName)(current, attrName, resolveInjectedValue(context, attrValue));
};
const getParentElement = (node) => {
    return isElement(node.parentNode) ? node.parentNode : null;
};
const setReferenceElementToParent = (state) => {
    const parent = getParentElement(state.current || state.referenceElement);
    if (parent) {
        state.referenceElement = parent;
    }
};
const setSiblingAnchor = (state) => {
    state.nextSibling = state.current || state.referenceElement;
    state.current = null;
};
const advanceToNextSibling = (state, combinator) => {
    if (combinator === "+" && state.nextSibling) {
        state.nextSibling = state.nextSibling.nextSibling;
    }
};
const moveToSiblingPosition = (state, combinator) => {
    setReferenceElementToParent(state);
    setSiblingAnchor(state);
    advanceToNextSibling(state, combinator);
};
const moveToParent = (state) => {
    const parent = (state.current || state.referenceElement).parentNode;
    if (!isElement(parent)) {
        return;
    }
    state.referenceElement = parent;
    state.current = parent;
};
const getBranchReferenceElement = (state, combinator, topReferenceElement) => {
    return combinator === ","
        ? state.topRef || topReferenceElement
        : state.current || state.referenceElement;
};
const resetInsertionPosition = (state) => {
    state.current = null;
    state.nextSibling = null;
};
const moveToBranch = (state, combinator, topReferenceElement) => {
    state.referenceElement = getBranchReferenceElement(state, combinator, topReferenceElement);
    resetInsertionPosition(state);
};
const moveSiblingFromContext = (context, combinator) => {
    moveToSiblingPosition(context.state, combinator);
};
const moveParentFromContext = (context) => {
    moveToParent(context.state);
};
const moveBranchFromContext = (context, combinator) => {
    moveToBranch(context.state, combinator, context.topReferenceElement);
};
const getCombinatorHandler = (combinator) => {
    if (combinator === "-" || combinator === "+") {
        return moveSiblingFromContext;
    }
    return combinator === "<" ? moveParentFromContext : moveBranchFromContext;
};
const syncReferenceElementWithCurrent = (state) => {
    if (state.current) {
        state.referenceElement = state.current;
    }
};
const applyCombinator = (context, combinator) => {
    if (!combinator) {
        return;
    }
    context.insertLastElement(context.argument);
    getCombinatorHandler(combinator)(context, combinator);
    syncReferenceElementWithCurrent(context.state);
};
const applyPrefixIfNeeded = (context, prefix, value) => {
    if (prefix) {
        applyPrefix(context, prefix, value);
    }
};
const applyAttributeIfNeeded = (context, attrName, attrValue) => {
    if (attrName) {
        applyAttribute(context, attrName, attrValue);
    }
};
const createSelectorToken = (replaceArgs) => {
    const [_token, combinator, prefix, value, attrName, attrValue] = replaceArgs;
    return {
        combinator,
        prefix,
        value,
        attrName,
        attrValue,
    };
};
const replaceSelectorToken = (context, selectorToken) => {
    const { combinator, prefix, value, attrName, attrValue } = selectorToken;
    applyCombinator(context, combinator);
    ensureCurrentElement(context, prefix, value, attrName);
    applyPrefixIfNeeded(context, prefix, value);
    applyAttributeIfNeeded(context, attrName, attrValue);
    return "";
};
const createSelectorContext = (input) => {
    const { processContext, argument } = input;
    return {
        argument,
        args: processContext.args,
        index: processContext.index,
        topReferenceElement: processContext.topReferenceElement,
        state: processContext.state,
        insertLastElement: processContext.insertLast,
    };
};
const parseSelectorArgument = (context) => {
    return context.argument.replace(selectorParse, (...replaceArgs) => replaceSelectorToken(context, createSelectorToken(replaceArgs)));
};
const assertValidSelectorArgument = (leftoverCharacters, argument) => {
    if (leftoverCharacters) {
        throw new SyntaxError(`Unexpected char ${leftoverCharacters} in ${argument}`);
    }
};
const finalizeSelectorArgument = (state, argument, insertLast) => {
    insertLast(argument);
    setReturnValueFromState(state);
};
const createProcessArgumentContext = (input) => {
    return input;
};
const processSelectorArgument = (processContext, argument) => {
    const context = createSelectorContext({ processContext, argument });
    const leftoverCharacters = parseSelectorArgument(context);
    assertValidSelectorArgument(leftoverCharacters, argument);
    finalizeSelectorArgument(processContext.state, argument, processContext.insertLast);
};
const appendSelectorTextIfNeeded = (state, argument) => {
    if (!state.lastSelectorArg) {
        return false;
    }
    appendTextForPreviousSelector(state, argument);
    return true;
};
const processNewSelectorArgument = (context, argument) => {
    markSelectorStart(context.state, context.index);
    processSelectorArgument(context, argument);
};
const handleStringArgument = (context, argument) => {
    if (appendSelectorTextIfNeeded(context.state, argument)) {
        return;
    }
    processNewSelectorArgument(context, argument);
};
const processObjectArgument = (context, argument) => {
    handleObjectArgument(context.state, argument, context.insertLast);
};
const processSingleArgument = (context, argument) => {
    if (typeof argument === "string") {
        handleStringArgument(context, argument);
        return;
    }
    processObjectArgument(context, argument);
};
const createInsertLastHandler = (state) => {
    return (argument) => {
        insertLastElement(state, argument);
    };
};
const processArguments = (state, topReferenceElement, args, insertLast) => {
    args.forEach((argument, index) => {
        const context = createProcessArgumentContext({
            state,
            topReferenceElement,
            args,
            index,
            insertLast,
        });
        processSingleArgument(context, argument);
    });
};
const appendFragmentToTopRef = (state) => {
    if (state.topRef && state.fragment) {
        state.topRef.appendChild(state.fragment);
    }
};
export const put = (topReferenceElement, ...args) => {
    const state = createInitialState(topReferenceElement);
    const insertLast = createInsertLastHandler(state);
    processArguments(state, topReferenceElement, args, insertLast);
    appendFragmentToTopRef(state);
    return state.returnValue;
};
