/**
    Copyright 2008 Wesley Swanepoel

    Licensed 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.
*/
package com.tracking {
    import com.google.analytics.AnalyticsTracker;
    import com.google.analytics.GATracker;
    import com.google.analytics.components.*;
    import com.omniture.AppMeasurement;
    import com.utils.Tracer;
    
    import flash.display.DisplayObject;
    import flash.events.IOErrorEvent;
    import flash.events.SecurityErrorEvent;
    import flash.events.TimerEvent;
    import flash.external.ExternalInterface;
    import flash.net.URLLoader;
    import flash.net.URLRequest;
    import flash.net.URLRequestMethod;
    import flash.net.navigateToURL;
    import flash.utils.Timer;
    import flash.utils.getQualifiedClassName;

    /**
     * A manager class dealing with tracking. It supports Omniture, GoogleAnalytics, Webtrends and general
     * image request tracking like Eyeblaster, Floodlight or Spotlight tracking.    
     * 
     * Ported from the AS2 version by Wesley Swanepoel. 
     * @author Wijnand.Warren
     * @version .314
     * @date 20080311
     * 
     * modified by Wesley Swanepoel 20080416
     */

    public class TrackingManager {

        private static const CHANNEL_ID : String = "channel";
        private static const PRODUCT_ID : String = "product";
        private static const PAGENAME_ID : String = "pageName";
        private static const CAMPAIGN_ID : String = "globalCampaign";
        private static const EVENTS_ID : String = "events";
        private static const TYPE_STATIC : String = "staticType";
        private static const TYPE_DYNAMIC : String = "dynamicType";
        private static const PAGENAME_DELIMITER : String = ":";
        private static const EVENT_DELIMITER : String = ",";
        private static const DEBUG : Boolean = true;

        private static var settings : Boolean = false;
        private static var instance : TrackingManager;    

        private static var sourceXML : XML;
        private static var aSource :AppMeasurement;

        private static var dataObj : Object;
        private static var pageNameSettings : XMLList;
        private static var channelSettings : XMLList;
        private static var propSettings : Array; 
        private static var evarSettings : Array;
        private static var requestList :Array;
        
        public static var stage:DisplayObject;

        
        public function TrackingManager() {
            if ( instance != null )
                Tracer.output(DEBUG, " Instantiate using getInstance()", toString(), Tracer.ERROR);
        }

        
        /**
         * Singleton creation. 
         * @return TrackingManager An instance of the TrackingManager class
         */    
        public static function getInstance() : TrackingManager {
            if ( instance == null )
                instance = new TrackingManager();
            
            return instance;
        }

        
        /**
         * Initialise the tracking XML object. This method is required to be called before anything else. 
         * @param _sourceXML The XML object that is required for use of all tracking tags
         * @param container A container DisplayObject representing the parent of the 
         */
        public function init( _sourceXML : XML, stage:DisplayObject) : void {
            TrackingManager.stage = stage;
            sourceXML = _sourceXML;
            dataObj = new Object();
            propSettings = new Array();
            evarSettings = new Array();
            requestList = new Array();
            channelSettings = new XMLList();
            pageNameSettings = new XMLList();
            
            var node : XMLList = new XMLList();
            node = getTrackingNodeByType("omniture");
            
            if (settings == false) {
                settings = true;
                omnitureSettings(node);
                setStaticValues(node);
            }
        }

        
        /**
         * Add props and eVars to the internal data object
         * @param arg_arr An array of XMLNodes containing the item nodes of a variable 
         *             in the format <item id="pageName"><![CDATA[home]]></item>
         * 
         * @param node The tracking node containing the entire tracking type
         *            <tracking type="omniture">..</tracking>
         */
        public static function addPropEvarVariables( arg_arr : XMLList ) : void {
            for (var i : Number = 0;i < arg_arr.length(); i++) 
                dataObj[ arg_arr[i].@id ] = { type: TYPE_DYNAMIC, value: arg_arr[i].text() };
        }

        
        /**
         * Add events to the internal data object
         * @param arr An Array of XMLNodes containing all the events to be added 
         */
        public static function addEventVariables( arr : XMLList ) : void {
            var value : String = "";
            for (var i : Number = 0;i < arr.length(); i++) {
                if ( value != "" ) value += EVENT_DELIMITER;
                value += arr[i].text();
            }
            
            if ( value != "" ) dataObj[EVENTS_ID] = { type: TYPE_DYNAMIC, value: value };/**/
        }

        
        /**
         * The main method to call when tracking Omniture using XML 
         * <p>
         * <p><b>Example:</b>
         *         <p>TrackingManager.getInstance().init( trackingXML, _level0 );
         *         <p>TrackingManager.trackOmniture( "home" );
         *         <p>TrackingManager.trackOmniture( "products", false, true );
         * <p>
         * @param trackID The item ID specified in the XML to track.
         *
         * @param doTrack A flag indicating if the ActionSource track() method should
         *             be invoked. This is useful should you want to set the 
         *             internal object variables but not actually track anything.
         *
         * @param reset A flag indicating if the internal data object should be reset.
         *             If not specified the internal object will retain the previous 
         *             values set and send those when the ActionSource track() method 
         *             is invoked.
         *
         * @param initObject Specify this object should you want to manually override 
         *                variables. Object should be in the format 
         *                obj = {key: value}; or obj = {prop2: "hello world"}
         *
         * @param delay A delay in MS for when the ActionSource track() method is 
         *             invoked.
         */    
        public static function trackOmniture( trackID : String, doTrack : Boolean = true, reset : Boolean = false, initObject : Object = null, delay : Number = 0 ) : void {

            if ( reset )
                resetData();
            
            var node : XMLList = new XMLList();
            node = getTrackingNodeByType("omniture");
            
            if ( aSource != null ) 
            {
                var trackItem:XMLList = new XMLList( node.track_item.(@id == trackID));
                var trackType:String = new XMLList( trackItem.@type );
                var propList:XMLList = new XMLList( trackItem.item.(attribute("group") == "prop") );
                var evarList:XMLList = new XMLList(trackItem.item.(attribute("group") == "eVar"));
                var eventList:XMLList = new XMLList(trackItem.item.(attribute("group") == "event"));
                var channel:XMLList = new XMLList(trackItem.item.(attribute("id") == "channel"));
                var pageName:XMLList = new XMLList(trackItem.item.(attribute("id") == "pageName"));
                var globalCampaign:XMLList = new XMLList(trackItem.item.(attribute("id") == "globalCampaign"));
                var product:XMLList = new XMLList(trackItem.item.(attribute("id") == "products"));
                var propEvarList:XMLList = new XMLList(propList.copy() + evarList.copy());
                                
                
                
                if ( channel != null ) dataObj[CHANNEL_ID] = { type: TYPE_DYNAMIC, value: channel };
                if ( pageName != null ) dataObj[PAGENAME_ID] = { type: TYPE_DYNAMIC, value: pageName };
                if ( product != null ) dataObj[PRODUCT_ID] = { type: TYPE_DYNAMIC, value: product };
                if ( globalCampaign != null ) dataObj[CAMPAIGN_ID] = { type: TYPE_DYNAMIC, value: globalCampaign };
                
                addPropEvarVariables(propEvarList);
                addEventVariables(eventList);
                updateASource(node, trackItem, doTrack);
                
                if ( initObject != null ) updateASourceNoBuild(initObject);
                if ( doTrack ) sendOmnitureTrack(trackType, pageName, delay);
                
                TrackingManager.trackGoogle(trackID);
            } 
            else 
            {
                Tracer.output(DEBUG, " No ActionSource component defined.", instance.toString(), Tracer.ERROR);    
            }
        }

        
        /**
         * Basically any tracking method requiring a direct request for a blank 
         * image with additional post vars. Used quite often with Floodlight/Spotlight/Eyeblaster
         * 
         * @param type The type of tracking (Floodlight, Spotlight or Eyeblaster)
         * @param trackID Page ID as defined by client
         */    
        public static function trackByImageRequest( type : String, trackID : String ) : void {
            var node : XMLList = getTrackingNodeByType(type);
            var pageID : String = node.track_item.(@id == trackID);
            var rand : Number = Math.floor(Math.random() * 10000000);
            var loader : URLLoader = new URLLoader();
            var req : URLRequest = new URLRequest(pageID + rand);
            req.method = URLRequestMethod.POST; 
            loader.load(req);
            loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, loaderSecurityErrorHandler);
            loader.addEventListener(IOErrorEvent.IO_ERROR, loaderIOErrorHandler);
            
            Tracer.output(DEBUG, " TrackingManager.trackEB(type, trackID): " + pageID, instance.toString(), Tracer.INFO);
        }

        
        /**
         * Google Analytics tracking method, using ga.js
         * More info: http://www.google.com/support/googleanalytics/bin/answer.py?answer=55520&topic=11006
         * 
         * @param trackID Page ID as defined by client
         */
        public static function trackGoogle( trackID : String ) : void {
            var node : XMLList = getTrackingNodeByType("Google");
            
            var action_node:XMLList = node.track_item.(@id==trackID);
            
            var tracker:AnalyticsTracker = new GATracker(TrackingManager.stage, node.account_settings.item.(@id=='setAccount'),'AS3',false);
            
            if(action_node.type == 'page') {
                var pageName:String = action_node.@pageName
                var application:String = action_node.@application;
                var language:String = action_node.@language;
                var section:String = action_node.@section;
                var rollupPage:String = action_node.@rollupPage;
                tracker.trackPageview(pageName);
                
            } else if(action_node.type == 'click event') {
                var category:String = action_node.@category;
                var action:String = action_node.@action;
                var label:String = action_node.@label;
                tracker.trackEvent(category,action,label);
            }
            
            
            
            /*
            <!--  SHARE: SHARE WITH AN AGENT - SHARE BUTTON    -->
            <track_item id="Share With An Agent Button" type="click event">            
            <item id="category"><![CDATA[Share]]></item>            
            <item id="action"><![CDATA[Button Click]]></item>
            <item id="label"><![CDATA[Share With Agent]]></item>
            </track_item>
            */
            
            
        }

        /**
         * Webtrends tracking method for an item in the XML file.
         * This method calls the javascript function dcsMultitrack that must be implemented in the html page. 
         * @param trackID    The item ID specified in the XML to track.
         */
        public static function trackWebtrends( trackID : String ) : void {
        
            Tracer.output(DEBUG, " TrackingManager.trackWebtrends : " + trackID, getInstance().toString(), Tracer.INFO);
        
            var node : XMLList = new XMLList();
            node = getTrackingNodeByType("webtrends");
            var method : String = node.@["method"];
            var trackItem : XMLList = node.track_item.(@id == trackID);
            
            var key : String = "";
            var value : String = "";
            var trackString : String = "";
            
            for each(var item:XML in trackItem.item) {
                key = item.@id;
                value = item.text();
                trackString += "'" + key + "'" + "," + value ;
                if(item.childIndex() != trackItem.children().length() - 1)
                    trackString += ",";
            }
            
            if ( trackString != null ) {
                if ( ExternalInterface.available )
                    ExternalInterface.call(method, trackString);
                else
                    navigateToURL(new URLRequest("javascript:method('" + trackString + "');"));
                Tracer.output(DEBUG, instance.toString(), Tracer.INFO, " TrackingManager.trackWebtrends( " + trackID + " )");
            }
            else {
                Tracer.output(DEBUG, instance.toString(), Tracer.ERROR, " No Webtrends params defined.");    
            }
        }

        
        /**
         * Populates the dataObj with the static values
         * @param node the xml node with the omniture tracking settings
         * @return n/a
         */
        private static function setStaticValues( node : XMLList ) : void {

            var staticList : XMLList = XMLList(node.staticVars.item);
            for each (var staticItem:XML in staticList)
                dataObj[ staticItem.@id ] = {type:TYPE_STATIC, value:staticItem.text()};
        }

        
        
        /**
         * Calls the ActionSource track() or trackLink() method if the delay is less or equal to zero.
         * If a delay is specified, the track values are added to the queue and the timer is invoked. If 
         * trackLink() is used, possible parameters are  url, type indicating which link report the URL 
         * or name will be displayed in. Type can have the following possible values “o” (Custom Links), 
         * “d” (File Downloads) and “e” (Exit Links). The Name parameter identifies the name that will 
         * appear in the link report. 
         *   
         * @param trackType "page" or "link" refers to the type of tracking that will happen.
         */        
        private static function sendOmnitureTrack( trackType : String, pageName : String, delay : Number = 0 ) : void {
            Tracer.output(DEBUG, " sendOmnitureTrack() - type: " + trackType + ", pageName: " + pageName + ", delay: " + delay, instance.toString());
            
            if ( delay <= 0 ) {
                trackType == "page" ? aSource.track() : aSource.trackLink(pageName, "o", pageName);
            }
            else {
                requestList.push(new Array(trackType, pageName));
                var delayTimer : Timer = new Timer(delay, 1);
                delayTimer.addEventListener(TimerEvent.TIMER, delayedOmnitureTrack);
                delayTimer.start();
            }
        }

        
        /**
         * Called after the delay set in sendOmnitureTrack is reached
         * @param e the TimerEvent sent by the Times
         * @return none
         */
        private static function delayedOmnitureTrack( e : TimerEvent ) : void {
            Tracer.output(DEBUG, " delayedOmnitureTrack()", instance.toString());
            if(requestList.length <= 0 ) return;
            var requestItem : Array = requestList.shift();
            sendOmnitureTrack(requestItem[0], requestItem[1]);
        }

        
        /**
         * These settings are required by the ActioSource component to successfully send tracking data
         * @param node is the target settings node for Omniture tracking  
         */
        private static function omnitureSettings( node : XMLList ) : void {

            var actionSourceStr : String = node.account_settings.item.(@id == "actionSource").text();
            var account : String = node.account_settings.item.(@id == "account").text();
            var charset : String = node.account_settings.item.(@id == "charSet").text();
            var currency : String = node.account_settings.item.(@id == "currencyCode").text();
            var clickmap : String = node.account_settings.item.(@id == "trackClickMap").text();
            var movieID : String = node.account_settings.item.(@id == "movieID").text();
            var nameSpace : String = node.account_settings.item.(@id == "visitorNamespace").text();
            var dc : String = node.account_settings.item.(@id == "dc").text();
            var trackingServer : String = node.account_settings.item.(@id == "trackingServer").text();
            var trackingServerSecure : String = node.account_settings.item.(@id == "trackingServerSecure").text();
            var vmk : String = node.account_settings.item.(@id == "vmk").text();
            
            pageNameSettings = node.settings.item.(@id == "pageName");
            channelSettings = node.settings.item.(@id == "channel");
                     
            aSource = new AppMeasurement();
            if ( aSource == null ) {
                Tracer.output(DEBUG, instance.toString(), Tracer.ERROR, " No ActionSource component defined.");
                return;
            }
            
            aSource.account = account;
            aSource.visitorNamespace = nameSpace;
            aSource.dc = dc;
                            
            if ( movieID != null) aSource.movieID = movieID;
            if ( charset != null ) aSource.charSet = charset;
            if ( currency != null) aSource.currencyCode = currency;
            if ( clickmap != null ) aSource.trackClickMap = Boolean(clickmap); 
            if ( trackingServer != null ) aSource.trackingServer = trackingServer;
            if ( trackingServerSecure != null ) aSource.trackingServerSecure = trackingServerSecure;
            if ( vmk != null ) aSource.vmk = vmk;
            
            Tracer.output(DEBUG, " OmnitureSettings: actionSourceStr " + actionSourceStr, instance.toString());
            Tracer.output(DEBUG, " OmnitureSettings: account " + account, instance.toString());
            
            if ( nameSpace != "" ) Tracer.output(DEBUG, " OmnitureSettings: nameSpace " + nameSpace, instance.toString());
            if ( dc != "" ) Tracer.output(DEBUG, " OmnitureSettings: dc " + dc, instance.toString());
            if ( charset != "" ) Tracer.output(DEBUG, " OmnitureSettings: charset " + charset, instance.toString());
            if ( currency != "") Tracer.output(DEBUG, " OmnitureSettings: currency " + currency, instance.toString());
            if ( clickmap != "" ) Tracer.output(DEBUG, " OmnitureSettings: clickmap " + clickmap, instance.toString());
            if ( movieID != "") Tracer.output(DEBUG, " OmnitureSettings: movieID " + movieID, instance.toString());
            if ( trackingServer != "" ) Tracer.output(DEBUG, " OmnitureSettings: trackingServer " + trackingServer, instance.toString());
            if ( trackingServerSecure != "" ) Tracer.output(DEBUG, " OmnitureSettings: trackingServerSecure " + trackingServerSecure, instance.toString());
            if ( vmk != "" ) Tracer.output(DEBUG, " OmnitureSettings: vmk " + vmk, instance.toString());
        }

        
        /**
         * Retrieves the XML node with the settings for the specified type of tracking.
         * @param type The type of tracking to retreive from the XML
         */
        private static function getTrackingNodeByType( type : String ) : XMLList {
            return sourceXML.tracking.(@type == type);
        }

        
        private static function updateASource( node : XMLList, trackItem : XMLList, doTrack : Boolean = true) : void {
            if ( doTrack )
                trace("\n-------------- TRACK ITEM: " + buildFinalValue(PAGENAME_ID, node, trackItem) + " --------------");
            
            for ( var a:String in dataObj ) {
                var str : String = buildFinalValue(a, node, trackItem);
                if ( str != "" ) 
                    aSource[a] = str; 
                
                if ( a != CAMPAIGN_ID && str != "" && ( doTrack )) Tracer.output(DEBUG, " " + a + " = " + aSource[a], instance.toString(), Tracer.INFO);
            }
        }

        
        private static function updateASourceNoBuild( data : Object ) : void {
            trace("\n-------------- TRACK ITEM --------------");
            for ( var a:String in data ) {
                var str : String = data[a];
                if ( str != "" ) 
                    aSource[a] = str; 
                
                if ( a != CAMPAIGN_ID) Tracer.output(DEBUG, " " + a + " = " + aSource[a], instance.toString(), Tracer.INFO);
            }
        }

        
        /**
         * Builds the string to send for tracking from XML track_item
         * 
         * First step: retrieve settings array
         * Second step: check if array is empty
         * Third step: get individual values from settings array
         * Fourth step: if individual values don't exist, look to internal dataObject to see if it has been previously added
         * Fifth step: if it was added, append to return string
         * Sixth step: if not added before, remove trailing delimiter
         * 
         * @return concatenated string (ie  ww:products:w960)
         */
        private static function buildFinalValue( id : String, node : XMLList, trackItem : XMLList ) : String {
            
            var value : String = "";
            var xValue : String = "";        
            
            var settingsArr : XMLList = node.settings.( attribute("id") == id ).item; 
            if ( id.substring(0, 4) == "prop") {
                settingsArr = node.settings.(@id == "prop").prop.(@id == id).item;
            }
            if ( id.substring(0, 4) == "eVar") {
                settingsArr = node.settings.(@id == "eVar").eVar.(@id == id).item;
            }
    
            if ( settingsArr.length() > 0 ) {
                var arrLength : int = settingsArr.length();
                for (var i : Number = 0;i < arrLength; i++ ) {
                    if ( xValue != "" ) value += PAGENAME_DELIMITER;
                    xValue = trackItem.track_item.item.(@id == settingsArr[i].@id).text();
                    
                    if ( xValue == "" ) {
                        (dataObj[settingsArr[i].@id] != undefined && dataObj[settingsArr[i].@id].value != null) ? xValue = dataObj[settingsArr[i].@id].value : xValue = "";
                        value += xValue;
                        if ( xValue == "" ) value = value.substring(0, value.lastIndexOf(":"));
                    }
                    else {
                        value += xValue;
                    }
                }
                return value;
            }
            else {
                if(dataObj[id] != null) {
                    return dataObj[id].value;
                }
                else {
                    return "";
                }
            }
        }

        
        private static function loaderSecurityErrorHandler( e : SecurityErrorEvent ) : void {
            Tracer.output(DEBUG, e.text, instance.toString(), Tracer.ERROR);
        }

        
        private static function loaderIOErrorHandler( e : IOErrorEvent) : void {
            Tracer.output(DEBUG, e.text, instance.toString(), Tracer.ERROR);
        }

        
        /**
         * Reset the dynamic values of internal data object 
         */
        public static function resetData() : void {
            for ( var a:String in dataObj ) { 
                if ( dataObj[a] != null && dataObj[a].type == TYPE_DYNAMIC ) {
                    dataObj[a] = null;    
                    aSource[a] = null;
                }
            }
        } 

        
        /**
         * Get an instance of the ActionSource component
         */
        public function getAsource() : AppMeasurement {
            return aSource;
        }

        
        /**
         * Gets a copy of the internal data object
         */
        public function copyInternalObject() : Object {
            var tempData : Object = new Object();
            for(var i:String in dataObj)
                tempData[i] = dataObj[i];
            
            return tempData;
        }

        
        /**
         * Sets the internal data object to the object sent to this function
         * @param data the data to set as the internal data object
         */
        public function setInternalObject( data : Object ) : void {
            dataObj = data;
        }

        
        /**
         * Set a static value manually on the internal data object
         */
        public function setStaticValue( key : String, value : String ) : void {
            dataObj[key] = {type:TYPE_STATIC, value:value};
        }

        
        /**
         * Get a static value from the internal data object
         */
        public function getStaticValue( key : String ) : Object {
            return dataObj[key];
        }

        
        /**
         * A string representation of this class.
         */
        public function toString() : String {
            return getQualifiedClassName(this);
        }
    }
}