import { AnimationReferenceMetadata } from '@angular/animations';
import { CurrencyPipe, formatDate as _formatDate, isPlatformBrowser } from '@angular/common';
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { mergeWith } from 'lodash-es';
import { Observable } from 'rxjs';
import {
    blink, fadeIn, fadeOut, flipBottom, flipHorBck, flipHorFwd, flipLeft, flipRight, flipTop,
    flipVerBck, flipVerFwd, growVerIn, growVerOut, heartbeat, pulsateBck, pulsateFwd, rotateInBl,
    rotateInBottom, rotateInBr, rotateInCenter, rotateInDiagonal1, rotateInDiagonal2, rotateInHor,
    rotateInLeft, rotateInRight, rotateInTl, rotateInTop, rotateInTr, rotateInVer, rotateOutBl,
    rotateOutBottom, rotateOutBr, rotateOutCenter, rotateOutDiagonal1, rotateOutDiagonal2,
    rotateOutHor, rotateOutLeft, rotateOutRight, rotateOutTl, rotateOutTop, rotateOutTr,
    rotateOutVer, scaleInBl, scaleInBottom, scaleInBr, scaleInCenter, scaleInHorCenter,
    scaleInHorLeft, scaleInHorRight, scaleInLeft, scaleInRight, scaleInTl, scaleInTop, scaleInTr,
    scaleInVerBottom, scaleInVerCenter, scaleInVerTop, scaleOutBl, scaleOutBottom, scaleOutBr,
    scaleOutCenter, scaleOutHorCenter, scaleOutHorLeft, scaleOutHorRight, scaleOutLeft,
    scaleOutRight, scaleOutTl, scaleOutTop, scaleOutTr, scaleOutVerBottom, scaleOutVerCenter,
    scaleOutVerTop, shakeBl, shakeBottom, shakeBr, shakeCenter, shakeHor, shakeLeft, shakeRight,
    shakeTl, shakeTop, shakeTr, shakeVer, slideInBl, slideInBottom, slideInBr, slideInLeft,
    slideInRight, slideInTl, slideInTop, slideInTr, slideOutBl, slideOutBottom, slideOutBr,
    slideOutLeft, slideOutRight, slideOutTl, slideOutTop, slideOutTr, swingInBottomBck,
    swingInBottomFwd, swingInLeftBck, swingInLeftFwd, swingInRightBck, swingInRightFwd,
    swingInTopBck, swingInTopFwd, swingOutBottomBck, swingOutBottomFwd, swingOutLeftBck,
    swingOutLefttFwd, swingOutRightBck, swingOutRightFwd, swingOutTopBck, swingOutTopFwd
} from '../animations/main';
import { setImmediate } from './setImmediate';
import { isDevMode } from '@angular/core';

/**
 * @hidden
 */
export const showMessage = (message: string, isMessageShown: boolean): boolean => {
    if (!isMessageShown && isDevMode()) {
        console.warn(message);
    }

    return true;
};

export const mkenum = <T extends { [index: string]: U }, U extends string>(x: T) => x;

/**
 *
 * @hidden @internal
 */
export const getResizeObserver = () => window.ResizeObserver;

/**
 * @hidden
 */
export const cloneArray = (array: any[], deep?: boolean) => {
    const arr = [];
    if (!array) {
        return arr;
    }
    let i = array.length;
    while (i--) {
        arr[i] = deep ? cloneValue(array[i]) : array[i];
    }
    return arr;
};

/**
 * Doesn't clone leaf items
 *
 * @hidden
 */
export const cloneHierarchicalArray = (array: any[], childDataKey: any): any[] => {
    const result: any[] = [];
    if (!array) {
        return result;
    }

    for (const item of array) {
        const clonedItem = cloneValue(item);
        if (Array.isArray(item[childDataKey])) {
            clonedItem[childDataKey] = cloneHierarchicalArray(clonedItem[childDataKey], childDataKey);
        }
        result.push(clonedItem);
    }
    return result;
};

/**
 * Creates an object with prototype from provided source and copies
 * all properties descriptors from provided source
 * @param obj Source to copy prototype and descriptors from
 * @returns New object with cloned prototype and property descriptors
 */
export const copyDescriptors = (obj) => {
    if (obj) {
        return Object.create(
            Object.getPrototypeOf(obj),
            Object.getOwnPropertyDescriptors(obj)
            );
    }
}


/**
 * Deep clones all first level keys of Obj2 and merges them to Obj1
 *
 * @param obj1 Object to merge into
 * @param obj2 Object to merge from
 * @returns Obj1 with merged cloned keys from Obj2
 * @hidden
 */
export const mergeObjects = (obj1: any, obj2: any): any => mergeWith(obj1, obj2, (objValue, srcValue) => {
    if (Array.isArray(srcValue)) {
        return objValue = srcValue;
    }
});

/**
 * Creates deep clone of provided value.
 * Supports primitive values, dates and objects.
 * If passed value is array returns shallow copy of the array.
 *
 * @param value value to clone
 * @returns Deep copy of provided value
 * @hidden
 */
export const cloneValue = (value: any): any => {
    if (isDate(value)) {
        return new Date(value.getTime());
    }
    if (Array.isArray(value)) {
        return [...value];
    }

    if (value instanceof Map || value instanceof Set) {
        return value;
    }

    if (isObject(value)) {
        const result = {};

        for (const key of Object.keys(value)) {
            result[key] = cloneValue(value[key]);
        }
        return result;
    }
    return value;
};

/**
 * Creates deep clone of provided value.
 * Supports primitive values, dates and objects.
 * If passed value is array returns shallow copy of the array.
 * For Objects property values and references are cached and reused.
 * This allows for circular references to same objects.
 *
 * @param value value to clone
 * @param cache map of cached values already parsed
 * @returns Deep copy of provided value
 * @hidden
 */
export const cloneValueCached = (value: any, cache: Map<any, any>): any => {
    if (isDate(value)) {
        return new Date(value.getTime());
    }
    if (Array.isArray(value)) {
        return [...value];
    }

    if (value instanceof Map || value instanceof Set) {
        return value;
    }

    if (isObject(value)) {
        if (cache.has(value)) {
            return cache.get(value);
        }

        const result = {};
        cache.set(value, result);

        for (const key of Object.keys(value)) {
            result[key] = cloneValueCached(value[key], cache);
        }
        return result;
    }
    return value;
};

/**
 * Parse provided input to Date.
 *
 * @param value input to parse
 * @returns Date if parse succeed or null
 * @hidden
 */
export const parseDate = (value: any): Date | null => {
    // if value is Invalid Date return null
    if (isDate(value)) {
        return !isNaN(value.getTime()) ? value : null;
    }
    return value ? new Date(value) : null;
};

/**
 * Returns an array with unique dates only.
 *
 * @param columnValues collection of date values (might be numbers or ISO 8601 strings)
 * @returns collection of unique dates.
 * @hidden
 */
export const uniqueDates = (columnValues: any[]) => columnValues.reduce((a, c) => {
    if (!a.cache[c.label]) {
        a.result.push(c);
    }
    a.cache[c.label] = true;
    return a;
}, { result: [], cache: {} }).result;

/**
 * Checks if provided variable is Object
 *
 * @param value Value to check
 * @returns true if provided variable is Object
 * @hidden
 */
export const isObject = (value: any): boolean => !!(value && value.toString() === '[object Object]');

/**
 * Checks if provided variable is Date
 *
 * @param value Value to check
 * @returns true if provided variable is Date
 * @hidden
 */
export const isDate = (value: any): value is Date => value instanceof Date;

/**
 * Checks if the two passed arguments are equal
 * Currently supports date objects
 *
 * @param obj1
 * @param obj2
 * @returns: `boolean`
 * @hidden
 */
export const isEqual = (obj1, obj2): boolean => {
    if (isDate(obj1) && isDate(obj2)) {
        return obj1.getTime() === obj2.getTime();
    }
    return obj1 === obj2;
};

/**
 * Checks if provided variable is the value NaN
 *
 * @param value Value to check
 * @returns true if provided variable is NaN
 * @hidden
 */
 export const isNaNvalue = (value: any): boolean => isNaN(value) && value !== undefined && typeof value !== 'string';

/**
 * Utility service taking care of various utility functions such as
 * detecting browser features, general cross browser DOM manipulation, etc.
 *
 * @hidden @internal
 */
@Injectable({ providedIn: 'root' })
export class PlatformUtil {
    public isBrowser: boolean = isPlatformBrowser(this.platformId);
    public isIOS = this.isBrowser && /iPad|iPhone|iPod/.test(navigator.userAgent) && !('MSStream' in window);
    public isFirefox = this.isBrowser && /Firefox[\/\s](\d+\.\d+)/.test(navigator.userAgent);
    public isEdge = this.isBrowser && /Edge[\/\s](\d+\.\d+)/.test(navigator.userAgent);
    public isChromium = this.isBrowser && (/Chrom|e?ium/g.test(navigator.userAgent) ||
        /Google Inc/g.test(navigator.vendor)) && !/Edge/g.test(navigator.userAgent);

    public KEYMAP = mkenum({
        ENTER: 'Enter',
        SPACE: ' ',
        ESCAPE: 'Escape',
        ARROW_DOWN: 'ArrowDown',
        ARROW_UP: 'ArrowUp',
        ARROW_LEFT: 'ArrowLeft',
        ARROW_RIGHT: 'ArrowRight',
        END: 'End',
        HOME: 'Home',
        PAGE_DOWN: 'PageDown',
        PAGE_UP: 'PageUp',
        F2: 'F2',
        TAB: 'Tab',
        SEMICOLON: ';',
        // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values#editing_keys
        DELETE: 'Delete',
        BACKSPACE: 'Backspace',
        CONTROL: 'Control',
        X: 'x',
        Y: 'y',
        Z: 'z'
    });

    constructor(@Inject(PLATFORM_ID) private platformId: any) { }

    /**
     * @hidden @internal
     * Returns the actual size of the node content, using Range
     * ```typescript
     * let range = document.createRange();
     * let column = this.grid.columnList.filter(c => c.field === 'ID')[0];
     *
     * let size = getNodeSizeViaRange(range, column.cells[0].nativeElement);
     *
     * @remarks
     * The last parameter is useful when the size of the element to measure is modified by a
     * parent element that has explicit size. In such cases the calculated size is never lower
     * and the function may instead remove the parent size while measuring to get the correct value.
     * ```
     */
    public getNodeSizeViaRange(range: Range, node: HTMLElement, sizeHoldingNode?: HTMLElement) {
        let overflow = null;
        let nodeStyles;

        if (!this.isFirefox) {
            overflow = node.style.overflow;
            // we need that hack - otherwise content won't be measured correctly in IE/Edge
            node.style.overflow = 'visible';
        }

        if (sizeHoldingNode) {
            const style = sizeHoldingNode.style;
            nodeStyles = [style.width, style.minWidth, style.flexBasis];
            style.width = '';
            style.minWidth = '';
            style.flexBasis = '';
        }

        range.selectNodeContents(node);
        const scale = node.getBoundingClientRect().width / node.offsetWidth;
        const width = range.getBoundingClientRect().width / scale;

        if (!this.isFirefox) {
            // we need that hack - otherwise content won't be measured correctly in IE/Edge
            node.style.overflow = overflow;
        }

        if (sizeHoldingNode) {
            sizeHoldingNode.style.width = nodeStyles[0];
            sizeHoldingNode.style.minWidth = nodeStyles[1];
            sizeHoldingNode.style.flexBasis = nodeStyles[2];
        }

        return width;
    }


    /**
     * Returns true if the current keyboard event is an activation key (Enter/Space bar)
     *
     * @hidden
     * @internal
     *
     * @memberof PlatformUtil
     */
    public isActivationKey(event: KeyboardEvent) {
        return event.key === this.KEYMAP.ENTER || event.key === this.KEYMAP.SPACE;
    }

    /**
     * Returns true if the current keyboard event is a combination that closes the filtering UI of the grid. (Escape/Ctrl+Shift+L)
     *
     * @hidden
     * @internal
     * @param event
     * @memberof PlatformUtil
     */
    public isFilteringKeyCombo(event: KeyboardEvent) {
        return event.key === this.KEYMAP.ESCAPE || (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'l');
    }

    /**
     * @hidden @internal
     */
    public isLeftClick(event: PointerEvent | MouseEvent) {
        return event.button === 0;
    }

    /**
     * @hidden @internal
     */
    public isNavigationKey(key: string) {
        return [
            this.KEYMAP.HOME, this.KEYMAP.END, this.KEYMAP.SPACE,
            this.KEYMAP.ARROW_DOWN, this.KEYMAP.ARROW_LEFT, this.KEYMAP.ARROW_RIGHT, this.KEYMAP.ARROW_UP
        ].includes(key as any);
    }
}

/**
 * @hidden
 */
export const flatten = (arr: any[]) => {
    let result = [];

    arr.forEach(el => {
        result.push(el);
        if (el.children) {
            const children = Array.isArray(el.children) ? el.children : el.children.toArray();
            result = result.concat(flatten(children));
        }
    });
    return result;
};

export interface CancelableEventArgs {
    /**
     * Provides the ability to cancel the event.
     */
    cancel: boolean;
}

export interface IBaseEventArgs {
    /**
     * Provides reference to the owner component.
     */
    owner?: any;
}

export interface CancelableBrowserEventArgs extends CancelableEventArgs {
    /** Browser event */
    event?: Event;
}

export interface IBaseCancelableBrowserEventArgs extends CancelableBrowserEventArgs, IBaseEventArgs { }

export interface IBaseCancelableEventArgs extends CancelableEventArgs, IBaseEventArgs { }

export const HORIZONTAL_NAV_KEYS = new Set(['arrowleft', 'left', 'arrowright', 'right', 'home', 'end']);

export const NAVIGATION_KEYS = new Set([
    'down',
    'up',
    'left',
    'right',
    'arrowdown',
    'arrowup',
    'arrowleft',
    'arrowright',
    'home',
    'end',
    'space',
    'spacebar',
    ' '
]);
export const ACCORDION_NAVIGATION_KEYS = new Set('up down arrowup arrowdown home end'.split(' '));
export const ROW_EXPAND_KEYS = new Set('right down arrowright arrowdown'.split(' '));
export const ROW_COLLAPSE_KEYS = new Set('left up arrowleft arrowup'.split(' '));
export const ROW_ADD_KEYS = new Set(['+', 'add', '≠', '±', '=']);
export const SUPPORTED_KEYS = new Set([...Array.from(NAVIGATION_KEYS),
...Array.from(ROW_ADD_KEYS), 'enter', 'f2', 'escape', 'esc', 'pagedown', 'pageup']);
export const HEADER_KEYS = new Set([...Array.from(NAVIGATION_KEYS), 'escape', 'esc', 'l',
    /** This symbol corresponds to the Alt + L combination under MAC. */
    '¬']);

/**
 * @hidden
 * @internal
 *
 * Creates a new ResizeObserver on `target` and returns it as an Observable.
 * Run the resizeObservable outside angular zone, because it patches the MutationObserver which causes an infinite loop.
 * Related issue: https://github.com/angular/angular/issues/31712
 */
export const resizeObservable = (target: HTMLElement): Observable<ResizeObserverEntry[]> => new Observable((observer) => {
    const instance = new (getResizeObserver())((entries: ResizeObserverEntry[]) => {
        observer.next(entries);
    });
    instance.observe(target);
    const unsubscribe = () => instance.disconnect();
    return unsubscribe;
});

/**
 * @hidden
 * @internal
 *
 * Compares two maps.
 */
export const compareMaps = (map1: Map<any, any>, map2: Map<any, any>): boolean => {
    if (!map2) {
        return !map1 ? true : false;
    }
    if (map1.size !== map2.size) {
        return false;
    }
    let match = true;
    const keys = Array.from(map2.keys());
    for (const key of keys) {
        if (map1.has(key)) {
            match = map1.get(key) === map2.get(key);
        } else {
            match = false;
        }
        if (!match) {
            break;
        }
    }
    return match;
};

/**
 *
 * Given a property access path in the format `x.y.z` resolves and returns
 * the value of the `z` property in the passed object.
 *
 * @hidden
 * @internal
 */
export const resolveNestedPath = (obj: any, path: string) => {
    const parts = path?.split('.') ?? [];
    let current = obj[parts.shift()];

    parts.forEach(prop => {
        if (current) {
            current = current[prop];
        }
    });

    return current;
};

/**
 *
 * Given a property access path in the format `x.y.z` and a value
 * this functions builds and returns an object following the access path.
 *
 * @example
 * ```typescript
 * console.log('x.y.z.', 42);
 * >> { x: { y: { z: 42 } } }
 * ```
 *
 * @hidden
 * @internal
 */
export const reverseMapper = (path: string, value: any) => {
    const obj = {};
    const parts = path?.split('.') ?? [];

    let _prop = parts.shift();
    let mapping: any;

    // Initial binding for first level bindings
    obj[_prop] = value;
    mapping = obj;

    parts.forEach(prop => {
        // Start building the hierarchy
        mapping[_prop] = {};
        // Go down a level
        mapping = mapping[_prop];
        // Bind the value and move the key
        mapping[prop] = value;
        _prop = prop;
    });

    return obj;
};

export const yieldingLoop = (count: number, chunkSize: number, callback: (index: number) => void, done: () => void) => {
    let i = 0;
    const chunk = () => {
        const end = Math.min(i + chunkSize, count);
        for (; i < end; ++i) {
            callback(i);
        }
        if (i < count) {
            setImmediate(chunk);
        } else {
            done();
        }
    };
    chunk();
};

export const isConstructor = (ref: any) => typeof ref === 'function' && Boolean(ref.prototype) && Boolean(ref.prototype.constructor);

export const reverseAnimationResolver = (animation: AnimationReferenceMetadata): AnimationReferenceMetadata =>
    oppositeAnimation.get(animation) ?? animation;

export const isHorizontalAnimation = (animation: AnimationReferenceMetadata): boolean => horizontalAnimations.includes(animation);

export const isVerticalAnimation = (animation: AnimationReferenceMetadata): boolean => verticalAnimations.includes(animation);

const oppositeAnimation: Map<AnimationReferenceMetadata, AnimationReferenceMetadata> = new Map([
    [fadeIn, fadeIn],
    [fadeOut, fadeOut],
    [flipTop, flipBottom],
    [flipBottom, flipTop],
    [flipRight, flipLeft],
    [flipLeft, flipRight],
    [flipHorFwd, flipHorBck],
    [flipHorBck, flipHorFwd],
    [flipVerFwd, flipVerBck],
    [flipVerBck, flipVerFwd],
    [growVerIn, growVerIn],
    [growVerOut, growVerOut],
    [heartbeat, heartbeat],
    [pulsateFwd, pulsateBck],
    [pulsateBck, pulsateFwd],
    [blink, blink],
    [shakeHor, shakeHor],
    [shakeVer, shakeVer],
    [shakeTop, shakeTop],
    [shakeBottom, shakeBottom],
    [shakeRight, shakeRight],
    [shakeLeft, shakeLeft],
    [shakeCenter, shakeCenter],
    [shakeTr, shakeTr],
    [shakeBr, shakeBr],
    [shakeBl, shakeBl],
    [shakeTl, shakeTl],
    [rotateInCenter, rotateInCenter],
    [rotateOutCenter, rotateOutCenter],
    [rotateInTop, rotateInBottom],
    [rotateOutTop, rotateOutBottom],
    [rotateInRight, rotateInLeft],
    [rotateOutRight, rotateOutLeft],
    [rotateInLeft, rotateInRight],
    [rotateOutLeft, rotateOutRight],
    [rotateInBottom, rotateInTop],
    [rotateOutBottom, rotateOutTop],
    [rotateInTr, rotateInBl],
    [rotateOutTr, rotateOutBl],
    [rotateInBr, rotateInTl],
    [rotateOutBr, rotateOutTl],
    [rotateInBl, rotateInTr],
    [rotateOutBl, rotateOutTr],
    [rotateInTl, rotateInBr],
    [rotateOutTl, rotateOutBr],
    [rotateInDiagonal1, rotateInDiagonal1],
    [rotateOutDiagonal1, rotateOutDiagonal1],
    [rotateInDiagonal2, rotateInDiagonal2],
    [rotateOutDiagonal2, rotateOutDiagonal2],
    [rotateInHor, rotateInHor],
    [rotateOutHor, rotateOutHor],
    [rotateInVer, rotateInVer],
    [rotateOutVer, rotateOutVer],
    [scaleInTop, scaleInBottom],
    [scaleOutTop, scaleOutBottom],
    [scaleInRight, scaleInLeft],
    [scaleOutRight, scaleOutLeft],
    [scaleInBottom, scaleInTop],
    [scaleOutBottom, scaleOutTop],
    [scaleInLeft, scaleInRight],
    [scaleOutLeft, scaleOutRight],
    [scaleInCenter, scaleInCenter],
    [scaleOutCenter, scaleOutCenter],
    [scaleInTr, scaleInBl],
    [scaleOutTr, scaleOutBl],
    [scaleInBr, scaleInTl],
    [scaleOutBr, scaleOutTl],
    [scaleInBl, scaleInTr],
    [scaleOutBl, scaleOutTr],
    [scaleInTl, scaleInBr],
    [scaleOutTl, scaleOutBr],
    [scaleInVerTop, scaleInVerBottom],
    [scaleOutVerTop, scaleOutVerBottom],
    [scaleInVerBottom, scaleInVerTop],
    [scaleOutVerBottom, scaleOutVerTop],
    [scaleInVerCenter, scaleInVerCenter],
    [scaleOutVerCenter, scaleOutVerCenter],
    [scaleInHorCenter, scaleInHorCenter],
    [scaleOutHorCenter, scaleOutHorCenter],
    [scaleInHorLeft, scaleInHorRight],
    [scaleOutHorLeft, scaleOutHorRight],
    [scaleInHorRight, scaleInHorLeft],
    [scaleOutHorRight, scaleOutHorLeft],
    [slideInTop, slideInBottom],
    [slideOutTop, slideOutBottom],
    [slideInRight, slideInLeft],
    [slideOutRight, slideOutLeft],
    [slideInBottom, slideInTop],
    [slideOutBottom, slideOutTop],
    [slideInLeft, slideInRight],
    [slideOutLeft, slideOutRight],
    [slideInTr, slideInBl],
    [slideOutTr, slideOutBl],
    [slideInBr, slideInTl],
    [slideOutBr, slideOutTl],
    [slideInBl, slideInTr],
    [slideOutBl, slideOutTr],
    [slideInTl, slideInBr],
    [slideOutTl, slideOutBr],
    [swingInTopFwd, swingInBottomFwd],
    [swingOutTopFwd, swingOutBottomFwd],
    [swingInRightFwd, swingInLeftFwd],
    [swingOutRightFwd, swingOutLefttFwd],
    [swingInLeftFwd, swingInRightFwd],
    [swingOutLefttFwd, swingOutRightFwd],
    [swingInBottomFwd, swingInTopFwd],
    [swingOutBottomFwd, swingOutTopFwd],
    [swingInTopBck, swingInBottomBck],
    [swingOutTopBck, swingOutBottomBck],
    [swingInRightBck, swingInLeftBck],
    [swingOutRightBck, swingOutLeftBck],
    [swingInBottomBck, swingInTopBck],
    [swingOutBottomBck, swingOutTopBck],
    [swingInLeftBck, swingInRightBck],
    [swingOutLeftBck, swingOutRightBck],
]);

const horizontalAnimations: AnimationReferenceMetadata[] = [
    flipRight,
    flipLeft,
    flipVerFwd,
    flipVerBck,
    rotateInRight,
    rotateOutRight,
    rotateInLeft,
    rotateOutLeft,
    rotateInTr,
    rotateOutTr,
    rotateInBr,
    rotateOutBr,
    rotateInBl,
    rotateOutBl,
    rotateInTl,
    rotateOutTl,
    scaleInRight,
    scaleOutRight,
    scaleInLeft,
    scaleOutLeft,
    scaleInTr,
    scaleOutTr,
    scaleInBr,
    scaleOutBr,
    scaleInBl,
    scaleOutBl,
    scaleInTl,
    scaleOutTl,
    scaleInHorLeft,
    scaleOutHorLeft,
    scaleInHorRight,
    scaleOutHorRight,
    slideInRight,
    slideOutRight,
    slideInLeft,
    slideOutLeft,
    slideInTr,
    slideOutTr,
    slideInBr,
    slideOutBr,
    slideInBl,
    slideOutBl,
    slideInTl,
    slideOutTl,
    swingInRightFwd,
    swingOutRightFwd,
    swingInLeftFwd,
    swingOutLefttFwd,
    swingInRightBck,
    swingOutRightBck,
    swingInLeftBck,
    swingOutLeftBck,
];
const verticalAnimations: AnimationReferenceMetadata[] = [
    flipTop,
    flipBottom,
    flipHorFwd,
    flipHorBck,
    growVerIn,
    growVerOut,
    rotateInTop,
    rotateOutTop,
    rotateInBottom,
    rotateOutBottom,
    rotateInTr,
    rotateOutTr,
    rotateInBr,
    rotateOutBr,
    rotateInBl,
    rotateOutBl,
    rotateInTl,
    rotateOutTl,
    scaleInTop,
    scaleOutTop,
    scaleInBottom,
    scaleOutBottom,
    scaleInTr,
    scaleOutTr,
    scaleInBr,
    scaleOutBr,
    scaleInBl,
    scaleOutBl,
    scaleInTl,
    scaleOutTl,
    scaleInVerTop,
    scaleOutVerTop,
    scaleInVerBottom,
    scaleOutVerBottom,
    slideInTop,
    slideOutTop,
    slideInBottom,
    slideOutBottom,
    slideInTr,
    slideOutTr,
    slideInBr,
    slideOutBr,
    slideInBl,
    slideOutBl,
    slideInTl,
    slideOutTl,
    swingInTopFwd,
    swingOutTopFwd,
    swingInBottomFwd,
    swingOutBottomFwd,
    swingInTopBck,
    swingOutTopBck,
    swingInBottomBck,
    swingOutBottomBck,
];


/**
 * Similar to Angular's formatDate. However it will not throw on `undefined | null | ''` instead
 * coalescing to an empty string.
 */
export const formatDate = (value: string | number | Date, format: string, locale: string, timezone?: string): string => {
    if (value === null || value === undefined || value === '') {
        return '';
    }
    return _formatDate(value, format, locale, timezone);
};

export const formatCurrency = new CurrencyPipe(undefined).transform;

/** Converts pixel values to their rem counterparts for a base value */
export const rem = (value: number | string) => {
    const base = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('font-size'))
    return Number(value) / base;
}
