/**
 * Element class provides static methods to get various elements from the DOM.
 */
class Element {
    /**
     * Get the import/export textarea element
     * @returns {HTMLTextAreaElement}
     */
    static getImportExportTextarea() {
        return document.querySelector('#input-export-import');
    }

    /**
     * Get the product list element
     * @returns {HTMLElement}
     */
    static getProductListElement() {
        return document.querySelector('.product-list');
    }

    /**
     * Get the settings product list element
     * @returns {HTMLElement}
     */
    static getSettingListElement() {
        return document.querySelector('.settings-product-list');
    }

    /**
     * Get the cart element
     * @returns {HTMLElement}
     */
    static getCartElement() {
        return document.querySelector('.cart-items');
    }

    /**
     * Get the cart empty alert element
     * @returns {HTMLElement}
     */
    static getCartEmptyAlertElement() {
        return document.querySelector('.alert.cart-empty');
    }

    /**
     * Get the create new product button element
     * @returns {HTMLButtonElement}
     */
    static getCreateNewProductButton() {
        return document.querySelector('#create-product');
    }

    /**
     * Get the cart button element
     * @returns {HTMLButtonElement}
     */
    static getCartButton() {
        return document.querySelector('button.cart-value');
    }

    /**
     * Get the navbar brand element
     * @returns {Element}
     */
    static getNavbarBrand() {
        return document.querySelector('nav a.navbar-brand');
    }
}

/**
 * TemplateElement class provides static methods to get various templates from the DOM.
 */
class TemplateElement {
    /**
     * Get the product list element template
     * @returns {HTMLElement}
     */
    static getProductTemplate() {
        const element = document.querySelector('[data-template=product]').cloneNode(true);
        return this.removeTemplate(element);
    }

    /**
     * Get the cart line element template
     * @returns {HTMLElement}
     */
    static getCartLineTemplate() {
        const element = document.querySelector('[data-template=product-line-item]').cloneNode(true);
        return this.removeTemplate(element);
    }

    /**
     * Get the settings product element template
     * @returns {HTMLElement}
     */
    static getSettingsProductTemplate() {
        const element = document.querySelector('[data-template=settings-product]').cloneNode(true);
        return this.removeTemplate(element);
    }

    /**
     * Remove the template attribute from the element
     * @param element {HTMLElement}
     * @returns {HTMLElement}
     */
    static removeTemplate(element) {
        element.removeAttribute('data-template');
        return element;
    }
}

/**
 * Product class represents a product with an id, name, price, and image.
 */
class Product {
    /**
     * Product constructor
     * @param name {string} Name of the product
     * @param price {number} Price of the product
     * @param image {string} Image of the product (source url or base64)
     */
    constructor(name, price, image) {
        this.id = Product.generateId();
        this.name = name;
        this.price = price;
        this.image = image;
    }

    /**
     * Generate a random id for the product
     * @returns {number}
     */
    static generateId() {
        return Math.floor(Math.random() * 1000000000) * Math.floor(Math.random() * 1000000000);
    }

    /**
     * Get the product html element by template and set the attributes by the product object
     * @returns {HTMLElement}
     */
    getProductElement() {
        const productElement = TemplateElement.getProductTemplate();
        productElement.setAttribute('data-id', this.id);
        productElement.querySelector('[data-attr=name]').textContent = this.name;
        productElement.querySelector('[data-attr=image]').src = this.image;
        productElement.addEventListener('click', () => {
            cartManager.addProduct(this);
        })
        return productElement;
    }

    /**
     * Get the settings product html element by template and set the attributes by the product object
     * @returns {HTMLElement}
     */
    getSettingsProductElement() {
        const productElement = TemplateElement.getSettingsProductTemplate();
        productElement.setAttribute('data-id', this.id);
        productElement.textContent = this.name;
        productElement.addEventListener('click', () => {
            productManager.removeProduct(this);
            productManager.setExportFieldJsonValue();
            productElement.remove();
        })
        return productElement;
    }
}

/**
 * CartLine class represents a line in the cart with a product and its quantity.
 */
class CartLine {
    /**
     * CartLine constructor
     * @param product {Product}
     * @param quantity {number}
     */
    constructor(product, quantity) {
        this.product = product;
        this.quantity = quantity;
    }
}

/**
 * Base64Image class provides static methods to convert an image URL or file to a base64 string.
 */
class Base64Image {
    /**
     * Convert an image URL to a base64 string
     * @param url
     * @returns {Promise<string>}
     */
    static fromImageUrl(url) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = 'Anonymous';
            img.onload = () => {
                const canvas = document.createElement('canvas');
                canvas.width = img.width;
                canvas.height = img.height;
                const ctx = canvas.getContext('2d');
                ctx.drawImage(img, 0, 0);
                const dataUrl = canvas.toDataURL();
                resolve(dataUrl);
            };
            img.onerror = reject;
            img.src = url;
        });
    }

    /**
     * Resize an image from a base64 string to a smaller size
     * @param base64
     * @returns {Promise<unknown>}
     */
    static imageResize(base64) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = 'Anonymous';
            img.onload = () => {
                const canvas = document.createElement('canvas');
                canvas.width = 250;
                canvas.height = 250;
                const ctx = canvas.getContext('2d');
                ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
                const dataUrl = canvas.toDataURL();
                resolve(dataUrl);
            };
            img.onerror = reject;
            img.src = base64;
        });
    }

    /**
     * Convert a file to a base64 string
     * @param file
     * @returns {Promise<string>}
     */
    static fromFile(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => {
                resolve(reader.result);
            };
            reader.onerror = reject;
            reader.readAsDataURL(file);
        });
    }
}

/**
 * ProductManager class manages the product list, including loading from JSON, adding, and removing products.
 */
class ProductManager {
    constructor() {
        this.loadFromJson(localStorage.getItem('products') ?? [])
        this.setExportFieldJsonValue();
        this.registerSettingsFormEvents();
        this.registerImportFormEvents();
        this.registerNavbarBrandToProductEvent();
    }

    /**
     * Register the event for the navbar brand to switch to the products tab when clicked.
     */
    registerNavbarBrandToProductEvent() {
        Element.getNavbarBrand().addEventListener('click', () => {
            TabManager.switchTab('products');
        })
    }

    /**
     * Register events for the import/export textarea.
     */
    registerImportFormEvents() {
        Element.getImportExportTextarea().addEventListener('input', (e) => {
            try {
                localStorage.setItem('products', e.target.value);
                window.location = window.location;
            } catch (e) {
                console.error(e);
            }
        });
    }

    /**
     * Set the JSON value of the export field to the current products.
     */
    setExportFieldJsonValue() {
        Element.getImportExportTextarea().textContent = JSON.stringify(this.products);
    }

    /**
     * Load products from JSON string and parse it into Product objects, then render the product list.
     * @param json
     */
    loadFromJson(json) {
        let parse = [];
        try {
            parse = JSON.parse(json);
        } catch (e) {}
        this.products = parse.map(e => new Product(e.name, e.price, e.image));
        this.renderProductList();
    }

    /**
     * Register events for the create new product button.
     */
    registerSettingsFormEvents() {
        Element.getCreateNewProductButton().addEventListener('click', async (e) => {
            e.preventDefault();

            const name = document.querySelector('#product-name').value;
            const price = document.querySelector('#product-price').value;
            const image = document.querySelector('#product-image').files[0];

            // Validate price
            if (name.length === 0) {
                alert('Name muss ausgefüllt sein');
                return;
            }

            // Turn image into base64 or use placeholder image
            if (image) {
                const imageSrcWithBase64 = Base64Image.fromFile(image);
                imageSrcWithBase64.then(async imageSrcWithBase64 => {
                    productManager.addProduct(new Product(name, price, await Base64Image.imageResize(imageSrcWithBase64)));
                    ProductManager.resetProductSettingsForm();
                });
            } else {
                let image1 = await Base64Image.imageResize(await Base64Image.fromImageUrl(`https://placehold.co/250x250?text=${name}`));
                productManager.addProduct(new Product(name, price, image1));
                ProductManager.resetProductSettingsForm();
            }
        });
    }

    /**
     * Reset the product settings form by clearing all input fields.
     */
    static resetProductSettingsForm() {
        document.querySelectorAll('form input:not([type=submit])').forEach(inputField => inputField.value = '');
    }

    /**
     * Render the product list by clearing the existing elements and appending new ones.
     */
    renderProductList() {
        // Clear the product list element before rendering except the template
        Element.getProductListElement().querySelectorAll('[data-id]').forEach(e => {
            if (e.getAttribute('data-template') === null) {
                e.remove();
            }
        });
        this.products.forEach(e => {
            Element.getProductListElement().appendChild(e.getProductElement());
            Element.getSettingListElement().appendChild(e.getSettingsProductElement());
        })
    }

    /**
     * Add a new product to the list and save it to local storage.
     * @param product {Product}
     */
    addProduct(product) {
        this.products.push(product);
        localStorage.setItem('products', JSON.stringify(this.products));
        Element.getProductListElement().appendChild(product.getProductElement());
        Element.getSettingListElement().appendChild(product.getSettingsProductElement());
        this.setExportFieldJsonValue();
    }

    /**
     * Remove a product from the list and local storage.
     * @param product {Product}
     */
    removeProduct(product) {
        this.products = this.products.filter(e => e.id !== product.id);
        const productElement = Element.getProductListElement().querySelector(`[data-id="${product.id}"]`);
        if (productElement) {
            productElement.remove();
        }
        localStorage.setItem('products', JSON.stringify(this.products));
        this.setExportFieldJsonValue();
    }
}

/**
 * CartManager class manages the cart, including adding, removing products, and rendering the cart.
 */
class CartManager {
    constructor() {
        this.cartLines = [];
        this.registerCartResetEvent();
    }

    /**
     * Register the cart reset event to clear the cart when the button is clicked.
     */
    registerCartResetEvent() {
        Element.getCartButton().addEventListener('click', () => {
            this.cartLines = [];
            this.renderCart();
        });
    }

    /**
     * Add a product to the cart. If the product already exists, increase the quantity.
     * @param product {Product}
     */
    addProduct(product) {
        const cartLine = this.cartLines.find(e => e.product.id === product.id);
        if (cartLine) {
            cartLine.quantity++;
        } else {
            this.cartLines.push(new CartLine(product, 1));
        }
        this.renderCart();
    }

    /**
     * Remove a product from the cart. If the quantity is 0, remove the product from the cart.
     * @param product {Product}
     */
    removeProduct(product) {
        const cartLine = this.cartLines.find(e => e.product.id === product.id);
        if (cartLine) {
            cartLine.quantity--;
            if (cartLine.quantity <= 0) {
                this.cartLines = this.cartLines.filter(e => e.product.id !== product.id);
            }
        }
        this.renderCart();
    }

    /**
     * Render the cart by clearing the existing elements and appending new ones.
     */
    renderCart() {
        // Clear the cart element before rendering except the template
        Element.getCartElement().querySelectorAll('[data-id]').forEach(e => {
            if (e.getAttribute('data-template') === null) {
                e.remove();
            }
        });

        if(this.cartLines.length === 0) {
            Element.getCartEmptyAlertElement().classList.remove('d-none');
        } else {
            Element.getCartEmptyAlertElement().classList.add('d-none');
        }
        this.calculateCartValue();

        // Render each cart line
        this.cartLines.forEach(cartLine => {
            const cartLineElement = this.getCartLineElement(cartLine);
            Element.getCartElement().appendChild(cartLineElement);
        });
    }

    /**
     * Get the cart line html element and set the attributes by the product object
     * @param cartLine {CartLine}
     * @returns {*}
     */
    getCartLineElement(cartLine) {
        const cartLineElement = TemplateElement.getCartLineTemplate();
        cartLineElement.setAttribute('data-id', cartLine.product.id);
        cartLineElement.querySelector('[data-attr=name]').textContent = cartLine.product.name;
        cartLineElement.querySelector('[data-attr=value]').textContent = CartManager.getNumberFormatter().format(cartLine.product.price);
        cartLineElement.querySelector('[data-attr=quantity]').textContent = cartLine.quantity;
        cartLineElement.addEventListener('click', () => {
            this.removeProduct(cartLine.product);
        })
        return cartLineElement;
    }

    /**
     * Calculate the total value of the cart by multiplying the product price with the quantity and setting it to the button.
     */
    calculateCartValue() {
        let cartValue = this.cartLines.reduce((acc, cartLine) => {
            return acc + (cartLine.product.price * cartLine.quantity);
        }, 0);
        Element.getCartButton().querySelector('[data-total-value]').textContent = CartManager.getNumberFormatter().format(cartValue);
    }

    /**
     * Get the number formatter for the currency.
     * @returns {Intl.NumberFormat}
     */
    static getNumberFormatter() {
        return new Intl.NumberFormat('de-DE', {
            currency: 'EUR',
            minimumFractionDigits: 2
        });
    }
}

/**
 * TabManager class manages the tabs in the settings page.
 */
class TabManager {
    /**
     * @type {boolean} If the settings tab is active or not
     */
    static isSettingsTabActive = false;

    constructor() {
        TabManager.isSettingsTabActive = false;
        document.querySelector('[data-toggle-tab]').addEventListener('click', (e) => {
            TabManager.toggleTab();
        });
    }

    /**
     * Toggle the active tab between settings and products.
     */
    static toggleTab() {
        if(!TabManager.isSettingsTabActive) {
            this.switchTab('settings');
        } else {
            this.switchTab('products');
        }
        TabManager.isSettingsTabActive = !TabManager.isSettingsTabActive;
    }

    /**
     * Switch the active tab by adding/removing the d-none class.
     * @param tab {string} The tab to switch to
     */
    static switchTab(tab) {
        const tabs = document.querySelectorAll('[data-tab]');
        tabs.forEach(e => {
            e.classList.add('d-none');
        });
        const activeTab = document.querySelector(`[data-tab=${tab}]`);
        activeTab.classList.remove('d-none');
    }
}

/**
 * ThemeManager class manages the theme of the application.
 */
class ThemeManager {
    constructor() {
        this.setBootstrapTheme(localStorage.getItem('bootstrap-theme') || 'light');
        this.registerThemeSwitchEvent();
    }

    /**
     * Register events for the theme switch button.
     */
    registerThemeSwitchEvent() {
        document.querySelectorAll('[data-toggle-bs-theme]').forEach(element => {
            element.addEventListener('click', event => {
                const theme = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark';
                this.setBootstrapTheme(theme);
            })
        });
    }

    /**
     * Set the bootstrap theme by setting the data-bs-theme attribute on the html element and saving it to local storage.
     * @param theme {string} The theme to set (light or dark)
     */
    setBootstrapTheme(theme) {
        document.documentElement.setAttribute('data-bs-theme', theme);
        localStorage.setItem('bootstrap-theme', theme);
    }
}

/**
 * Main function to initialize the application.
 */
const cartManager = new CartManager();
const productManager = new ProductManager();
const tabManager = new TabManager();
const themeManager = new ThemeManager();