$(function(){

  var editor
    , converter
    , autoInterval
    , githubUser
    , paperImgPath = '/img/notebook_paper_200x200.gif'
    , profile =
      {
        theme: '/static/js/ace/theme/idle_fingers'
      , showPaper: false
      , currentMd: ''
      , autosave:
        {
          enabled: true
        , interval: 3000 // might be too aggressive; don't want to block UI for large saves.
        }
      , wordcount: true
      , current_filename : $("#page-name").val()
      , dropbox:
        {
          filepath: '/Dillinger/'
        }
      }

  // Feature detect ish
  var dillinger = 'dillinger'
    , dillingerElem = document.createElement(dillinger)
    , dillingerStyle = dillingerElem.style
    , domPrefixes = 'Webkit Moz O ms Khtml'.split(' ')

  // Cache some shit
  var $theme = $('#theme-list')
    , $preview = $('#preview')
    , $autosave = $('#autosave')
    , $wordcount = $('#wordcount')
    , $import_github = $('#import_github')
    , $wordcounter = $('#wordcounter')
    , $filename = $('#filename'),
      $pagename = $("#page-name")


  // Hash of themes and their respective background colors
  var bgColors =
    {
      'chrome': '#bbbbbb'
    , 'clouds': '#7AC9E3'
    , 'clouds_midnight': '#5F9EA0'
    , 'cobalt': '#4d586b'
    , 'crimson_editor': '#ffffff'
    , 'dawn': '#DADCAD'
    , 'eclipse': '#6C7B8A'
    , 'idle_fingers': '#DEB887'
    , 'kr_theme': '#434343'
    , 'merbivore': '#3E353E'
    , 'merbivore_soft': '#565156'
    , 'mono_industrial': '#C0C0C0'
    , 'monokai': '#F5DEB3'
    , 'pastel_on_dark': '#676565'
    , 'solarized-dark': '#0E4B5A'
    , 'solarized_light': '#dfcb96'
    , 'textmate': '#fff'
    , 'tomorrow': '#0e9211'
    , 'tomorrow_night': '#333536'
    , 'tomorrow_night_blue': '#3a4150'
    , 'tomorrow_night_bright': '#3A3A3A'
    , 'tomorrow_night_eighties': '#474646'
    , 'twilight': '#534746'
    , 'vibrant_ink': '#363636'
    }


  /// UTILS =================


  /**
   * Utility method to async load a JavaScript file.
   *
   * @param {String} The name of the file to load
   * @param {Function} Optional callback to be executed after the script loads.
   * @return {void}
   */
  function asyncLoad(filename,cb){
    (function(d,t){

      var leScript = d.createElement(t)
        , scripts = d.getElementsByTagName(t)[0]

      leScript.async = 1
      leScript.src = filename
      scripts.parentNode.insertBefore(leScript,scripts)

      leScript.onload = function(){
        cb && cb()
      }

    }(document,'script'))
  }

  /**
   * Utility method to determin if localStorage is supported or not.
   *
   * @return {Boolean}
   */
  function hasLocalStorage(){
   // http://mathiasbynens.be/notes/localstorage-pattern
   var storage
   try{ if(localStorage.getItem) {storage = localStorage} }catch(e){}
   return storage
  }

  /**
   * Grab the user's profile from localStorage and stash in "profile" variable.
   *
   * @return {Void}
   */
  function getUserProfile(){

    var p;

    try{
      p = JSON.parse( localStorage.profile );
      // Need to merge in any undefined/new properties from last release
      // Meaning, if we add new features they may not have them in profile
      p = $.extend(true, profile, p)
    }catch(e){
      p = profile
    }

    if (p.filename != $pagename.val()) {
        updateUserProfile({ filename: $pagename.val(), currentMd: "" });

    }
    profile = p
  }

  /**
   * Update user's profile in localStorage by merging in current profile with passed in param.
   *
   * @param {Object}  An object containg proper keys and values to be JSON.stringify'd
   * @return {Void}
   */
  function updateUserProfile(obj){
    localStorage.clear()
    localStorage.profile = JSON.stringify( $.extend(true, profile, obj) )
  }

  /**
   * Utility method to test if particular property is supported by the browser or not.
   * Completely ripped from Modernizr with some mods.
   * Thx, Modernizr team!
   *
   * @param {String}  The property to test
   * @return {Boolean}
   */
  function prefixed(prop){ return testPropsAll(prop, 'pfx') }

  /**
   * A generic CSS / DOM property test; if a browser supports
   * a certain property, it won't return undefined for it.
   * A supported CSS property returns empty string when its not yet set.
   *
   * @param  {Object}  A hash of properties to test
   * @param  {String}  A prefix
   * @return {Boolean}
   */
  function testProps( props, prefixed ) {

      for ( var i in props ) {

          if( dillingerStyle[ props[i] ] !== undefined ) {
              return prefixed === 'pfx' ? props[i] : true
          }

      }
      return false
  }

  /**
   * Tests a list of DOM properties we want to check against.
   * We specify literally ALL possible (known and/or likely) properties on
   * the element including the non-vendor prefixed one, for forward-
   * compatibility.
   *
   * @param  {String}  The name of the property
   * @param  {String}  The prefix string
   * @return {Boolean}
   */
  function testPropsAll( prop, prefixed ) {

      var ucProp  = prop.charAt(0).toUpperCase() + prop.substr(1)
        , props   = (prop + ' ' + domPrefixes.join(ucProp + ' ') + ucProp).split(' ')

      return testProps(props, prefixed)
  }

  /**
   * Normalize the transitionEnd event across browsers.
   *
   * @return {String}
   */
  function normalizeTransitionEnd()
  {

    var transEndEventNames =
      {
        'WebkitTransition' : 'webkitTransitionEnd'
      , 'MozTransition'    : 'transitionend'
      , 'OTransition'      : 'oTransitionEnd'
      , 'msTransition'     : 'msTransitionEnd' // maybe?
      , 'transition'       : 'transitionend'
      }

     return transEndEventNames[ prefixed('transition') ]
  }


  /**
   * Generate a random filename.
   *
   * @param  {String}  The file type's extension
   * @return {String}
   */
  function generateRandomFilename(ext){
    return 'dillinger_' +(new Date()).toISOString().replace(/[\.:-]/g, "_")+ '.' + ext
  }


  /**
   * Get current filename from contenteditable field.
   *
   * @return {String}
   */
  function getCurrentFilenameFromField(){
    return $('#filename > span[contenteditable="true"]').text()
  }


  /**
   * Set current filename from profile.
   *
   * @param {String}  Optional string to force set the value.
   * @return {String}
   */
  function setCurrentFilenameField(str){
    $('#filename > span[contenteditable="true"]').text( str || profile.current_filename || "Untitled Document")
  }

  /**
   * Returns the full text of an element and all its children.
   * The script recursively traverses all text nodes, and returns a
   * concatenated string of all texts.
   *
   * Taken from
   * http://stackoverflow.com/questions/2653670/innertext-textcontent-vs-retrieving-each-text-node
   *
   * @param node
   * @return {int}
   */
  function getTextInElement(node) {
      if (node.nodeType === 3) {
          return node.data;
      }

      var txt = '';

      if (node = node.firstChild) do {
          txt += getTextInElement(node);
      } while (node = node.nextSibling);

      return txt;
  }

  /**
   * Counts the words in a string
   *
   * @param string
   * @return int
   */
  function countWords(string) {
    var words = string.replace(/W+/g, ' ').match(/\S+/g);
    return words && words.length || 0;
  }


  /**
   * Initialize application.
   *
   * @return {Void}
   */
  function init(){

    if( !hasLocalStorage() ) { sadPanda() }
    else{

      // Attach to jQuery support object for later use.
      $.support.transitionEnd = normalizeTransitionEnd();

      getUserProfile();

      initAce();

      initUi();

      marked.setOptions({
        gfm: true,
        tables: true,
        pedantic: false,
        sanitize: false,
        smartLists: true,
        smartypants: false,
        langPrefix: 'lang-'
      });

      converter = marked;

      bindPreview();

      bindNav();

      bindKeyboard();

      bindDelegation();

      bindFilenameField();

      bindWordCountEvents();

      autoSave();

      initWordCount();
      refreshWordCount();

    }

  }

  /**
   * Initialize theme and other options of Ace editor.
   *
   * @return {Void}
   */
  function initAce(){

    editor = ace.edit("editor");

  } // end initAce

  /**
   * Initialize various UI elements based on userprofile data.
   *
   * @return {Void}
   */
  function initUi(){

    // Set proper theme value in theme dropdown
    fetchTheme(profile.theme, function(){
      $theme.find('li > a[data-value="'+profile.theme+'"]').addClass('selected')

      editor.getSession().setUseWrapMode(true);
      editor.setShowPrintMargin(false);

      editor.getSession().setMode('ace/mode/markdown');

      editor.getSession().setValue( profile.currentMd || editor.getSession().getValue())

      // Immediately populate the preview <div>
      previewMd();

    });


    // Set text for dis/enable autosave / word counter
    $autosave.html( profile.autosave.enabled ? '<i class="icon-remove"></i>&nbsp;Disable Autosave' : '<i class="icon-ok"></i>&nbsp;Enable Autosave' )
    $wordcount.html( !profile.wordcount ? '<i class="icon-remove"></i>&nbsp;Disabled Word Count' : '<i class="icon-ok"></i>&nbsp;Enabled Word Count' )

    // Check for logged in Github user and notifiy
    githubUser = $import_github.attr('data-github-username');

    githubUser && Notifier.showMessage("What's Up " + githubUser, 1000);

    setCurrentFilenameField();

    /* BEGIN RE-ARCH STUFF */

    $('.dropdown-toggle').dropdown();

    /* END RE-ARCH STUFF */

  }


  /// HANDLERS =================


  /**
   * Clear the markdown and text and the subsequent HTML preview.
   *
   * @return {Void}
   */
  function clearSelection(){
    editor.getSession().setValue("");
    previewMd();
  }

  // TODO: WEBSOCKET MESSAGE?
  /**
   * Save the markdown via localStorage - isManual is from a click or key event.
   *
   * @param {Boolean}
   * @return {Void}
   */
  function saveFile(isManual){

    updateUserProfile({currentMd: editor.getSession().getValue()})

    isManual && Notifier.showMessage(Notifier.messages.docSavedLocal);

    if (isManual) {
        var data = {
            name: $("#page-name").val(),
            message: $("#page-message").val(),
            content: editor.getSession().getValue()
        };
        $.post(window.location, data, function(){
            location.href = "/" + data['name'];
        });
    }

  }

  /**
   * Enable autosave for a specific interval.
   *
   * @return {Void}
   */
  function autoSave(){

    if(profile.autosave.enabled){
      autoInterval = setInterval( function(){
        // firefox barfs if I don't pass in anon func to setTimeout.
        saveFile();
      }, profile.autosave.interval);

    }
    else{
      clearInterval( autoInterval )
    }

  }

  /**
   * Clear out user profile data in localStorage.
   *
   * @return {Void}
   */
  function resetProfile(){
    // For some reason, clear() is not working in Chrome.
    localStorage.clear();
    // Let's turn off autosave
    profile.autosave.enabled = false
    // Delete the property altogether --> need ; for JSHint bug.
    ; delete localStorage.profile;
    // Now reload the page to start fresh
    window.location.reload();
//    Notifier.showMessage(Notifier.messages.profileCleared, 1400)
  }

  /**
   * Dropbown nav handler to update the current theme.
   *
   * @return {Void}
   */
   function changeTheme(e){
     // check for same theme
     var $target = $(e.target)
     if( $target.attr('data-value') === profile.theme) { return }
     else{
       // add/remove class
       $theme.find('li > a.selected').removeClass('selected')
       $target.addClass('selected')
       // grabnew theme
       var newTheme = $target.attr('data-value')
       $(e.target).blur()
       fetchTheme(newTheme, function(){
         Notifier.showMessage(Notifier.messages.profileUpdated)
       })
      }
   }

  // TODO: Maybe we just load them all once and stash in appcache?
  /**
   * Dynamically appends a script tag with the proper theme and then applies that theme.
   *
   * @param {String}  The theme name
   * @param {Function}   Optional callback
   * @return {Void}
   */
  function fetchTheme(th, cb){
    var name = th.split('/').pop()

    asyncLoad("/static/js/ace/theme-"+ name +".js", function(){

      editor.setTheme(th)

      cb && cb()

      updateBg(name)

      updateUserProfile({theme: th})

    }) // end asyncLoad

  } // end fetchTheme(t)

  /**
   * Change the body background color based on theme.
   *
   * @param {String}  The theme name
   * @return {Void}
   */
  function updateBg(name){
    // document.body.style.backgroundColor = bgColors[name]
  }

  /**
   * Clientside update showing rendered HTML of Markdown.
   *
   * @return {Void}
   */
  function previewMd(){

    var unmd = editor.getSession().getValue()
      , md = converter(unmd)

    $preview
      .html('') // unnecessary?
      .html(md)

    refreshWordCount();
  }

  function refreshWordCount(selectionCount){
    var msg = "Words: ";
    if (selectionCount !== undefined) {
      msg += selectionCount + " of ";
    }
    if(profile.wordcount){
      $wordcounter.text(msg + countWords(getTextInElement($preview[0])));
    }
  }

  /**
   * Stash current file name in the user's profile.
   *
   * @param {String}  Optional string to force the value
   * @return {Void}
   */
  function updateFilename(str){
    // Check for string because it may be keyup event object
    var f
    if(typeof str === 'string'){
      f = str
    }else
    {
      f = getCurrentFilenameFromField()
    }
    updateUserProfile( {current_filename: f })
  }

  /**
   * XHR Post Markdown to get a md file.  Appends response to hidden iframe to
   * automatically download the file.
   *
   * @return {Void}
   */
  function fetchMarkdownFile(){

    // TODO: UPDATE TO SUPPORT FILENAME NOT JUST A RANDOM FILENAME
    var unmd = editor.getSession().getValue()

    function _doneHandler(a, b, response){
      a = b = null // JSHint complains if a, b are null in method
      var resp = JSON.parse(response.responseText)
      // console.dir(resp)
      document.getElementById('downloader').src = '/files/md/' + resp.data
    }

    function _failHandler(){
        alert("Roh-roh. Something went wrong. :(")
    }

    var mdConfig = {
                      type: 'POST',
                      data: "unmd=" + encodeURIComponent(unmd),
                      dataType: 'json',
                      url: '/factory/fetch_markdown',
                      error: _failHandler,
                      success: _doneHandler
                    }

    $.ajax(mdConfig)

  }

  /**
   * XHR Post Markdown to get a html file.  Appends response to hidden iframe to
   * automatically download the file.
   *
   * @return {Void}
   */
  function fetchHtmlFile(){

    // TODO: UPDATE TO SUPPORT FILENAME NOT JUST A RANDOM FILENAME

    var unmd = editor.getSession().getValue()

    function _doneHandler(jqXHR, data, response){
      // console.dir(resp)
      var resp = JSON.parse(response.responseText)
      document.getElementById('downloader').src = '/files/html/' + resp.data
    }

    function _failHandler(){
      alert("Roh-roh. Something went wrong. :(")
    }

    var config = {
                      type: 'POST',
                      data: "unmd=" + encodeURIComponent(unmd),
                      dataType: 'json',
                      url: '/factory/fetch_html',
                      error: _failHandler,
                      success: _doneHandler
                    }

    $.ajax(config)

  }

  function fetchPdfFile(){

    var unmd = editor.getSession().getValue()

    function _doneHandler(jqXHR, data, response){
      var resp = JSON.parse(response.responseText)
      document.getElementById('downloader').src = '/files/pdf/' + resp.data
    }

    function _failHandler(){
      alert("Roh-roh. Something went wrong. :(")
    }

    var config = {
                      type: 'POST',
                      data: "unmd=" + encodeURIComponent(unmd),
                      dataType: 'json',
                      url: '/factory/fetch_pdf',
                      error: _failHandler,
                      success: _doneHandler
                    }

    $.ajax(config)

  }

  function showHtml(){

    // TODO: UPDATE TO SUPPORT FILENAME NOT JUST A RANDOM FILENAME

    var unmd = editor.getSession().getValue()

    function _doneHandler(jqXHR, data, response){
      // console.dir(resp)
      var resp = JSON.parse(response.responseText)
      $('#myModalBody').text(resp.data)
      $('#myModal').modal()
    }

    function _failHandler(){
      alert("Roh-roh. Something went wrong. :(")
    }

    var config = {
                      type: 'POST',
                      data: "unmd=" + encodeURIComponent(unmd),
                      dataType: 'json',
                      url: '/factory/fetch_html_direct',
                      error: _failHandler,
                      success: _doneHandler
                    }

    $.ajax(config)

  }

  /**
   * Show a sad panda because they are using a shitty browser.
   *
   * @return {Void}
   */
  function sadPanda(){
    // TODO: ACTUALLY SHOW A SAD PANDA.
    alert('Sad Panda - No localStorage for you!')
  }

  /**
   * Show the modal for the "About Dillinger" information.
   *
   * @return {Void}
   */
  function showAboutInfo(){

    $('.modal-header h3').text("What's the deal with Dillinger?")

    // TODO: PULL THIS OUT AND RENDER VIA TEMPLATE FROM XHR OR STASH IN PAGE FOR SEO AND CLONE
    var aboutContent =  "<p>Dillinger is an online cloud-enabled, HTML5, buzzword-filled Markdown editor.</p>"
                      + "<p>Dillinger was designed and developed by <a href='http://twitter.com/joemccann'>@joemccann</a> because he needed a decent Markdown editor.</p>"
                      + "<p>Dillinger is a 100% open source project so <a href='https://github.com/joemccann/dillinger'>fork the code</a> and contribute!</p>"
                      + "<p>Follow Dillinger on Twitter at <a href='http://twitter.com/dillingerapp'>@dillingerapp</a></p>"
                      + "<p>Follow Joe McCann on Twitter at <a href='http://twitter.com/joemccann'>@joemccann</a></p>"

    $('.modal-body').html(aboutContent)

    $('#modal-generic').modal({
      keyboard: true,
      backdrop: true,
      show: true
    })

  }

  /**
   * Show the modal for the "Preferences".
   *
   * @return {Void}
   */
  function showPreferences(){

    $('.modal-header h3').text("Preferences")

    // TODO: PULL THIS OUT AND RENDER VIA TEMPLATE FROM XHR OR STASH IN PAGE FOR SEO AND CLONE
    var prefContent =  '<div>'
                          +'<ul>'
                            +'<li><a href="#" id="paper">Toggle Paper</a></li>'
                            +'<li><a href="#" id="reset">Reset Profile</a></li>'
                          +'</ul>'
                        +'</div>'

    $('.modal-body').html(prefContent)

    $('#modal-generic').modal({
      keyboard: true,
      backdrop: true,
      show: true
    })

  }


  /// UI RELATED =================

  /**
   * Toggles the paper background image.
   *
   * @return {Void}
   */
  function togglePaper(){

    $preview.css('backgroundImage', !profile.showPaper ? 'url("'+paperImgPath+'")' : 'url("")'  )

    updateUserProfile({showPaper: !profile.showPaper})

    Notifier.showMessage(Notifier.messages.profileUpdated)

  }

  /**
   * Toggles the autosave feature.
   *
   * @return {Void}
   */
  function toggleAutoSave(){

    $autosave.html( profile.autosave.enabled ? '<i class="icon-remove"></i>&nbsp;Disable Autosave' : '<i class="icon-ok"></i>&nbsp;Enable Autosave' )

    updateUserProfile({autosave: {enabled: !profile.autosave.enabled }})

    autoSave()

  }

  function initWordCount(){
    if (profile.wordcount) {
      $wordcounter.removeClass('hidden');
      // Modify the width of the document name
      $filename.addClass('show-word-count-filename-adjust')
    } else {
      $wordcounter.addClass('hidden');
      // Modify the width of the document name
      $filename.removeClass('show-word-count-filename-adjust')
    }
  }

  /**
   * Toggles the wordcounter feature.
   *
   * @return {Void}
   */
  function toggleWordCount() {
    $wordcount.html( profile.wordcount ? '<i class="icon-remove"></i>&nbsp;Disabled Word Count' : '<i class="icon-ok"></i>&nbsp;Enabled Word Count' )

    updateUserProfile({wordcount: !profile.wordcount })

    initWordCount();

  }


  /**
   * Bind keyup handler to the editor.
   *
   * @return {Void}
   */
  function bindFilenameField(){
    $('#filename > span[contenteditable="true"]').bind('keyup', updateFilename)
  }

  /**
   * Makes the selection check fire after every mouse up event.
   *
   * @return void
   */
  function bindWordCountEvents() {
    $preview.bind('mouseup', checkForSelection);
  }

  /**
   * Checks if there is some selected text. If so, the word counter gets updated.
   *
   * @return void
   */
  function checkForSelection() {
    if (profile.wordcount) {
      var selection = window.getSelection().toString();
      if (selection !== "") {
        refreshWordCount(countWords(selection));
      } else {
        refreshWordCount();
      }
    }
  }

  /**
   * Bind keyup handler to the editor.
   *
   * @return {Void}
   */
  function bindPreview(){
    $('#editor').bind('keyup', previewMd);
  }

  /**
   * Bind navigation elements.
   *
   * @return {Void}
   */
  function bindNav(){

    $theme
      .find('li > a')
      .bind('click', function(e){
        changeTheme(e)
        return false
      })

    $('#clear')
      .on('click', function(){
        clearSelection()
        return false
      })

    $("#save_dropbox")
      .on('click', function(){
      profile.current_filename = profile.current_filename || '/Dillinger/' + generateRandomFilename('md')

      Dropbox.putMarkdownFile()

      saveFile()

      return false
    })

    $("#save_googledrive")
      .on('click', function() {
        //profile.current_filename = profile.current_filename || generateRandomFilename('md')
        GoogleDrive.save()
        saveFile()
      })

    $("#save-native").on('click', function() {
        saveFile(true);
    });

    $(".modal-body").delegate("#paper", "click", function(){
      togglePaper()
      return false
    })

    $("#autosave")
      .on('click', function(){
        toggleAutoSave()
        return false
    })

    $("#wordcount")
      .on('click', function(){
        toggleWordCount()
        return false
    })

    $('#reset')
      .on('click', function(){
        resetProfile()
        return false
      })

    $import_github
      .on('click', function(){
        Github.fetchRepos()
        return false
      })

    $('#import_dropbox')
      .on('click', function(){
        Dropbox.searchDropbox()
        return false
      })

    $('#import_googledrive')
      .on('click', function(){
        GoogleDrive.search()
        return false
      })

    $('#export_md')
      .on('click', function(){
        fetchMarkdownFile()
        $('.dropdown').removeClass('open')
        return false
      })

    $('#export_html')
      .on('click', function(){
        fetchHtmlFile()
        $('.dropdown').removeClass('open')
        return false
      })

    $('#export_pdf')
      .on('click', function(){
        fetchPdfFile()
        $('.dropdown').removeClass('open')
        return false
      })

    $('#show_html')
      .on('click', function(){
        showHtml()
        $('.dropdown').removeClass('open')
        return false
      })

    $('#preferences').
      on('click', function(){
        showPreferences()
        return false
      })

    $('#about').
      on('click', function(){
        showAboutInfo()
        return false
      })

    $('#cheat').
      on('click', function(){
        window.open("https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet", "_blank")
        return false
      })

  } // end bindNav()

  /**
   * Bind special keyboard handlers.
   *
   * @return {Void}
   */
  function bindKeyboard(){
    // CMD+s TO SAVE DOC
    key('command+s, ctrl+s', function(e){
     saveFile(true)
     e.preventDefault() // so we don't save the webpage - native browser functionality
    })

    var saveCommand = {
       name: "save",
       bindKey: {
                mac: "Command-S",
                win: "Ctrl-S"
              },
       exec: function(){
         saveFile(true)
       }
    }
    var fileForUrlNamer = {
       name: "filenamer",
       bindKey: {
                mac: "Command-Shift-M",
                win: "Ctrl-Shift-M"
              },
       exec: function(){
        var profile = JSON.parse(localStorage.profile);
        alert( profile.current_filename.replace(/\s/g, '-').toLowerCase())
      }
    }

    editor.commands.addCommand(saveCommand)
    editor.commands.addCommand(fileForUrlNamer)
  }

  /**
   * Bind dynamically added elements' handlers.
   *
   * @return {Void}
   */
  function bindDelegation(){
    $(document)
      .on('click', '.repo', function(){
        var repoName = $(this).parent('li').attr('data-repo-name')

        Github.isRepoPrivate = $(this).parent('li').attr('data-repo-private') === 'true' ? true : false

        Github.fetchBranches( repoName )
        return false
      })
      .on('click', '.branch', function(){

        var repo = $(this).parent('li').attr('data-repo-name')
          , sha = $(this).parent('li').attr('data-commit-sha')

        Github.currentBranch = $(this).text()

        Github.fetchTreeFiles( repo, sha )
        return false
      })
      .on('click', '.tree_file', function(){

        var file = $(this).parent('li').attr('data-tree-file')

        Github.fetchMarkdownFile(file)

        return false
      })
      .on('click', '.dropbox_file', function(){

        // We stash the current filename in the local profile only; not in localStorage.
        // Upon success of fetching, we add it to localStorage.

        var dboxFilePath = $(this).parent('li').attr('data-file-path')

        profile.current_filename = dboxFilePath.split('/').pop().replace('.md', '')

        Dropbox.setFilePath( dboxFilePath )

        Dropbox.fetchMarkdownFile( dboxFilePath )

        return false

      })
      .on('click', '.googledrive_file', function(){
        var fileId = $(this).parent('li').attr('data-file-id')
        profile.current_filename = $(this).html()
        GoogleDrive.fileId = fileId
        GoogleDrive.get()
        return false
      })

      // Check for support of drag/drop
      if('draggable' in document.createElement('span')){
        $('#editor')
          .on('dragover', function (e) {
            e.preventDefault()
            e.stopPropagation()
          })
          .on('drop', function(e) {
            e.preventDefault()
            e.stopPropagation()

            // fetch FileList object
            var originalEvent = e.originalEvent
                , files = originalEvent.target.files || originalEvent.dataTransfer.files
                , reader = new FileReader()
                , i = 0
                , file
                , name

            // find the first text file
            do {
              file = files[i++]
            } while (file && file.type.substr(0, 4) !== 'text' && file.name.substr(file.name.length - 3) !== '.md')

            if (!file) return

            reader.onload = function (lE) {
              editor.getSession().setValue(lE.target.result)
              previewMd()
            }
            reader.readAsText(file)
          })
      } // end if draggable

  } // end bindDelegation()


  /// MODULES =================


  // Notification Module
  var Notifier = (function(){

    var _el = $('#notify')

      return {
        messages: {
          profileUpdated: "Profile updated"
          , profileCleared: "Profile cleared"
          , docSavedLocal: "Document saved locally"
          , docSavedServer: "Document saved on our server"
          , docSavedDropbox: "Document saved on dropbox"
          , dropboxImportNeeded: "Please import a file from dropbox first."
        },
        showMessage: function(msg,delay){

          // TODO: FIX ANIMATION QUEUE PROBLEM - .stop() doesn't work.

          _el
            .text('')
            .stop()
            .text(msg)
            .slideDown(250, function(){
              _el
                .delay(delay || 1000)
                .slideUp(250)
            })

          } // end showMesssage
      } // end return obj
  })() // end IIFE

  // Github API Module
  var Github = (function(){

    // Sorting regardless of upper/lowercase
    function _alphaNumSort(m,n) {
      var a = m.url.toLowerCase()
      var b = n.url.toLowerCase()
      if (a === b) { return 0 }
      if (isNaN(m) || isNaN(n)){ return ( a > b ? 1 : -1)}
      else {return m-n}
    }

    // Test for md file extension
    function _isMdFile(file){
      return (/(\.md)|(\.markdown)/i).test(file)
    }

    // Returns an array of only md files from a tree
    function _extractMdFiles(repoName, treefiles){
      /*
      mode: "100644"
      path: ".gitignore"
      sha: "7a1aeb2497018aeb0c44e220d4b84f2d245e3033"
      size: 110
      type: "blob"
      url: "https://api.github.com/repos/joemccann/express/git/blobs/7a1aeb2497018aeb0c44e220d4b84f2d245e3033"
      */
      // https://raw.github.com/joemccann/express/master/History.md

      var sorted = []
        , raw = 'https://raw.github.com'
        , slash = '/'

      treefiles.forEach(function(el){

        if( _isMdFile(el.path) ){

          var fullpath

          if( Github.isRepoPrivate){
            fullpath = el.url
          }
          else{
            // we go straight to raw as it's faster (don't need to base64 decode the sha as in the private case)
            fullpath = raw + slash + githubUser + slash + repoName + slash + Github.currentBranch + slash + el.path
          }

          var item =
          {
            link: fullpath
          , path: el.path
          , sha: el.sha
          }

          sorted.push( item )
        }

      }) // end forEach()

      return sorted

    }

    // Show a list of repos
    function _listRepos(repos){

      var list = '<ul>'

      // Sort alpha
      repos.sort(_alphaNumSort)

      repos.forEach(function(item){
        list += '<li data-repo-name="' + item.name + '" data-repo-private="' + item.private + '"><a class="repo" href="#">' + item.name + '</a></li>'
      })

      list += '</ul>'

      $('.modal-header h3').text('Your Github Repos')

      $('.modal-body').html(list)

      $('#modal-generic').modal({
        keyboard: true,
        backdrop: true,
        show: true
      })

      return false

    }

    // Show a list of branches
    function _listBranches(repoName, branches){

      var list = ''

      branches.forEach(function(item){
        var name = item.name
          , commit = item.commit.sha
        list += '<li data-repo-name="' + repoName + '" data-commit-sha="' + commit + '"><a class="branch" href="#">' + name + '</a></li>'
      })

      $('.modal-header h3').text(repoName)

      $('.modal-body')
        .find('ul')
          .find('li')
          .remove()
          .end()
        .append(list)
    }

    // Show a list of tree files
    function _listTreeFiles(repoName, treefiles){

      var mdFiles = _extractMdFiles(repoName, treefiles)
        , list = ''

      mdFiles.forEach(function(item){
        // add class to <li> if private
        list += Github.isRepoPrivate
                ? '<li data-tree-file-sha="' + item.sha + '" data-tree-file="' + item.link + '" class="private_repo"><a class="tree_file" href="#">' + item.path + '</a></li>'
                : '<li data-tree-file="' + item.link + '"><a class="tree_file" href="#">' + item.path + '</a></li>'

      })

      $('.modal-header h3').text(repoName)

      $('.modal-body')
        .find('ul')
          .find('li')
          .remove()
          .end()
        .append(list)
    }

    return{
      currentBranch: '',
      isRepoPrivate: false,
      fetchRepos: function(){

        function _beforeSendHandler(){
          Notifier.showMessage('Fetching Repos...')
        }

        function _doneHandler(a, b, response){
          a = b = null
          response = JSON.parse(response.responseText)
          // console.dir(response)
          if( !response.length ) { Notifier.showMessage('No repos available!') }
          else {
            _listRepos(response)
          } // end else
        } // end done handler

        function _failHandler(resp,err){
          alert(resp.responseText || "Roh-roh. Something went wrong. :(")
        }

        var config = {
                        type: 'POST',
                        dataType: 'text',
                        url: '/import/github/repos',
                        beforeSend: _beforeSendHandler,
                        error: _failHandler,
                        success: _doneHandler
                      }

        $.ajax(config)

      }, // end fetchRepos
      fetchBranches: function(repoName){

        function _beforeSendHandler(){
          Notifier.showMessage('Fetching Branches for Repo '+repoName)
        }

        function _doneHandler(a, b, response){
          a = b = null
          response = JSON.parse(response.responseText)
          //console.dir(response)
          if( !response.length ) {
            Notifier.showMessage('No branches available!')
            $('#modal-generic').modal('hide')
          }
          else {
            _listBranches(repoName, response)
          } // end else
        } // end done handler

        function _failHandler(){
          alert("Roh-roh. Something went wrong. :(")
        }

        var config = {
                        type: 'POST',
                        dataType: 'json',
                        data: 'repo=' + repoName,
                        url: '/import/github/branches',
                        beforeSend: _beforeSendHandler,
                        error: _failHandler,
                        success: _doneHandler
                      }

        $.ajax(config)

      }, // end fetchBranches()
      fetchTreeFiles: function(repoName, sha){

        function _beforeSendHandler(){
          Notifier.showMessage('Fetching Tree for Repo '+repoName)
        }

        function _doneHandler(a, b, response){
          a = b = null
          response = JSON.parse(response.responseText)
          // console.log('\nFetch Tree Files...')
          // console.dir(response)
          if( !response.tree.length ) {
            Notifier.showMessage('No tree files available!')
            $('#modal-generic').modal('hide')
          }
          else {
            _listTreeFiles(repoName, response.tree)
          } // end else
        } // end done handler

        function _failHandler(){
          alert("Roh-roh. Something went wrong. :(")
        }

        var config = {
                        type: 'POST',
                        dataType: 'json',
                        data: 'repo=' + repoName + '&sha=' + sha,
                        url: '/import/github/tree_files',
                        beforeSend: _beforeSendHandler,
                        error: _failHandler,
                        success: _doneHandler
                      }

        $.ajax(config)

      }, // end fetchTreeFiles()
      fetchMarkdownFile: function(filename){

        function _doneHandler(a, b, response){
          a = b = null
          response = JSON.parse(response.responseText)
          // console.dir(response)
          if( response.error ) {

            Notifier.showMessage('No markdown for you!')
            $('#modal-generic').modal('hide')

          }
          else{

            $('#modal-generic').modal('hide')

            editor.getSession().setValue( response.data )

            // Update it in localStorage
            var name = filename.split('/').pop()
            updateFilename(name)
            // Show it in the field
            setCurrentFilenameField(name)

            previewMd()

          } // end else
        } // end done handler

        function _failHandler(){
          alert("Roh-roh. Something went wrong. :(")
        }

        function _alwaysHandler(){
          $('.dropdown').removeClass('open')
        }

        var config = {
                        type: 'POST',
                        dataType: 'json',
                        data: 'mdFile=' + filename,
                        url: '/import/github/file',
                        error: _failHandler,
                        success: _doneHandler,
                        complete: _alwaysHandler
                      }

        $.ajax(config)

      } // end fetchMarkdownFile()

    } // end return obj

  })() // end IIFE

  var GoogleDrive = (function() {
    function _errorHandler(a, b, res) {
      Notifier.showMessage(res.responseText );
    }

    function renderSearchResults(a, b, res) {


      var result = JSON.parse(res.responseText)
        , list = '<ul>'

      // Handle empty array case.
      if(!Array.isArray(result.items)) return _errorHandler(null, null, {responseText: "No Markdown files found!"} )

      result.items.forEach(function(item){
        list += '<li data-file-id="'
              + item.id + '"><a class="googledrive_file" href="#">'
              + item.title + '</a></li>'
      })

      list += '</ul>'
      $('.modal-header h3').text('Your Google Drive Files')
      $('.modal-body').html(list)
      $('#modal-generic').modal({
        keyboard: true,
        backdrop: true,
        show: true
      })
    }

    function renderFile(a, b, res) {
      var result = JSON.parse(res.responseText);
      $('#modal-generic').modal('hide')
      editor.getSession().setValue(result.content)
      previewMd()
    }

    // TODO: what to do if access token expires?
    return {
      fileId: null,
      search: function() {
        $.ajax({
          dataType: 'json',
          url: '/import/googledrive',
          beforeSend: function() {
            Notifier.showMessage('Searching for .md files')
          },
          error: _errorHandler,
          success: renderSearchResults
        });
      },
      get: function() {
        $.ajax({
          dataType: 'json',
          url: '/fetch/googledrive?fileId=' + this.fileId,
          error: _errorHandler,
          success: renderFile
        });
      },
      save: function() {
        var content = encodeURIComponent(editor.getSession().getValue());
        var postData = 'title=' + encodeURIComponent(profile.current_filename)+ '.md' +
            '&content=' + content

         $.ajax({
          dataType: 'json',
          type: 'post',
          data: postData,
          url: '/save/googledrive?fileId=' + (GoogleDrive.fileId || ''),
          error: _errorHandler,
          success: function(a, b, res) {
            var response = JSON.parse(res.responseText);
            if (response.id) {
              GoogleDrive.fileId = response.id
              Notifier.showMessage('Document saved on Google Drive')
            } else {
              Notifier.showMessage('An error occurred!')
            }
          }
        });
      }
    }
  })();

  // Dropbox Module
  var Dropbox = (function(){

    // Sorting regardless of upper/lowercase
    // TODO: Let's be DRY and merge this with the
    // sort method in Github module.
    function _alphaNumSort(m,n) {
      var a = m.path.toLowerCase()
      var b = n.path.toLowerCase()
      if (a === b) { return 0 }
      if (isNaN(m) || isNaN(n)){ return ( a > b ? 1 : -1)}
      else {return m-n}
    }

    function _listMdFiles(files){

      var list = '<ul>'

      // Sort alpha
      files.sort(_alphaNumSort)

      files.forEach(function(item){
        // var name = item.path.split('/').pop()
        list += '<li data-file-path="'
              + item.path + '"><a class="dropbox_file" href="#">'
              + item.path + '</a></li>'
      })

      list += '</ul>'

      $('.modal-header h3').text('Your Dropbox Files')

      $('.modal-body').html(list)

      $('#modal-generic').modal({
        keyboard: true,
        backdrop: true,
        show: true
      })

      return false

    }

    function _encodeFilename(path){
      return encodeURIComponent( path.split('/').pop() )
    }

    function _removeFilenameFromPath(path){
      // capture the name
      var name = path.split('/').pop()
      // then just replace with nothing on the path. boom.
      return path.replace(name, '')
    }

    return {
      fetchAccountInfo: function(){

        function _beforeSendHandler(){
          Notifier.showMessage('Fetching User Info from Dropbox')
        }

        function _doneHandler(a, b, response){
          var resp = JSON.parse(response.responseText)
          // console.log('\nFetch User Info...')
          // console.dir(resp)
          Notifier
            .showMessage('Sup '+ resp.display_name)
        } // end done handler

        function _failHandler(){
          alert("Roh-roh. Something went wrong. :(")
        }

        var config = {
                        type: 'GET',
                        dataType: 'json',
                        url: '/account/dropbox',
                        beforeSend: _beforeSendHandler,
                        error: _failHandler,
                        success: _doneHandler
                      }

        $.ajax(config)

      }, // end fetchAccuntInfo()
      fetchMetadata: function(){

        function _beforeSendHandler(){
          Notifier.showMessage('Fetching Metadata')
        }

        function _doneHandler(a, b, response){
          var resp = JSON.parse(response.responseText)
          window.console && window.console.log && console.dir(resp)
        } // end done handler

        function _failHandler(){
          alert("Roh-roh. Something went wrong. :(")
        }

        var config = {
                        type: 'GET',
                        dataType: 'json',
                        url: '/dropbox/metadata',
                        beforeSend: _beforeSendHandler,
                        error: _failHandler,
                        success: _doneHandler
                      }

        $.ajax(config)

      }, // end fetchMetadata()
      searchDropbox: function(){

        function _beforeSendHandler(){
          Notifier.showMessage('Searching for .md Files')
        }

        function _doneHandler(a, b, response){

          a = b = null

          var resp = JSON.parse(response.responseText)

          if(resp.hasOwnProperty('statusCode') && resp.statusCode === 401){
            // {"statusCode":401,"data":"{\"error\": \"Access token is disabled.\"}"}

            var respData = JSON.parse(resp.data)

            Notifier.showMessage('Error! ' + respData.error, 1000)

            return setTimeout(function(){
              Notifier.showMessage('Reloading!')
              window.location.reload()
            }, 1250)

          }

          if(!resp.length){
            Notifier.showMessage('No .md files found!')
          }
          else{
            // console.dir(resp)
            _listMdFiles(resp)
          }
        } // end done handler

        function _failHandler(resp,err){
          alert(resp.responseText || "Roh-roh. Something went wrong. :(")
        }

        var config = {
                        type: 'GET',
                        dataType: 'json',
                        url: '/import/dropbox',
                        beforeSend: _beforeSendHandler,
                        error: _failHandler,
                        success: _doneHandler
                      }

        $.ajax(config)

      }, // end searchDropbox()
      fetchMarkdownFile: function(filename){

        function _doneHandler(a, b, response){
          response = JSON.parse(response.responseText)
          // console.dir(response)
          if( response.statusCode === 404 ) {

            var msg = JSON.parse( response.data )

            Notifier.showMessage(msg.error)

          }
          else{

            $('#modal-generic').modal('hide')

            // Update it in localStorage
            updateFilename(profile.current_filename)
            // Show it in the field
            setCurrentFilenameField()

            editor.getSession().setValue( response.data )
            previewMd()

          } // end else
        } // end done handler

        function _failHandler(){
          alert("Roh-roh. Something went wrong. :(")
        }

        // Weird encoding mumbo jumbo columbo
        var enc = _encodeFilename(filename)
        var path = _removeFilenameFromPath(filename)

        filename = path + enc

        var config = {
                        type: 'POST',
                        dataType: 'json',
                        data: 'mdFile=' + filename,
                        url: '/fetch/dropbox',
                        error: _failHandler,
                        success: _doneHandler
                      }

        $.ajax(config)

      }, // end fetchMarkdownFile()
      setFilePath: function(path){
        path = _removeFilenameFromPath(path)
        updateUserProfile({dropbox: {filepath: path }})
      },
      putMarkdownFile: function(){

        function _doneHandler(a, b, response){
          a = b = null
          response = JSON.parse(response.responseText)
          // console.dir(response)
          if( response.statusCode >= 204 ) {

            var msg = JSON.parse( response.data )

            Notifier.showMessage(msg.error, 5000)

          }
          else{

            $('#modal-generic').modal('hide')

            // console.dir(JSON.parse(response.data))

            Notifier.showMessage( Notifier.messages.docSavedDropbox )

          } // end else
        } // end done handler

        function _failHandler(){
          alert("Roh-roh. Something went wrong. :(")
        }

        var md = encodeURIComponent( editor.getSession().getValue() )

        var postData = 'pathToMdFile=' + profile.dropbox.filepath + encodeURIComponent(profile.current_filename) + '.md' + '&fileContents=' + md

        var config = {
                        type: 'POST',
                        dataType: 'json',
                        data: postData,
                        url: '/save/dropbox',
                        error: _failHandler,
                        success: _doneHandler
                      }

        $.ajax(config)

      } // end fetchMarkdownFile()
    } // end return obj
  })() // end IIFE


  init()

  // TODO:  add window.resize() handlers.

})

/**
 * Get scrollHeight of preview div
 * (code adapted from https://github.com/anru/rsted/blob/master/static/scripts/editor.js)
 *
 * @param {Object} The jQuery object for the preview div
 * @return {Int} The scrollHeight of the preview area (in pixels)
 */
function getScrollHeight($prevFrame) {
    // Different browsers attach the scrollHeight of a document to different
    // elements, so handle that here.
    if ($prevFrame[0].scrollHeight !== undefined) {
        return $prevFrame[0].scrollHeight;
    } else if ($prevFrame.find('html')[0].scrollHeight !== undefined &&
               $prevFrame.find('html')[0].scrollHeight !== 0) {
        return $prevFrame.find('html')[0].scrollHeight;
    } else {
        return $prevFrame.find('body')[0].scrollHeight;
    }
}

/**
 * Scroll preview to match cursor position in editor session
 * (code adapted from https://github.com/anru/rsted/blob/master/static/scripts/editor.js)
 *
 * @return {Void}
 */

function syncPreview() {
  var $ed = window.ace.edit('editor');
  var $prev = $('#preview');

  var editorScrollRange = ($ed.getSession().getLength());

  var previewScrollRange = (getScrollHeight($prev));

  // Find how far along the editor is (0 means it is scrolled to the top, 1
  // means it is at the bottom).
  var scrollFactor = $ed.getFirstVisibleRow() / editorScrollRange;

  // Set the scroll position of the preview pane to match.  jQuery will
  // gracefully handle out-of-bounds values.
  $prev.scrollTop(scrollFactor * previewScrollRange);
}

window.onload = function(){
  var $loading = $('#loading')

  if ($.support.transition){
    $loading
      .bind( $.support.transitionEnd, function(){
        $('#main').removeClass('bye')
        $loading.remove()
      })
      .addClass('fade_slow');
  } else {
    $('#main').removeClass('bye')
    $loading.remove()
  }

  /**
   * Bind synchronization of preview div to editor scroll and change
   * of editor cursor position.
   */
  window.ace.edit('editor').session.on('changeScrollTop', syncPreview);
  window.ace.edit('editor').session.selection.on('changeCursor', syncPreview);
}