/**
 * Created by rburson on 1/6/16.
 */

import * as React from 'react'
import {
    NavRequest,
    AppWinDef,
    AppContext,
    FormContext,
    Log,
    PaneContext,
    StringUtil,
    EntityRec,
    EntityBuffer,
    NullEntityRec,
    EntityRecImpl,
    WebRedirection,
    WorkbenchLaunchAction,
    PropDef, DialogRedirection, DialogHandle, SessionContext, SessionContextImpl, SystemContext, SystemContextImpl,
    Future, ActionSource, ContextAction
} from 'catavolt-sdk'

/*
 ******************************************************************
 * Base Interfaces/Objects
 ******************************************************************
 */

/**
  Base interface for Catavolt component properties
  Components may choose to support any of these properties
 */
export interface CvProps {

    /**
     *  The Catavolt SDK entry point, an instance of the sdk {AppContext} which is always available to components.
     *  A singleton instance is used by the underlying components to interact with the sdk.
     */
    catavolt?:AppContext;

    /**
     *  The {@link CvEventRegistry} handles decoupled communication between components.  Any component may subscribe to and publish
     *  {@link CvEvent}s.  See also {@link CvListener} and {@link CvEventType}
     */
    eventRegistry?:CvEventRegistry;

    /**
     * Allows for a component's rendering logic to be overridden and completely customized.
     * The first argument, the {@link CvContext}, exposes the {@link CvScopeContext} which provides the relevant SDK object
     * for use by the renderer.  {@link CvScopeContext.scopeObj} will always be an SDK object exposing the data related to
     * the current component.
     */
    renderer?:(cvContext:CvContext, callbackObj?:any)=>{}
}

/**
 Base interface for catavolt component state
 */
export interface CvState {
}

/**
 Top-level Catavolt Context Object
 */
export interface CvContext {

    /**
     *  The Catavolt SDK entry point, an instance of the sdk {AppContext} which is always available to components.
     *  A singleton instance is used by the underlying components to interact with the sdk.
     */
    catavolt?:AppContext;

    /**
     *  The {@link CvEventRegistry} handles decoupled communication between components.  Any component may subscribe to and publish
     *  {@link CvEvent}s.  See also {@link CvListener} and {@link CvEventType}
     */
    eventRegistry?:CvEventRegistry;

    /**
     Allows for exposing a 'scope' object to child components
     This is usually the corresponding SDK object
     */
    scopeCtx?:CvScopeContext;
}

/**
    Allows for exposing a 'scope' object to child components
    This is usually the corresponding SDK object
 */
export interface CvScopeContext {
    /**
     * The object relevant to this components 'scope'.  Usually an SDK object corresponding to the component type.
     */
    scopeObj:any;
    /**
     * The parent's scope context (if any)
     * This allows for navigation back up the component hierarchy
     */
    parentScopeCtx:CvScopeContext;
}

/**
 Base Mixin for all catavolt components
 */
export var CvBaseMixin = {

    contextTypes: {
        cvContext: React.PropTypes.object
    },

    childContextTypes: {
        cvContext: React.PropTypes.object
    },

    catavolt: function ():AppContext {
        return this.props.catavolt || (this.context.cvContext && this.context.cvContext.catavolt);
    },

    eventRegistry: function ():CvEventRegistry {
        return this.props.eventRegistry || (this.context.cvContext && this.context.cvContext.eventRegistry);
    },

    findFirstDescendant: function (elem, filter:(o)=>boolean) {
        var result = null;
        if (elem.props && elem.props.children) {
            var elems:Array<any> = React.Children.toArray(elem.props.children);
            for (let i = 0; i < elems.length; i++) {
                const child = elems[i];
                console.log(child);
                if (filter(child)) {
                    result = child;
                } else if (child.props.children) {
                    result = this.findFirstDescendant(child, filter);
                }
            }
        }
        return result ? result : null;
    },

    findAllDescendants: function (elem, filter:(o)=>boolean, results:Array<any> = []):Array<any> {
        if (elem.props && elem.props.children) {
            var elems:Array<any> = React.Children.toArray(elem.props.children);
            for (let i = 0; i < elems.length; i++) {
                const child = elems[i];
                console.log(child);
                if (filter(child)) {
                    results.push(child);
                }
                if (child.props && child.props.children) {
                    this.findAllDescendants(child, filter, results);
                }
            }
        }
        return results;
    },

    findFirstScopeCtx: function (matchFn:(scopeCtx:CvScopeContext)=>boolean, startingCtx?:CvScopeContext):CvScopeContext {
        let scopeCtx = startingCtx || this.scopeCtx();
        let result:CvScopeContext = null;
        while (scopeCtx && !result) {
            if (matchFn(scopeCtx)) {
                result = scopeCtx;
            } else {
                scopeCtx = scopeCtx.parentScopeCtx;
            }
        }
        return result;
    },

    findEntityRec: function (startingContext?:CvScopeContext) {

        if (this.props.entityRec) {
            return this.props.entityRec;
        } else {
            var e:EntityRec = null;
            const scopeCtx:CvScopeContext = this.findFirstScopeCtx((scopeCtx:CvScopeContext)=> {
                return scopeCtx.scopeObj instanceof EntityBuffer ||
                    scopeCtx.scopeObj instanceof EntityRecImpl ||
                    scopeCtx.scopeObj instanceof NullEntityRec;
            }, startingContext)
            return scopeCtx ? scopeCtx.scopeObj : null;
        }
    },

    findPaneContext: function (startingContext:CvScopeContext) {
        if (this.props.paneContext) {
            return this.props.paneContext;
        } else {
            const scopeCtx:CvScopeContext = this.findFirstScopeCtx((scopeCtx:CvScopeContext)=> {
                return scopeCtx.scopeObj instanceof PaneContext;
            }, startingContext)
            return scopeCtx ? (scopeCtx.scopeObj as PaneContext) : null;
        }
    },

    findPaneTitle: function (paneContext:PaneContext) {
        let title = paneContext.paneTitle;
        if (!title) title = paneContext.actionSource instanceof WorkbenchLaunchAction ? (paneContext.actionSource as WorkbenchLaunchAction).name : null;
        if (!title) title = paneContext.paneDef.settings['dialogDescription'];
        if (!title || title === 'null') {
            return '';
        } else {
            return title;
        }
    },

    firstInScope: function (type, startingScopeCtx?:CvScopeContext) {
        const scopeCtx:CvScopeContext = this.findFirstScopeCtx((scopeCtx:CvScopeContext)=> {
            return scopeCtx.scopeObj instanceof type;
        }, startingScopeCtx)
        return scopeCtx ? scopeCtx.scopeObj : null;
    },


    getDefaultChildContext: function () {
        return {
            cvContext: {
                catavolt: this.catavolt(),
                eventRegistry: this.eventRegistry(),
                scopeCtx: {
                    scopeObj: null,
                    parentScopeCtx: this.scopeCtx()
                }
            }
        }
    },

    scopeCtx: function ():CvScopeContext {
        return this.context.cvContext && this.context.cvContext.scopeCtx;
    }

}

/**
 ******************************************************************
 * General Callback mechanism
 ******************************************************************
 */

export interface CvResultCallback<A> {(success:A, error?:any):void
}

/*
 ******************************************************************
 Component Event Registry
 Framework for decoupled communication between our components
 ******************************************************************
 */


export interface CvListener<T> {
    (event:CvEvent<T>):void;
}

export interface CvEvent<T> {
    type:CvEventType;
    eventObj:T;
    resourceId?:string;
}


/**
 *  Enumeration of event types to be used with {@link CvEvent}
 *  There is also a corresponding 'payload' object type for each of these
 *  enum values, that is used as the {@link CvEvent.eventObj} type
 */

export enum CvEventType {
    LOGIN,
    LOGOUT,
    ACTION_FIRED,
    NAVIGATION,
    STATE_CHANGE,
    MESSAGE,
    SESSION_UPDATE
}

/* Event type payloads */

export interface CvLoginResult {
    appWinDef:AppWinDef
}

export interface CvLogoutResult {
    tenantId:string;
}


export enum CvNavigationResultType {
    FORM, URL, NULL
}

export interface CvNavigationResult {
    navRequest:NavRequest;
    workbenchId?:string;
    actionId?:string;
    navTarget?:string;
    sourceIsDestroyed?:boolean;
    noTransition?:boolean;
    type:CvNavigationResultType;
}

export enum CvMessageType {
    ERROR, WARN, INFO
}

export interface CvMessage {
    type:CvMessageType;
    message:string;
    messageObj?:any;
}

export interface CvSessionUpdateResult {
    appWinDef:AppWinDef
}


export enum CvStateChangeType {
    PANE_DEF_CHANGE, DATA_CHANGE, DESTROYED, MODE_CHANGE_READ, MODE_CHANGE_WRITE
}

export interface CvStateChangeResult {
    source?:PaneContext;
    type:CvStateChangeType;
}

export enum CvActionFiredResultType {
    ACTION_STARTED, ACTION_COMPLETED   
}

export interface CvActionFiredResult {
    actionId:string;
    type:CvActionFiredResultType;
    source:PaneContext;
    clientAction:boolean;
}

/**
 *  The {@link CvEventRegistry} handles decoupled communication between components.  Any component may subscribe to and publish
 *  {@link CvEvent}s.  See also {@link CvListener} and {@link CvEventType}.  It also provides resource caching (i.e. caching of
 *  some server provided objects to allow retrieval by 'id')
 */

export class CvEventRegistry {

    private _cache:{[index:string]:CvEvent<any>} = null;
    private _listenerMap:{[index:number]:Array<CvListener<any>>} = [];

    constructor() {
    }

    clearAll():void {
        this.clearCache();
        this.removeAllListeners();
    }

    clearCache():void{
        this._cache = {}
    }

    enableCache():void {
        this._cache = this._cache || {};
    }

    disableCache():void {
        this._cache = null;
    }

    isCacheEnabled():boolean {
        return this._cache !== null;
    }

    getEventByKey<T>(key:string):CvEvent<T> {
        const result = this.isCacheEnabled() ? this._cache[key] : null;
        if (!result) {
            Log.debug('No resource entry exists for resourceId ' + key);
        }
        return result;
    }

    publish<T>(event:CvEvent<T>, shouldCache:boolean = false):void {
        const listenerArray:Array<CvListener<any>> = this._listenerMap[event.type];
        if (listenerArray) {
            listenerArray.forEach((listener:CvListener<any>)=> {
                Log.debug('publishing ' + JSON.stringify(CvEventType[event.type]) + ' to ' + listener);
                listener(event);
            });
        }
        if (this.isCacheEnabled() && shouldCache && event.resourceId) {
            this.cacheObjectAt(event.resourceId, event);
        }
    }
    
    publishError(message:string, messageObj?:string):void {
        const event:CvEvent<CvMessage> = {
            type: CvEventType.MESSAGE,
            eventObj: {
                message: message,
                messageObj: messageObj,
                type: CvMessageType.ERROR
            }
        }
        this.publish(event, false);
    }
    
    cacheObjectAt(id:string, obj:any) {
        this._cache[id] = obj;
    }

    removeFromCache(key:string) {
        if(this.isCacheEnabled()) {
            delete this._cache[key];
        }
    }

    subscribe<T>(listener:CvListener<T>, eventType:CvEventType):void {

        let listenerArray:Array<CvListener<any>> = this._listenerMap[eventType];
        if (!listenerArray) {
            listenerArray = [];
            this._listenerMap[eventType] = listenerArray;
        }
        if (listenerArray.indexOf(listener) < 0) {
            listenerArray.push(listener);
        }
    }

    removeAllListeners():void {
        this._listenerMap = [];
    }


    unsubscribe(listener:CvListener<any>):void {
        for (const eventType in this._listenerMap) {
            const listenerArray:Array<CvListener<any>> = this._listenerMap[eventType];
            if (listenerArray) {
                var index = listenerArray.indexOf(listener);
                if (index > -1) {
                    listenerArray.splice(index, 1);
                }
            }
        }
    }

}

/**
 *  This is a mechanism used throughout the framework
 *  A generalized interface for allowing two components to communicate 'state' changes
 *  i.e. a workbench display and a workbench selector menu
 *  The following compose this mechanism: {@link CvValueListener}, {@link CvValueProvider}, {@link CvValueAdapter}
 */
export interface CvValueListener<T> {
    (value:T):void;
}

export interface CvValueProvider<T> {
    subscribe(updateListener:CvValueListener<T>):void;
}

export class CvValueAdapter<T> implements CvValueProvider<T> {

    private _value:T;
    private _subscriberArray:Array<CvValueListener<T>> = [];

    /*
        Create a listener that can be 'handed off' to components that are the SOURCE of events, and that
        expect a listener.  When this listener is notified, it will notify the rest of the listeners in the chain -
        i.e. the one's that have 'subscribed' to this provider
     */
    getDelegateValueListener():CvValueListener<T> {
        return ((value:T) => {
            this._value = value;
            this._subscriberArray.forEach((updateListener:CvValueListener<T>)=> {
                updateListener(value)
            });
        });
    }

    /**
     * @deprecated since 2.0.32  Use getNotifyFunction() instead
     */
    createValueListener():CvValueListener<T> {
        return this.getDelegateValueListener();
    }

    subscribe(updateListener:CvValueListener<T>):void {
        if (this._subscriberArray.indexOf(updateListener) < 0) {
            this._subscriberArray.push(updateListener);
            if(this._value) {
                updateListener(this._value);
            }
        }
    }

}

/**
 Utilities
 */

export class CvNavigationResultUtil {

    static determineType(navRequest:NavRequest):CvNavigationResultType {
        if(navRequest instanceof FormContext) {
            return CvNavigationResultType.FORM;
        } else if(navRequest instanceof  WebRedirection) {
            return CvNavigationResultType.URL;
        } else {
            return CvNavigationResultType.NULL;
        }
    }

    static publishNavigation(catavolt:AppContext, eventRegistry:CvEventRegistry, navRequest:NavRequest,
                                 actionId:string, workbenchId:string, navTarget:string,
                                 navigationListeners:Array<(event:CvEvent<CvNavigationResult>)=>void>,
                                 sourceIsDestroyed:boolean, noTransition:boolean):CvEvent<CvNavigationResult> {

        const resourceId = CvResourceManager.resourceIdForObject(navRequest, catavolt);
        const e:CvEvent<CvNavigationResult> = {
            type: CvEventType.NAVIGATION,
            resourceId: resourceId,
            eventObj: {
                navRequest: navRequest,
                actionId: actionId,
                workbenchId: workbenchId,
                navTarget: navTarget,
                noTransition: noTransition,
                sourceIsDestroyed: sourceIsDestroyed,
                type: CvNavigationResultUtil.determineType(navRequest)

            }
        };
        eventRegistry.publish<CvNavigationResult>(e, CvResourceManager.shouldCacheResult(eventRegistry));
        if(navigationListeners){
            navigationListeners.forEach((listener)=> {
                listener(e)
            });
        }
        return e;
    } 
}

export class CvSessionManager {

    static removeSession() {
        if(typeof(sessionStorage) != 'undefined') {
            sessionStorage.removeItem('catavolt::session');
        }
    }

    static storeSession(sessionContext:SessionContext) {
        if(typeof(sessionStorage) != 'undefined') {
            sessionStorage.setItem('catavolt::session', JSON.stringify(sessionContext));
        }
    }

    static getSession():SessionContext {
        if(typeof(sessionStorage) != 'undefined') {
            const sessionStr = sessionStorage.getItem('catavolt::session');
            if (sessionStr) {
                const sessionObj = JSON.parse(sessionStr);
                if (sessionObj) {
                    const systemContext:SystemContextImpl = new SystemContextImpl(sessionObj.systemContext._urlString,
                        sessionObj.systemContext._appVersion);
                    return new SessionContextImpl(sessionObj.sessionHandle, sessionObj.userName,
                        sessionObj.currentDivision, sessionObj.serverVersion, systemContext, sessionObj.tenantId);
                } else {
                    return null;
                }
            } else {
                return null;
            }
        } else {
            return null;
        }

    }

    static updateSession(catavolt:AppContext, eventRegistry:CvEventRegistry):Future<AppWinDef> {

        if (typeof(sessionStorage) != 'undefined') {
            const sessionContext = this.getSession();
            if (sessionContext) {
                const refreshFuture:Future<AppWinDef> = catavolt.refreshContext(sessionContext);
                refreshFuture.onComplete(appWinDefTry=> {
                    if (appWinDefTry.isFailure) {
                        const event:CvEvent<CvMessage> = {
                            type: CvEventType.MESSAGE,
                            eventObj: {
                                message: 'Update session failed',
                                messageObj: appWinDefTry.failure,
                                type: CvMessageType.ERROR
                            }
                        }
                        eventRegistry.publish(event, false);
                    } else {
                        const event:CvEvent<CvSessionUpdateResult> = {
                            type: CvEventType.SESSION_UPDATE,
                            resourceId: CvResourceManager.resourceIdForObject(appWinDefTry.success, catavolt),
                            eventObj: {appWinDef: appWinDefTry.success}
                        };
                        eventRegistry.publish<CvSessionUpdateResult>(event, CvResourceManager.shouldCacheResult(eventRegistry));
                    }
                });
                return refreshFuture;
            } else {
                return Future.createFailedFuture<AppWinDef>('CvReact::updateSession', 'no persistent session found');
            }
        } else {
            return Future.createFailedFuture<AppWinDef>('CvReact::updateSession', 'sessionStorage not supported');
        }
    }
}

export class CvResourceManager {

    private static PARAM_DELIM:string = '~';

    static deserializeRedirection(token:string):{redirection:DialogRedirection, actionSource:ActionSource} {
        const [handleValue, sessionHandle, dialogMode, dialogType,
            objectId, actionId, actionObjId, actionType] = token.split(CvResourceManager.PARAM_DELIM);
       
        let actionSource:ActionSource = null;
        if(actionType && actionType === 'ca') {
            actionSource = new ContextAction(actionId, actionObjId, null);
        } else if (actionType && actionType === 'wla'){
            actionSource = new WorkbenchLaunchAction(actionId, actionObjId, '','','');
        }
        
        return {redirection: new DialogRedirection(new DialogHandle(Number(handleValue), sessionHandle), dialogType, dialogMode,
            null, objectId, false, null, null, {}, {}), actionSource: actionSource}
    }


    static shouldCacheResult(eventRegistry:CvEventRegistry):boolean {
        //cache everything for now, can later set this preference component-by-component via a property
        return eventRegistry.isCacheEnabled();
    }
    
    static resourceIdForObject(o:any, catavolt:AppContext):string {

        if (o instanceof FormContext) {
            const formContext:FormContext = o;
            return CvResourceManager.serializeRedirection(formContext.paneDef.dialogRedirection, formContext.actionSource);
        } else if (o instanceof AppWinDef) {
            if (catavolt.sessionContextTry.isSuccess) {
                //right now the 'windowId' is the session handle. may need to change this if want to have multiple windows...
                return catavolt.sessionContextTry.success.sessionHandle;
            } else {
                return null;
            }
        } else if (o instanceof WebRedirection) {
            const webRedirection:WebRedirection = o;
            return 'ext_' + StringUtil.hashCode(webRedirection.webURL);
        } else {
            Log.debug('No resourceId found for object');
            return null;
        }

    }

    /**
     * @private 
     * 
     * This is an attempt to preserve a redirection and all it's state along with the action source,
     * in a single 'token' that can be used on the URL.  This along with the session information, are used to make a
     * 'stateless' transition to a new URL.
     * The sdk and server require quite a bit of state to be retained by the client in order to 'rebuild' a navigation.
     * 
     * @param redirection
     * @param actionSource
     * @returns {string}
     */
    private static serializeRedirection(redirection:DialogRedirection, actionSource:ActionSource):string {
       
        let [actionId, actionObjId, actionType] = ['', '', ''];
        if(actionSource instanceof ContextAction) {
            actionId = (actionSource as ContextAction).actionId;
            actionObjId = (actionSource as ContextAction).objectId;
            actionType = 'ca';
        } else if (actionSource instanceof WorkbenchLaunchAction) {
            actionId = (actionSource as WorkbenchLaunchAction).id;
            actionObjId = (actionSource as WorkbenchLaunchAction).workbenchId;
            actionType = 'wla'
        }

        const dialogHandle = redirection.dialogHandle;
        const PARAM_DELIM = CvResourceManager.PARAM_DELIM; 
        return (dialogHandle.handleValue ? dialogHandle.handleValue : '') + PARAM_DELIM
            + (dialogHandle.sessionHandle ? dialogHandle.sessionHandle : '') + PARAM_DELIM
            + (redirection.dialogMode ? redirection.dialogMode : '') + PARAM_DELIM
            + (redirection.dialogType ? redirection.dialogType : '') + PARAM_DELIM
            + (redirection.objectId ? redirection.objectId : '') + PARAM_DELIM
            + actionId + PARAM_DELIM + actionObjId + PARAM_DELIM + actionType;
    }

}

export class ColorUtil {
    public static toColor(c:{red:number, green:number, blue:number, alpha:number }):string {
        return "rgba(" + [c.red, c.green, c.blue, c.alpha].join(",") + ")";
    }
    public static toColorFromNum(num):string {
        num >>>= 0;
        var b = num & 0xFF,
            g = (num & 0xFF00) >>> 8,
            r = (num & 0xFF0000) >>> 16,
            a = ( (num & 0xFF000000) >>> 24 ) / 255 ;
        return "rgba(" + [r, g, b, a].join(",") + ")";
    }
    public static toColorFromNumWithAlpha(num, a):string {
        num >>>= 0;
        var b = num & 0xFF,
            g = (num & 0xFF00) >>> 8,
            r = (num & 0xFF0000) >>> 16;
        return "rgba(" + [r, g, b, a].join(",") + ")";
    }
}

export class DateUtil {
    public static toISOFormatNoOffset(d:Date):string {
        // Build the ISO string because toISOString() will adjust for timezone, which we don't want.
        // d.toISOString();  - This will adjust the date based on timezone which is undesirable.
        let a = d.getFullYear() + "-";
        let x = d.getMonth() + "-";
        if (x.length == 2) {
            x = "0" + x;
        }
        a += x;
        x = d.getDate() + "";
        if (x.length == 1) {
            x = "0" + x;
        }
        a += x;
        return a;
    }
}

/**
 * CvImage Action Components
 * A CvImage has a property for actions.  An action (CvImageAction) simply represents a button with a button style
 * and a parameterless callback action to be executed when the button is pressed.
 *
 * A CvImageProducer is a candidate representation of a CvAction.  It has flags to determine when a CvAction should be
 * created or not (i.e. you can't delete an image if there is not already an image).  UI will configure a set of
 * CvImageProducers that are appopriate for the UI.  A signature control never supports a delete, while an image
 * control might support create/delete/undo.
 *
 * A CvImagePackage is an abstract class with 5 concrete subclasses.  An instance of CvImagePackage is the value to
 * be set on the CvImageProducer call back listener.
 */
    export enum CvImageExistenceState { Missing, Present, Deleted };

    export interface CvImageAction {
        callback:()=>void;
        buttonClassName:string;
    }

    export interface CvImageProducer {
        prodImageCB?:(listener:CvValueListener<Object>)=>void;
        buttonClassName:string;
        includeWhenImageisPresent:boolean;
        includeWhenImageIsDeleted:boolean;
        includeWhenImageIsMissing:boolean;
    }

    export class CvImagePackage {
        public isUrl():boolean  { return false }
        public isDelete():boolean  { return false }
        public isPick():boolean  { return false }
        public isUndo():boolean  { return false }
        public isRedo():boolean  { return false }
    }
    export class CvImagePackageUrl extends CvImagePackage {
        constructor(url:any) {
            super();
            this._url = url;
        }
        private _url:string;
        public get url():string { return this._url; }
        public isUrl():boolean  { return true }
    }
    export class CvImagePackagePick extends CvImagePackage {
        public isPick():boolean  { return true }
    }
    export class CvImagePackageUndo extends CvImagePackage {
        public isUndo():boolean  { return true }
    }
    export class CvImagePackageRedo extends CvImagePackage {
        public isRedo():boolean  { return true }
    }
    export class CvImagePackageDelete extends CvImagePackage {
        public isDelete():boolean  { return true }
    }


export class ImageUtil {
    /**
     * Conserve aspect ratio of the orignal region. Useful when shrinking/enlarging
     * images to fit into a certain area.
     *
     * In most cases this is not needed as the object-fit:contain CSS will obtain
     * the desired result.
     *
     * @param {Number} srcWidth Source area width
     * @param {Number} srcHeight Source area height
     * @param {Number} maxWidth Fittable area maximum available width
     * @param {Number} maxHeight Fittable area maximum available height
     * @return {Object} { width, heigth }
     */
    public static calculateAspectRatioFit(srcWidth, srcHeight, maxWidth, maxHeight) {
        let ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight);
        return { width: srcWidth*ratio, height: srcHeight*ratio };
    }
}

export class UIUtil {

    /*
        Returns a function, that, as long as it continues to be invoked, will not
        be triggered. The function will be called after it stops being called for
        N milliseconds. If `immediate` is passed, trigger the function on the
        leading edge, instead of the trailing.

        Note: to use debounce with a React event handler (w/ SyntheticEvent),
        see http://blog.revathskumar.com/2016/02/reactjs-using-debounce-in-react-components.html
    */
    public static debounce(func:Function, wait:number, immediate:boolean = false):Function {
        let timeout;
        return function() {
            const context = this, args = arguments;
            const later = function() {
                timeout = null;
                if (!immediate) func.apply(context, args);
            };
            const callNow = immediate && !timeout;
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
            if (callNow) func.apply(context, args);
        };
    };

}