# version 3.5.2
{{http_require}}
{{signalr_require}}
FormUrlencoded = require 'form-urlencoded'

module.exports = class MojioClient

    defaults = { hostname: 'api.moj.io', port: '443', version: 'v1', scheme: 'https', signalr_scheme: '{{signalr_default_scheme}}', signalr_port: '{{signalr_default_port}}', signalr_hub: 'ObserverHub', live: true }

    constructor: (@options) ->
        @options ?= { hostname: @defaults.hostname, port: @defaults.port, version: @defaults.version, scheme: @defaults.scheme, live: @defaults.live }
        @options.hostname ?= defaults.hostname
        @options.port ?= defaults.port
        @options.version ?= defaults.version
        @options.scheme ?= defaults.scheme
        @options.signalr_port ?= defaults.signalr_port
        @options.signalr_scheme ?= defaults.signalr_scheme
        @options.signalr_hub ?= defaults.signalr_hub
        @options.application = @options.application
        @options.secret = @options.secret
        @options.observerTransport = 'SingalR'
        @conn = null
        @hub = null
        @connStatus = null
        @auth_token = null
        @options.tokenRequester ?= (() -> return document.location.hash.match(/access_token=([0-9a-f-]{36})/))

        @signalr = new SignalR(@options.signalr_scheme+"://"+@options.hostname+":"+@options.signalr_port+"/v1/signalr",[@options.signalr_hub]{{extra_signalr_params}})

    ###
        Helpers
    ###
    getResults: (type, results) ->
        objects = []
        if (results instanceof Array)
            arrlength = results.length;
            objects.push(new type(result)) for result in results
        else if (results.Data instanceof Array)
            arrlength = results.Data.length;
            objects.push(new type(result)) for result in results.Data
        else if ((result.Data != null))
            objects.push(new type(result.Data))
        else
            objects.push(new type(result))

        return objects


    @_makeParameters: (params) ->
        '' if params.length==0
        query = '?'
        for property, value of params
            query += "#{encodeURIComponent property}=#{encodeURIComponent value}&"
        return query.slice(0,-1)

    getPath: (resource, id, action, key) ->
        if (key && id && action && id != '' && action != '')
            return "/" + encodeURIComponent(resource) + "/" + encodeURIComponent(id) + "/" + encodeURIComponent(action) + "/" + encodeURIComponent(key);
        else if (id && action && id != '' && action != '')
            return "/" + encodeURIComponent(resource) + "/" + encodeURIComponent(id) + "/" + encodeURIComponent(action);
        else if (id && id != '')
            return "/" + encodeURIComponent(resource) + "/" + encodeURIComponent(id);
        else if (action && action != '')
            return "/" + encodeURIComponent(resource) + "/" + encodeURIComponent(action);

        return "/" + encodeURIComponent(resource);

    dataByMethod: (data, method) ->
        switch (method.toUpperCase())
            when 'POST' or 'PUT' then return @stringify(data)
            else return data

    stringify: (data) ->
        return JSON.stringify(data)

    request: (request, callback, isOauth = false) ->
        parts = {
            hostname: @options.hostname
            host: @options.hostname
            port: @options.port
            scheme: @options.scheme
            path: (if isOauth then '' else '/' + @options.version)
            method: request.method,
            withCredentials: false
        }
        parts.path = parts.path + @getPath(request.resource, request.id, request.action, request.key)

        if (request.parameters? and Object.keys(request.parameters).length > 0)
            parts.path += MojioClient._makeParameters(request.parameters)

        parts.headers = {}
        parts.headers["MojioAPIToken"] = @getTokenId() if @getTokenId()?
        parts.headers += request.headers if (request.headers?)
        #parts.headers["Access-Control-Allow-Credentials"] = 'true'
        parts.headers["Content-Type"] = 'application/json'

        if (request.body?)
            if (isOauth)
                parts.body = FormUrlencoded.encode(request.body)
            else
                parts.body = request.body

        {{http_request}}
        http.request(parts, callback)

    ###
        Authorize and Login
    ###
    login_resource: 'Login'

    authorize: (redirect_url, scope='full') ->
        parts = {
            hostname: @options.hostname
            host: @options.hostname
            port: @options.port
            scheme: @options.scheme
            path: if @options.live then '/OAuth2/authorize' else '/OAuth2Sandbox/authorize'
            method: 'Get'
            withCredentials: false
        }
        parts.path += "?response_type=token"
        parts.path += "&client_id=" + @options.application
        parts.path += "&redirect_uri="+redirect_url
        parts.path += "&scope="+scope
        parts.headers = {}
        parts.headers["MojioAPIToken"] = @getTokenId() if @getTokenId()?
        parts.headers["Content-Type"] = 'application/json'

        url = parts.scheme+"://"+parts.host+":"+parts.port+parts.path
        window.location = url

    token: (callback) ->
        @user = null

        token = @options.tokenRequester()
        match = !!token && token[1]
        if (!match)
            callback("token for authorization not found.", null)
        else
            # get the user id by requesting login information, then set the auth_token:
            @request(
                {
                    method: 'GET', resource: @login_resource, id: match
                },
            (error, result) =>
                if error
                    callback(error, null)
                else
                    # set the @auth_token
                    @auth_token = result
                    callback(null, @auth_token)
            )

    unauthorize: (redirect_url) ->
        parts = {
            hostname: @options.hostname
            host: @options.hostname
            port: @options.port
            scheme: @options.scheme
            path: '/account/logout'
            method: 'Get'
            withCredentials: false
        }
        parts.path += "?Guid=" + @getTokenId()
        parts.path += "&client_id=" + @options.application
        parts.path += "&redirect_uri="+redirect_url
        parts.headers = {}
        parts.headers["MojioAPIToken"] = @getTokenId() if @getTokenId()?
        parts.headers["Content-Type"] = 'application/json'

        url = parts.scheme+"://"+parts.host+":"+parts.port+parts.path
        window.location = url

    _login: (username, password, callback) -> # Use if you want the raw result of the call.
        @request(
            {
                method: 'POST', resource: if @options.live then '/OAuth2/token' else '/OAuth2Sandbox/token',
                # method: 'POST', resource: '/OAuth2/token',
                body:
                    {
                        username: username
                        password: password
                        client_id: @options.application
                        client_secret: @options.secret
                        grant_type: 'password'
                    }

            }, callback, true
        )

    # Login
    login: (username, password, callback) ->
        @_login(username, password, (error, result) =>
            if (result?)
                @auth_token = if result.access_token then result.access_token else result
            callback(error, result)
        )

    _logout: (callback) ->
        @request(
            {
                method: 'DELETE', resource: @login_resource,
                id: if mojio_token? then mojio_token else @getTokenId()
            }, callback
        )

    # Logout
    logout: (callback) ->
        @_logout((error, result) =>
            @auth_token = null
            callback(error, result)
        )

    mojio_models = {}  # this is so model can use a string to constuct the model.

{%each models%}
    {{Model}} = require('../models/{{Model}}');
    mojio_models['{{Model}}'] = {{Model}}
{%endeach%}

    Observer = require('../models/Observer');
    mojio_models['Observer'] = Observer

    # Make an app from a result
    model: (type, json=null) ->
        if (json == null)
            return mojio_models[type]
        else if (json.Data? and json.Data instanceof Array)
            object = json
            object.Objects = new Array()
            object.Objects.push(new mojio_models[type](data)) for data in json.Data
        else if (json.Data?)
            object = new mojio_models[type](json.Data)
        else
            object = new mojio_models[type](json)
        object._client = @
        return object

    # Model CRUD
    # query(model, { criteria={ }, limit=10, offset=0, sortby="name", desc=false }, callback) # take parameters as parameters.
    # query(model, { criteria={ name="blah", field="blah" }, limit=10, offset=0, sortby="name", desc=false }, callback) # take parameters as parameters.
    # query(model, { criteria="name=blah; field=blah", limit=10, offset=0, sortby="name", desc=false }, callback)
    # query(model, { limit=10, offset=0, sortby="name", desc=false }, callback)
    query: (model, parameters, callback) ->
        if (parameters instanceof Object)
            # convert criteria to a semicolon separated list of property values if it's an object.
            if (parameters.criteria instanceof Object) # if the list contain "Criteria" as an object
                # convert to semicolon separated list.
                query_criteria = ""
                for property, value of parameters.criteria
                    query_criteria += "#{property}=#{value};"
                parameters.criteria = query_criteria;

            @request({ method: 'GET',  resource: model.resource(), parameters: parameters}, (error, result) =>
                callback(error, @model(model.model(), result))
            )
        else if (typeof parameters == "string") # instanceof only works for coffeescript classes.

            @request({ method: 'GET',  resource: model.resource(), parameters: {id: parameters} }, (error, result) =>
                callback(error, @model(model.model(), result))
            )
        else
            callback("criteria given is not in understood format, string or object.",null)


    get: (model, criteria, callback) ->
        @query(model, criteria, callback)

    save: (model, callback) ->
        @request({ method: 'PUT', resource: model.resource(), body: model.stringify(), parameters: {id: model._id} }, callback)

    put: (model, callback) ->
        @save(model, callback)

    create: (model, callback) ->
        @request({ method: 'POST', resource: model.resource(), body: model.stringify() }, callback)

    post: (model, callback) ->
        @create(model, callback)

    delete: (model, callback) ->
        @request({ method: 'DELETE',  resource: model.resource(), parameters: {id: model._id} }, callback)

    ###
            Schema
    ###
    _schema: (callback) -> # Use if you want the raw result of the call.
        @request({ method: 'GET', resource: 'Schema'}, callback)

    schema: (callback) ->
        @_schema((error, result) => callback(error, result))

    ###
            Observers
    ###

    watch: (observer, observer_callback, callback) ->
        @request({ method: 'POST', resource: Observer.resource(), body: observer.stringify()}, (error, result) =>
            if error
                callback(error, null)
            else
                observer = new Observer(result)
                @signalr.subscribe(@options.signalr_hub, 'Subscribe', observer.id(), observer.Subject, observer_callback, (error, result) ->
                    callback(null, observer)
                )
                return observer
        )

    ignore: (observer, observer_callback, callback) ->
        if !observer
            callback("Observer required.")
        if (observer['subject']?)
            if (observer.parent == null)
                @signalr.unsubscribe(@options.signalr_hub, 'Unsubscribe', observer.id(), observer.subject.id(), observer_callback, callback)
            else
                @signalr.unsubscribe(@options.signalr_hub, 'Unsubscribe', observer.id(), observer.subject.model(), observer_callback, callback)
        else
            if (observer.parent == null)
                @signalr.unsubscribe(@options.signalr_hub, 'Unsubscribe', observer.id(), observer.SubjectId, observer_callback, callback)
            else
                @signalr.unsubscribe(@options.signalr_hub, 'Unsubscribe', observer.id(), observer.Subject, observer_callback, callback)

    observe: (subject, parent=null, observer_callback, callback) ->
        # subject is { model: type, _id: id }
        if (parent == null)
            observer = new Observer(
                {
                    ObserverType: "Generic", Status: "Approved", Name: "Test"+Math.random(),
                    Subject: subject.model(), SubjectId: subject.id(), "Transports": "SignalR"
                }
            )
            @request({ method: 'PUT', resource: Observer.resource(), body: observer.stringify()}, (error, result) =>
                if error
                    callback(error, null)
                else
                    observer = new Observer(result)
                    @signalr.subscribe(@options.signalr_hub, 'Subscribe', observer.id(), observer.SubjectId, observer_callback, (error, result) ->
                        callback(null, observer)
                    )
            )

        else
            observer = new Observer(
                {
                    ObserverType: "Generic", Status: "Approved", Name: "Test"+Math.random(),
                    Subject: subject.model(),
                    Parent: parent.model(), ParentId: parent.id(), "Transports": "SignalR"
                }
            )
            @request({ method: 'PUT', resource: Observer.resource(), body: observer.stringify()}, (error, result) =>
                if error
                    callback(error, null)
                else
                    observer = new Observer(result)
                    @signalr.subscribe(@options.signalr_hub, 'Subscribe', observer.id(), subject.model(), observer_callback, (error, result) ->
                        callback(null, observer)
                    )
            )

    unobserve: (observer, subject, parent, observer_callback, callback) ->
        if !observer || !subject?
            callback("Observer and subject required.")
        else if (parent == null)
            @signalr.unsubscribe(@options.signalr_hub, 'Unsubscribe', observer.id(), subject.id(), observer_callback, callback)
        else
            @signalr.unsubscribe(@options.signalr_hub, 'Unsubscribe', observer.id(), subject.model(), observer_callback, callback)

    ###
        Storage
    ###

    store: (model, key, value, callback) ->
        if !model || !model._id
            callback("Storage requires an object with a valid id.")
        else
            @request({ method: 'PUT', resource: model.resource(), id: model.id(), action: 'store', key: key, body: JSON.stringify(value) }, (error, result) =>
                if error
                    callback(error, null)
                else
                    callback(null, result)
            )
    storage: (model, key, callback) ->
        if !model || !model._id
            callback("Get of storage requires an object with a valid id.")
        else
            @request({ method: 'GET', resource: model.resource(), id: model.id(), action: 'store', key: key}, (error, result) =>
                if error
                    callback(error, null)
                else
                    callback(null, result)
            )

    unstore: (model, key, callback) ->
        if !model || !model._id
            callback("Storage requires an object with a valid id.")
        else
            @request({ method: 'DELETE', resource: model.resource(), id: model.id(), action: 'store', key: key}, (error, result) =>
                if error
                    callback(error, null)
                else
                    callback(null, result)
            )

    ###
        Token/User
    ###

    getTokenId:  () ->
        return @auth_token._id if @auth_token? and @auth_token._id?
        return @auth_token if @auth_token?
        return null;

    getUserId:  () ->
        return @auth_token.UserId if @auth_token? and @auth_token.UserId
        return null

    isLoggedIn: () ->
        return @getUserId() != null || typeof(@auth_token) is "string"

    getCurrentUser: (callback) ->
        if (@user?)
            callback(null, @user)
        else if (@isLoggedIn())
            @get(Login, @auth_token, (error, result) =>
                if error?
                    callback(error, null)
                else if (result.UserId?)
                    @get(User, result.UserId, (error, result) =>
                        if error?
                            callback(error, null)
                        else
                            @user = result
                            callback(null, @user)
                    )
                else
                    callback("User not found", null)
            )
        else
            callback("User not found", null)
            return false
        return true
