/**
 * @module node-opcua-enum
 *
 */
// tslint:disable:no-bitwise
// tslint:disable:max-classes-per-file

/**
 * Represents an Item of an Enum.
 */
export class EnumItem {
    public key: string;
    public value: number;

    /**
     *
     * @param key the enum key
     * @param value the enum value
     */
    public constructor(key: string, value: number) {
        this.key = key;
        this.value = value;
    }

    /**
     * Checks if the EnumItem is the same as the passing object.
     * 
     * @param  {EnumItem | String | Number} item The object to check with.
     * @return {Boolean}                          The check result.
     */
    public is(item: EnumItem | string | number): boolean {
        if (item instanceof EnumItem) {
            return this.value === item.value;
        } else if (typeof item === "string") {
            return this.key === item;
        } else {
            return this.value === item;
        }
    }

    /**
     * Checks if the flagged EnumItem has the passing object.
     * 
     * @param  {EnumItem | String |Number} value The object to check with.
     * @return {Boolean}                            The check result.
     */
    public has(value: string | number | EnumItem): boolean {
        if (value instanceof EnumItem) {
            return (value.value & this.value) !== 0;
        } else if (typeof value === "string") {
            return this.key.indexOf(value) >= 0;
        } else {
            return (value & this.value) !== 0;
        }
    }

    /**
     * Returns String representation of this EnumItem.
     * 
     * @return {String} String representation of this EnumItem.
     */
    public toString(): string {
        return this.key;
    }

    /**
     * Returns JSON object representation of this EnumItem.
     * @return {String} JSON object representation of this EnumItem.
     */

    public toJSON(): any {
        return this.key;
    }

    /**
     * Returns the value to compare with.
     * @return {String} The value to compare with.
     */

    public valueOf(): number {
        return this.value;
    }
}

function powerOfTwo(n: number): boolean {
    return n && !(n & (n - 1)) ? true : false;
}
// check if enum is flaggable
function checkIsFlaggable(enums: EnumItem[]): boolean {
    for (const e of enums) {
        const value = +e.value;
        if (isNaN(value)) {
            continue; // skipping none number value
        }
        if (value !== 0 && value !== 1 && !powerOfTwo(value)) {
            return false;
        }
    }
    return true;
}

export interface _TypescriptEnum {
    [key: string | number]: number | string;
}

export function adaptTypescriptEnum(map: _TypescriptEnum | string[]) {
    if (Array.isArray(map)) {
        let mm: _TypescriptEnum | null = null;
        // create map as flaggable enum
        mm = {};
        for (let i = 0; i < map.length; i++) {
            mm[map[i]] = 1 << i;
        }
        return mm;
    }
    return map as _TypescriptEnum;
}

const regexpSignedNumber = /^[-+]?[0-9]+$/;
/**
 * @class Enum
 * @constructor
 * Represents an Enum with enum items.
 * @param {Array || Object}  map     This are the enum items.
 */
export class Enum {
    private readonly enumItems: EnumItem[];
    private readonly _isFlaggable: boolean;

    constructor(map: _TypescriptEnum | string[]) {
        this.enumItems = [];
        let mm: _TypescriptEnum | null = null;
        let isFlaggable = null;
        if (Array.isArray(map)) {
            mm = adaptTypescriptEnum(map);
            isFlaggable = true;
        } else {
            mm = map;
        }

        for (const  [key, val] of Object.entries(mm)) {
            if (typeof val !== "number") {
                continue;
            }
            const kv = new EnumItem(key, val);
            const pThis = this as any;
            pThis[key] = kv;
            pThis[val] = kv;

            this.enumItems.push(kv);
        }

        if (!isFlaggable) {
            isFlaggable = checkIsFlaggable(this.enumItems);
        }
        this._isFlaggable = isFlaggable;
    }

    public get isFlaggable(): boolean {
        return this._isFlaggable;
    }
    /**
     * Returns the appropriate EnumItem.

     * @param  key The object to get with.
     * @return the get result.
     */
    public get(key: EnumItem | string | number): EnumItem | null {
        const pThis = this as any;
        if (key instanceof EnumItem) {
            if (!pThis[key.key]) {
                throw new Error("Invalid key");
            }
            return key;
        }

        if (key === null || key === undefined) {
            return null;
        }
        const prop = pThis[key];
        if (prop) {
            return prop;
        } else if (this._isFlaggable) {
            if (typeof key === "string") {
                return this._getByString(key);
            } else if (typeof key === "number") {
                return this._getByNum(key);
            }
        }
        return null;
    }

    public getDefaultValue(): EnumItem {
        return this.enumItems[0];
    }

    public toString(): string {
        return this.enumItems.join(" , ");
    }

    private _getByString(key: string): EnumItem | null {
        const pThis = this as any;
        const parts = key.split(" | ");

        let val = 0;
        for (const part of parts) {
            const item = pThis[part];
            if (undefined === item) {
                return null;
            }
            val |= item.value;
        }
        const kv = new EnumItem(key, val);

        // add in cache for later
        let prop = pThis[val];
        if (prop === undefined) {
            pThis[val] = kv;
        }
        prop = pThis[key];
        if (prop === undefined) {
            pThis[key] = kv;
        }
        return kv;
    }

    private _getByNum(key: number): EnumItem | null {
        if (key === 0) {
            return null;
        }
        const pThis = this as any;

        let name;
        let c = 1;
        for (let i = 0; c < key; i++) {
            if ((c & key) === c) {
                const item = pThis[c];
                if (undefined === item) {
                    return null;
                }
                if (name) {
                    name = name + " | " + item.key;
                } else {
                    name = item.key;
                }
            }
            c *= 2;
        }
        const kv = new EnumItem(name, key);
        // add in cache for later
        pThis[name] = kv;
        pThis[key] = kv;
        return kv;
    }
}
