import {ParsedMessagePart, ParsedMessagePartType} from './parsed-message-part';
import {ParsedMessagePartText} from './parsed-message-part-text';
import {ParsedMessagePartPlaceholder} from './parsed-message-part-placeholder';
import {ParsedMessagePartStartTag} from './parsed-message-part-start-tag';
import {ParsedMessagePartEndTag} from './parsed-message-part-end-tag';
import {INormalizedMessage, ValidationErrors} from '../api/i-normalized-message';
import {DOMUtilities} from './dom-utilities';
import {IMessageParser} from './i-message-parser';
import {format, isNullOrUndefined} from 'util';
import {IICUMessage, IICUMessageTranslation} from '../api/i-icu-message';
import {ParsedMessagePartICUMessage} from './parsed-message-part-icu-message';
import {ParsedMessagePartICUMessageRef} from './parsed-message-part-icu-message-ref';
import {ICUMessage} from './icu-message';
import {ParsedMessagePartEmptyTag} from './parsed-message-part-empty-tag';
/**
 * Created by martin on 05.05.2017.
 * A message text read from a translation file.
 * Can contain placeholders, tags, text.
 * This class is a representation independent of the concrete format.
 */
export class ParsedMessage implements INormalizedMessage {

    /**
     * Parser that created this message (determines the native format).
     */
    private _parser: IMessageParser;

    /**
     * The message where this one stems from as translation.
     * Optional, set only for messages created by calling translate.
     */
    private sourceMessage: ParsedMessage;

    /**
     * The parts of the message.
     */
    private _parts: ParsedMessagePart[];

    /**
     * messages xml representation.
     */
    private _xmlRepresentation: Element;

    constructor(parser: IMessageParser, sourceMessage: ParsedMessage) {
        this._parser = parser;
        this.sourceMessage = sourceMessage;
        this._parts = [];
    }

    /**
     * Get the parser (for tests only, not part of API)
     * @return parser
     */
    getParser(): IMessageParser {
        return this._parser;
    }

    /**
     * Create a new normalized message as a translation of this one.
     * @param normalizedString the translation in normalized form.
     * If the message is an ICUMessage (getICUMessage returns a value), use translateICUMessage instead.
     * @throws an error if normalized string is not well formed.
     * Throws an error too, if this is an ICU message.
     */
    translate(normalizedString: string): INormalizedMessage {
        if (isNullOrUndefined(this.getICUMessage())) {
            return this._parser.parseNormalizedString(<string> normalizedString, this);
        } else {
            throw new Error(format('cannot translate ICU message with simple string, use translateICUMessage() instead ("%s", "%s")',
                normalizedString, this.asNativeString()));
        }
    }

    /**
     * Create a new normalized icu message as a translation of this one.
     * @param icuTranslation the translation, this is the translation of the ICU message,
     * which is not a string, but a collections of the translations of the different categories.
     * The message must be an ICUMessage (getICUMessage returns a value)
     * @throws an error if normalized string is not well formed.
     * Throws an error too, if this is not an ICU message.
     */
    translateICUMessage(icuTranslation: IICUMessageTranslation): INormalizedMessage {
        const icuMessage: IICUMessage = this.getICUMessage();
        if (isNullOrUndefined(icuMessage)) {
            throw new Error(format('this is not an ICU message, use translate() instead ("%s", "%s")',
                icuTranslation,  this.asNativeString()));
        } else {
            const translatedICUMessage: IICUMessage = icuMessage.translate(icuTranslation);
            return this._parser.parseICUMessage(translatedICUMessage.asNativeString(), this);
        }
    }

    /**
     * Create a new normalized message from a native xml string as a translation of this one.
     * @param nativeString xml string in the format of the underlying file format.
     * Throws an error if native string is not acceptable.
     */
    translateNativeString(nativeString: string): INormalizedMessage {
        return this._parser.createNormalizedMessageFromXMLString(nativeString, this);
    }

    /**
     * normalized message as string.
     * @param displayFormat optional way to determine the exact syntax.
     * Allowed formats are defined as constants NORMALIZATION_FORMAT...
     */
    public asDisplayString(displayFormat?: string) {
        return this._parts.map((part) => part.asDisplayString(displayFormat)).join('');
    }

    /**
     * Returns the message content as format dependent native string.
     * Includes all format specific markup like <ph id="INTERPOLATION" ../> ..
     */
    asNativeString(): string {
        if (isNullOrUndefined(this.getICUMessage())) {
            return DOMUtilities.getXMLContent(this._xmlRepresentation);
        } else {
            return this.getICUMessage().asNativeString();
        }
    }

    /**
     * Validate the message.
     * @return null, if ok, error object otherwise.
     */
    public validate(): ValidationErrors | null {
        let hasErrors = false;
        const errors: ValidationErrors = {};
        let e;
        e = this.checkPlaceholderAdded();
        if (!isNullOrUndefined(e)) {
            errors.placeholderAdded = e;
            hasErrors = true;
        }
        e = this.checkICUMessageRefRemoved();
        if (!isNullOrUndefined(e)) {
            errors.icuMessageRefRemoved = e;
            hasErrors = true;
        }
        e = this.checkICUMessageRefAdded();
        if (!isNullOrUndefined(e)) {
            errors.icuMessageRefAdded = e;
            hasErrors = true;
        }
        return hasErrors ? errors : null;
    }

    /**
     * Validate the message, check for warnings only.
     * A warning shows, that the message is acceptable, but misses something.
     * E.g. if you remove a placeholder or a special tag from the original message, this generates a warning.
     * @return null, if no warning, warnings as error object otherwise.
     */
    validateWarnings(): ValidationErrors | null {
        let hasWarnings = false;
        const warnings: ValidationErrors = {};
        let w;
        w = this.checkPlaceholderRemoved();
        if (!isNullOrUndefined(w)) {
            warnings.placeholderRemoved = w;
            hasWarnings = true;
        }
        w = this.checkTagRemoved();
        if (!isNullOrUndefined(w)) {
            warnings.tagRemoved = w;
            hasWarnings = true;
        }
        w = this.checkTagAdded();
        if (!isNullOrUndefined(w)) {
            warnings.tagAdded = w;
            hasWarnings = true;
        }
        return hasWarnings ? warnings : null;
    }

    /**
     * Test wether this message is an ICU message.
     * @return true, if it is an ICU message.
     */
    isICUMessage(): boolean {
        return this._parts.length === 1 && this._parts[0].type === ParsedMessagePartType.ICU_MESSAGE;
    }

    /**
     * Test wether this message contains an ICU message reference.
     * ICU message references are something like <x ID="ICU"../>.
     * @return true, if there is an ICU message reference in the message.
     */
    containsICUMessageRef(): boolean {
        return this._parts.findIndex(part => part.type === ParsedMessagePartType.ICU_MESSAGE_REF) >= 0;
    }

    /**
     * If this message is an ICU message, returns its structure.
     * Otherwise this method returns null.
     * @return ICUMessage or null.
     */
    public getICUMessage(): IICUMessage {
        if (this._parts.length === 1 && this._parts[0].type === ParsedMessagePartType.ICU_MESSAGE) {
            const icuPart = <ParsedMessagePartICUMessage> this._parts[0];
            return icuPart.getICUMessage();
        } else {
            return null;
        }
    }


    /**
     * Check for added placeholder.
     * @return null or message, if fulfilled.
     */
    private checkPlaceholderAdded(): any {
        let e = null;
        const suspiciousIndexes = [];
        if (this.sourceMessage) {
            const sourcePlaceholders = this.sourceMessage.allPlaceholders();
            const myPlaceholders = this.allPlaceholders();
            myPlaceholders.forEach((index) => {
                if (!sourcePlaceholders.has(index)) {
                    suspiciousIndexes.push(index);
                }
            });
        }
        if (suspiciousIndexes.length === 1) {
            e = 'added placeholder ' + suspiciousIndexes[0] + ', which is not in original message';
        } else if (suspiciousIndexes.length > 1) {
            let allSuspiciousIndexes = '';
            let first = true;
            suspiciousIndexes.forEach((index) => {
                if (!first) {
                    allSuspiciousIndexes = allSuspiciousIndexes + ', ';
                }
                allSuspiciousIndexes = allSuspiciousIndexes + index;
                first = false;
            });
            e = 'added placeholders ' + allSuspiciousIndexes + ', which are not in original message';
        }
        return e;
    }

    /**
     * Check for removed placeholder.
     * @return null or message, if fulfilled.
     */
    private checkPlaceholderRemoved(): any {
        let w = null;
        const suspiciousIndexes = [];
        if (this.sourceMessage) {
            const sourcePlaceholders = this.sourceMessage.allPlaceholders();
            const myPlaceholders = this.allPlaceholders();
            sourcePlaceholders.forEach((index) => {
                if (!myPlaceholders.has(index)) {
                    suspiciousIndexes.push(index);
                }
            });
        }
        if (suspiciousIndexes.length === 1) {
            w = 'removed placeholder ' + suspiciousIndexes[0] + ' from original message';
        } else if (suspiciousIndexes.length > 1) {
            let allSuspiciousIndexes = '';
            let first = true;
            suspiciousIndexes.forEach((index) => {
                if (!first) {
                    allSuspiciousIndexes = allSuspiciousIndexes + ', ';
                }
                allSuspiciousIndexes = allSuspiciousIndexes + index;
                first = false;
            });
            w = 'removed placeholders ' + allSuspiciousIndexes + ' from original message';
        }
        return w;
    }

    /**
     * Check for added ICU Message Refs.
     * @return null or message, if fulfilled.
     */
    private checkICUMessageRefAdded(): any {
        let e = null;
        const suspiciousIndexes = [];
        if (this.sourceMessage) {
            const sourceICURefs = this.sourceMessage.allICUMessageRefs();
            const myICURefs = this.allICUMessageRefs();
            myICURefs.forEach((index) => {
                if (!sourceICURefs.has(index)) {
                    suspiciousIndexes.push(index);
                }
            });
        }
        if (suspiciousIndexes.length === 1) {
            e = 'added ICU message reference ' + suspiciousIndexes[0] + ', which is not in original message';
        } else if (suspiciousIndexes.length > 1) {
            let allSuspiciousIndexes = '';
            let first = true;
            suspiciousIndexes.forEach((index) => {
                if (!first) {
                    allSuspiciousIndexes = allSuspiciousIndexes + ', ';
                }
                allSuspiciousIndexes = allSuspiciousIndexes + index;
                first = false;
            });
            e = 'added ICU message references ' + allSuspiciousIndexes + ', which are not in original message';
        }
        return e;
    }

    /**
     * Check for removed ICU Message Refs.
     * @return null or message, if fulfilled.
     */
    private checkICUMessageRefRemoved(): any {
        let e = null;
        const suspiciousIndexes = [];
        if (this.sourceMessage) {
            const sourceICURefs = this.sourceMessage.allICUMessageRefs();
            const myICURefs = this.allICUMessageRefs();
            sourceICURefs.forEach((index) => {
                if (!myICURefs.has(index)) {
                    suspiciousIndexes.push(index);
                }
            });
        }
        if (suspiciousIndexes.length === 1) {
            e = 'removed ICU message reference ' + suspiciousIndexes[0] + ' from original message';
        } else if (suspiciousIndexes.length > 1) {
            let allSuspiciousIndexes = '';
            let first = true;
            suspiciousIndexes.forEach((index) => {
                if (!first) {
                    allSuspiciousIndexes = allSuspiciousIndexes + ', ';
                }
                allSuspiciousIndexes = allSuspiciousIndexes + index;
                first = false;
            });
            e = 'removed ICU message references ' + allSuspiciousIndexes + ' from original message';
        }
        return e;
    }

    /**
     * Get all indexes of placeholders used in the message.
     */
    private allPlaceholders(): Set<number> {
        const result = new Set<number>();
        this.parts().forEach((part) => {
            if (part.type === ParsedMessagePartType.PLACEHOLDER) {
                const index = (<ParsedMessagePartPlaceholder> part).index();
                result.add(index);
            }
        });
        return result;
    }

    /**
     * Return the disp-Attribute of placeholder
     * @param index index of placeholder
     * @return disp or null
     */
    public getPlaceholderDisp(index: number): string {
        let placeHolder: ParsedMessagePartPlaceholder = null;
        this.parts().forEach((part) => {
            if (part.type === ParsedMessagePartType.PLACEHOLDER) {
                const phPart: ParsedMessagePartPlaceholder = <ParsedMessagePartPlaceholder> part;
                if (phPart.index() === index) {
                    placeHolder = phPart;
                }
            }
        });
        return placeHolder ? placeHolder.disp() : null;
    }

    /**
     * Get all indexes of ICU message refs used in the message.
     */
    private allICUMessageRefs(): Set<number> {
        const result = new Set<number>();
        this.parts().forEach((part) => {
            if (part.type === ParsedMessagePartType.ICU_MESSAGE_REF) {
                const index = (<ParsedMessagePartICUMessageRef> part).index();
                result.add(index);
            }
        });
        return result;
    }

    /**
     * Return the disp-Attribute of icu message ref
     * @param index of ref
     * @return disp or null
     */
    public getICUMessageRefDisp(index: number): string {
        let icuMessageRefPart: ParsedMessagePartICUMessageRef = null;
        this.parts().forEach((part) => {
            if (part.type === ParsedMessagePartType.ICU_MESSAGE_REF) {
                const refPart: ParsedMessagePartICUMessageRef = <ParsedMessagePartICUMessageRef> part;
                if (refPart.index() === index) {
                    icuMessageRefPart = refPart;
                }
            }
        });
        return icuMessageRefPart ? icuMessageRefPart.disp() : null;
    }

    /**
     * Check for added tags.
     * @return null or message, if fulfilled.
     */
    private checkTagAdded(): any {
        let e = null;
        const suspiciousTags = [];
        if (this.sourceMessage) {
            const sourceTags = this.sourceMessage.allTags();
            const myTags = this.allTags();
            myTags.forEach((tagName) => {
                if (!sourceTags.has(tagName)) {
                    suspiciousTags.push(tagName);
                }
            });
        }
        if (suspiciousTags.length === 1) {
            e = 'added tag <' + suspiciousTags[0] + '>, which is not in original message';
        } else if (suspiciousTags.length > 1) {
            let allSuspiciousTags = '';
            let first = true;
            suspiciousTags.forEach((tag) => {
                if (!first) {
                    allSuspiciousTags = allSuspiciousTags + ', ';
                }
                allSuspiciousTags = allSuspiciousTags + '<' + tag + '>';
                first = false;
            });
            e = 'added tags ' + allSuspiciousTags + ', which are not in original message';
        }
        return e;
    }

    /**
     * Check for removed tags.
     * @return null or message, if fulfilled.
     */
    private checkTagRemoved(): any {
        let w = null;
        const suspiciousTags = [];
        if (this.sourceMessage) {
            const sourceTags = this.sourceMessage.allTags();
            const myTags = this.allTags();
            sourceTags.forEach((tagName) => {
                if (!myTags.has(tagName)) {
                    suspiciousTags.push(tagName);
                }
            });
        }
        if (suspiciousTags.length === 1) {
            w = 'removed tag <' + suspiciousTags[0] + '> from original message';
        } else if (suspiciousTags.length > 1) {
            let allSuspiciousTags = '';
            let first = true;
            suspiciousTags.forEach((tag) => {
                if (!first) {
                    allSuspiciousTags = allSuspiciousTags + ', ';
                }
                allSuspiciousTags = allSuspiciousTags + '<' + tag + '>';
                first = false;
            });
            w = 'removed tags ' + allSuspiciousTags + ' from original message';
        }
        return w;
    }

    /**
     * Get all tag names used in the message.
     */
    private allTags(): Set<string> {
        const result = new Set<string>();
        this.parts().forEach((part) => {
            if (part.type === ParsedMessagePartType.START_TAG || part.type === ParsedMessagePartType.EMPTY_TAG) {
                const tagName = (<ParsedMessagePartStartTag> part).tagName();
                result.add(tagName);
            }
        });
        return result;
    }

    public parts(): ParsedMessagePart[] {
        return this._parts;
    }

    setXmlRepresentation(xmlRepresentation: Element) {
        this._xmlRepresentation = xmlRepresentation;
    }

    addText(text: string) {
        this._parts.push(new ParsedMessagePartText(text));
    }

    addPlaceholder(index: number, disp: string) {
        this._parts.push(new ParsedMessagePartPlaceholder(index, disp));
    }

    addStartTag(tagname: string, idcounter: number) {
        this._parts.push(new ParsedMessagePartStartTag(tagname, idcounter));
    }

    addEndTag(tagname: string) {
        // check if well formed
        const openTag = this.calculateOpenTagName();
        if (!openTag || openTag !== tagname) {
            // oops, not well formed
            throw new Error(format('unexpected close tag %s (currently open is %s, native xml is "%s")',
                tagname, openTag, this.asNativeString()));
        }
        this._parts.push(new ParsedMessagePartEndTag(tagname));
    }

    addEmptyTag(tagname: string, idcounter: number) {
        this._parts.push(new ParsedMessagePartEmptyTag(tagname, idcounter));
    }

    addICUMessageRef(index: number, disp) {
        this._parts.push(new ParsedMessagePartICUMessageRef(index, disp));
    }

    addICUMessage(text: string) {
        this._parts.push(new ParsedMessagePartICUMessage(text, this._parser));
    }

    /**
     * Determine, wether there is an open tag, that is not closed.
     * Returns the latest one or null, if there is no open tag.
     */
    private calculateOpenTagName(): string {
        const openTags = [];
        this._parts.forEach((part) => {
            switch (part.type) {
                case ParsedMessagePartType.START_TAG:
                    openTags.push((<ParsedMessagePartStartTag> part).tagName());
                    break;
                case ParsedMessagePartType.END_TAG:
                    const tagName = (<ParsedMessagePartEndTag> part).tagName();
                    if (openTags.length === 0 || openTags[openTags.length - 1] !== tagName) {
                        // oops, not well formed
                        const openTag = (openTags.length === 0) ? 'nothing' : openTags[openTags.length - 1];
                        throw new Error(format('unexpected close tag %s (currently open is %s, native xml is "%s")',
                            tagName, openTag, this.asNativeString()));
                    }
                    openTags.pop();
            }
        });
        return openTags.length === 0 ? null : openTags[openTags.length - 1];
    }
}
