/* $ SELECTOR */

HTMLElement.prototype.$ = function(selector) {
    return window.$(selector, this)
}
DocumentFragment.prototype.$ = function(selector) {
    return window.$(selector, this)
}
window.$ = function(selector, el = document) {
    return el.querySelector(selector)
}

window.$$ = function(selector, el = document) {
    return Array.from(el.querySelectorAll(selector))
}

/* CONSOLE */

console.red = function(message) {
    this.log(`%c${message}`, "color: rgb(254, 79, 42);");
};

console.green = function(message) {
    this.log(`%c${message}`, "color: rgb(79, 254, 42);");

}

/* GET CSS VARIABLES FOR DARK OR LIGHT MODE */
window.darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.add(darkMode ? 'dark' : 'light');

window.getColor = function(name) {
    const rootStyles = getComputedStyle(document.documentElement);
    const color = rootStyles.getPropertyValue(`--${name}`).trim();
    if(!color) {
        throw new Error("Color not found")
    }
    return color
}

/* MOBILE */

window.isMobile = function isMobile() {
    return /Android|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(navigator.userAgent);
}

window.css = function css(cssString) {
    let container = document.querySelector("style#pageStyle");
    if(!container) {
        container = document.createElement('style');
        container.id = "pageStyle";
        document.head.appendChild(container);
    }
  
    let primarySelector = cssString.substring(0, cssString.indexOf("{")).trim();
    primarySelector = primarySelector.replace(/\*/g, "all");
    primarySelector = primarySelector.replace(/#/g, "id-");
    primarySelector = primarySelector.replace(/,/g, "");
    let stylesheet = container.querySelector(`:scope > style[id='${primarySelector}']`)
    if(!stylesheet) {
        stylesheet = document.createElement('style');
        stylesheet.id = primarySelector;
        stylesheet.appendChild(document.createTextNode(cssString));
        container.appendChild(stylesheet);
    } else {
        stylesheet.innerText = cssString
    }
}

window.html = function html(elementString) {
    let parser = new DOMParser();
    let doc = parser.parseFromString(elementString, 'text/html');
    return doc.body.firstChild;
}

window.util = {}
window.util.observeClassChange = (el, callback) => {
    if (!el || !(el instanceof Element)) {
        throw new Error("observeClassChange requires a valid DOM element.");
    }

    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
        if (mutation.type === "attributes" && mutation.attributeName === "class") {
            callback(el.classList);
        }
        }
    });

    observer.observe(el, {
        attributes: true,
        attributeFilter: ["class"]
    });

    return observer; // Optional: return it so you can disconnect later
}

/* PAGE SETUP */

Object.defineProperty(Array.prototype, 'last', {
    get() {
      return this[this.length - 1];
    },
    enumerable: false,
});

/* QUILL */

window.quill = {
    rendering: [],

    render: (el) => {
        if(el instanceof Shadow) {
            let parent = quill.rendering[quill.rendering.length-1]
            if(!parent) {
                parent = document.body
            }
            parent.appendChild(el)
        } else {
            if(!el.render) {el.render = () => {}}
            let parent = quill.rendering[quill.rendering.length-1]
            if(!parent) throw new Error("Quill: no parent for element")
            parent.appendChild(el)
        }
        
        quill.rendering.push(el)
        el.render()
        quill.rendering.pop(el)
    },

    rerender: (el) => {
        Array.from(el.attributes).forEach(attr => el.removeAttribute(attr.name));
        el.innerHTML = ""
        el.removeAllListeners()

        quill.rendering.push(el)
        el.render()
        quill.rendering.pop()
    },

    loadPage: () => {
        let URL = window.location.pathname
        if(!window.routes[URL]) {
            throw new Error("No URL for this route: ", URL)
        }

        let pageClass = window[routes[URL]]
        document.title = pageClass.title ?? document.title
        document.body.innerHTML = ""
        let page = new pageClass()
        quill.render(page)
    },

    isStack: (el) => {
        return el.classList.contains("HStack") || el.classList.contains("ZStack") || el.classList.contains("VStack")
    },
}

window.Shadow = class extends HTMLElement {
    constructor() {
        super()
    }
}
window.registerShadow = (el, tagname) => {
    if (typeof el.prototype.render !== 'function') {
        throw new Error("Element must have a render: " + el.prototype.constructor.name)
    }
    if(!tagname) {
        tagname = el.prototype.constructor.name.toLowerCase() + "-"
    }
    customElements.define(tagname, el)
    if(el.css) {
        css(el.css)
    }

    window[el.prototype.constructor.name] = function (...params) {
        let instance = new el(...params)
        quill.render(instance)
        return instance
    }
}

HTMLElement.prototype.rerender = function() {
    quill.rerender(this)
}

/* Styling */

window.vh = "vh"
window.vw = "vw"
window.px = "px"
window.em = "em"
window.rem = "rem"
window.inches = "in"

HTMLElement.prototype.addStyle = function(func) {
    return func(this)
}

window.css = function css(cssString) {
    let container = document.querySelector("style#pageStyle");
    if(!container) {
        container = document.createElement('style');
        container.id = "pageStyle";
        document.head.appendChild(container);
    }
  
    let primarySelector = cssString.substring(0, cssString.indexOf("{")).trim();
    primarySelector = primarySelector.replace(/\*/g, "all");
    primarySelector = primarySelector.replace(/#/g, "id-");
    primarySelector = primarySelector.replace(/,/g, "");
    let stylesheet = container.querySelector(`:scope > style[id='${primarySelector}']`)
    if(!stylesheet) {
        stylesheet = document.createElement('style');
        stylesheet.id = primarySelector;
        stylesheet.appendChild(document.createTextNode(cssString));
        container.appendChild(stylesheet);
    } else {
        stylesheet.innerText = cssString
    }
}

function extendHTMLElementWithStyleSetters() {
    let allStyleProps = Object.keys(document.createElement("div").style)
    allStyleProps.forEach(prop => {
        if(prop === "translate") return
        HTMLElement.prototype[prop] = function(value) {
            this.style[prop] = value;
            return this;
        };
    });
}
extendHTMLElementWithStyleSetters();

HTMLElement.prototype.padding = function(one, two, three = "px") {

    const setPadding = (side, val) => {
        const directionName = `padding${side.charAt(0).toUpperCase()}${side.slice(1)}`;
        this.style[directionName] = (typeof val === 'number') ? `${val}${three}` : val;
    };

    if(one === "horizontal" || one === "vertical") { // is one a direction
        if (one === "horizontal") {
            setPadding("left", two);
            setPadding("right", two);
        } else if (one === "vertical") {
            setPadding("top", two);
            setPadding("bottom", two);
        }
    } else { // is two a value
        if(typeof one !== 'number' || isNaN(one)) {
            this.style.padding = one
        } else {
            this.style.padding = one + two
        }
    }
    
    return this;
};

    HTMLElement.prototype.paddingTop = function(value, unit = "px") {
        this.style.paddingTop = value + unit
        return this
    }

    HTMLElement.prototype.paddingLeft = function(value, unit = "px") {
        this.style.paddingLeft = value + unit
        return this
    }

    HTMLElement.prototype.paddingBottom = function(value, unit = "px") {
        this.style.paddingBottom = value + unit
        return this
    }

    HTMLElement.prototype.paddingRight = function(value, unit = "px") {
        this.style.paddingRight = value + unit
        return this
    }

HTMLElement.prototype.margin = function(direction, value, unit = "px") {
    if (!value) {
        this.style.margin = direction;
        return this;
    }

    const setMargin = (side, val) => {
        const directionName = `margin${side.charAt(0).toUpperCase()}${side.slice(1)}`;
        this.style[directionName] = (typeof val === 'number') ? `${val}${unit}` : val;
    };

    if (direction === "horizontal") {
        setMargin("left", value);
        setMargin("right", value);
    } else if (direction === "vertical") {
        setMargin("top", value);
        setMargin("bottom", value);
    } else {
        setMargin(direction, value);
    }

    return this;
};

    HTMLElement.prototype.marginTop = function(value, unit = "px") {
        this.style.marginTop = value + unit
        return this
    }

    HTMLElement.prototype.marginLeft = function(value, unit = "px") {
        this.style.marginLeft = value + unit
        return this
    }

    HTMLElement.prototype.marginBottom = function(value, unit = "px") {
        this.style.marginBottom = value + unit
        return this
    }

    HTMLElement.prototype.marginRight = function(value, unit = "px") {
        this.style.marginRight = value + unit
        return this
    }

HTMLElement.prototype.width = function(value, unit = "px") {
    if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
        throw new Error(`Invalid value: ${value}. Expected a number.`);
    this.style.width = value + unit
    return this
}

HTMLElement.prototype.minWidth = function(value, unit = "px") {
    if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
        throw new Error(`Invalid value: ${value}. Expected a number.`);
    this.style.minWidth = value + unit
    return this
}

HTMLElement.prototype.height = function(value, unit = "px") {
    if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
        throw new Error(`Invalid value: ${value}. Expected a number.`);
    this.style.height = value + unit
    return this
}

HTMLElement.prototype.minHeight = function(value, unit = "px") {
    if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
        throw new Error(`Invalid value: ${value}. Expected a number.`);
    this.style.minHeight = value + unit
    return this
}

HTMLElement.prototype.fontSize = function(value, unit = "px") {
    switch(value) {
        case "6xl": 
            value = "3.75"; unit = "rem"
            break;
        
        case "5xl": 
            value = "3"; unit = "rem"
            break;

        case "4xl": 
            value = "2.25"; unit = "rem"
            break;

        case "3xl": 
            value = "1.875"; unit = "rem"
            break;

        case "2xl": 
            value = "1.5"; unit = "rem"
            break;

        case "xl":
            value = "1.25"; unit = "rem"
            break;

        case "l":
            value = "1.125"; unit = "rem"
            break;

        case "s":
            value = "0.875"; unit = "rem"
            break;

        case "xs":
            value = "0.75"; unit = "rem"
            break;

        default:
            break;
    }
    this.style.fontSize = value + unit
    return this
}

function checkPositionType(el) {
    let computed = window.getComputedStyle(el).position
    if(!(computed === "absolute" || computed === "fixed")) {
        el.style.position = "absolute"
    }
}

HTMLElement.prototype.x = function(value, unit = "px") {
    if (typeof value !== 'number' || isNaN(value))
        throw new Error(`Invalid value: ${value}. Expected a number.`);
    checkPositionType(this)
    this.style.left = value + unit
    return this
}

HTMLElement.prototype.y = function(value, unit = "px") {
    if (typeof value !== 'number' || isNaN(value))
        throw new Error(`Invalid value: ${value}. Expected a number.`);
    checkPositionType(this)
    this.style.top = value + unit
    return this
}

HTMLElement.prototype.xRight = function(value, unit = "px") {
    if (typeof value !== 'number' || isNaN(value))
        throw new Error(`Invalid value: ${value}. Expected a number.`);
    checkPositionType(this)
    this.style.right = value + unit
    return this
}

HTMLElement.prototype.yBottom = function(value, unit = "px") {
    if (typeof value !== 'number' || isNaN(value))
        throw new Error(`Invalid value: ${value}. Expected a number.`);
    checkPositionType(this)
    this.style.bottom = value + unit
    return this
}

HTMLElement.prototype.borderRadius = function(value, unit = "px") {
    if (typeof value !== 'number' || isNaN(value))
        throw new Error(`Invalid value: ${value}. Expected a number.`);
    this.style.borderRadius = value + unit
    return this
}

HTMLElement.prototype.positionType = function (value) {
    if(!(value === "absolute" || value === "relative" || value === "static" || value === "fixed" || value === "sticky")) {
        console.error("HTMLElement.overlflow: must have valid overflow value!")
        return;
    }
    this.style.position = value
    return this
}

/* Elements */

quill.setChildren = function(el, innerContent) {
    if(typeof innerContent === "string") {
        el.innerText = innerContent
    } else if(typeof innerContent === "function") {
        el.render = innerContent
    } else {
        throw new Error("Children of unknown type")
    }
}

window.a = function a( href, inner=href ) {
    if(!href) throw new Error("quill a: missing href argument. Function: a( href, inner=href )")
    let link = document.createElement("a")
    link.setAttribute('href', href);
    quill.setChildren(link, inner)
    quill.render(link)
    return link
}

window.img = function img(src, width="", height="") {
    let image = document.createElement("img")

    if(!src || !(typeof src==="string")) {
        throw new Error("img: missing first argument: src | String")
    } else {
        image.src = src
    }
    if(width && typeof width === "string") {
        image.style.width = width
    } else if(width) {
        image.style.width = width + "px"
    }
    if(height && typeof height === "string") {
        image.style.height = height
    } else if(height) {
        image.style.height = height + "px"
    }
    quill.render(image)
    return image
}

HTMLImageElement.prototype.backgroundColor = function(value) {
    if (this.src.endsWith('.svg') || this.src.startsWith('data:image/svg+xml')) {
      fetch(this.src).then(response => response.text())
        .then(svgText => {
          const modifiedSvg = svgText.replace(/\bfill="[^"]*"/g, `fill="${value}"`);
          const blob = new Blob([modifiedSvg], { type: 'image/svg+xml' });
          this.src = URL.createObjectURL(blob);
        }).catch(error => {
          console.error('Error updating SVG fill:', error);
        });
    } else {
      this.style.backgroundColor = value;
    }
  
    return this; // Always returns the element itself
  };

window.p = function p(innerText) {
    let el = document.createElement("p")
    if(typeof innerText === "function") {
        el.render = innerText
    } else {
        el.innerText = innerText
    }
    el.style.margin = "0";
    quill.render(el)
    return el
}

window.h1 = function h1(innerText) {
    let el = document.createElement("h1")
    el.innerText = innerText
    quill.render(el)
    return el
}

window.h2 = function h2(innerText) {
    let el = document.createElement("h2")
    el.innerText = innerText
    quill.render(el)
    return el
}

window.h3 = function h3(innerText) {
    let el = document.createElement("h3")
    el.innerText = innerText
    quill.render(el)
    return el
}

window.div = function (innerText) {
    let el = document.createElement("div")
    el.innerText = innerText ?? ""
    quill.render(el)
    return el
}

window.span = function (innerText) {
    let el = document.createElement("span")
    el.innerText = innerText
    quill.render(el)
    return el
}

window.button = function (children) {
    let el = document.createElement("button")
    quill.setChildren(el, children)
    quill.render(el)
    return el
}

window.form = function(cb) {
    let el = document.createElement("form")
    el.render = cb
    quill.render(el)
    return el
}

window.input = function(placeholder, width, height) {
    let el = document.createElement("input")
    el.placeholder = placeholder
    el.style.width = width
    el.style.height = height
    quill.render(el)
    return el
}

window.textarea = function(placeholder) {
    let el = document.createElement("textarea")
    el.placeholder = placeholder
    quill.render(el)
    return el
}


/* STACKS */

window.VStack = function (cb = () => {}) {
    let styles =   `
        display: flex;
        flex-direction: column;
    `
    let nowRendering = quill.rendering[quill.rendering.length-1]
    if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
        nowRendering.style.cssText += styles
        nowRendering.classList.add("VStack")
        cb()
        return nowRendering
    }

    let div = document.createElement("div")
    div.classList.add("VStack")
    div.style.cssText += styles
    div.render = cb
    quill.render(div)
    return div
}

window.HStack = function (cb = () => {}) {
    let styles = `
    display: flex;
    flex-direction: row;
    `;
    let nowRendering = quill.rendering[quill.rendering.length - 1];
    if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
        nowRendering.style.cssText += styles;
        nowRendering.classList.add("HStack")
        cb();
        return nowRendering;
    }

    let div = document.createElement("div");
    div.classList.add("HStack");
    div.style.cssText += styles;
    div.render = cb;
    quill.render(div);
    return div;
};

window.ZStack = function (cb = () => {}) {
    let nowRendering = quill.rendering[quill.rendering.length - 1];
    if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
        nowRendering.classList.add("ZStack")
        cb();
        return nowRendering;
    }

    let div = document.createElement("div");
    div.classList.add("ZStack");
    div.render = cb;
    quill.render(div);
    return div;
};

/* EVENTS */

HTMLElement.prototype.onAppear = function(func) {
    func(this);
    return this;
};

HTMLElement.prototype.onClick = function(func) {
    this._storeListener("click", func);
    return this;
};

HTMLElement.prototype.onMouseDown = function(func) {
    this._storeListener("mousedown", func);
    return this;
};

HTMLElement.prototype.onMouseUp = function(func) {
    this._storeListener("mouseup", func);
    return this;
};

HTMLElement.prototype.onRightClick = function(func) {
    this._storeListener("contextmenu", func);
    return this;
};

HTMLElement.prototype.onHover = function(cb) {
    const onEnter = () => cb.call(this, true);
    const onLeave = () => cb.call(this, false);
    this._storeListener("mouseover", onEnter);
    this._storeListener("mouseleave", onLeave);
    return this;
};

HTMLElement.prototype.onFocus = function(cb) {
    if (!this.matches('input, textarea, select, button')) {
        throw new Error("Can't put focus event on non-form element!");
    }
    this._storeListener("focus", cb);
    return this;
};

HTMLElement.prototype.onBlur = function(cb) {
    if (!this.matches('input, textarea, select, button')) {
        throw new Error("Can't put blur event on non-form element!");
    }
    this._storeListener("blur", cb);
    return this;
};

HTMLElement.prototype.onKeyDown = function(cb) {
    this._storeListener("keydown", cb);
    return this;
};

HTMLElement.prototype.onEvent = function(name, cb) {
    window._storeListener(window, name, cb);
    return this;
};

HTMLElement.prototype._storeListener = function(type, handler, options) {
    window._storeListener(this, type, handler, options)
}

window.__listeners = []

function _storeListener(target, type, handler, options) {
    if (!target.__listeners) target.__listeners = [];

    const optionsString = JSON.stringify(options);

    const index = target.__listeners.findIndex(listener =>
        listener.type === type &&
        listener.handler.toString() === handler.toString() &&
        JSON.stringify(listener.options) === optionsString
    );

    if (index === -1) {
        target.addEventListener(type, handler, options);
        target.__listeners.push({ type, handler, options });
    } else {
        const old = target.__listeners[index];
        target.removeEventListener(old.type, old.handler, old.options);

        // Replace with the new one
        target.addEventListener(type, handler, options);
        target.__listeners[index] = { type, handler, options };
    }
}

HTMLElement.prototype.removeAllListeners = function() {
    if (!this.__listeners) return;
    for (const { type, handler, options } of this.__listeners) {
        this.removeEventListener(type, handler, options);
    }
    this.__listeners = [];
    return this;
};

/* ATTRIBUTES */

HTMLElement.prototype.attr = function(attributes) {
    if (
        typeof attributes !== "object" || 
        attributes === null || 
        Array.isArray(attributes)
    ) {
        throw new TypeError("attr() expects an object with key-value pairs");
    }

    for (const [key, value] of Object.entries(attributes)) {
        this.setAttribute(key, value);
    }
    console.log(this)
    return this;
};
