import { Obj, } from "@intelligentgraphics/ig.utilities";
import { IGXB } from "@intelligentgraphics/igd.core";
import BaseLogger from "../Logger";
const Logger = BaseLogger.createSubLogger("IGXBManager");
/**
 * IGXB Manager class for BOM processing.
 * This is still subject to change and far from complete, use at your own risk.
 */
export class IGXBManager {
    /**
     * Creates an instance of IGXBManager.
     *
     * DO NOT USE THIS DIRECTLY UNLESS YOU KNOW WHAT YOU'RE DOING.
     *
     * Use IGXBManager.Create instead
     *
     * @param {IIGXBManagerSettings} settings
     * @memberof IGXBManager
     */
    constructor(settings) {
        if (settings.autoGrouping !== true) {
            settings.autoGrouping = false;
        }
        this._settings = settings;
        this.data = settings.data;
    }
    static Create(settings) {
        const tManager = new IGXBManager(settings);
        return tManager;
    }
    // private helpers
    static getUUID() {
        return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
            const r = (Math.random() * 16) | 0;
            const v = c === "x" ? r : (r & 0x3) | 0x8;
            return v.toString(16);
        });
    }
    getAllArticles() {
        return this.findByConditions({ ItemType: IGXB.EItemType.Article }, { searchMode: "all" });
    }
    removeHeaderCalculation(onlyIndex) {
        if (onlyIndex === undefined) {
            this.data.HeaderCalculation = undefined;
        }
        else {
            if (this.data.HeaderCalculation &&
                this.data.HeaderCalculation.length > onlyIndex) {
                this.data.HeaderCalculation.splice(onlyIndex, 1);
            }
        }
    }
    addHeaderCalculation(entry, beforeIndex) {
        if (!this.data.HeaderCalculation) {
            this.data.HeaderCalculation = [];
        }
        if (beforeIndex === undefined ||
            beforeIndex >= this.data.HeaderCalculation.length) {
            this.data.HeaderCalculation.push(entry);
        }
        else {
            this.data.HeaderCalculation.splice(beforeIndex, 0, entry);
        }
    }
    /**
     * Removes an item.
     *  @param itemId ID of the item. It must not have sub-items.
     *  @returns true when the item has been removed.
     */
    removeItem(itemId, force = false) {
        Logger.info("IGXBManager.removeItem()", itemId);
        const parent = this.getParentItem(itemId);
        const parentSet = parent === undefined ? this.data.Items : this.getSubItems(parent);
        const itemIndex = parentSet.findIndex(child => child.UID === itemId);
        const item = parentSet[itemIndex];
        if (!item || itemIndex < 0) {
            return false;
        }
        const itemSubItems = this.getSubItems(item);
        if (!force && itemSubItems.length) {
            return false;
        }
        parentSet.splice(itemIndex, 1);
        if (this.Settings.autoGrouping &&
            parent &&
            parent.ItemType === IGXB.EItemType.Group &&
            !parentSet.length) {
            this.removeItem(parent.UID);
        }
        return true;
    }
    addSetItem(data, inParentId, beforeId) {
        if (this.Settings.autoGrouping && !inParentId) {
            inParentId = this.findOrCreateAutoGroup(data.Source, inParentId);
        }
        const setItem = {
            ...data,
            UID: IGXBManager.getUUID(),
            ItemType: IGXB.EItemType.Set,
            Items: [],
        };
        if (inParentId) {
            this.insertItemIntoParent(setItem, inParentId, beforeId);
        }
        else {
            this.insertItemIntoSet(undefined, setItem, beforeId);
        }
        return setItem.UID;
    }
    /** Adds item group at root-level
     * @param caption Optional caption for the group
     * @param beforeId Optional UID of the item of which the group should
     * be inserted before. If not specified, the group will be
     * inserted as last item.
     * @returns UID of the new group item.
     */
    addItemGroup(caption, beforeId, groupIdentifier) {
        Logger.info("IGXBManager.addItemGroup()", caption, beforeId);
        const tNewItem = {
            UID: IGXBManager.getUUID(),
            ItemType: IGXB.EItemType.Group,
            Caption: caption,
            Items: new Array(),
            Identifier: groupIdentifier,
        };
        let tBeforeIdx;
        if (beforeId !== undefined) {
            tBeforeIdx = this.findItemIndex(beforeId, this.data.Items);
            if (tBeforeIdx === -1) {
                Logger.error("IGXBManager.addItemGroup(): Invalid beforeId", beforeId);
            }
        }
        if (tBeforeIdx !== undefined) {
            this.data.Items.splice(tBeforeIdx, 0, tNewItem);
        }
        else {
            this.data.Items.push(tNewItem);
        }
        return tNewItem.UID;
    }
    /** Adds a text item.
     * @param text Item text
     * @param important Flag this text as important for an emphasized
     *        representation
     * @param inParentId UID of a GroupItem or ArticleItem to place this
     *        item into as sub-position.
     * @param beforeId optional UID of the item of which this item should
     *        be inserted before. If not specified, the item will be
     *        inserted as last item at root level or (if specified) in the
     *        parent's set of sub-items.
     * @returns UID of the new text item.
     */
    addTextItem(text, important, inParentId, beforeId) {
        Logger.info("IGXBManager.addTextItem()", text, {
            important: important,
            inParentId: inParentId,
            beforeId: beforeId,
        });
        const tNewItem = {
            UID: IGXBManager.getUUID(),
            ItemType: IGXB.EItemType.Text,
            Text: text,
            Important: important,
        };
        let tParentItem;
        if (inParentId) {
            tParentItem = this.findItem(inParentId);
            if (!tParentItem ||
                (tParentItem.ItemType !== IGXB.EItemType.Group &&
                    tParentItem.ItemType !== IGXB.EItemType.Article)) {
                Logger.error("IGXBManager.addTextItem: Invalid BOM Parent Id " +
                    inParentId);
                throw new Error("Invalid BOM Parent Id " + inParentId);
            }
        }
        this.insertItemIntoSet(tParentItem, tNewItem, beforeId);
        return tNewItem.UID;
    }
    // public addSetItem(
    // )
    /** Adds a article item.
     * @param spec ArticleItem specification.
     * @param inParentId UID of a GroupItem or ArticleItem to place this
     * item into as sub-position.
     * @param beforeId optional UID of the item of which this item should
     * be inserted before. If not specified, the item will be
     * inserted as last item at root level or (if specified) in the
     * parent's set of sub-items.
     * @returns UID of the new item.
     */
    addArticleItem(article, inParentId, beforeId) {
        Logger.info("IGXBManager.addArticleItem()", article, {
            inParentId: inParentId,
            beforeId: beforeId,
        });
        if (this.Settings.autoGrouping && !inParentId && article.IsCommercial) {
            inParentId = this.findOrCreateAutoGroup(article.Source, article.ManufacturerName || article.ManufacturerId, article.ProgramName || article.ProgramId);
        }
        const tNewItem = {
            ...article,
            UID: IGXBManager.getUUID(),
            ItemType: IGXB.EItemType.Article,
            SubItems: [],
        };
        if (inParentId) {
            this.insertItemIntoParent(tNewItem, inParentId, beforeId);
        }
        else {
            this.insertItemIntoSet(undefined, tNewItem, beforeId);
        }
        return tNewItem.UID;
    }
    addImpressionItem(impression, inParentId, beforeId) {
        const newItem = {
            ...impression,
            UID: IGXBManager.getUUID(),
            ItemType: IGXB.EItemType.Impression,
        };
        let tParentItem;
        if (inParentId) {
            tParentItem = this.findItem(inParentId);
            if (!tParentItem ||
                (tParentItem.ItemType !== IGXB.EItemType.Group &&
                    tParentItem.ItemType !== IGXB.EItemType.Article)) {
                Logger.error("Invalid BOM Parent ID " + inParentId);
                throw new Error("Invalid BOM Parent ID " + inParentId);
            }
        }
        this.insertItemIntoSet(tParentItem, newItem, beforeId);
        return newItem.UID;
    }
    moveItem(itemId, inParentId, beforeId) {
        const currentParent = this.getParentItem(itemId);
        const currentSet = currentParent
            ? this.getSubItems(currentParent)
            : this.data.Items;
        const currentIndex = currentSet.findIndex(entry => entry.UID === itemId);
        const item = currentSet[currentIndex];
        currentSet.splice(currentIndex, 1);
        const newParent = inParentId === undefined ? undefined : this.getItem(inParentId);
        return this.insertItemIntoSet(newParent, item, beforeId);
    }
    copyItem(input, inParentId, beforeId) {
        let tR;
        const tOldItem = typeof input === "string" ? this.getItem(input) : input;
        let tNewItem;
        let tParentItem;
        let tBeforeItem;
        const regenUIDs = (itemSet) => {
            for (const item of itemSet) {
                item.UID = IGXBManager.getUUID();
                regenUIDs(this.getSubItems(item));
            }
        };
        if (!tOldItem) {
            Logger.error("IGXBManager.copyItem() failed", {
                item: tNewItem,
                toParent: tParentItem,
                beforeItem: tBeforeItem,
            });
            return undefined;
        }
        if (tOldItem.ItemType === IGXB.EItemType.Article ||
            tOldItem.ItemType === IGXB.EItemType.Impression) {
            // add item to the correct group
            const groupIdentifier = tOldItem.ManufacturerId + "." + tOldItem.ProgramId;
            const existingGroup = this.findByConditions({
                ItemType: IGXB.EItemType.Group,
                Identifier: groupIdentifier,
            });
            if (existingGroup) {
                inParentId = existingGroup.UID;
            }
            else {
                // create new group
                let tName = tOldItem.ManufacturerName || "";
                if (tName.length === 0) {
                    tName = tOldItem.ManufacturerId;
                }
                let tProg = tOldItem.ProgramName || "";
                if (tProg.length === 0) {
                    tProg = tOldItem.ProgramId;
                }
                inParentId = this.addItemGroup(tName + " " + tProg, undefined, groupIdentifier);
            }
        }
        tParentItem =
            inParentId === undefined ? undefined : this.getItem(inParentId);
        if (tParentItem) {
            if (tParentItem.ItemType !== IGXB.EItemType.Article &&
                tParentItem.ItemType !== IGXB.EItemType.Group) {
                tParentItem = undefined;
            }
        }
        tBeforeItem =
            beforeId === undefined ? undefined : this.getItem(beforeId);
        if (tBeforeItem === tOldItem) {
            tBeforeItem = undefined;
        }
        Logger.info("IGXBManager.copyItem()", {
            item: tOldItem,
            toParent: tParentItem,
            beforeItem: tBeforeItem,
        });
        let tNewParentSet = this.data.Items;
        if (tParentItem) {
            if (tParentItem.ItemType === IGXB.EItemType.Article) {
                tNewParentSet = tParentItem.SubItems;
            }
            else if (tParentItem.ItemType === IGXB.EItemType.Group) {
                tNewParentSet = tParentItem.Items;
            }
        }
        if (tNewParentSet) {
            tNewItem = Obj.deepClone(tOldItem);
            regenUIDs([tNewItem]);
            let tBeforeIdx;
            if (tBeforeItem) {
                tBeforeIdx = this.findItemIndex(beforeId, tNewParentSet);
            }
            if (tBeforeIdx !== undefined) {
                tNewParentSet.splice(tBeforeIdx, 0, tNewItem);
            }
            else {
                tNewParentSet.push(tNewItem);
            }
            tR = tNewItem.UID;
        }
        else {
            Logger.error("IGXBManager.copyItem() could find parent set", {
                item: tOldItem,
                toParent: tParentItem,
                beforeItem: tBeforeItem,
            });
        }
        return tR;
    }
    /** Returns the BOM position string of an item.
     *  @param itemId ID of the item.
     *  @returns The position string, like "1" or "3.2.1" or an empty
     * string, if this item is not an ArticleItem.
     */
    getPositionString(itemId) {
        const articles = [];
        // collect all articles, which are either located in the root level or under a root level group
        for (const item of this.data.Items) {
            if (item.ItemType === IGXB.EItemType.Text) {
                continue;
            }
            if (item.ItemType === IGXB.EItemType.Group) {
                for (const subItem of item.Items) {
                    if (subItem.ItemType === IGXB.EItemType.Article) {
                        articles.push(subItem);
                    }
                }
            }
            if (item.ItemType === IGXB.EItemType.Set) {
                articles.push(item);
            }
            if (item.ItemType === IGXB.EItemType.Article) {
                articles.push(item);
            }
        }
        const parts = [];
        const buildParts = (items) => {
            // keep track of skipped textItems so the count of these can later be used to create and article only index
            let skipped = 0;
            for (let index = 0; index < items.length; index++) {
                const item = items[index];
                if (item.ItemType === IGXB.EItemType.Text || item.Hidden) {
                    skipped++;
                    continue;
                }
                if (item.UID === itemId) {
                    // found the required item. return true to let parent know, that searching is done
                    parts.push(index - skipped + 1);
                    return true;
                }
                const subItems = this.getSubItems(item);
                const result = buildParts(subItems);
                if (result) {
                    parts.push(index - skipped + 1);
                    return true;
                }
            }
        };
        buildParts(articles);
        // indicies need to be reversed, since they get added depth-first
        return parts.reverse().join(".");
    }
    /**
     * Returns the accumulated price for one position, including subitems and additional prices
     */
    // public getItemTotal(itemId: string): IGXB.IPrice {
    // 	const price: IGXB.IPrice = {
    // 		Currency: undefined,
    // 		Value: 0,
    // 	};
    // 	const item = this.findItem(itemId);
    // 	if (!item) {
    // 		return price;
    // 	}
    // 	if (item.ItemType === IGXB.EItemType.Article && item.IsCommercial) {
    // 		price.Currency = price.Currency || item.Currency;
    // 		if (typeof item.SinglePrice === "number") {
    // 			price.Value +=
    // 				item.SinglePrice *
    // 				(item.Quantity === undefined ? 1 : item.Quantity);
    // 		} else {
    // 			Logger.warn("Commercial article has no price", item);
    // 		}
    // 	}
    // 	price.Value += this.getSubTotal(this.getSubItems(item)).amount;
    // 	return price;
    // }
    getSubTotal(items) {
        let subTotal = 0;
        let currency;
        for (const item of items) {
            if (item.ItemType === IGXB.EItemType.Set) {
                if (typeof item.SinglePrice === "number") {
                    currency = item.Currency;
                    subTotal += item.SinglePrice;
                }
                continue;
            }
            if (item.ItemType === IGXB.EItemType.Article && item.IsCommercial) {
                if (typeof item.SinglePrice === "number") {
                    currency = item.Currency;
                    subTotal +=
                        item.SinglePrice *
                            (item.Quantity === undefined ? 1 : item.Quantity);
                }
                else {
                    Logger.warn("Commercial article has no price", item);
                }
            }
            const { currency: subCurrency, amount } = this.getSubTotal(this.getSubItems(item));
            if (currency === undefined) {
                currency = subCurrency;
            }
            subTotal += amount;
        }
        return { amount: subTotal, currency };
    }
    getParentSet(item) {
        const parent = this.getParentItem(item);
        if (!parent) {
            return this.Settings.data.Items;
        }
        return this.getSubItems(parent);
    }
    getParentItem(item) {
        const id = typeof item === "string" ? item : item.UID;
        return this.findByConditions({ UID: id }, { searchMode: "parent" });
    }
    /**
     * Finds the parent ItemGroup of an item.
     * @param item: The item whose parent should be returned.
     * @returns: The parent ItemGroup or undefined, if the item has no parent group
     */
    getParentGroupItem(item) {
        const id = typeof item === "string" ? item : item.UID;
        return this.findByConditions({ UID: id }, { searchMode: "parent" });
    }
    /**
     * Finds the parent ArticleItem of an item.
     * @param item: The item whose parent should be returned.
     * @returns: The parent ArticleItem or undefined, if the item has no parent Article Item
     */
    getParentArticleItem(item) {
        const id = typeof item === "string" ? item : item.UID;
        return this.findByConditions({ UID: id }, { searchMode: "parent" });
    }
    getItem(itemID) {
        return this.findItem(itemID);
    }
    /** Returns the IGXB article item with the given commercial ID */
    getItemByComId(comId) {
        const found = this.findByConditions({
            ComId: comId,
        });
        if (!found ||
            (found.ItemType !== IGXB.EItemType.Article &&
                found.ItemType !== IGXB.EItemType.Impression &&
                found.ItemType !== IGXB.EItemType.Set &&
                found.ItemType !== IGXB.EItemType.HeadData)) {
            return undefined;
        }
        return found;
    }
    /** returns a flat list of IGXB article items that match the given source */
    getItemsBySource(source) {
        return this.findByConditions({ Source: source, ItemType: IGXB.EItemType.Article }, { searchMode: "all" });
    }
    get IGXB() {
        return this.data;
    }
    set IGXB(newIGXB) {
        // TODO: consistency-check, maybe remove gfx-ids, ...
        this.data = newIGXB;
    }
    /** Creates a new BOM structure
     * @param priceType: IGXB price type (None, Net, Gross)
     * @param vat: VAT in percent must be given when price type is Gross
     */
    static createNewIGXB(priceType, vat) {
        if (priceType === IGXB.EPriceType.Gross && vat === undefined) {
            throw new Error("VAT must be given when priceType is Gross");
        }
        return {
            FormatVersion: "" + IGXB.CurrentMajorVersion + "." + IGXB.CurrentMinorVersion,
            PriceType: priceType,
            VAT: vat,
            Items: [],
            ProjectData: {
                Id: this.getUUID(),
                Title: "A new Project",
                Creation: new Date().toUTCString(),
            },
            CustomData: {},
        };
    }
    get Settings() {
        return this._settings;
    }
    findItem(uid, items, searchMode = "item") {
        if (!uid) {
            return undefined;
        }
        return this.findByConditions({ UID: uid }, { itemSet: items, searchMode });
    }
    findByConditions(conditions, options = {}) {
        const searchMode = options.searchMode || "item";
        const itemSet = options.itemSet || this.data.Items;
        if (!conditions) {
            return searchMode === "all" ? [] : undefined;
        }
        let matches;
        if (typeof conditions === "object") {
            const conditionProps = Object.getOwnPropertyNames(conditions);
            if (!conditionProps.length) {
                return searchMode === "all" ? [] : undefined;
            }
            matches = (item) => conditionProps.every(prop => item[prop] === conditions[prop]);
        }
        else {
            matches = conditions;
        }
        const foundItems = [];
        const recurse = (entries) => {
            if (!entries) {
                return undefined;
            }
            for (const item of entries) {
                if (matches(item)) {
                    if (searchMode === "all") {
                        foundItems.push(item);
                        recurse(this.getSubItems(item));
                        continue;
                    }
                    return searchMode === "item" ? item : true;
                }
                if (item.ItemType === IGXB.EItemType.Text) {
                    continue;
                }
                const subItems = this.getSubItems(item);
                const res = recurse(subItems);
                if (res === true) {
                    return item;
                }
                if (res) {
                    return res;
                }
            }
            return undefined;
        };
        const result = recurse(itemSet);
        if (result === true) {
            return undefined;
        }
        if (searchMode === "all") {
            return foundItems;
        }
        return result;
    }
    getSubItems(item) {
        if (item === undefined) {
            return this.data.Items;
        }
        if (item.ItemType === IGXB.EItemType.Article) {
            return item.SubItems || [];
        }
        if (item.ItemType === IGXB.EItemType.Group ||
            item.ItemType === IGXB.EItemType.Set) {
            return item.Items;
        }
        return [];
    }
    /** Finds the index of an item in a given item set.
     *  @param uid: ID of an item (any type)
     *  @param items: list of items to be searched
     *  @returns: zero-based index of the item in the set or undefined if
     * the item is not part of the set.
     */
    findItemIndex(uid, items) {
        return items.findIndex(item => item.UID === uid);
    }
    insertItemIntoParent(item, parentId, beforeId) {
        const tParentItem = this.findItem(parentId);
        if (!tParentItem ||
            (tParentItem.ItemType !== IGXB.EItemType.Group &&
                tParentItem.ItemType !== IGXB.EItemType.Article &&
                tParentItem.ItemType !== IGXB.EItemType.Set)) {
            Logger.error("Invalid BOM Parent ID " + parentId);
            throw new Error("Invalid BOM Parent ID " + parentId);
        }
        if (!this.insertItemIntoSet(tParentItem, item, beforeId)) {
            throw new Error(`Could not insert item into set`);
        }
        return true;
    }
    insertItemIntoSet(newParent, item, beforeId) {
        let newSet;
        if (newParent === undefined) {
            newSet = this.data.Items;
        }
        else if (newParent.ItemType === IGXB.EItemType.Article) {
            newParent.SubItems = newParent.SubItems || [];
            newSet = newParent.SubItems;
        }
        else if (newParent.ItemType === IGXB.EItemType.Group ||
            newParent.ItemType === IGXB.EItemType.Set) {
            newParent.Items = newParent.Items || [];
            newSet = newParent.Items;
        }
        else {
            return false;
        }
        const insertIndex = newSet.findIndex(entry => entry.UID === beforeId);
        if (insertIndex < 0) {
            newSet.push(item);
            return true;
        }
        const oldLength = newSet.length;
        newSet.splice(insertIndex, 0, item);
        if (newSet.length <= oldLength) {
            return false;
        }
        return true;
    }
    findOrCreateAutoGroup(source, manufacturerName = "", programName = "") {
        const existingGroup = this.findByConditions({
            ItemType: IGXB.EItemType.Group,
            Identifier: source,
        });
        if (existingGroup) {
            return existingGroup.UID;
        }
        return this.addItemGroup(manufacturerName + " " + programName, undefined, source);
    }
}
