// Collection Service
// ------------------

// Transformers
import { keys } from 'ts-transformer-keys'

// Runtime
import {
    cloneDeep,
    extend,
    find,
    forEach,
    get,
    isArray,
    isEmpty,
    isFunction,
    isObject,
    isUndefined,
    map,
    set,
    throttle,
    reduce, clone, has
} from 'lodash'
import {auto} from 'angular'
import {Stratus} from '@stratusjs/runtime/stratus'

// Stratus Core
import {ErrorBase} from '@stratusjs/core/errors/errorBase'
import {ModelBase} from '@stratusjs/core/datastore/modelBase'
import {EventManager} from '@stratusjs/core/events/eventManager'
import {cookie} from '@stratusjs/core/environment'
import {
    isJSON,
    LooseFunction,
    LooseObject,
    ucfirst
} from '@stratusjs/core/misc'
import {XHR, XHRRequest} from '@stratusjs/core/datastore/xhr'

// AngularJS Services
import {Model, ModelOptions} from './model'

// Third-Party
import Toastify from 'toastify-js'

export interface HttpPrototype {
    headers: LooseObject
    method: string
    url: string
    data?: string
}

export interface CollectionOptions {
    autoSave?: boolean,
    autoSaveInterval?: number,
    cache?: boolean,
    // decay?: number
    direct?: boolean,
    // infinite?: boolean,
    // qualifier?: string,
    target?: string,
    targetSuffix?: string,
    // threshold?: number,
    urlRoot?: string,
    watch?: boolean,
    payload?: string,
    convoy?: string,
    headers?: LooseObject<any>,
}

export interface CollectionModelOptions extends ModelOptions {
    // This adds a new model to the beginning of the collection.models
    prepend?: boolean,

    // This forces a save (intended for use without autoSave enabled)
    save?: boolean

    // This triggers the collection add event
    trigger?: boolean
}

export const CollectionOptionKeys = keys<CollectionOptions>()

export interface CollectionSyncOptions {
    headers?: LooseObject<any>
    nocache?: boolean
}

export class Collection<T = LooseObject> extends EventManager {
    // Base Information
    name = 'Collection'

    // Environment
    direct = false
    target?: any = null
    targetSuffix?: string = null
    urlRoot = '/Api'
    toast = true

    // Unsure usage
    qualifier = '' // data-ng-if
    serviceId?: number = null

    // Infinite Scrolling
    infinite = false
    threshold = 0.5
    decay = 0

    // Infrastructure
    header = new ModelBase<T>()
    meta = new ModelBase<T>()
    model = Model
    models: Model<T>[] | (Model<T>['data'])[] = []
    types: Array<string> = []
    xhr: XHR
    withCredentials = false
    headers: LooseObject<any> = {}
    cacheResponse: LooseObject<LooseObject|Array<LooseObject>|string> = {}
    cacheHeaders: LooseObject<LooseObject<string>> = {}

    // Internals
    cache = false
    pending = false
    error = false
    completed = false

    // Action Flags
    filtering = false
    paginate = false
    collectionApiHydratedFromUrl = false

    // Allow watching models
    watch = false

    // Allow AutoSaving
    autoSave = false
    autoSaveInterval = 2500

    // Methods
    throttle = throttle(this.fetch, 1000)

    constructor(options: CollectionOptions = {}) {
        super()

        // Initialize required options
        options = (!options || typeof options !== 'object') ? {} : options

        // Inject Options
        // extend(this, this.sanitizeOptions(options))
        extend(this, options)

        // Generate URL
        if (this.target) {
            this.urlRoot += '/' + ucfirst(this.target)
        }

        // Handle Convoy
        if (options.convoy) {
            const convoy = isJSON(options.convoy) ? JSON.parse(options.convoy) : options.convoy
            if (isObject(convoy)) {
                this.meta.set((convoy as LooseObject).meta || {})
                const models = (convoy as LooseObject).payload
                if (isArray(models)) {
                    this.inject(models)
                    this.completed = true
                } else {
                    console.error('malformed payload:', models)
                }
            } else {
                console.error('malformed convoy:', convoy)
            }
        }

        // Handle Payload
        if (options.payload) {
            const models = isJSON(options.payload) ? JSON.parse(options.payload) : options.payload
            if (isArray(models)) {
                this.inject(models)
                this.completed = true
            } else {
                console.error('malformed payload:', models)
            }
        }

        // Scope Binding
        // this.serialize = this.serialize.bind(this)
        // this.url = this.url.bind(this)
        // this.inject = this.inject.bind(this)
        // this.sync = this.sync.bind(this)
        // this.fetch = this.fetch.bind(this)
        // this.filter = this.filter.bind(this)
        // this.throttleFilter = this.throttleFilter.bind(this)
        // this.page = this.page.bind(this)
        // this.toJSON = this.toJSON.bind(this)
        // this.add = this.add.bind(this)
        // this.remove = this.remove.bind(this)
        // this.find = this.find.bind(this)
        // this.pluck = this.pluck.bind(this)
        // this.exists = this.exists.bind(this)

        // Infinite Scrolling
        // this.infiniteModels = {
        //     numLoaded_: 0,
        //     toLoad_: 0,
        //     // Required.
        //     getItemAtIndex: function(index) {
        //         if (index > this.numLoaded_) {
        //             this.fetchMoreItems_(index)
        //             return null
        //         }
        //         return index
        //     },
        //     // Required.
        //     // For infinite scroll behavior, we always return a slightly higher
        //     // number than the previously loaded items.
        //     getLength: function() {
        //         return this.numLoaded_ + 5
        //     },
        //     fetchMoreItems_: function(index) {
        //         // For demo purposes, we simulate loading more items with a timed
        //         // promise. In real code, this function would likely contain an
        //         // XHR request.
        //         if (this.toLoad_ < index) {
        //             this.toLoad_ += 20
        //             $timeout(angular.noop, 300).then(angular.bind(this, function() {
        //                 this.numLoaded_ = this.toLoad_
        //             }))
        //         }
        //     }
        // }
    }

    sanitizeOptions(options: LooseObject): LooseObject {
        const sanitizedOptions = {}
        forEach(CollectionOptionKeys, (key) => {
            const data = get(options, key)
            if (isUndefined(data)) {
                return
            }
            set(sanitizedOptions, key, data)
        })
        return sanitizedOptions
    }

    serialize(obj: any, chain?: any) {
        const str: string[] = []
        obj = obj || {}
        forEach(obj, (value: any, key: any) => {
            if (isObject(value)) {
                if (chain) {
                    key = chain + '[' + key + ']'
                }
                const serialized = this.serialize(value, key)
                if (serialized) {
                    str.push(serialized)
                }
            } else {
                let encoded = ''
                if (chain) {
                    encoded += chain + '['
                }
                encoded += key
                if (chain) {
                    encoded += ']'
                }
                str.push(encoded + '=' + value)
            }
        })
        return str.join('&')
    }

    url() {
        return this.urlRoot + (this.targetSuffix || '')
    }

    inject(data: Array<LooseObject>, type?: string) {
        if (!isArray(data)) {
            return
        }
        if (this.types && this.types.indexOf(type) === -1) {
            this.types.push(type)
        }
        // TODO: Make this able to be flagged as direct entities
        if (!this.direct) {
            data.forEach((target: any) => {
                // TODO: Add references to the Catalog when creating these models
                (this.models as Model<T>[]).push(new Model<T>({
                    autoSave: this.autoSave,
                    autoSaveInterval: this.autoSaveInterval,
                    collection: this,
                    completed: true,
                    received: true,
                    toast: this.toast,
                    type: type || null,
                    watch: this.watch
                }, target))
            })
        }
    }

    // TODO: Abstract this deeper
    sync(action?: string, data?: LooseObject, options?: CollectionSyncOptions) {
        // XHR Flags
        this.pending = true

        return new Promise(async (resolve: any, reject: any) => {
            action = action || 'GET'
            options = options || {}
            const request: XHRRequest = {
                method: action,
                url: this.url(),
                headers: clone(this.headers),
                withCredentials: this.withCredentials,
            }
            if (!isUndefined(data)) {
                if (isObject(data) && Object.prototype.hasOwnProperty.call(data, 'p')) {
                    const validPage = this.normalizePositiveWholeNumber((data as LooseObject).p)
                    if (isUndefined(validPage)) {
                        delete (data as LooseObject).p
                    } else {
                        (data as LooseObject).p = validPage
                    }
                }
                if (action === 'GET') {
                    if (isObject(data) && Object.keys(data).length) {
                        request.url += request.url.includes('?') ? '&' : '?'
                        request.url += this.serialize(data)
                    }
                } else {
                    request.headers['Content-Type'] = 'application/json'
                    request.data = JSON.stringify(data)
                }
            }

            if (Object.prototype.hasOwnProperty.call(options, 'headers') && typeof options.headers === 'object') {
                Object.keys(options.headers).forEach((headerKey: string) => {
                    request.headers[headerKey] = options.headers[headerKey]
                })
            }

            // Create QueryHash for Responses
            const queryHash = `${request.method}:${request.url}`

            // Clear Cache upon Request
            if (options.nocache) {
                if (queryHash in this.cacheResponse) {
                    delete this.cacheResponse[queryHash]
                }
                if (queryHash in this.cacheHeaders) {
                    delete this.cacheHeaders[queryHash]
                }
            }

            // begin request
            const xhr = new XHR(request)
            this.xhr = xhr

            // TODO: Make this into an over-writable function
            const handler = (response: LooseObject | Array<LooseObject> | string, responseXhr: XHR = xhr) => {
                if (!isObject(response) && !isArray(response)) {
                    // Build Report
                    const error = new ErrorBase({
                        payload: response,
                        message: `Invalid Payload: ${request.method} ${request.url}`
                    }, {})

                    // XHR Flags
                    this.error = true
                    this.pending = false

                    // Note: I've disabled this because a model should not be marked
                    // as completed if it hasn't received a proper entity or prototype
                    // initially.  This is to ensure we don't save entities with the
                    // possibility of nullified fields due to a broken retrieval,
                    // resulting in the replacement of good data for bad.
                    // this.completed = true

                    // Trigger Change Event
                    this.throttleTrigger('change')
                    this.trigger('error', error)

                    // Promise
                    reject(error)

                    return
                }

                // TODO: Make this able to wipe the cache
                let responseHeaders: LooseObject<string> = null

                // Handle Cache on GET methods
                if (this.cache && request.method === 'GET') {
                    // Cache Request
                    if (!(queryHash in this.cacheResponse)) {
                        this.cacheResponse[queryHash] = cloneDeep(response)
                    }
                    // Cache Headers
                    if (!(queryHash in this.cacheHeaders)) {
                        this.cacheHeaders[queryHash] = responseXhr.getAllResponseHeaders()
                    } else {
                        responseHeaders = this.cacheHeaders[queryHash]
                    }
                }

                // Data
                this.header.set(responseHeaders || responseXhr.getAllResponseHeaders())
                this.meta.set((response as LooseObject).meta || {})
                this.models = []
                const payload = (response as LooseObject).payload || response

                // XHR Flags
                this.error = false

                // Check Status and Associate Payload
                if (
                    (this.meta.has('success') && !this.meta.get('success'))
                    // Removing checks for status[0]
                    // || (!this.meta.has('success') && this.meta.has('status') && this.meta.get('status[0].code') !== 'SUCCESS')
                ) {
                    this.error = true
                } else if (this.direct) {
                    this.models = payload
                } else if (isArray(payload)) {
                    this.inject(payload)
                } else if (isObject(payload)) {
                    // Note: this is explicitly stated due to context binding
                    forEach(payload, (value: any, key: any) => {
                        this.inject(value, key)
                    })
                } else {
                    // If we've gotten this far, it's passed the status check if one is available
                    if (!this.meta.has('status') && !this.meta.has('success')) {
                        // If the status check was not available, this classifies as an error, since the payload is invalid.
                        this.error = true
                    }
                    console.warn(`Invalid Payload: ${request.method} ${request.url}`)
                }

                // XHR Flags
                this.pending = false
                this.completed = true

                // Action Flags
                this.filtering = !isEmpty(this.meta.get('api.q'))
                this.paginate = !isEmpty(this.meta.get('api.p'))

                // Clear Meta Temps
                this.meta.clearTemp()

                // Trigger Change Event
                this.throttleTrigger('change')
                this.trigger('complete')

                // Promise
                resolve(this.models)
            }
            // handle response cache (headers are cached in the handler)
            if (this.cache && request.method === 'GET' && queryHash in this.cacheResponse) {
                handler(this.cacheResponse[queryHash])
                return
            }
            const prewarmRequest = typeof window !== 'undefined' && (window as any).StratusApiPrewarm
                ? (window as any).StratusApiPrewarm[request.url]
                : null
            const prewarm: Promise<Response|LooseObject|Array<LooseObject>|string> | null = request.method === 'GET'
                && prewarmRequest
                && typeof prewarmRequest.then === 'function'
                ? prewarmRequest
                : null
            if (prewarm) {
                prewarm
                    .then((response: Response|LooseObject|Array<LooseObject>|string) => {
                        if (response && typeof (response as Response).clone === 'function') {
                            return (response as Response).clone().json()
                        }
                        return response
                    })
                    .then((response: LooseObject|Array<LooseObject>|string) => {
                        const prewarmXhr = {
                            getAllResponseHeaders: () => ({})
                        } as XHR
                        this.xhr = prewarmXhr
                        handler(response, prewarmXhr)
                    })
                    .catch(() => {
                        const retryXhr = new XHR(request)
                        this.xhr = retryXhr
                        const xhrPromise = retryXhr.send()
                        if (request.method === 'GET' && typeof window !== 'undefined') {
                            (window as any).StratusApiPrewarm = (window as any).StratusApiPrewarm || {}
                            ;(window as any).StratusApiPrewarm[request.url] = xhrPromise
                        }
                        xhrPromise.then((response: LooseObject | Array<LooseObject> | string) => handler(response, retryXhr))
                            .catch((error: any) => {
                                console.error(`XHR: ${request.method} ${request.url}`)
                                this.throttleTrigger('change')
                                this.trigger('error', error)
                                reject(error)
                            })
                    })
                return
            }
            // make the call!
            const xhrPromise = this.xhr.send()
            if (request.method === 'GET' && typeof window !== 'undefined') {
                (window as any).StratusApiPrewarm = (window as any).StratusApiPrewarm || {}
                ;(window as any).StratusApiPrewarm[request.url] = xhrPromise
            }
            xhrPromise.then((response: LooseObject | Array<LooseObject> | string) => handler(response, xhr))
                .catch((error: any) => {
                    // (/(.*)\sReceived/i).exec(error.message)[1]
                    console.error(`XHR: ${request.method} ${request.url}`)
                    this.throttleTrigger('change')
                    this.trigger('error', error)
                    reject(error)
                    return
                })
        })
    }

    fetch(action?: string, data?: LooseObject, options?: CollectionSyncOptions) {
        return new Promise(async (resolve: any, reject: any) => {
            this.hydrateCollectionApiFromUrl()
            this.sync(action, data || this.meta.get('api'), options)
                .then(resolve)
                .catch(async (error: XMLHttpRequest|ErrorBase) => {
                    console.error('FETCH:', error)
                    if (!this.toast) {
                        reject(error)
                        return
                    }
                    const errorMessage = this.errorMessage(error)
                    const formatMessage = errorMessage ? `: ${errorMessage}` : '.'
                    Toastify({
                        text: `Unable to Fetch ${this.target}${formatMessage}`,
                        duration: 12000,
                        close: true,
                        stopOnFocus: true,
                        style: {
                            background: '#E14D45',
                        }
                    }).showToast()
                    reject(error)
                    return
                })
        })
    }

    hydrateCollectionApiFromUrl(force = false) {
        if ((!force && this.collectionApiHydratedFromUrl) || typeof window === 'undefined') {
            return
        }
        const url = new URL(window.location.href)
        const urlApi = this.getUrlSearchObject(url)
        let filterApplied = false
        let rawPage: any = null
        if (typeof this.target === 'string' && this.target.length) {
            rawPage = this.getUrlSearchValue(url, [`page[${this.target}]`, `p[${this.target}]`])
        }
        if (!rawPage) {
            rawPage = this.getUrlSearchValue(url, ['p', 'page'])
        }
        const page = this.normalizePositiveWholeNumber(rawPage)
        if (!isUndefined(page)) {
            this.meta.set('api.p', page)
            this.meta.set('pagination.pageCurrent', page)
            this.paginate = true
        }

        const query = this.getUrlSearchValue(url, ['q', 'query', 'keyword', 'search'])
        if (!isUndefined(query) && query !== null && query !== '') {
            this.meta.set('api.q', query)
            this.filtering = true
            filterApplied = true
        }

        const tags = this.getUrlSearchArray(url, ['tags', 'tag'])
        if (!isEmpty(tags)) {
            this.meta.set('api.tags', map(tags, this.parseNumberLikeValue))
            filterApplied = true
        }

        const contentTypes = this.getUrlSearchArray(url, ['contentType', 'contentTypes'])
        if (!isEmpty(contentTypes)) {
            this.meta.set('api.contentType', map(contentTypes, this.parseNumberLikeValue))
            filterApplied = true
        }

        forEach([
            'authorId',
            'status',
            'filterPublished',
            'filterTimeField',
            'filterTimeMin',
            'filterTimeMax',
            'filterTimeRange',
            'sort',
            'sortOrder',
            'filter'
        ], (apiKey: string) => {
            if (!has(urlApi, apiKey)) {
                return
            }
            const value = get(urlApi, apiKey)
            if (isUndefined(value) || value === null || value === '') {
                return
            }
            this.meta.set(`api.${apiKey}`, this.parseUrlValue(value))
            filterApplied = true
        })

        if (filterApplied) {
            this.meta.set('filterApplied', true)
        }
        this.collectionApiHydratedFromUrl = true
    }

    private getUrlSearchValue(url: URL, keys: string[]): any {
        for (const key of keys) {
            const value = url.searchParams.get(key)
            if (!isUndefined(value) && value !== null && value !== '') {
                return this.parseUrlValue(value)
            }
        }
        return undefined
    }

    private getUrlSearchArray(url: URL, keys: string[]): any[] {
        const values: any[] = []
        url.searchParams.forEach((value: string, key: string) => {
            if (!find(keys, (name: string) => key === name || key === `${name}[]` || new RegExp(`^${name}\\[\\d+\\]$`).test(key))) {
                return
            }
            const parsedValue = this.parseUrlValue(value)
            if (isArray(parsedValue)) {
                values.push(...parsedValue)
                return
            }
            if (typeof parsedValue === 'string' && parsedValue.indexOf(',') !== -1) {
                values.push(...parsedValue.split(',').map((item: string) => item.trim()).filter((item: string) => item !== ''))
                return
            }
            values.push(parsedValue)
        })
        return values
    }

    private getUrlSearchObject(url: URL): LooseObject {
        const output: LooseObject = {}
        url.searchParams.forEach((value: string, key: string) => {
            this.assignUrlSearchValue(output, this.getUrlSearchParts(key), value)
        })
        return output
    }

    private getUrlSearchParts(key: string): string[] {
        const parts: string[] = []
        const pattern = /([^\[\]]+)|\[(.*?)\]/g
        let match: RegExpExecArray | null
        while ((match = pattern.exec(key)) !== null) {
            parts.push(match[1] || match[2] || '')
        }
        return parts
    }

    private assignUrlSearchValue(output: LooseObject, parts: string[], rawValue: string) {
        if (!parts.length) {
            return
        }
        let cursor: any = output
        forEach(parts, (part: string, index: number) => {
            const last = index === parts.length - 1
            const key: any = part === '' ? this.nextArrayKey(cursor) : part
            if (last) {
                if (isUndefined(cursor[key])) {
                    cursor[key] = rawValue
                } else if (isArray(cursor[key])) {
                    cursor[key].push(rawValue)
                } else {
                    cursor[key] = [cursor[key], rawValue]
                }
                return
            }
            if (!isObject(cursor[key])) {
                cursor[key] = parts[index + 1] === '' || /^\d+$/.test(parts[index + 1]) ? [] : {}
            }
            cursor = cursor[key]
        })
    }

    private nextArrayKey(value: any): number {
        return isArray(value) ? value.length : Object.keys(value).length
    }

    private parseUrlValue(value: any): any {
        if (isArray(value)) {
            return map(value, (item: any) => this.parseUrlValue(item))
        }
        if (isObject(value)) {
            return reduce(value, (result: LooseObject, item: any, key: string) => {
                result[key] = this.parseUrlValue(item)
                return result
            }, {})
        }
        if (typeof value !== 'string') {
            return value
        }
        if (isJSON(value)) {
            return JSON.parse(value)
        }
        return this.parseNumberLikeValue(value)
    }

    private parseNumberLikeValue(value: any): any {
        if (typeof value !== 'string') {
            return value
        }
        if (/^-?\d+(\.\d+)?$/.test(value)) {
            return Number(value)
        }
        return value
    }

    filter(query: string) {
        this.filtering = !isEmpty(query)
        this.meta.set('api.q', !isUndefined(query) ? query : '')
        this.meta.set('api.p', 1)
        return this.fetch()
    }

    throttleFilter(query: string) {
        this.meta.set('api.q', !isUndefined(query) ? query : '')
        return new Promise((resolve: any, reject: any) => {
            const request = this.throttle()
            if (cookie('env')) {
                console.log('request:', request)
            }
            request.then((models: any) => {
                if (cookie('env')) {
                    // TODO: Finish handling throttled data
                    /* *
                     console.log('throttled:', map(models, function (model: Model) {
                     return model.domainPrimary
                     }))
                     /* */
                }
                resolve(models)
            }).catch(reject)
        })
    }

    page(page: any) {
        const validPage = this.normalizePositiveWholeNumber(page)
        this.paginate = !isUndefined(validPage)
        if (isUndefined(validPage)) {
            this.clearApiPage()
            return
        }
        this.meta.set('api.p', validPage)
        this.fetch().then()
        this.clearApiPage()
    }

    private clearApiPage() {
        const collectionApi = this.meta.get('api')
        if (isObject(collectionApi) && Object.prototype.hasOwnProperty.call(collectionApi, 'p')) {
            delete (collectionApi as LooseObject).p
        }
    }

    private normalizePositiveWholeNumber(value: any): number | undefined {
        const page = Number(value)
        return Number.isFinite(page) && Number.isInteger(page) && page >= 1 ? page : undefined
    }

    toJSON() {
        return !this.direct ? (this.models as Model<T>[]).map((model: Model<T>) => model.toJSON()) : this.models
    }

    add(target?: any, options?: CollectionModelOptions): Model {
        if (!isObject(target)) {
            console.error('collection.add: target object not set!')
            return
        }
        if (!options || typeof options !== 'object') {
            options = {}
        }
        if (target instanceof Model) {
            target.collection = this
        } else {
            options.collection = this
            target = new Model(options, target)
            target.initialize()
            if (options.autoSave || options.watch) {
                if (target.isNew()) {
                    target.save()
                } else if (!target.completed) {
                    target.fetch()
                }
            }
        }
        if (options.save) {
            target.save()
        }
        if (options.prepend) {
            this.models.unshift(target)
        } else {
            this.models.push(target)
        }
        if (options.trigger) {
            this.trigger('add', target)
        }
        this.throttleTrigger('change')
        return target
    }

    remove(target: Model<T>) {
        if (!this.direct) {
            this.models.splice((this.models as Model<T>[]).indexOf(target), 1)
            this.throttleTrigger('change')
        }
        return this
    }

    find(predicate: string|number|LooseFunction<boolean>) {
        return find(this.models, isFunction(predicate) ? predicate : (model: Model) => model.get('id') === predicate)
    }

    map(predicate: string) {
        // return filter(map(this.models, model => model instanceof Model ? model.get(predicate) : null), model => !!model)
        return map(this.models, model => model instanceof Model ? model.get(predicate) : null)
    }

    pluck(attribute: string) {
        return map(this.models, model => model instanceof Model ? model.pluck(attribute) : null)
    }

    exists(attribute: string) {
        return !!reduce(this.pluck(attribute) || [], (memo: any, data: any) => memo || !isUndefined(data))
    }

    errorMessage(error: XMLHttpRequest|ErrorBase): string|null {
        if (error instanceof ErrorBase) {
            console.error(`[${error.code}] ${error.message}`, error)
            return error.code !== 'Internal' ? error.message : null
        }
        const digest = (error.responseText && isJSON(error.responseText)) ? JSON.parse(error.responseText) : null
        if (!digest) {
            return null
        }
        const message = get(digest, 'meta.status[0].message') || get(digest, 'error.exception[0].message') || null
        if (!message) {
            return null
        }
        if (!cookie('env') && has(digest, 'error.exception[0].message')) {
            console.error('[xhr] server:', message)
            return null
        }
        return message
    }
}

// TODO: Build out the query-only structure here as a separate set
// This Collection Service handles data binding for multiple objects with the
// registered collections and models
Stratus.Services.Collection = [
    '$provide',
    ($provide: auto.IProvideService) => {
        $provide.factory('Collection', [() => Collection])
    }
]
Stratus.Data.Collection = Collection
