import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
import { create } from 'xmlbuilder2';
import Dataset from '#models/dataset';
import Field from './Field.js';
import BaseModel from '#models/base_model';
import { DateTime } from 'luxon';

export default class Strategy {
    private version: number;
    private config;
    private xml: XMLBuilder;

    constructor(config: any) {
        this.version = 1.0;
        this.config = config;
    }
    public async createDomDocument(): Promise<XMLBuilder> {
        if (this.config.model === null) {
            throw new Error('No Model given for serialization.');
        }
        // domDocument = create({ version: '1.0', encoding: 'UTF-8', standalone: true }, '<root></root>');

        this.xml = create({ version: '1.0', encoding: 'UTF-8' }, '<Opus></Opus>');
        this.xml.root().att('version', this.version.toString());
        // this.xml.root().att('version', this.getVersion());
        this.xml.root().att('xmlns:xlink', 'http://www.w3.org/1999/xlink');

        await this._mapModel(this.config.model);
        return this.xml; //.end({ prettyPrint: true });
    }

    private async _mapModel(model: Dataset) {
        const fields: Array<string> = await model.describe();
        const excludeFields = this.getConfig().excludeFields;
        let fieldsDiff;

        if (excludeFields.length > 0) {
            fieldsDiff = fields.filter((fieldname) => !excludeFields.includes(fieldname));
        } else {
            fieldsDiff = fields;
        }

        const modelNode: XMLBuilder = this.createModelNode(model);
        // rootNode.appendChild(childNode);

        for (const fieldname of fieldsDiff) {
            const field = model.getField(fieldname);
            this.mapField(field as Field, modelNode);
        }
    }

    private mapField(field: Field, modelNode: XMLBuilder) {
        const modelClass = field.getValueModelClass();
        let fieldValues = field.getValue();

        if (this.config.excludeEmpty) {
            if (
                fieldValues === null ||
                (typeof fieldValues === 'string' && fieldValues.trim() === '') ||
                (Array.isArray(fieldValues) && fieldValues.length === 0)
            ) {
                return;
            }
        }

        if (modelClass === null) {
            this.mapSimpleField(modelNode, field);
        } else {
            // map related models with values:
            // let modelInstance = new modelClass();
            const fieldName = field.getName();

            if (!Array.isArray(fieldValues)) {
                fieldValues = [fieldValues];
            }

            for (const value of fieldValues) {
                const childNode = modelNode.ele(fieldName);
                // rootNode.appendChild(childNode);
                // if a field has no value then there is nothing more to do
                // TODO maybe there must be another solution
                if (value === null) {
                    continue;
                }
                if (modelClass.prototype instanceof BaseModel) {
                    this.mapModelAttributes(value, childNode);
                } else if (modelClass instanceof DateTime) {
                    // console.log('Value is a luxon date');
                    this.mapDateAttributes(value, childNode);
                } else if (Array.isArray(value)) {
                    console.log('Value is an array');
                    // this.mapArrayAttributes(value, childNode);
                }
            }
        }
    }

    private mapDateAttributes(model: DateTime, childNode: XMLBuilder) {
        childNode.att('Year', model.year.toString());
        childNode.att('Month', model.month.toString());
        childNode.att('Day', model.day.toString());
        childNode.att('Hour', model.hour.toString());
        childNode.att('Minute', model.minute.toString());
        childNode.att('Second', model.second.toString());
        childNode.att('UnixTimestamp', model.toUnixInteger().toString());
        let zoneName = model.zoneName ? model.zoneName : '';
        childNode.att('Timezone', zoneName);
    }

    private mapModelAttributes(myObject: any, childNode: XMLBuilder) {
        Object.keys(myObject).forEach((prop) => {
            let value = myObject[prop];
            // console.log(`${prop}: ${value}`);
            if (value != null) {
                if (value instanceof DateTime) {
                    value = value.toFormat('yyyy-MM-dd HH:mm:ss').trim();
                } else {
                    value = value.toString().trim();
                }

                // Replace invalid XML-1.0-Characters by UTF-8 replacement character.
                let fieldValues = value.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '\xEF\xBF\xBD ');

                // Create an attribute with the field name and field values
                // const attr = { [fieldName]: fieldValues };

                // Add the attribute to the root element
                childNode.att(prop, fieldValues);
            }
        });
    }

    private mapSimpleField(modelNode: XMLBuilder, field: Field) {
        const fieldName = field.getName();
        let fieldValues = this.getFieldValues(field);

        if (fieldValues != null) {
            // Replace invalid XML-1.0-Characters by UTF-8 replacement character.
            fieldValues = fieldValues.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '\xEF\xBF\xBD ');

            // Create an attribute with the field name and field values
            // const attr = { [fieldName]: fieldValues };

            // Add the attribute to the root element
            modelNode.att(fieldName, fieldValues);
        }
    }

    private getFieldValues(field: any): string {
        let fieldValues: number | string | Array<string> = field.getValue(); //275

        // workaround for simple fields with multiple values
        if (field.hasMultipleValues() === true && Array.isArray(fieldValues)) {
            fieldValues = fieldValues.join(',');
        }
        // Uncomment if needed
        // if (fieldValues instanceof DateTimeZone) {
        //     fieldValues = fieldValues.getName();
        // }

        return fieldValues?.toString().trim();
    }

    private createModelNode(model: Dataset) {
        const className = 'Rdr_' + model.constructor.name.split('\\').pop(); //Rdr_Dataset
        // return dom.createElement(className);
        return this.xml.root().ele(className);
    }

    // private getVersion() {
    //     return Math.floor(this.version);
    // }

    private getConfig() {
        return this.config;
    }
}
