/**
 * @module botbuilder-ai
 */
/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */
import { Middleware, TurnContext, ActivityTypes } from 'botbuilder';
import * as DateTimeRecognizers from '@microsoft/recognizers-text-date-time';
import * as moment from 'moment';

export interface LocaleConverterSettings {
    toLocale: string,
    fromLocale?: string,
    getUserLocale?: (context: TurnContext) => string,
    setUserLocale?: (context: TurnContext) => Promise<boolean>
}

/**
 * The LocaleConverter converts all locales in a message to a given locale.
 */
export class LocaleConverter implements Middleware {
    private localeConverter: ILocaleConverter;
    private fromLocale: string | undefined;
    private toLocale: string;
    private getUserLocale: ((context: TurnContext) => string) | undefined;
    private setUserLocale: ((context: TurnContext) => Promise<boolean>) | undefined;

    public constructor(settings: LocaleConverterSettings) {
        this.localeConverter = new MicrosoftLocaleConverter();
        this.toLocale = settings.toLocale;
        this.fromLocale = settings.fromLocale;
        this.getUserLocale = settings.getUserLocale;
        this.setUserLocale = settings.setUserLocale;
    }

    /// Incoming activity
    public async onTurn(context: TurnContext, next: () => Promise<void>): Promise<void> {
        if (context.activity.type != ActivityTypes.Message) {
            return next();
        }
        
        if (this.setUserLocale != undefined) {
            let changedLocale = await this.setUserLocale(context);
            if (changedLocale) {
                return Promise.resolve();
            }
        }

        return this.convertLocalesAsync(context)
        .then(() => next());
    }

    private async convertLocalesAsync(context: TurnContext): Promise<void> {
        let message = context.activity;
        let fromLocale: string;
        if (this.fromLocale != undefined) {
            fromLocale = this.fromLocale;
        } else if (this.getUserLocale != undefined) {
            fromLocale = this.getUserLocale(context);
        } else {
            fromLocale = 'en-us';
        }

        return this.localeConverter.convert(message.text, fromLocale, this.toLocale)
        .then(result => {
            message.text = result;
            return Promise.resolve();
        });
    }

    public getAvailableLocales(): Promise<string[]> {
        return this.localeConverter.getAvailableLocales()
        .then(result => Promise.resolve(result));
    }
}

interface ILocaleConverter {
    
    isLocaleAvailable(locale: string): boolean;
    
    convert(message: string, fromLocale: string, toLocale: string): Promise<string>;

    getAvailableLocales(): Promise<string[]>;
}

class MicrosoftLocaleConverter implements ILocaleConverter {

    mapLocaleToFunction: { [id: string] : DateAndTimeLocaleFormat } = {};

    constructor() {
        this.initLocales();
    }

    private initLocales() {
        let yearMonthDay = new DateAndTimeLocaleFormat('hh:mm', 'yyyy-MM-dd');
        let dayMonthYear = new DateAndTimeLocaleFormat('hh:mm', 'dd/MM/yyyy');
        let monthDayYEar = new DateAndTimeLocaleFormat('hh:mm', 'MM/dd/yyyy');

        let yearMonthDayLocales = [ "en-za", "en-ie", "en-gb", "en-ca", "fr-ca", "zh-cn", "zh-sg", "zh-hk", "zh-mo", "zh-tw" ];
        yearMonthDayLocales.forEach(locale => {
            this.mapLocaleToFunction[locale] = yearMonthDay;
        });

        let dayMonthYearLocales = [ "en-au", "fr-be", "fr-ch", "fr-fr", "fr-lu", "fr-mc", "de-at", "de-ch", "de-de", "de-lu", "de-li" ];
        dayMonthYearLocales.forEach(locale => {
            this.mapLocaleToFunction[locale] = dayMonthYear;
        });

        this.mapLocaleToFunction["en-us"] = monthDayYEar;
    }

    isLocaleAvailable(locale: string): boolean {
        return !(typeof this.mapLocaleToFunction[locale] === "undefined")
    }

    private extractDates(message: string, fromLocale:string): TextAndDateTime[] {
        let fndDates: string[];
        let culture = DateTimeRecognizers.Culture.English;
        if (fromLocale.startsWith("fr")) {
            culture = DateTimeRecognizers.Culture.French;
        } else if (fromLocale.startsWith("pt"))  {
            culture = DateTimeRecognizers.Culture.Portuguese;
        } else if (fromLocale.startsWith("zh"))  {
            culture = DateTimeRecognizers.Culture.Chinese;
        } else if (fromLocale.startsWith("es")) {
            culture = DateTimeRecognizers.Culture.Spanish;
        } else if(!fromLocale.startsWith("en")) {
            throw new Error("Unsupported from locale");
        }

        let model = new DateTimeRecognizers.DateTimeRecognizer(culture).getDateTimeModel();
        let results = model.parse(message);
        
        let foundDates: TextAndDateTime[] = [];
        results.forEach(result => {
            let curDateTimeText: TextAndDateTime;
            let momentTime: Date;
            let momentTimeEnd: Date;
            let foundType: string;
            let resolutionValues = result.resolution["values"][0];
            let type = result.typeName.replace('datetimeV2.', '');
            if (type.includes('range')) {
                if (type.includes('date') && type.includes('time')) {
                    momentTime = moment(resolutionValues["start"]).toDate();
                    momentTimeEnd = moment(resolutionValues["end"]).toDate();
                    foundType = 'datetime';
                } else if (type.includes('date')) {
                    momentTime = moment(resolutionValues["start"]).toDate();
                    momentTimeEnd = moment(resolutionValues["end"]).toDate();
                    foundType = 'date';
                } else { // Must be a time-only result with no date
                    momentTime = new Date();
                    momentTime.setHours(parseInt(String(resolutionValues['start']).substr(0, 2)));
                    momentTime.setMinutes(parseInt(String(resolutionValues['start']).substr(3, 2)));
                    
                    momentTimeEnd = new Date();
                    momentTimeEnd.setHours(parseInt(String(resolutionValues['end']).substr(0, 2)));
                    momentTimeEnd.setMinutes(parseInt(String(resolutionValues['end']).substr(3, 2)));
                    foundType = 'time';
                }

                curDateTimeText = {
                    text: new RegExp(`\\b${result.text}\\b`, "gi"),
                    dateTimeObj: momentTime,
                    endDateTimeObj: momentTimeEnd,
                    type: foundType,
                    range: true
                }
            } else {
                if (type.includes('date') && type.includes('time')) {
                    momentTime = moment(resolutionValues["value"]).toDate();
                    foundType = 'datetime';
                } else if (type.includes('date')) {
                    momentTime = moment(resolutionValues["value"]).toDate();
                    foundType = 'date';
                } else { // Must be a time-only result with no date
                    momentTime = new Date();
                    momentTime.setHours(parseInt(String(resolutionValues['value']).substr(0, 2)));
                    momentTime.setMinutes(parseInt(String(resolutionValues['value']).substr(3, 2)));
                    foundType = 'time';
                }

                curDateTimeText = {
                    text: new RegExp(`\\b${result.text}\\b`, "gi"),
                    dateTimeObj: momentTime,
                    type: foundType,
                    range: false
                }
            }
            
            foundDates.push(curDateTimeText);
        });
        return foundDates;
    }

    private formatDate(date: Date, toLocale: string): string {
        return this.mapLocaleToFunction[toLocale].dateFormat
                .replace('yyyy', (date.getFullYear()).toLocaleString(undefined, {minimumIntegerDigits: 4}).replace(',', ''))
                .replace('MM', (date.getMonth() + 1).toLocaleString(undefined, {minimumIntegerDigits: 2}))
                .replace('dd', (date.getDate()).toLocaleString(undefined, {minimumIntegerDigits: 2}));
    }

    private formatTime(date: Date, toLocale: string): string {
        return this.mapLocaleToFunction[toLocale].timeFormat
                .replace('hh', (date.getHours()).toLocaleString(undefined, {minimumIntegerDigits: 2}))
                .replace('mm', (date.getMinutes()).toLocaleString(undefined, {minimumIntegerDigits: 2}));
    }

    private formatDateAndTime(date: Date, toLocale: string): string {
        return `${this.formatDate(date, toLocale)} ${this.formatTime(date, toLocale)}`
    }

    convert(message: string, fromLocale: string, toLocale: string): Promise<string> {

        if (!this.isLocaleAvailable(toLocale)) {
           return Promise.reject(`Unsupported to locale ${toLocale}`);
        }


        try {
            let dates: TextAndDateTime[] = this.extractDates(message, fromLocale);
            let processedMessage = message;
            dates.forEach(date => {
                if (date.range) {
                    if (date.type == 'time') {
                        let convertedStartDate = this.formatTime(date.dateTimeObj, toLocale);
                        let convertedEndDate = this.formatTime(date.endDateTimeObj, toLocale);
                        processedMessage = processedMessage.replace(date.text, `${convertedStartDate} - ${convertedEndDate}`);
                    } else if (date.type == 'date') { 
                        let convertedStartDate = this.formatDate(date.dateTimeObj, toLocale);
                        let convertedEndDate = this.formatDate(date.endDateTimeObj, toLocale);
                        processedMessage = processedMessage.replace(date.text, `${convertedStartDate} - ${convertedEndDate}`);
                    } else {
                        let convertedStartDate = this.formatDateAndTime(date.dateTimeObj, toLocale);
                        let convertedEndDate = this.formatDateAndTime(date.endDateTimeObj, toLocale);
                        processedMessage = processedMessage.replace(date.text, `${convertedStartDate} - ${convertedEndDate}`);
                    }
                } else {
                    if (date.type == 'time') {
                        let convertedDate = this.formatTime(date.dateTimeObj, toLocale);
                        processedMessage = processedMessage.replace(date.text, convertedDate);
                    } else if (date.type == 'date') { 
                        let convertedDate = this.formatDate(date.dateTimeObj, toLocale);
                        processedMessage = processedMessage.replace(date.text, convertedDate);
                    } else {
                        let convertedDateTime = this.formatDateAndTime(date.dateTimeObj, toLocale);
                        processedMessage = processedMessage.replace(date.text, convertedDateTime);
                    }
                }
                
            });
            return Promise.resolve(processedMessage);
        }
        catch(e) {
            return Promise.reject(e);
        }
        
    }

    getAvailableLocales(): Promise<string[]> {
        return Promise.resolve(Object.keys(this.mapLocaleToFunction));
    }
}

class DateAndTimeLocaleFormat { 
    public timeFormat: string;
    public dateFormat: string;

    constructor(timeFormat: string, dateFormat: string) {
        this.timeFormat = timeFormat;
        this.dateFormat = dateFormat;
    }
}

class TextAndDateTime { 
    public text: RegExp;
    public dateTimeObj: Date;
    public type: string;
    public endDateTimeObj?: Date;
    public range: boolean
}