import { ClientListener } from "../client/ClientListener";
import { StreamConsumer } from '../io/StreamConsumer';
import { StreamProducer } from '../io/StreamProducer';
import {
    ClientType,
    Dialog,
    DialogRedirection,
    Form,
    Login,
    NullRedirection,
    PropertyFormatter,
    Redirection,
    RedirectionUtil,
    Session,
    TypeNames,
    WorkbenchAction
} from '../models';
import { LargeProperty } from '../models/LargeProperty';
import { ReadLargePropertyParameters } from '../models/ReadLargePropertyParameters';
import { DialogProxy } from '../proxy/DialogProxy';
import { CvLocale } from '../util/CvLocale';
import { Log } from '../util/Log';
import { ObjUtil } from '../util/ObjUtil';
import {SessionTimer} from "../util/SessionTimer";
import { CatavoltApi } from './CatavoltApi';
import { DialogApi } from './DialogApi';
import { DialogService } from './DialogService';

/**
 * Top-level entry point into the Catavolt API
 */
export class CatavoltApiImpl implements CatavoltApi {
    private static _singleton: CatavoltApiImpl;

    private static ONE_HOUR_IN_MILLIS: number = 60 * 60 * 1000;
    private static CHECK_SESSION_INTERVAL_MILLIS: number = 30 * 1000;
    // defaults
    private static SERVER_URL: string = 'https://dialog.hxgn-api.net';
    private static SERVER_VERSION = 'v0';

    public readonly DEFAULT_LOCALE: CvLocale = new CvLocale('en');

    public dataLastChangedTime: Date = new Date();
    private _dialogApi: DialogApi;
    private _session: Session;
    private _devicePropsDynamic: { [index: string]: () => string };
    private _devicePropsStatic: { [index: string]: string };
    private _locale: CvLocale = null;
    private _sessionTimer:SessionTimer = null;
    private _onSessionExpiration = null;

    /* ********************
            Statics
     *********************** */

    /**
     * Get the default session time
     * @returns {number}
     */
    public static get defaultTTLInMillis(): number {
        return CatavoltApiImpl.ONE_HOUR_IN_MILLIS;
    }

    /**
     * Get the singleton instance of the CatavoltApiImpl
     * @returns {CatavoltApiImpl}
     */
    static get singleton(): CatavoltApiImpl {
        if (!CatavoltApiImpl._singleton) {
            CatavoltApiImpl._singleton = new CatavoltApiImpl(
                CatavoltApiImpl.SERVER_URL,
                CatavoltApiImpl.SERVER_VERSION
            );
        }
        return CatavoltApiImpl._singleton;
    }

    /**
     * Construct an CatavoltApiImpl
     * This should not be called directly, instead use the 'singleton' method
     * @private
     */
    private constructor(serverUrl: string, serverVersion: string) {
        if (CatavoltApiImpl._singleton) {
            throw new Error('Singleton instance already created');
        }
        this._devicePropsStatic = {};
        this._devicePropsDynamic = {};

        this._sessionTimer = new SessionTimer(()=>this.remainingSessionTime,
                                              CatavoltApiImpl.CHECK_SESSION_INTERVAL_MILLIS,
                                              ()=>{ this.logout().then(()=>{
                                                  this._onSessionExpiration && this._onSessionExpiration();
                                              }); });

        this.initDialogApi(serverUrl, serverVersion);

        CatavoltApiImpl._singleton = this;
    }

    /* *****************
        Public Ops
       ******************* */

    /**
     * Add or replace a dynamic device property (func)
     * @param propName
     * @param propFn
     */
    public addDynamicDeviceProp(propName: string, propFn: () => string): void {
        this._devicePropsDynamic[propName] = propFn;
    }

    /**
     * Add or replace a static device property
     *
     * @param propName
     * @param propValue
     */
    public addStaticDeviceProp(propName: string, propValue: string): void {
        this._devicePropsStatic[propName] = propValue;
    }

    /**
     *
     * @param {ClientListener} clientListener
     */
    public addClientListener(clientListener:ClientListener):void {
       this._dialogApi.addClientListener(clientListener, this.locale);
    }

    /**
     * Get the preferred locale
     * @returns {CvLocale}
     */
    get locale(): CvLocale {
        if (!this._locale) {
            const defaultLocale = this.session.tenantProperties.browserLocale;
            if (defaultLocale) {
                try {
                    const localeJson = JSON.parse(defaultLocale);
                    if (localeJson.language) {
                        this._locale = new CvLocale(localeJson.language, localeJson.country);
                    }
                } catch (err) {
                    this._locale = this.DEFAULT_LOCALE;
                }
            }
        }

        if (!this._locale) {
            this._locale = this.DEFAULT_LOCALE;
        }

        return this._locale;
    }

    set locale(locale: CvLocale) {
        this._locale = locale;
    }

    /*@TODO*/
    public changePasswordAndLogin(
        tenantId: string,
        clientType: ClientType,
        userId: string,
        existingPassword: string,
        newPassword: string
    ): Promise<Session | Redirection> {
        return Promise.reject(new Error('Not Yet Implemented'));
    }

    /**
     * Get the number of millis that the client will remain active between calls
     * to the server.
     * @returns {number}
     */
    get clientTimeoutMillis(): number {
        const mins = this.session.tenantProperties.clientTimeoutMinutes;
        return mins ? Number(mins) * 60 * 1000 : CatavoltApiImpl.defaultTTLInMillis;
    }

    /**
     * Get the currency symbol override if defined from the server.
     * @returns {string}
     */
    get currencySymbol(): string {
        const currencySymbol = this.session.tenantProperties.currencySymbol;
        return currencySymbol ? currencySymbol : null;
    }

    /**
     * Get the device props
     * @returns {{[p: string]: string}}
     */
    get deviceProps(): { [index: string]: string } {
        const newProps: { [index: string]: string } = ObjUtil.addAllProps(this._devicePropsStatic, {});
        for (const attr in this._devicePropsDynamic) {
            if (this._devicePropsDynamic.hasOwnProperty(attr)) {
                newProps[attr] = this._devicePropsDynamic[attr]();
            }
        }
        return newProps;
    }

    /**
     * Get the DialogApi instance
     * @returns {DialogApi}
     */
    get dialogApi(): DialogApi {
        return this._dialogApi;
    }

    /**
     * Initialize a dialog service implementation for use by this CatavoltApiImpl
     *
     * @param serverVersion
     * @param serverUrl
     */
    public initDialogApi(serverUrl: string, serverVersion: string = CatavoltApiImpl.SERVER_VERSION): void {
        this._dialogApi = new DialogService(new DialogProxy(), serverUrl, serverVersion);
    }

    /**
     * Check for the availability of the given featureSet
     * @see FeatureSet
     * @param featureSet
     * @returns {boolean}
     */
    public isFeatureSetAvailable(featureSet: FeatureSet): boolean {
        try {
            const currentVersion = AppVersion.getAppVersion(this.session.serverVersion);
            const featureMinimumVersion = FeatureVersionMap[featureSet];
            return featureMinimumVersion.isLessThanOrEqualTo(currentVersion);
        } catch (error) {
            Log.error('Failed to compare appVersions for feature ' + featureSet);
            Log.error(error);
            return false;
        }
    }

    /**
     * Checked logged in status
     * @returns {boolean}
     */
    get isLoggedIn() {
        return !!this._session;
    }

    /**
     * Check offline status
     *
     * @param tenantId
     */
    public isAnyUserInBriefcaseMode(tenantId: string): Promise<boolean> {
        return this.dialogApi.isAnyUserInBriefcaseMode(tenantId);
    }

    /**
     * Check offline status
     *
     * @param userInfo
     * @returns {Promise<boolean>}
     */
    public isUserInBriefcaseMode(userInfo:{}): Promise<boolean> {
        return this.dialogApi.isUserInBriefcaseMode(userInfo);
    }

    /**
     * Log in and retrieve the Session
     *
     * @param tenantId
     * @param clientType
     * @param userId
     * @param password
     *
     * @returns {Promise<Session | Redirection>}
     */
    public login(
        tenantId: string,
        clientType: ClientType,
        userId: string,
        password: string
    ): Promise<Session | Redirection> {

        if (this.isLoggedIn) {
            return this.logout().then(result => this.processLogin(tenantId, clientType, userId, password));
        } else {
            return this.processLogin(tenantId, clientType, userId, password);
        }
    }

    public loginWithToken(
        tenantId: string,
        clientType: ClientType,
        permissionToken: string,
        proofKey: string
    ): Promise<Session | Redirection> {
        if (this.isLoggedIn) {
            return this.logout().then(result =>
                this.processLogin(tenantId, clientType, null, null, permissionToken, proofKey)
            );
        } else {
            return this.processLogin(tenantId, clientType, null, null, permissionToken, proofKey);
        }
    }

    /**
     * Logout and destroy the session
     * @returns {{sessionId:string}}
     */
    public logout(): Promise<{ sessionId: string }> {
        if (this.isLoggedIn) {
            const sessionId = this.session.id;
            return this.dialogApi
                .deleteSession(this.session.tenantId, this.session.id)
                .then(result => {
                    this._session = null;
                    this._sessionTimer.stopSessionTimer();
                    return result;
                })
                .catch(error => {
                    Log.error(`Error logging out ${error}`);
                    this._session = null;
                    this._sessionTimer.stopSessionTimer();
                    return { sessionId };
                });
        } else {
            return Promise.resolve({ sessionId: null });
        }
    }

    public openDialogWithId(dialogId: string): Promise<Dialog> {
        return this.dialogApi.getDialog(this.session.tenantId, this.session.id, dialogId).then((dialog: Dialog) => {
            dialog.initialize(this);
            if (dialog.view instanceof Form) {
                return dialog;
            } else {
                throw new Error(`Unexpected top-level dialog view type: ${dialog.view.type}`);
            }
        });
    }

    public openDialog(redirection: DialogRedirection): Promise<Dialog> {
        return this.openDialogWithId(redirection.dialogId);
    }

    /**
     * Not yet implemented
     * @param {string} url
     * @returns {Promise<StreamProducer>}
     */
    public openStream(url: string): Promise<StreamProducer> {
        // return this.dialogApi.streamUrl(null, null, url);
        throw Error('not yet implemented');
    }

    public toDialogOrRedirection(resultPr: Promise<{}>): Promise<Dialog | Redirection> {
        return resultPr.then((actionResult: {}) => {
            if (RedirectionUtil.isDialogRedirection(actionResult)) {
                return this.openDialog(actionResult as DialogRedirection) as Promise<Dialog | Redirection>;
            } else if (RedirectionUtil.isRedirection(actionResult)) {
                return Promise.resolve(actionResult) as Promise<Dialog | Redirection>;
            } else {
                // @TODO - this shouldn't be a null redirection - what should it be?
                return Promise.resolve(actionResult as NullRedirection);
            }
        });
    }

    public getRedirection(redirectionId: string): Promise<Redirection> {
        return this.dialogApi.getRedirection(this.session.tenantId, this.session.id, redirectionId);
    }

    /**
     * Open a {@link WorkbenchAction}
     * @param workbenchAction
     * @returns {Promise<{actionId:string} | Redirection>}
     */
    public performWorkbenchAction(workbenchAction: WorkbenchAction): Promise<Redirection> {
        return this.performWorkbenchActionForId(workbenchAction.workbenchId, workbenchAction.id);
    }

    /**
     * Open a {@link WorkbenchWorkbenchAction}
     * @param workbenchId
     * @param workbenchActionId
     * @returns {Promise<{actionId:string} | Redirection>}
     */
    public performWorkbenchActionForId(workbenchId: string, workbenchActionId: string): Promise<Redirection> {
        if (!this.isLoggedIn) {
            return Promise.reject(new Error('User is not logged in'));
        }

        return this.dialogApi.performWorkbenchAction(
            this.session.tenantId,
            this.session.id,
            workbenchId,
            workbenchActionId
        );
    }

    /**
     * Refresh the CatavoltApiImpl
     *
     * @returns {Promise<Session>}
     */
    public refreshSession(tenantId: string, sessionId: string): Promise<Session> {
        return this.dialogApi.getSession(tenantId, sessionId).then(session => {
            this._session = session;
            return session;
        });
    }

    /**
     * Time remaining before this session is expired by the server
     * @returns {number}
     */
    get remainingSessionTime(): number {
        return this.clientTimeoutMillis - (new Date().getTime() - this.dialogApi.lastServiceActivity.getTime());
    }

    /**
     *
     * @param {ClientListener} clientListener
     */
    public removeClientListener(clientListener:ClientListener): void {
        this._dialogApi.removeClientListener(clientListener);
    }

    /**
     * Get the Session
     * @returns {Session}
     */
    get session(): Session {
        return this._session;
    }

    /**
     * Return whether or not the session has expired
     * @returns {boolean}
     */
    get sessionHasExpired(): boolean {
        return this.remainingSessionTime < 0;
    }

    /**
     * Function that will be notified when the session expires
     * @param {() => void} onSessionExpiration
     */
    set onSessionExpiration(onSessionExpiration:()=>void) {
        this._onSessionExpiration = onSessionExpiration;
    }

    /**
     *
     * @param {string} contentId
     * @param {StreamConsumer} streamConsumer
     * @returns {Promise<LargeProperty>}
     */
    public streamContent(contentId: string, streamConsumer: StreamConsumer): Promise<LargeProperty> {
        return Dialog.loadLargeProperty((params: ReadLargePropertyParameters): Promise<LargeProperty> => {
            return this.dialogApi.getContent(this.session.tenantId, this.session.id, contentId, params);
        }, streamConsumer);
    }

    private processLogin(
        tenantId: string,
        clientType: ClientType,
        userId: string,
        password: string,
        permissionToken?: string,
        proofKey?: string
    ): Promise<Session | Redirection> {
        const login: Login = {
            userId,
            password,
            permissionToken,
            proofKey,
            clientType,
            deviceProperties: this.deviceProps,
            type: TypeNames.LoginTypeName
        };

        return this.dialogApi.createSession(tenantId, login).then((result: Session | Redirection) => {
            if (result.type === TypeNames.SessionTypeName) {
                this._session = result as Session;
                this._sessionTimer.resetSessionTimer();
                return result;
            } else {
                return result;
            }
        });
    }
}

class AppVersion {
    public static getAppVersion(versionString: string): AppVersion {
        const [major, minor, patch] = versionString.split('.');
        return new AppVersion(Number(major || 0), Number(minor || 0), Number(patch || 0));
    }

    constructor(public major: number, public minor: number, public patch: number) {}

    /**
     * Is 'this' version less than or equal to the supplied version?
     * @param anotherVersion - the version to compare to 'this' version
     * @returns {boolean}
     */
    public isLessThanOrEqualTo(anotherVersion: AppVersion): boolean {
        if (anotherVersion.major > this.major) {
            return true;
        } else if (anotherVersion.major === this.major) {
            if (anotherVersion.minor > this.minor) {
                return true;
            } else if (anotherVersion.minor === this.minor) {
                return anotherVersion.patch >= this.patch;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }
}

export type FeatureSet = 'View_Support' | 'Unified_Search';
const FeatureVersionMap: { [featureSet: string]: AppVersion } = {
    View_Support: AppVersion.getAppVersion('1.3.447'),
    Unified_Search: AppVersion.getAppVersion('1.3.463')
};

export const Catavolt: CatavoltApi = CatavoltApiImpl.singleton;
export const propertyFormatter: PropertyFormatter = PropertyFormatter.singleton(Catavolt);
