//===================================================================================
//
//  ADOBE CONFIDENTIAL
//
//  Copyright © 2015 Adobe Systems Incorporated
//  All Rights Reserved.
//
//  NOTICE:  All information contained herein is, and remains
//  the property of Adobe Systems Incorporated and its suppliers,
//  if any.  The intellectual and technical concepts contained
//  herein are proprietary to Adobe Systems Incorporated and its
//  suppliers and are protected by all applicable intellectual property
//  laws, including trade secret and copyright laws.
//
//  Dissemination of this information or reproduction of this material
//  is strictly forbidden unless prior written permission is obtained
//  from Adobe Systems Incorporated.
//
// @author mortimer
//===================================================================================

/** 
 * The includes() method determines whether one string may be found within 
 * another string, returning true or false as appropriate. The method is 
 * case sensitive.  This method has been added to the ECMAScript 6 
 * specification and may not be available in all JavaScript implementations yet.
 *
 * @param searchString       string to be searched for within this string
 * @param position           optional param specifying the position in this 
 *                           string at which to begin searching for searchString; 
 *                           [defaults to 0]
 * @return A boolean value indicating true if the search string was found, 
 *          false otherwise.
 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes
 */
if ( typeof String.prototype.includes === "undefined" ) 
{
    String.prototype.includes = function() 
    {
        'use strict';
        return String.prototype.indexOf.apply(this, arguments) !== -1;
    };
}

/**
 * Simple singleton style logging container so all Hello related stuff has a centeralized point.
 */
var HelloLog = new (function()
{
    /**
     * Send a message to the log. An optional prefix can be passed which will get
     * prepended to the message.
     *
     * @param msg           string object containing the log message
     * @param optParam      optional object containing either a string to prepend
     *                      to the message or an object containing multiple display
     *                      options. the format is: 
     *                          { prefix : "string", indent : number, alert : true }
     */
    this.log = function( msg, optParam )
    {
        var kIndentation = "     ";
        var prefix       = (void 0);
        var depth        = 0;
        var doAlert      = false;
        var browserIsIE  = HelloUtil.browserIsIE();

        // parse out the optional parameters
        if ( typeof optParam === "object" )
        {
            prefix  = (typeof optParam.prefix !== "undefined" ) ? " - " + optParam.prefix : prefix;
            depth   = (typeof optParam.indent !== "undefined" ) ? optParam.indent : depth;
            doAlert = (typeof optParam.alert  !== "undefined" ) ? optParam.alert  : doAlert;
        }
        else if ( typeof optParam === "string" )
        {
            prefix =  " - " + optParam;
        }

        // build up the default message
        prefix = ((typeof prefix !== "undefined") ? prefix : "");
        var logmsg = (browserIsIE === false ? ("<< AdobeHello "+ prefix + " >> ") : (prefix+" "));

        // add indentation if requested
        for ( ; depth > 0; depth-- ) { logmsg += kIndentation; }
        logmsg += msg;

        // show alert box if requested
        if ( doAlert ) { alert( logmsg ); }
        
        // dump to the log file
        if ( browserIsIE === false ) { console.log( logmsg ); }
 
        // legacy support for HelloMuse  -- this should be deprecated after Muse transitions to CEP
        if (typeof window.projectHello.logToInternalProductLog !== "undefined") { window.projectHello.logToInternalProductLog(logmsg); }
    };

    /**
     * Dumps a JavaScript object out to the log. An optional prefix can be passed which will get
     * prepended to the message.  This only works in the Chrome debugger.
     *
     * @param msg           string object containing the log message
     * @param prefix        optional object containing a string to prepend to the message
     */
    this.dump = function( obj, prefix )
    {
        if ( HelloUtil.browserIsIE() === false )
        {
            console.log( "<< AdobeHello >> object: "+ ((typeof prefix !== "undefined") ? prefix + " - %o" : "%o"), obj );
        }
    };

    /**
     * Shortcut to add a debugging prefixed message to the log.
     *
     * @param msg           string object containing the log message
     */
    this.debug = function( msg )
    {
        this.log( msg, "DEBUG" );
    };

    /**
     * Place a separator message in the log.
     */
    this.separator = function()
    {
        this.log( "==================================================================" );
    };

    /**
     * Send a separated message out to the log . An optional prefix can be passed which 
     * will get prepended to the message.
     *
     * @param msg           string object containing the log message
     * @param prefix        optional object containing a string to prepend to the message
     */
    this.mark = function( msg, prefix )
    {
        this.separator();
        this.log( "@"+Date()+" "+msg, prefix );
    };

    /**
     * Dumps out an options structure to the log.
     *
     * @param msg           string object containing the log message
     * @param obj           object to output
     * @param step          indentation step for recursive call
     */
    this.logObject = function( msg, obj, step )
    {
        try
        {
            this.log( msg );
            step = (typeof step !== "undefined") ? step : 1;

            // walk throug the object
            if ( typeof obj !== "undefined" && obj != null )
            {
                if (typeof obj === "object")
                {
                    for ( var member in obj )
                    {
                        var otype = typeof obj[member];

                        // special case for logging options - we want to expand it
                        if ( member !== "overrides") { this.log(member +" = "+obj[member]+" ("+otype+")", { indent:step }); }
                        else { this.logObject( "     "+member +" = "+obj[member]+" ("+otype+")", obj[member], step+1); }
                    }
                }
                // process array
                else if (typeof obj === "array")
                {
                    var arraylen = obj.length;

                    for ( var idx = 0; idx < arraylen; idx++ )
                    {
                        var otype = typeof obj[idx];
                        this.log("array["+idx+"]= "+obj[idx]+" ("+otype+")", { indent:step });
                    }
                }
                // atomic type
                else { this.log("("+typeof obj+") : "+obj, { indent:step }); }
            }
            else { throw new Error("Undefined object to parse"); }
        }
        catch( e ) { this.exception( e ); }
    };

    /**
     * Dumps out a JSON object structure to the log.
     *
     * @param msg           string object containing the log message
     * @param jsonData      string or object containing the JSON
     */
    this.logJSON = function( msg, jsonData )
    {
        try
        {
            this.log(msg+"\n"+JSON.stringify(jsonData, null, 4));
        }
        catch( e ) { this.exception( e ); }
    };

    /**
     * Send an error object to the log. The error contains a message and a call stack list
     *
     * @param e             error object containing the exceptions
     */
    this.exception = function( e )
    {
        var logmsg = "<< AdobeHello >> EXCEPTION! "+e.message;

        // console.log halts IE :(
        if ( HelloUtil.browserIsIE() === false ) { console.error( logmsg+"\n"+e.stack ); }

        // legacy support for HelloMuse  -- this should be deprecated after Muse transitions to CEP
        if (typeof window.projectHello.logToInternalProductLog !== "undefined") { window.projectHello.logToInternalProductLog(logmsg); }
    };

    /**
     * Send an event to the host application indicating that the data contained within the
     * event should be logged in HighBeam.
     *
     * @param category          string object containing the HighBeam category to log
     * @param subCategory       string object containing the HighBeam sub-category to log
     * @param eventName         string object containing the HighBeam event name to log
     */
    this.logPIPEvent = function( category, subCategory, eventName )
    {
        try
        {
            var pipLog = {
                            "dataType"    : "event",
                            "category"    : category,
                            "subcategory" : subCategory,
                            "eventname"   : eventName
                         };
            
            var eventData = JSON.stringify( pipLog );
            
            if (typeof window.projectHello.logToAnalytics !== "undefined") { window.projectHello.logToAnalytics(eventData); }
            else { throw new Error("Javascript linkage:  projectHello.logToAnalytics is undefined!"); }
        }
        catch( e ) { HelloLog.exception( e ); }
    };

    /**
     * Wrapper/shortcut method for logging HighBeam events corresponding to Hello interactions.
     * This method simply calls 'logPIPEvent' with the category & subCategory already set.
     *
     * @param eventName         string object containing what is essentially the eventName\
     *                          parameter to 'logPIPEvent'
     */
    this.logHelloInteractionPIPEvent = function( eventName )
    {
        this.logPIPEvent( "ProjectHello", "Interaction", eventName );
    };    
})();

/**
 * Simple singleton style utility container so all Hello related stuff has a centeralized point.
 */
var HelloUtil = new (function()
{
    /**
     * Asynchronous method to check the existence (or lack there of) of a 
     * specified path. The path can be a local or remote URL.  This method 
     * will make a callback to success or fail when the action is completed.
     *
     * @param url           url of item to check
     * @param callback      callbak method which takes the following parameters:
     *                          a boolean for success(true) or fail(false)
     *                          optional xhr object for resquest status
     *                          optional string with error status
     * @param timeout       optional timeout override
     */
    this.asyncCheckExists = function( url, callback, timeout )
    {
        try
        {
            // set timeout to default value if not passed in
            if ( typeof timeout === "undefined" ) { timeout = 5000; } // 5sec timeout

            $.ajax({
                   url     : url,
                   type    : 'HEAD',
                   timeout : timeout,
                   error   : function(jqXHR, textStatus, errorThrown) { callback(false, jqXHR, textStatus ); },
                   success : function() { callback(true);  }
            });
        }
        catch ( e ) { HelloLog.exception( e ); }
    };

    /**
     * Determine if the current browser is running on a MS Windows based OS.
     *
     * @return A boolean value indicating true if the browser is on Windows, false otherwise.
     */
    this.isWindowsOS = function()
    {
        try
        {
            return ( navigator.userAgent.indexOf('Mac OS X') === -1 );
        }
        catch( e ) { HelloLog.exception( e ); }
    };

    /**
     * Determine if the hosting browser is InternetExplorer
     *
     * @return true if the hosting browser is IE, false otherwise
     */
    this.browserIsIE = function ()
    {
        return ((navigator.userAgent.indexOf("MSIE") > -1 && !window.opera) || (!!navigator.userAgent.match(/Trident.*rv[ :]*[1-9][0-9]\./)));
    }

    /**
     * Convert a URL parameter string into an object.
     *
     * @return A object with key/value members created from the URL string.
     */
    this.convertURLParameters = function( search )
    {
        try
        {
            var hasOverrides = false;

            // build out the JS object - note: this does not handle sub-objects (like the "overrides" one)
            var jsObj        =  ((typeof search !== "undefined" && search != null && search !== "" )
                                    ? JSON.parse('{"' + search.replace(/&/g, '","').replace(/=/g,'":"') + '"}',
                                            function(key, value)
                                            {
                                                hasOverrides = ( hasOverrides === false && key.indexOf("override_") === 0) ? true : hasOverrides;
                                                var val      = (key === "" ? value : decodeURIComponent(value));

                                                // check for numeric values & boolean - other assume string
                                                val = (! $.isNumeric( val ))
                                                         ? ((val !== "true")
                                                             ? ((val !== "false") ? val : false)
                                                             : true)
                                                         : parseFloat(val);

                                                return val;
                                            })
                                    : { } );

            // build the override object if necessary, this will remove
            // the "override" params from the object
            if ( hasOverrides === true )
            {
                var overrides = {};

                for ( var objKey in jsObj )
                {
                    if ( objKey.indexOf("override_") === 0 )
                    {
                        overrides[objKey.replace( "override_", "")] = jsObj[objKey];
                        delete jsObj[objKey];
                    }
                }
                jsObj.overrides = overrides;
            }

            return jsObj;
        }
        catch( e ) { HelloLog.exception( e ); }
    };

    /**
     * Attempts to load the testing module if it exists.
     *
     * @param options           options object to use
     * @param continueFn        function to call upon completion
     */
    this.loadTestModule = function( options, continueFn )
    {
        // setup default script path & check for presence of the override script
        var contentpath = (typeof options.contentpath !== "undefined" ) ? options.contentpath : "";
        var kHelloTest  = contentpath+"scripts/hello-test.js";

        // check for presence of test script
        this.asyncCheckExists( kHelloTest, function( isTesting )
        {
            try
            {
                if ( isTesting === true )
                {
                    HelloLog.log("Testing and overrides module detected...attempting import of diagnostic/test mode options", "TEST");

                    // load the data from the testing script
                    $.getScript( kHelloTest )
                        .done(function( script, textStatus )
                        {
                            // merge the current options with any imported test options
                            options = $.extend({}, options, HelloTest.options);

                            // and continue on
                            continueFn( options );
                        })
                        .fail(function( jqxhr, settings, e ) { throw new Error(kHelloTest+" failed to load"); });
                }
                              else { continueFn( options ); }
            }
            catch( e ) { HelloLog.exception( e ); }
        }, 250);
    };

    /**
     * Retrieves the Hello CEP extension's version from the manifest XML.
     *
     * @return A string containing the version from the manifest.
     */
    this.getCEPExtensionVersion = function( useBundleVersion )
    {
        var ver = "0.0";
        try
        {
            var csInterface     = new CSInterface();
            var extensionDir    = csInterface.getSystemPath(SystemPath.EXTENSION);

            // since we are reading from a local file, it is OS specific so we
            // will need the proper path separator
            var os              = csInterface.getOSInformation();
            var pathSeparator   = os.indexOf("Win") >= 0 ? "\\" : "/";
            var manifestPath    = extensionDir + pathSeparator + "CSXS" + pathSeparator + "manifest.xml";
            var fileContents    = window.cep.fs.readFile( manifestPath );

            if ( window.DOMParser )
            {
                var parser              = new window.DOMParser();
                var xmlDoc              = parser.parseFromString( fileContents.data, "text/xml" );
                var extManifestElement  = xmlDoc.getElementsByTagName( "ExtensionManifest" )[0];

                if ( typeof extManifestElement !== "undefined" && extManifestElement )
                {
                    ver = ( ! useBundleVersion )
                            ? extManifestElement.getElementsByTagName( "ExtensionList" )[0].firstElementChild.attributes.getNamedItem("Version").nodeValue
                            : extManifestElement.attributes.getNamedItem("ExtensionBundleVersion").nodeValue;
                }
            }
            else { throw new Error("DOMParser unavailable"); }
        }
        catch( e ) { HelloLog.exception( e ); }

        return ver;
    };
})();

/**
 * Simple container for holding the internationalization string data (I18N).
 */
var HelloI18N = function()
{
    var localizedStringTable = [];
    this.size = 0;

    /**
     * Add a localized string to the object's string table.
     *
     * @param stringID      string representing the index key into the table
     * @param value         string value to store with associated key
     */
    HelloI18N.prototype.addLocalizedString = function( stringID, value )
    {
        if ( typeof stringID !== "undefined" && stringID != null && stringID !== "" )
        {
            localizedStringTable[stringID] = value;
            this.size++;
        }
    };

    /**
     * Retrieve a localized string to the object's string table. If the string
     * is not present, then a standard error string is returned instead.
     *
     * @param stringID      string representing the index key into the table
     * @return String value to associated with the key
     */
    HelloI18N.prototype.getLocalizedString = function( stringID )
    {
        return (( typeof stringID !== "undefined" && stringID != null
                 && stringID !== "" && typeof localizedStringTable[stringID] !== "undefined" )
                    ? localizedStringTable[stringID]
                    : "NON-LOCALIZED:"+stringID );
    };

    /**
     * Add a series of strings into the table from a file.
     *
     * @param xmlURL            url to the XML file containing the strings to add
     * @param doneCallback      method to call upon completion
     */
    HelloI18N.prototype.addLocalizedStringsFromFile = function( xmlURL, doneCallback )
    {
        try
        {
            var self = this;  // this will change depending on lambda's

            // check validity & existence of data file
            if ( typeof xmlURL !== "undefined" && xmlURL != null && xmlURL !== "" )
            {
                HelloUtil.asyncCheckExists( xmlURL, function(doesExist, jqXHR, textStatus)
                        {
                            try
                            {
                                if ( doesExist === true )
                                {
                                    $.get( xmlURL, {}, function(xml)
                                             {
                                                 try
                                                 {
                                                     $(xml).find("string").each(function()
                                                            {
                                                                self.addLocalizedString($(this).attr("name"), $(this).text());
                                                         HelloLog.debug("Added: "+$(this).attr("name")+" = "+ $(this).text());
                                                            });

                                                    // establish the completion callback
                                                    if ( typeof doneCallback !== "undefined" && doneCallback != null && doneCallback !== "") { doneCallback(); }
                                                 }
                                                 catch( e ) { HelloLog.exception( e ); }
                                             });
                                }
                                else { throw new Error("Existence failed: "+textStatus); }
                            }
                            catch( e ) { HelloLog.exception( e ); }
                        });
            }
            else { throw new Error("XML URL is undefined"); }
        }
        catch( e ) { HelloLog.exception( e ); }
    };
};
