/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
'use strict';

var integration = require('@segment/analytics.js-integration');
var extend  = require('extend');

var Unomi = (module.exports = integration('Apache Unomi')
    .global('cxs')
    .assumesPageview()
    .readyOnLoad()
    .option('scope', 'systemscope')
    .option('url', 'http://localhost:8181')
    .option('timeoutInMilliseconds', 3000)
    .option('sessionCookieName', 'unomiSessionId')
    .option('sessionId'));

/**
 * Initialize.
 *
 * @api public
 */
Unomi.prototype.initialize = function() {
    var self = this;

    console.info('[UNOMI] Initializing...', arguments);

    this.analytics.on('invoke', function(msg) {
        var action = msg.action();
        var listener = 'on' + msg.action();
        self.debug('%s %o', action, msg);
        if (self[listener]) self[listener](msg);
    });

    this.analytics.personalize = function(personalization, callback) {
        this.emit('invoke', {action:function() {return "personalize"}, personalization:personalization, callback:callback});
    };

    // Standard to check if cookies are enabled in this browser
    if (!navigator.cookieEnabled) {
        this.executeFallback();
        return;
    }

    // digitalData come from a standard so we can keep the logic around it which can allow complex website to load more complex data
    window.digitalData = window.digitalData || {
        scope: self.options.scope
    };

    window.digitalData.page = window.digitalData.page || {
        path: location.pathname + location.hash,
        pageInfo: {
            pageName: document.title,
            pageID : location.pathname + location.hash,
            pagePath : location.pathname + location.hash,
            destinationURL: location.href
        }
    }

    var unomiPage = window.digitalData.page;
    if (!unomiPage) {
        unomiPage = window.digitalData.page = { pageInfo:{} }
    }
    if (self.options.initialPageProperties) {
        var props = self.options.initialPageProperties;
        this.fillPageData(unomiPage, props);
    }
    window.digitalData.events = window.digitalData.events || [];
    window.digitalData.events.push(this.buildEvent('view', this.buildPage(unomiPage), this.buildSource(this.options.scope, 'site')))

    if (!self.options.sessionId) {
        var cookie = require('component-cookie');

        self.sessionId = cookie(self.options.sessionCookieName);
        // so we should not need to implement our own
        if (!self.sessionId || self.sessionId === '') {
            self.sessionId = self.generateGuid();
            cookie(self.options.sessionCookieName, self.sessionId);
        }
    } else {
        this.sessionId = self.options.sessionId;
    }

    setTimeout(self.loadContext.bind(self), 0);
};

/**
 * Loaded.
 *
 * @api private
 * @return {boolean}
 */
Unomi.prototype.loaded = function() {
    return !!window.cxs;
};

/**
 * Page.
 *
 * @api public
 * @param {Page} page
 */
Unomi.prototype.page = function(page) {
    var unomiPage = { };
    this.fillPageData(unomiPage, page.json().properties);

    this.collectEvent(this.buildEvent('view', this.buildPage(unomiPage), this.buildSource(this.options.scope, 'site')));
};

Unomi.prototype.fillPageData = function(unomiPage, props) {
    unomiPage.attributes = [];
    unomiPage.consentTypes = [];
    unomiPage.interests = props.interests || {};
    unomiPage.pageInfo = extend({}, unomiPage.pageInfo, props.pageInfo);
    unomiPage.pageInfo.pageName = unomiPage.pageInfo.pageName || props.title;
    unomiPage.pageInfo.pageID = unomiPage.pageInfo.pageID || props.path;
    unomiPage.pageInfo.pagePath = unomiPage.pageInfo.pagePath || props.path;
    unomiPage.pageInfo.destinationURL = unomiPage.pageInfo.destinationURL || props.url;
    unomiPage.pageInfo.referringURL = unomiPage.pageInfo.referringURL || props.referrer;
    this.processReferrer();
};

Unomi.prototype.processReferrer = function() {
    var referrerURL = document.referrer;
    if (referrerURL) {
        // parse referrer URL
        var referrer = document.createElement('a');
        referrer.href = referrerURL;

        // only process referrer if it's not coming from the same site as the current page
        var local = document.createElement('a');
        local.href = document.URL;
        if (referrer.host !== local.host) {
            // get search element if it exists and extract search query if available
            var search = referrer.search;
            var query = undefined;
            if (search && search != '') {
                // parse parameters
                var queryParams = [], param;
                var queryParamPairs = search.slice(1).split('&');
                for (var i = 0; i < queryParamPairs.length; i++) {
                    param = queryParamPairs[i].split('=');
                    queryParams.push(param[0]);
                    queryParams[param[0]] = param[1];
                }

                // try to extract query: q is Google-like (most search engines), p is Yahoo
                query = queryParams.q || queryParams.p;
                query = decodeURIComponent(query).replace(/\+/g, ' ');
            }

            // add data to digitalData
            if (window.digitalData && window.digitalData.page && window.digitalData.page.pageInfo) {
                window.digitalData.page.pageInfo.referrerHost = referrer.host;
                window.digitalData.page.pageInfo.referrerQuery = query;
            }

            // register referrer event
            this.registerEvent(this.buildEvent('viewFromReferrer', this.buildTargetPage()));
        }
    }
};


/**
 * Identify.
 *
 * @api public
 * @param {Identify} identify
 */
Unomi.prototype.identify = function(identify) {
    this.collectEvent(this.buildEvent("identify",
        this.buildTarget(identify.userId(), "analyticsUser", identify.traits()),
        this.buildSource(this.options.scope, 'site', identify.context())));
};

/**
 * ontrack.
 *
 * @api private
 * @param {Track} track
 */
Unomi.prototype.track = function(track) {
    // we use the track event name to know that we are submitted a form because Analytics.js trackForm method doesn't give
    // us another way of knowing that we are processing a form.
    if (track.event() && track.event().indexOf("form") === 0) {
        var form = document.forms[track.properties().formName];
        var formEvent = this.buildFormEvent(form.name);
        formEvent.properties = this.extractFormData(form);
        this.collectEvent(formEvent);
    } else {
        this.collectEvent(this.buildEvent(track.event(),
            this.buildTargetPage(),
            this.buildSource(this.options.scope, 'site', track.context()),
            track.properties()
        ));
    }
};

/**
 * This function is used to load the current context in the page
 *
 * @param {boolean} [skipEvents=false] Should we send the events
 * @param {boolean} [invalidate=false] Should we invalidate the current context
 */
Unomi.prototype.loadContext = function (skipEvents, invalidate) {
    this.contextLoaded = true;
    var jsonData = {
        requiredProfileProperties: ['j:nodename'],
        source: this.buildPage(window.digitalData.page)
    };
    if (!skipEvents) {
        jsonData.events = window.digitalData.events
    }
    if (window.digitalData.personalizationCallback) {
        jsonData.personalizations = window.digitalData.personalizationCallback.map(function (x) {
            return x.personalization
        })
    }

    jsonData.sessionId = this.sessionId;

    var contextUrl = this.options.url + '/context.json';
    if (invalidate) {
        contextUrl += '?invalidateSession=true&invalidateProfile=true';
    }

    var self = this;

    var onSuccess = function (xhr) {

        window.cxs = JSON.parse(xhr.responseText);

        self.ready();

        if (window.digitalData.loadCallbacks) {
            console.info('[UNOMI] Found context server load callbacks, calling now...');
            for (var i = 0; i < window.digitalData.loadCallbacks.length; i++) {
                window.digitalData.loadCallbacks[i](digitalData);
            }
        }
        if (window.digitalData.personalizationCallback) {
            console.info('[UNOMI] Found context server personalization, calling now...');
            for (var i = 0; i < window.digitalData.personalizationCallback.length; i++) {
                window.digitalData.personalizationCallback[i].callback(cxs.personalizations[window.digitalData.personalizationCallback[i].personalization.id]);
            }
        }
    };

    this.ajax({
        url: contextUrl,
        type: 'POST',
        async: true,
        contentType: 'text/plain;charset=UTF-8', // Use text/plain to avoid CORS preflight
        jsonData: jsonData,
        dataType: 'application/json',
        invalidate: invalidate,
        success: onSuccess,
        error: this.executeFallback
    });

    console.info('[UNOMI] Context loading...');
};

Unomi.prototype.onpersonalize = function (msg) {
    if (this.contextLoaded) {
        console.error('[UNOMI] Already loaded, too late...');
        return;
    }
    window.digitalData = window.digitalData || {
        scope: this.options.scope
    };
    window.digitalData.personalizationCallback = window.digitalData.personalizationCallback || [];
    window.digitalData.personalizationCallback.push({personalization: msg.personalization, callback: msg.callback});
};

/**
 * This function return the basic structure for an event, it must be adapted to your need
 *
 * @param {string} eventType The name of your event
 * @param {object} [target] The target object for your event can be build with this.buildTarget(targetId, targetType, targetProperties)
 * @param {object} [source] The source object for your event can be build with this.buildSource(sourceId, sourceType, sourceProperties)
 * @param {object} [properties] a map of properties for the event
 * @returns {{eventType: *, scope}}
 */
Unomi.prototype.buildEvent = function (eventType, target, source, properties) {
    var event = {
        eventType: eventType,
        scope: window.digitalData.scope
    };

    if (target) {
        event.target = target;
    }

    if (source) {
        event.source = source;
    }

    if (properties) {
        event.properties = properties;
    }

    return event;
};

/**
 * This function return an event of type form
 *
 * @param {string} formName The HTML name of id of the form to use in the target of the event
 * @returns {*|{eventType: *, scope, source: {scope, itemId: string, itemType: string, properties: {}}, target: {scope, itemId: string, itemType: string, properties: {}}}}
 */
Unomi.prototype.buildFormEvent = function (formName) {
    return this.buildEvent('form', this.buildTarget(formName, 'form'), this.buildSourcePage());
};

/**
 * This function return the source object for a source of type page
 *
 * @returns {*|{scope, itemId: *, itemType: *}}
 */
Unomi.prototype.buildTargetPage = function () {
    return this.buildTarget(window.digitalData.page.pageInfo.pageID, 'page', window.digitalData.page);
};

/**
 * This function return the source object for a source of type page
 *
 * @returns {*|{scope, itemId: *, itemType: *}}
 */
Unomi.prototype.buildSourcePage = function () {
    return this.buildSource(window.digitalData.page.pageInfo.pageID, 'page', window.digitalData.page);
};


/**
 * This function return the source object for a source of type page
 *
 * @returns {*|{scope, itemId: *, itemType: *}}
 */
Unomi.prototype.buildPage = function (page) {
    return this.buildSource(page.pageInfo.pageID, 'page', page);
};

/**
 * This function return the basic structure for the target of your event
 *
 * @param {string} targetId The ID of the target
 * @param {string} targetType The type of the target
 * @param {object} [targetProperties] The optional properties of the target
 * @returns {{scope, itemId: *, itemType: *}}
 */
Unomi.prototype.buildTarget = function (targetId, targetType, targetProperties) {
    return this.buildObject(targetId, targetType, targetProperties);
};

/**
 * This function return the basic structure for the source of your event
 *
 * @param {string} sourceId The ID of the source
 * @param {string} sourceType The type of the source
 * @param {object} [sourceProperties] The optional properties of the source
 * @returns {{scope, itemId: *, itemType: *}}
 */
Unomi.prototype.buildSource = function (sourceId, sourceType, sourceProperties) {
    return this.buildObject(sourceId, sourceType, sourceProperties);
};


/**
 * This function will send an event to Apache Unomi
 * @param {object} event The event object to send, you can build it using this.buildEvent(eventType, target, source)
 * @param {function} successCallback will be executed in case of success
 * @param {function} errorCallback will be executed in case of error
 */
Unomi.prototype.collectEvent = function (event, successCallback, errorCallback) {
    this.collectEvents({events: [event]}, successCallback, errorCallback);
};

/**
 * This function will send the events to Apache Unomi
 *
 * @param {object} events Javascript object { events: [event1, event2] }
 * @param {function} successCallback will be executed in case of success
 * @param {function} errorCallback will be executed in case of error
 */
Unomi.prototype.collectEvents = function (events, successCallback, errorCallback) {
    events.sessionId = this.sessionId;

    var data = JSON.stringify(events);
    this.ajax({
        url: this.options.url + '/eventcollector',
        type: 'POST',
        async: true,
        contentType: 'text/plain;charset=UTF-8', // Use text/plain to avoid CORS preflight
        data: data,
        dataType: 'application/json',
        success: successCallback,
        error: errorCallback
    });
};

/*******************************/
/* Private Function under this */
/*******************************/

Unomi.prototype.registerEvent = function (event) {
    if (window.digitalData) {
        if (window.cxs) {
            console.error('[UNOMI] already loaded, too late...');
        } else {
            window.digitalData.events = window.digitalData.events || [];
            window.digitalData.events.push(event);
        }
    } else {
        window.digitalData = {};
        window.digitalData.events = window.digitalData.events || [];
        window.digitalData.events.push(event);
    }
};

Unomi.prototype.registerCallback = function (onLoadCallback) {
    if (window.digitalData) {
        if (window.cxs) {
            console.info('[UNOMI] digitalData object loaded, calling on load callback immediately and registering update callback...');
            if (onLoadCallback) {
                onLoadCallback(window.digitalData);
            }
        } else {
            console.info('[UNOMI] digitalData object present but not loaded, registering load callback...');
            if (onLoadCallback) {
                window.digitalData.loadCallbacks = window.digitalData.loadCallbacks || [];
                window.digitalData.loadCallbacks.push(onLoadCallback);
            }
        }
    } else {
        console.info('[UNOMI] No digital data object found, creating and registering update callback...');
        window.digitalData = {};
        if (onLoadCallback) {
            window.digitalData.loadCallbacks = [];
            window.digitalData.loadCallbacks.push(onLoadCallback);
        }
    }
};

/**
 * This is an utility function to generate a new UUID
 *
 * @returns {string}
 */
Unomi.prototype.generateGuid = function () {
    function s4() {
        return Math.floor((1 + Math.random()) * 0x10000)
            .toString(16)
            .substring(1);
    }

    return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
        s4() + '-' + s4() + s4() + s4();
};

Unomi.prototype.buildObject = function (itemId, itemType, properties) {
    var object = {
        scope: window.digitalData.scope,
        itemId: itemId,
        itemType: itemType
    };

    if (properties) {
        object.properties = properties;
    }

    return object;
};

/**
 * This is an utility function to execute AJAX call
 *
 * @param {object} ajaxOptions
 */
Unomi.prototype.ajax = function (ajaxOptions) {
    var xhr = new XMLHttpRequest();
    if ('withCredentials' in xhr) {
        xhr.open(ajaxOptions.type, ajaxOptions.url, ajaxOptions.async);
        xhr.withCredentials = true;
    } else if (typeof XDomainRequest != 'undefined') {
        xhr = new XDomainRequest();
        xhr.open(ajaxOptions.type, ajaxOptions.url);
    }

    if (ajaxOptions.contentType) {
        xhr.setRequestHeader('Content-Type', ajaxOptions.contentType);
    }
    if (ajaxOptions.dataType) {
        xhr.setRequestHeader('Accept', ajaxOptions.dataType);
    }

    if (ajaxOptions.responseType) {
        xhr.responseType = ajaxOptions.responseType;
    }

    var requestExecuted = false;
    if (this.options.timeoutInMilliseconds !== -1) {
        setTimeout(function () {
            if (!requestExecuted) {
                console.error('[UNOMI] XML request timeout, url: ' + ajaxOptions.url);
                requestExecuted = true;
                if (ajaxOptions.error) {
                    ajaxOptions.error(xhr);
                }
            }
        }, this.options.timeoutInMilliseconds);
    }

    xhr.onreadystatechange = function () {
        if (!requestExecuted) {
            if (xhr.readyState === 4) {
                if (xhr.status === 200 || xhr.status === 204 || xhr.status === 304) {
                    if (xhr.responseText != null) {
                        requestExecuted = true;
                        if (ajaxOptions.success) {
                            ajaxOptions.success(xhr);
                        }
                    }
                } else {
                    requestExecuted = true;
                    if (ajaxOptions.error) {
                        ajaxOptions.error(xhr);
                    }
                    console.error('[UNOMI] XML request error: ' + xhr.statusText + ' (' + xhr.status + ')');
                }
            }
        }
    };

    if (ajaxOptions.jsonData) {
        xhr.send(JSON.stringify(ajaxOptions.jsonData));
    } else if (ajaxOptions.data) {
        xhr.send(ajaxOptions.data);
    } else {
        xhr.send();
    }
};

Unomi.prototype.executeFallback = function () {
    console.warn('[UNOMI] execute fallback');
    window.cxs = {};
    for (var index in window.digitalData.loadCallbacks) {
        window.digitalData.loadCallbacks[index]();
    }
    if (window.digitalData.personalizationCallback) {
        for (var i = 0; i < window.digitalData.personalizationCallback.length; i++) {
            window.digitalData.personalizationCallback[i].callback([window.digitalData.personalizationCallback[i].personalization.strategyOptions.fallback]);
        }
    }
};

Unomi.prototype.extractFormData = function (form) {
    var params = {};
    for (var i = 0; i < form.elements.length; i++) {
        var e = form.elements[i];
        if (typeof(e.name) != 'undefined') {
            switch (e.nodeName) {
                case 'TEXTAREA':
                case 'INPUT':
                    switch (e.type) {
                        case 'checkbox':
                            var checkboxes = document.querySelectorAll('input[name="' + e.name + '"]');
                            if (checkboxes.length > 1) {
                                if (!params[e.name]) {
                                    params[e.name] = [];
                                }
                                if (e.checked) {
                                    params[e.name].push(e.value);
                                }

                            }
                            break;
                        case 'radio':
                            if (e.checked) {
                                params[e.name] = e.value;
                            }
                            break;
                        default:
                            if (!e.value || e.value === '') {
                                // ignore element if no value is provided
                                break;
                            }
                            params[e.name] = e.value;
                    }
                    break;
                case 'SELECT':
                    if (e.options && e.options[e.selectedIndex]) {
                        if (e.multiple) {
                            params[e.name] = [];
                            for (var j = 0; j < e.options.length; j++) {
                                if (e.options[j].selected) {
                                    params[e.name].push(e.options[j].value);
                                }
                            }
                        } else {
                            params[e.name] = e.options[e.selectedIndex].value;
                        }
                    }
                    break;
                default:
                    console.warn("[UNOMI] " + e.nodeName + " form element type not implemented and will not be tracked.");
            }
        }
    }
    return params;
};
