"use strict";
var connect = require('connect')
  , serveIndex = require('serve-index')
  , serveStatic = require('serve-static')
  , serveMarkdown = require('serve-markdown')
  , chalk = require('chalk')
  , moment = require('moment')
  , debug = require('debug')('mdserver')
  , path = require('path');

/**
 * init options
 */
function initOptions(options) {
  var defaultOptions = {
    root: process.cwd()
    , port: '3333'
    , style: path.resolve(__dirname, '../public/screen.css')
    , dtpl: path.resolve(__dirname, '../public/dir_template.html')
    , ftpl: path.resolve(__dirname, '../public/file_template.html')
    , view: 'details'
    , silent: false
  };
  return merge(defaultOptions, options);
}

/**
 * start server with provided options
 */
function startServer(options) {

  options = initOptions(options);

  var app = connect()
    , root = options.root
    , TEST = process.env.TEST
    , isSilent = options.silent || TEST;

  if (!isSilent) {
    app.use(log);
  }

  var smOpts = {};
  var smOptMap = {
    ftpl: 'template'
    , style: 'style'
  };
  Object.keys(smOptMap).forEach(function (key) {
    if (options[key] !== undefined) smOpts[smOptMap[key]] = options[key];
  });

  // serve markdown file
  app.use(serveMarkdown(root, smOpts));
  // common files
  app.use(serveStatic(root, {index: ['index.html']}));

  // serve directory
  app.use(serveIndex(root, {
    icon: true
    , template: options.dtpl
    , stylesheet: options.style
    , view: options.view
  }));

  debug('server run in ' + (process.env.TEST ? 'TEST' : 'PRODUCTION') + ' mode')
  if (!TEST) {
    app.listen(options.port);
    showSuccessInfo(options);
  }

  return app;
}

module.exports = startServer;

/**
 * show server starting information
 */
function showSuccessInfo(options) {

  // server start success
  if (options.silent) return;

  console.log(chalk.blue('serve start Success: ')
      + '\n'
      + chalk.green('\t url   ')
      + chalk.grey('http://127.0.0.1:')
      + chalk.red(options.port)
      + chalk.grey('/')
      + '\n'
      + chalk.green('\t serve ')
      + chalk.grey(options.root)
  );

}

/**
 * simple log middleware, output the access log
 **/
function log(req, res, next) {
  console.log('['
      + chalk.grey(ts())
      + '] '
      + chalk.white(decodeURI(req.url))
  );
  next();
}

/**
 * get current timestamp
 */
function ts() {
  return moment().format('HH:mm:ss')
}

/**
 * @description Return a new object containing all of the properties of supplied objects. The properties of later object will override earlier objects.
 * @method  merge
 * @param   {Object} objects * One or more objects to merge
 * @return  {Obejct} A new merged object
 **/
function merge() {
    var hasOwnProperty = Object.prototype.hasOwnProperty;
    var len = arguments.length
        , result = {}
        , key
        , obj;

    for (var i = 0; i < len; i++) {
        obj = arguments[i];
        for (key in obj) {
            if (hasOwnProperty.call(obj, key) && obj[key] !== undefined) {
                result[key] = obj[key];
            }
        }
    }
    return result;
}


/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Pascal Garber
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

// sudo apt-get install libdbus-1-dev
// http://wiki.ubuntuusers.de/D-Bus
// https://github.com/Shouqun/node-dbus

var util = require('util');
var debug = require('debug')('info');
var debugEvent = require('debug')('event');
var _debugIface = require('debug')('iface');
var debugIface = function(object) {
  if(_debugIface)
    _debugIface(object.interfaceName, "\n"+util.inspect(object, showHidden=false, depth=2, colorize=true)+"\n");
};
var DBus = require('dbus');
var dbus = new DBus();
var spawn = require('child_process').spawn;
var exec = require('child_process').exec;
var events = require('events');
var mc = new events.EventEmitter();
mc.Player = new events.EventEmitter();
mc.TrackList = new events.EventEmitter();
mc.Playlists = new events.EventEmitter();

var bus;
var TIMEOUTDELAY = 30000;
var INTERVALDELAY = 100;
/*
 * Wait for the dbus name of the player
 */
waitForService = function (serviceName, timeoutDelay, intervalDelay, callback) {
  bus.getInterface('org.freedesktop.DBus', '/', 'org.freedesktop.DBus', function(err, iface) {
    var timeout, interval;

    if(err) { return callback(err); } else {
      var checkService = function (callback) {
        debug("looking for dbus service");
        iface.ListNames['finish'] = function(serviceList) {
          for (index = 0; index < serviceList.length; ++index) {
            if(serviceList[index] === serviceName) {
              return callback(true);
            }
          }
          return callback(false);
        }
        iface.ListNames();
      }

      timeout = setTimeout(function () {
        debug("timeout");
        clearInterval(interval);
        return callback("timeout");
      }, timeoutDelay);

      interval = setInterval(function() {
        checkService(function (found) {
          if (found) {
            clearInterval(interval);
            clearTimeout(timeout);
            callback(null);
          }
        });
      }, intervalDelay);
    }
  });
}

var watchProperties = function (valueToSet, dbusName, callback) {
  bus.getInterface(dbusName, '/org/mpris/MediaPlayer2', 'org.freedesktop.DBus.Properties', function(err, iface) {
    if(err) {
      callback(err);
    } else {
      iface.on('PropertiesChanged', function(interfaceName, value) {
        var valueKey = Object.keys(value)[0];
        var signalName = valueKey+"Changed";
        debugEvent(interfaceName+"."+signalName, value[valueKey]);
        if (typeof valueKey == 'string' || valueKey instanceof String) {
          switch(interfaceName) {
            case 'org.mpris.MediaPlayer2':
              valueToSet.emit(signalName, value[valueKey], valueToSet[valueKey]);
              valueToSet[valueKey] = value[valueKey];
            break;
            case 'org.mpris.MediaPlayer2.Player':
              valueToSet.Player.emit(signalName, value[valueKey], valueToSet.Player[valueKey]);
              valueToSet.Player[valueKey] = value[valueKey];
            break;
            case 'org.mpris.MediaPlayer2.TrackList':
              valueToSet.TrackList.emit(signalName, value[valueKey], valueToSet.TrackList[valueKey]);
              valueToSet.TrackList[valueKey] = value[valueKey];
            break;
            case 'org.mpris.MediaPlayer2.Playlists':
              valueToSet.Playlists.emit(signalName, value[valueKey], valueToSet.Playlists[valueKey]);
              valueToSet.Playlists[valueKey] = value[valueKey];
            break;
          }
        } else { // WORKAROUND if value is not sent, e.g. http://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Property:Tracks
          switch(interfaceName) {
            case 'org.mpris.MediaPlayer2':
            break;
            case 'org.mpris.MediaPlayer2.Player':
            break;
            case 'org.mpris.MediaPlayer2.TrackList':
              valueToSet.TrackList.emit("TracksChanged");
            break;
            case 'org.mpris.MediaPlayer2.Playlists':
            break;
          }
        }

      });
      callback(null);
    }
  });
}

var integrateMethods = function (valueToSet, iface, methodKeys) {
  methodKeys.forEach(function(methodKey) {
    valueToSet[methodKey] =  function () {
      // var arguments = Array.prototype.slice.call(arguments); ?
      iface[methodKey]['timeout'] = TIMEOUTDELAY;
      if(arguments.length >=0) {
        callback = arguments[arguments.length-1];
        iface[methodKey]['finish'] = callback;
      }
      debug("Call method "+methodKey+" with arguments: ");
      debug(arguments);
      switch(arguments.length) {
        case 0:
          iface[methodKey]();
        break;
        case 1:
          iface[methodKey](callback);
        break;
        case 2:
          iface[methodKey](arguments[0], callback);
        break;
        case 3:
          iface[methodKey](arguments[0], arguments[1], callback);
        break;
        case 4:
          iface[methodKey](arguments[0], arguments[1], arguments[2], callback);
        break;
        case 5:
          iface[methodKey](arguments[0], arguments[1], arguments[2], arguments[3], callback);
        break;
      }
    }
  });
}

// type = "get" | "set"
var integrateProperties = function (valueToSet, type, iface, propertyKeys) {
    type = type.toLowerCase();
    propertyKeys.forEach(function(propertyKey) {
      if(type == 'get')
        valueToSet['Get'+propertyKey] = function (callback) {
          iface.getProperty(propertyKey, callback);
        }
      else
        valueToSet['Set'+propertyKey] = function (value, callback) {
          iface.setProperty(propertyKey, value, callback);
        }
    });
}

var integrateSignals = function (valueToSet, iface, signalKeys) {
  signalKeys.forEach(function(signalKey) {
    iface.on(signalKey, function(arg1, arg2, arg3, arg4, arg5) {
      valueToSet.emit(signalKey, arg1, arg2, arg3, arg4, arg5);
    });
  });
}

var loadInterface = function (dbusName, interfaceName, callback) {
  bus.getInterface(dbusName, '/org/mpris/MediaPlayer2', interfaceName, function(err, iface) {
    if(err) {
      callback(err);
    } else {
      debugIface(iface);

      var methodKeys = Object.keys(iface.object.method);
      var propertyGetterKeys = Object.keys(iface.object.property); // generate getters for all properties
      var propertySetterKeys = []; // generate setters just for writeable properties
      var signalKeys = Object.keys(iface.object.signal);

      switch(iface.interfaceName) {
        case 'org.mpris.MediaPlayer2':
          valueToSet = mc;
          propertySetterKeys = ['Fullscreen'];
        break;
        case 'org.mpris.MediaPlayer2.Player':
          valueToSet = mc.Player;
          propertySetterKeys = ['LoopStatus', 'Rate', 'Shuffle', 'Volume'];
        break;
        case 'org.mpris.MediaPlayer2.TrackList':
          valueToSet = mc.TrackList;
        break;
        case 'org.mpris.MediaPlayer2.Playlists':
          valueToSet = mc.Playlists;
        break;
      }

      /* =========== Methods ===========*/
      integrateMethods(valueToSet, iface, methodKeys);

      /* =========== Properties (getter and setter) ===========*/
      integrateProperties(valueToSet, 'get', iface, propertyGetterKeys);
      integrateProperties(valueToSet, 'set', iface, propertySetterKeys);

      /* =========== Signals ===========*/
      integrateSignals(valueToSet, iface, signalKeys);

      callback(null);
    }
  });
};

mc.disconnect = function (playerName, callback) {
  delete bus;
};

mc.connect = function (playerName, callback) {
  bus = dbus.getBus('session');
  mc.playerName = playerName;
  mc.dbusName = 'org.mpris.MediaPlayer2.' + this.playerName;

  waitForService(mc.dbusName, TIMEOUTDELAY, INTERVALDELAY, function (error) {
    if(error) {
      console.error (error);
    } else {
      debug("player found! :)");
      loadInterface(mc.dbusName, 'org.mpris.MediaPlayer2', function (errorBase) {
        loadInterface(mc.dbusName, 'org.mpris.MediaPlayer2.Player', function (errorPlayer) {
          loadInterface(mc.dbusName, 'org.mpris.MediaPlayer2.TrackList', function (errorTrackList) {
            loadInterface(mc.dbusName, 'org.mpris.MediaPlayer2.Playlists', function (errorPlaylists) {
              watchProperties(mc, mc.dbusName, function (errorWatchProperties) {
                // Raise an error if we can't get at least the base or player
                // Other interfaces such as TrackList + Playlists are optional
                if(errorBase || errorPlayer || errorWatchProperties ) callback(errorBase+" "+errorPlayer+" "+errorTrackList+" "+errorPlaylists+" "+errorWatchProperties, mc);
                else callback(null, mc);
              });
            });
          });
        });
      });
    }
  });
}

mc.stop = function(all, callback) {
  var processName;

  switch(mc.playerName) {
    case 'omxplayer':
      processName = 'omxplayer.bin';
    break;
    default:
      processName = mc.playerName;
    break;
  }

  mc.Quit(function (error) {
    if(all) {
      exec('killall '+processName , function () {
          mc.disconnect();
          mc.emit('Stop');
          if (callback) return callback(error);
      });
    } else {
      mc.disconnect();
      mc.emit('Stop');
      if (callback) return callback(error);
    }
  });
};

mc.start = function(playerName, arguments, callback) {
  var command, arguments, playerApp;

  switch(playerName) {
    case 'omxplayer':
      command ='omxplayer';
      // if no arguments set default
      if( !(arguments instanceof Array) ) {
        arguments = [
          '-o', 'hdmi', // show video on hdmi
          '--blank'     // black background
        ]
      }
    break;
    case 'vlc':
      command ='vlc';
      if( !(arguments instanceof Array) ) {
        arguments = [
          '-I', 'dummy',        // no gui
          '--control', 'dbus',  // with d-bus support
          '-f'                  // fullscreen
        ]
      }
    break;
    case 'audacious':
      // TODO
    break;
    case 'bmp':
      // TODO
    break;
    case 'xmms2':
      // TODO
    break;
    case 'rhythmbox':
      // TODO
    break;
    case 'totem':
      command ='totem';
      // NOTE you need to enable the d-bus plugin in totem
      if( !(arguments instanceof Array) ) {
        arguments = [
          '--fullscreen',                  // fullscreen
          '--replace'
        ]
      }
    break;
    case 'spotify':
      // TODO http://www.mabishu.com/blog/2010/11/15/playing-with-d-bus-interface-of-spotify-for-linux/
    break;
  }

  playerApp = spawn(command, arguments, {stdio: [ 'ignore', 'ignore', 'ignore' ]});

  playerApp.on('exit', function (code) {
    debug('playerApp exit code: ' + code);
    mc.emit('exit', code);
  });

  playerApp.on('close', function (code, signal) {
    debug('playerApp close code: '+code+' signal: '+signal);
    mc.emit('close', code, signal);
  });

  playerApp.on('error', function (error) {
    console.error('playerApp error: '+error);
    mc.emit('error', error);
  });

  mc.connect(playerName, callback);
};

module.exports = mc;


/*
 * Sendpulse REST API Node.js class
 *
 * Documentation
 * https://login.sendpulse.com/manual/rest-api/
 * https://sendpulse.com/api
 *
 */

'use strict';

var https = require('https');
var crypto = require('crypto');
var fs = require('fs');

var API_URL = 'api.sendpulse.com';
var API_USER_ID="";
var API_SECRET="";
var TOKEN_STORAGE="";
var TOKEN="";

/**
 * MD5
 *
 * @param data
 * @return string
 */
function md5(data){
    var md5sum = crypto.createHash('md5');
    md5sum.update(data);
    return md5sum.digest('hex');
}

/**
 * Basse64
 *
 * @param data
 * @return string
 */
function base64(data){
    var b = new Buffer(data);
    return b.toString('base64');
}

/**
 * Sendpulse API initialization
 *
 * @param userId
 * @param secret
 * @param storage
 */
function init(user_id,secret,storage) {
    API_USER_ID=user_id;
    API_SECRET=secret;
    TOKEN_STORAGE=storage;

    var hashName = md5(API_USER_ID+'::'+API_SECRET);
    if (fs.existsSync(TOKEN_STORAGE+hashName)) {
        TOKEN = fs.readFileSync(TOKEN_STORAGE+hashName,{encoding:'utf8'});
    }

    if (! TOKEN.length) {
        getToken();
    }
}

/**
 * Form and send request to API service
 *
 * @param path
 * @param method
 * @param data
 * @param useToken
 * @param callback
 *        Define the function  that will be called
 *        when a response is received.
 */
function sendRequest(path, method, data, useToken, callback){
    var headers = {}
    headers['Content-Type'] = 'application/json';
    headers['Content-Length'] =  Buffer.byteLength(JSON.stringify(data));

    if (useToken && TOKEN.length) {
        headers['Authorization'] = 'Bearer '+TOKEN;
    }
    if (method === undefined) {
        method = 'POST';
    }
    if (useToken === undefined) {
        useToken = false;
    }

    var options = {
        //uri: API_URL,
        path: '/'+path,
        port: 443,
        hostname: API_URL,
        method: method,
        headers: headers,
    };

    var req = https.request(
        options,
        function(response) {
            var str = '';
            response.on('data', function (chunk) {
                if (response.statusCode==401) {
                    getToken();
                    sendRequest(path, method, data, true, callback);
                } else {
                    str += chunk;
                }
            });

            response.on('end', function () {
                if (response.statusCode != 401) {
                    try {
                        var answer = JSON.parse(str);
                    } catch (ex) {
                        var answer = returnError();
                    }
                    callback(answer);
                }
            });
        }
    );
    req.write(JSON.stringify(data));
    req.end();
}

/**
 * Get token and store it
 *
 */
function getToken(){
    var data={
        grant_type:'client_credentials',
        client_id: API_USER_ID,
        client_secret: API_SECRET
    }
    sendRequest( 'oauth/access_token', 'POST', data, false, saveToken );
    function saveToken(data) {
        TOKEN = data.access_token;
        var hashName = md5(API_USER_ID+'::'+API_SECRET);
        fs.writeFileSync(TOKEN_STORAGE+hashName, TOKEN);
    }
}

/**
 * Form error object
 *
 *  @return array
 */
function returnError(message){
    var data = {is_error:1};
    if (message !== undefined && message.length) {
        data['message'] = message
    }
    return data;
}

/**
 * Serializing of the array
 *
 * @param mixed_value
 * @return string
 */
function serialize(mixed_value) {
    var val, key, okey,
        ktype = '',
        vals = '',
        count = 0,
        _utf8Size = function (str) {
            var size = 0,
                i = 0,
                l = str.length,
                code = '';
            for (i = 0; i < l; i++) {
                code = str.charCodeAt(i);
                if (code < 0x0080) {
                    size += 1;
                } else if (code < 0x0800) {
                    size += 2;
                } else {
                    size += 3;
                }
            }
            return size;
        },
        _getType = function (inp) {
            var match, key, cons, types, type = typeof inp;

            if (type === 'object' && !inp) {
                return 'null';
            }

            if (type === 'object') {
                if (!inp.constructor) {
                    return 'object';
                }
                cons = inp.constructor.toString();
                match = cons.match(/(\w+)\(/);
                if (match) {
                    cons = match[1].toLowerCase();
                }
                types = ['boolean', 'number', 'string', 'array'];
                for (key in types) {
                    if (cons == types[key]) {
                        type = types[key];
                        break;
                    }
                }
            }
            return type;
        },
        type = _getType(mixed_value);

    switch (type) {
        case 'function':
            val = '';
            break;
        case 'boolean':
            val = 'b:' + (mixed_value ? '1' : '0');
            break;
        case 'number':
            val = (Math.round(mixed_value) == mixed_value ? 'i' : 'd') + ':' + mixed_value;
            break;
        case 'string':
            val = 's:' + _utf8Size(mixed_value) + ':"' + mixed_value + '"';
            break;
        case 'array':
        case 'object':
            val = 'a';
            for (key in mixed_value) {
                if (mixed_value.hasOwnProperty(key)) {
                    ktype = _getType(mixed_value[key]);
                    if (ktype === 'function') {
                        continue;
                    }

                    okey = (key.match(/^[0-9]+$/) ? parseInt(key, 10) : key);
                    vals += serialize(okey) + serialize(mixed_value[key]);
                    count++;
                }
            }
            val += ':' + count + ':{' + vals + '}';
            break;
        case 'undefined':
        default:
            val = 'N';
            break;
    }
    if (type !== 'object' && type !== 'array') {
        val += ';';
    }
    return val;
}

/**
 * API interface implementation
 */

/**
 * Get list of address books
 *
 * @param callback
 * @param limit
 * @param offset
 */
function listAddressBooks(callback,limit,offset){
    var data={}
    if (limit === undefined) {
        limit = null;
    } else {
        data['limit'] = limit;
    }
    if (offset === undefined) {
        offset = null;
    } else {
        data['offset'] = offset;
    }
    sendRequest('addressbooks', 'GET', data, true, callback);
}

/**
 * Create address book
 *
 * @param callback
 * @param bookName
 */
function createAddressBook(callback,bookName) {
    if ((bookName === undefined) || (! bookName.length)) {
        return callback(returnError("Empty book name"));
    }
    var data = {bookName: bookName};
    sendRequest( 'addressbooks', 'POST', data, true, callback );
}

/**
 * Edit address book name
 *
 * @param callback
 * @param id
 * @param bookName
 */
function editAddressBook(callback,id,bookName) {
    if ((id===undefined) || (bookName === undefined) || (! bookName.length)) {
        return callback(returnError("Empty book name or book id"));
    }
    var data = {name: bookName};
    sendRequest( 'addressbooks/' + id, 'PUT', data, true, callback );
}

/**
 * Remove address book
 *
 * @param callback
 * @param id
 */
function removeAddressBook(callback,id){
    if (id===undefined) {
        return callback(returnError('Empty book id'));
    }
    sendRequest( 'addressbooks/' + id, 'DELETE', {}, true, callback );
}

/**
 * Get information about book
 *
 * @param callback
 * @param id
 */
function getBookInfo(callback,id){
    if (id===undefined) {
        return callback(returnError('Empty book id'));
    }
    sendRequest( 'addressbooks/' + id, 'GET', {}, true, callback );
}

/**
 * List email addresses from book
 *
 * @param callback
 * @param id
 */
function getEmailsFromBook(callback,id){
    if (id===undefined) {
        return callback(returnError('Empty book id'));
    }
    sendRequest( 'addressbooks/' + id + '/emails', 'GET', {}, true, callback );
}

/**
 * Add new emails to address book
 *
 * @param callback
 * @param id
 * @param emails
 */
function addEmails(callback,id,emails){
    if ((id===undefined) || (emails === undefined) || (! emails.length)) {
        return callback(returnError("Empty email or book id"));
    }
    var data = {emails: serialize(emails)};
    sendRequest( 'addressbooks/' + id + '/emails', 'POST', data, true, callback );
}

/**
 * Remove email addresses from book
 *
 * @param callback
 * @param id
 * @param emails
 */
function removeEmails(callback,id,emails){
    if ((id===undefined) || (emails === undefined) || (! emails.length)) {
        return callback(returnError("Empty email or book id"));
    }
    var data = {emails: serialize(emails)};
    sendRequest( 'addressbooks/' + id + '/emails', 'DELETE', data, true, callback );
}

/**
 * Get information about email address from book
 *
 * @param callback
 * @param id
 * @param email
 */
function getEmailInfo(callback,id,email){
    if ((id===undefined) || (email === undefined) || (! email.length)) {
        return callback(returnError("Empty email or book id"));
    }
    sendRequest( 'addressbooks/' + id + '/emails/' + email, 'GET', {}, true, callback );

}

/**
 * Get cost of campaign based on address book
 *
 * @param callback
 * @param id
 */
function campaignCost(callback,id) {
    if (id===undefined) {
        return callback(returnError('Empty book id'));
    }
    sendRequest( 'addressbooks/' + id + '/cost', 'GET', {}, true, callback );

}

/**
 * Get list of campaigns
 *
 * @param callback
 * @param limit
 * @param offset
 */
function listCampaigns(callback,limit,offset){
    var data={}
    if (limit === undefined) {
        limit = null;
    } else {
        data['limit'] = limit;
    }
    if (offset === undefined) {
        offset = null;
    } else {
        data['offset'] = offset;
    }
    sendRequest('campaigns', 'GET', data, true, callback);
}

/**
 * Get information about campaign
 *
 * @param callback
 * @param id
 */
function getCampaignInfo(callback,id){
    if (id===undefined) {
        return callback(returnError('Empty book id'));
    }
    sendRequest( 'campaigns/' + id, 'GET', {}, true, callback );
}

/**
 * Get campaign statistic by countries
 *
 * @param callback
 * @param id
 */
function campaignStatByCountries(callback,id){
    if (id===undefined) {
        return callback(returnError('Empty book id'));
    }
    sendRequest( 'campaigns/' + id + '/countries', 'GET', {}, true, callback );
}

/**
 * Get campaign statistic by referrals
 *
 * @param callback
 * @param id
 */
function campaignStatByReferrals(callback,id){
    if (id===undefined) {
        return callback(returnError('Empty book id'));
    }
    sendRequest( 'campaigns/' + id + '/referrals', 'GET', {}, true, callback );
}

/**
 * Create new campaign
 *
 * @param callback
 * @param senderName
 * @param senderEmail
 * @param subject
 * @param body
 * @param bookId
 * @param name
 * @param attachments
 */
function createCampaign(callback, senderName, senderEmail, subject, body, bookId, name, attachments){
    if ((senderName===undefined)||(! senderName.length)||(senderEmail===undefined)||(! senderEmail.length)||(subject===undefined)||(! subject.length)||(body===undefined)||(! body.length)||(bookId===undefined)){
        return callback(returnError('Not all data.'));
    }
    if (name===undefined){
        name='';
    }
    if (attachments===undefined) {
        attachments='';
    }
    if (attachments.length){
        attachments = serialize(attachments);
    }
    var data = {
        sender_name: senderName,
        sender_email: senderEmail,
        //subject: encodeURIComponent(subject),
        //subject: urlencode(subject),
        subject: subject,
        body: base64(body),
        list_id: bookId,
        name: name,
        attachments: attachments
    }
    sendRequest( 'campaigns', 'POST', data, true, callback );
}

/**
 * Cancel campaign
 *
 * @param callback
 * @param id
 */
function cancelCampaign(callback, id){
    if (id===undefined) {
        return callback(returnError('Empty campaign id'));
    }
    sendRequest( 'campaigns/' + id, 'DELETE', {}, true, callback );
}

/**
 * List all senders
 *
 * @param callback
 */
function listSenders(callback){
    sendRequest( 'senders', 'GET', {}, true, callback );
}

/**
 * Add new sender
 *
 * @param callback
 * @param senderName
 * @param senderEmail
 */
function addSender(callback, senderName, senderEmail){
    if ((senderEmail===undefined)||(!senderEmail.length)||(senderName===undefined)||(!senderName.length)) {
        return callback(returnError('Empty sender name or email'));
    }
    var data = {
        email: senderEmail,
        name: senderName
    }
    sendRequest( 'senders', 'POST', data, true, callback );
}

/**
 * Remove sender
 *
 * @param callback
 * @param senderEmail
 */
function removeSender(callback, senderEmail) {
    if ((senderEmail===undefined)||(!senderEmail.length)){
        return callback(returnError('Empty email'));
    }
    var data = {
        email: senderEmail
    }
    sendRequest( 'senders', 'DELETE', data, true, callback );
}

/**
 * Activate sender using code
 *
 * @param callback
 * @param senderEmail
 * @param code
 */
function activateSender(callback, senderEmail, code){
    if ((senderEmail===undefined)||(!senderEmail.length)||(code===undefined)||(!code.length)){
        return callback(returnError('Empty email or activation code'));
    }
    var data = {
        code: code
    }
    sendRequest( 'senders/' + senderEmail + '/code', 'POST', data, true, callback );
}

/**
 * Request mail with activation code
 *
 * @param callback
 * @param senderEmail
 */
function getSenderActivationMail(callback, senderEmail ) {
    if ((senderEmail===undefined)||(!senderEmail.length)){
        return callback(returnError('Empty email'));
    }
    sendRequest( 'senders/' + senderEmail + '/code', 'GET', {}, true, callback );
}

/**
 * Get global information about email
 *
 * @param callback
 * @param email
 */
function getEmailGlobalInfo(callback, email) {
    if ((email===undefined)||(!email.length)){
        return callback(returnError('Empty email'));
    }
    sendRequest( 'emails/' + email, 'GET', {}, true, callback );
}

/**
 * Remove email from all books
 *
 * @param callback
 * @param email
 */
function removeEmailFromAllBooks(callback, email){
    if ((email===undefined)||(!email.length)){
        return callback(returnError('Empty email'));
    }
    sendRequest( 'emails/' + email, 'DELETE', {}, true, callback );
}

/**
 * Get email statistic by all campaigns
 *
 * @param callback
 * @param email
 */
function emailStatByCampaigns(callback,email) {
    if ((email===undefined)||(!email.length)){
        return callback(returnError('Empty email'));
    }
    sendRequest( 'emails/' + email + '/campaigns', 'GET', {}, true, callback );
}

/**
 * Get all emails from blacklist
 *
 * @param callback
 */
function getBlackList(callback){
    sendRequest( 'blacklist', 'GET', {}, true, callback );
}

/**
 * Add email to blacklist
 *
 * @param callback
 * @param emails
 * @param comment
 */
function addToBlackList(callback, emails, comment){
    if ((emails===undefined)||(!emails.length)){
        return callback(returnError('Empty email'));
    }
    if (comment === undefined) {
        comment = '';
    }
    var data = {
        emails: base64(emails),
        comment: comment
    }
    sendRequest( 'blacklist', 'POST', data, true, callback );
}

/**
 * Remove emails from blacklist
 *
 * @param callback
 * @param emails
 */
function removeFromBlackList(callback, emails){
    if ((emails===undefined)||(!emails.length)){
        return callback(returnError('Empty emails'));
    }
    var data = {
        emails: base64(emails),
    }
    sendRequest( 'blacklist', 'DELETE', data, true, callback );
}

/**
 * Get balance
 *
 * @param callback
 * @param currency
 */
function getBalance(callback, currency){
    if (currency === undefined) {
        var url = 'balance';
    } else {
        var url =  'balance/' + currency.toUpperCase();
    }
    sendRequest( url, 'GET', {}, true, callback );
}

/**
 * SMTP: get list of emails
 *
 * @param callback
 * @param limit
 * @param offset
 * @param fromDate
 * @param toDate
 * @param sender
 * @param recipient
 */
function smtpListEmails(callback, limit, offset, fromDate, toDate, sender, recipient) {
    if (limit === undefined) {
        limit = 0;
    }
    if (offset === undefined) {
        offset = 0;
    }
    if (fromDate === undefined) {
        fromDate = '';
    }
    if (toDate === undefined) {
        toDate = '';
    }
    if (sender === undefined) {
        sender = '';
    }
    if (recipient === undefined) {
        recipient = '';
    }
    var data = {
        limit: limit,
        offset: offset,
        from: fromDate,
        to: toDate,
        sender: sender,
        recipient: recipient
    }
    sendRequest( 'smtp/emails', 'GET', data, true, callback );
}

/**
 * Get information about email by id
 *
 * @param callback
 * @param id
 */
function smtpGetEmailInfoById(callback,id){
    if ((id===undefined)||(! id.length)) {
        return callback(returnError('Empty id'));
    }
    sendRequest( 'smtp/emails/' + id, 'GET', {}, true, callback );
}

/**
 * SMTP: add emails to unsubscribe list
 *
 * @param callback
 * @param emails
 */
function smtpUnsubscribeEmails(callback, emails ) {
    if (emails===undefined){
        return callback(returnError('Empty emails'));
    }
    var data = {
        emails: serialize(emails)
    }
    sendRequest( 'smtp/unsubscribe', 'POST', data, true, callback );
}

/**
 * SMTP: remove emails from unsubscribe list
 *
 * @param callback
 * @param emails
 */
function smtpRemoveFromUnsubscribe( callback, emails ) {
    if (emails===undefined){
        return callback(returnError('Empty emails'));
    }
    var data = {
        emails: serialize(emails)
    }
    sendRequest( 'smtp/unsubscribe', 'DELETE', data, true, callback );
}

/**
 * Get list of IP
 *
 * @param callback
 */
function smtpListIP(callback) {
    sendRequest( 'smtp/ips', 'GET', {}, true, callback );
}

/**
 * SMTP: get list of allowed domains
 *
 * @param callback
 */
function smtpListAllowedDomains(callback) {
    sendRequest( 'smtp/domains', 'GET', {}, true, callback );
}

/**
 * SMTP: add new domain
 *
 * @param callback
 * @param email
 */
function smtpAddDomain(callback, email) {
    if ((email===undefined)||(!email.length)){
        return callback(returnError('Empty email'));
    }
    var data = {
        email: email
    }
    sendRequest( 'smtp/domains', 'POST', data, true, callback );
}

/**
 * SMTP: verify domain
 *
 * @param callback
 * @param email
 */
function smtpVerifyDomain(callback,email) {
    if ((email===undefined)||(!email.length)){
        return callback(returnError('Empty email'));
    }
    sendRequest( 'smtp/domains/'+email, 'GET', {}, true, callback );
}

/**
 * SMTP: send mail
 *
 * @param callback
 * @param email
 */
function smtpSendMail( callback, email ) {
    if (email===undefined){
        return callback(returnError('Empty email data'));
    }
    email['html'] = base64(email['html']);
    var data = {
        email: serialize(email)
    };
    sendRequest( 'smtp/emails', 'POST', data, true, callback );
}

exports.init = init;
exports.listAddressBooks = listAddressBooks;
exports.createAddressBook = createAddressBook;
exports.editAddressBook = editAddressBook;
exports.removeAddressBook = removeAddressBook;
exports.getBookInfo = getBookInfo;
exports.getEmailsFromBook = getEmailsFromBook;
exports.addEmails = addEmails;
exports.removeEmails = removeEmails;
exports.getEmailInfo = getEmailInfo;
exports.campaignCost = campaignCost;
exports.listCampaigns = listCampaigns;
exports.getCampaignInfo = getCampaignInfo;
exports.campaignStatByCountries = campaignStatByCountries;
exports.campaignStatByReferrals = campaignStatByReferrals;
exports.createCampaign = createCampaign;
exports.cancelCampaign = cancelCampaign;
exports.listSenders = listSenders;
exports.addSender = addSender;
exports.removeSender = removeSender;
exports.activateSender = activateSender;
exports.getSenderActivationMail = getSenderActivationMail;
exports.getEmailGlobalInfo = getEmailGlobalInfo;
exports.removeEmailFromAllBooks = removeEmailFromAllBooks;
exports.emailStatByCampaigns = emailStatByCampaigns;
exports.getBlackList = getBlackList;
exports.addToBlackList = addToBlackList;
exports.removeFromBlackList = removeFromBlackList;
exports.getBalance = getBalance;
exports.smtpListEmails = smtpListEmails;
exports.smtpGetEmailInfoById = smtpGetEmailInfoById;
exports.smtpUnsubscribeEmails = smtpUnsubscribeEmails;
exports.smtpRemoveFromUnsubscribe = smtpRemoveFromUnsubscribe;
exports.smtpListIP = smtpListIP;
exports.smtpListAllowedDomains = smtpListAllowedDomains;
exports.smtpAddDomain = smtpAddDomain;
exports.smtpVerifyDomain = smtpVerifyDomain;
exports.smtpSendMail = smtpSendMail;


#!/usr/bin/env node

var commandLine = require('commander');
var Promise = require('bluebird');
var path = require('path');
var fs = Promise.promisifyAll(require('fs'));
var pkg = require('../package.json');
var standalone = require('../index');
var colors = require('colors');
var minify = Promise.promisifyAll(require('html-minifier').minify);

var arg = process.argv;

// options
commandLine
	.version('standalone-html v' + pkg.version)
	.usage('[options] <full path to source html> --output <path>')
	.option('-o, --output <path>', 'full path to output file')
	.option('-e, --escape "[ regex ]"', 'ignore the given regular expression when minify')
	.option('-m, --minifyall', 'minify the html file, include all scripts, css & image')
	.option('-j, --justminify', 'minify the html')
	.parse(arg)

var html = commandLine.args[0];
var output = (commandLine.output === undefined) ? 'index_standalone.html' : commandLine.output;
var escapeChar = (commandLine.escape === undefined) ? '[]' : commandLine.escape;

var opt = {
	removeAttributeQuotes: false,
	minifyCSS: true,
	minifyJS: true,
	collapseWhitespace: true,
	removeComments: true,
	ignoreCustomFragments: eval(escapeChar)
};

if (!html) {
	commandLine.help();
} else {
	// ensure file exist and proceed or exit
	fs.stat(html, function (err, stat) {
		if (err === null) {
			console.log('');
			console.log('Target file : ' + html);
			console.log('');
			startApp();
		} else if (err.code == 'ENOENT') {
			console.log('File does not exist.'.red + ' Exit.');
			process.exit;
		} else {
			console.log('File error: ' + err.code + '. Exit.');
			process.exit;
		}
	});
}

function startApp() {
	var inputPath = path.dirname(html);
	var inputFile = fs.readFileSync(html, 'utf-8');
	var outputPath = output;
	var currentDir = process.cwd();

	if (output && html && commandLine.justminify) {
		minifyFile(inputFile, outputPath);
	} else if (output && html) {
		standalone.cli(inputPath, inputFile, outputPath, getOpt);
	} else {
		commandLine.help();
	}
}

//parse options
function getOpt(resHtml, outputPath) {
	if (commandLine.minifyall) {
		console.log('');
		console.log('minify all. Process may take a few minutes with large file.');
		console.log('');
		minifyFile(resHtml, outputPath);
	} else {
		console.log('');
		console.log('Output file name : ' + outputPath);
		console.log('');
		writeFile(resHtml, outputPath);
	}
}

//minify the result
function minifyFile(resHtml, outputPath) {
	var resHtml = minify(resHtml, opt, function (err) {
		if (err) {
			console.error('error will processing file.');
		}
	});

	console.log('');
	console.log('Output file name : ' + outputPath);
	console.log('');
	writeFile(resHtml, outputPath);
}

//write result to file
function writeFile(resHtml, outputPath) {
	fs.writeFile(outputPath, resHtml, function (err) {
		if (err) {
			console.log('');
			console.log('File error: ' + err + '. Exit.');
		} else {
			console.log('');
			console.log('All done. Exit.'.green);
		}
	});
}


'use strict';

var uuid = require('uuid');

function providedBy (obj) {
    // Does the specified object implement this interface
    if (obj && Array.isArray(obj._implements)) {
        // Object has a list of interfaces it implements
        for (var i=0, imax = obj._implements.length; i<imax; i++) {
            if (obj._implements[i].interfaceId === this.interfaceId) {
                return true;
            };
        }
    } else if (obj && obj._implements && obj._implements.interfaceId === this.interfaceId) {
        // Object implements a single interface (probably a utility)
        return true;
    }
    // If we came this far, the object doesn't implement this interface
    return false;
}

var createInterface = function (params) {
    // Make sure we don't get an undefined params list
    params = params || {};

    var extendsThese = params.extends,
        schema = params.schema,
        name = params.name;

    // TODO: If extends other interfaces then concatenate schemas from those, order sets precedence (first is overrides).
    // Then superimpose given schema on top of these.
    var Interface = function (params) {
        // Schema defines fields and can be used for validation and form generation (optional)
        this.schema = params.schema;
        // The name of the interface, this should be unique
        this.name = params.name;
        // Additional properties that aren't exposed as form data, in the future this might be
        // used to check that objects fulfill the implementation
        this.members = params.members;

        this.interfaceId = uuid.v4();

        // console.log("[SCHEMA] Created interface [" + this.name + "] with id: " + this.interfaceId);
    }

    Interface.prototype.providedBy = providedBy

    // Set name for better debugging
    Object.defineProperty(Interface, 'name', {value: name, configurable: true})

    var newInterface = new Interface({
        schema: schema,
        name: name
    });

    return newInterface
}

module.exports.create = createInterface;


'use strict';

/*

    Exceptions

*/

function AdapterRegistryException(message, context) {
   this.message = message;
   this.name = "AdapterRegistryException";
   this.context = context;
   this.stack = (new Error()).stack;
}

/*

    Adapter Registry Code

*/

var AdapterRegistry = function () {
    this.adapters = {};
}

AdapterRegistry.prototype.registerAdapter = function (adapter) {
    /*
        Add an adapter to the registry

        adapts -- interface or object prototype that the adapter decorates
        implementsInterface -- the interface that the adapter implements
        adapter -- the prototype of the adapter to instantiate on get
    */
    var adapts = adapter.prototype._adapts,
        implementsInterfaces = adapter.prototype._implements;

    // TODO: Check that the adapter implements the interface
    // TODO: else throw InterfaceNotImplementedError

    // INTEGRITY CHECK: Have we specified what the adapter implements?
    if (!implementsInterfaces || !implementsInterfaces[0].interfaceId) {
        throw new AdapterRegistryException(
            "Registration error: You haven't specified interface that this adapter implements",
            {
                adapter: adapter
            }
        )
    }

    // Register the adapter (interfaces are stored in a list)
    var tmpInterfaceId = implementsInterfaces[0].interfaceId;
    if (typeof this.adapters[tmpInterfaceId] === 'undefined') {
        this.adapters[tmpInterfaceId] = {
            implementsInterface: implementsInterfaces[0],
            interfaceAdapters: [],
            objectAdapters: []
        }
    }

    // INTEGRITY CHECK: Have we specified what the adapter has specified what it adapts?
    if (!adapts) {
        throw new AdapterRegistryException(
            "Registration error: You haven't specified interface or object that this adapter adapts",
            {
                adapter: adapter
            }
        )
    }

    var adapters = this.adapters[tmpInterfaceId];

    if (adapts.interfaceId) {
        // This should be registered as an interface
        adapters.interfaceAdapters.push({
            adapts: adapts,
            adapter: adapter
        });
    } else {
        // This should be registered as an object adaptor
        adapters.objectAdapters.push({
            adapts: adapts,
            adapter: adapter
        });

    }
}

// TODO: implement hasAdapter (returns true or false), look at getAdapter

AdapterRegistry.prototype.getAdapter = function (obj, implementsInterface, adaptsInterface) {
    /*
        Return an instance of an adapter for the provided object which implements
        the provided interface.

        Optionally add a specific param adaptsInterface in case there are several
        adapters that implement the interface and match the object.
    */
    var adapters = this.adapters[implementsInterface.interfaceId];

    // if we didn't find an adapter for this we throw an error
    if (typeof adapters === 'undefined') {
        var message = "No registered adapter(s) found for: " + implementsInterface.name;
        throw new AdapterRegistryException(message, obj);
    }

    // Ok so we found adapters that implement this interface, let's see if they
    // adapt the provided object.
    if (adapters) {
        // First check if an object adapter matches
        for (var i = 0, imax = adapters.objectAdapters.length; i < imax; i++) {
            var tmp = adapters.objectAdapters[i];
            // TODO: THIS IS A NOOP right now, there is no interfaceId-property on objects unless they are interfaces
            if (obj.interfaceId === tmp.adapts.interfaceId) {
                // Found the adapter, instantiate and return (adapter should set obj as context on creation)
                var Adapter = new tmp.adapter(obj);
                return Adapter;
            }

        }
        // Then check if an interface adapter matches, iterate over each interface implemented by the
        // passed object to find the first match.

        // INTEGRITY CHECK: Throw a useful error if the passed object doesn't have _implements
        if (!(obj.interfaceId || (obj._implements && obj._implements.length > 0))) {
            throw new AdapterRegistryException(
                "Context (first param) doesn't have any _implements property, and thus no interfaces to use for look up",
                {
                    context: obj,
                    implements: implementsInterface,
                    registry: this
                }
            )
        };

        // TODO: Should investigate inheritance and overriding here, the use case is that you want to override
        // adapters registered on a general interface in a component with that registered in an application. I
        // need to think about how this works and write the docs first, then tests and then implement it so it
        // is easy to understand and reason about.

        // Now support finding adapter by supplying an interface. Useful if no object exists yet such as
        // in a schema with ObjectField or for create views.
        var tmpImplements = obj._implements || [obj];
        for (var j = 0, jmax = tmpImplements.length; j < jmax; j++) {
            var tmpInterface = tmpImplements[j];
            for (var i = 0, imax = adapters.interfaceAdapters.length; i < imax; i++) {
                var tmp = adapters.interfaceAdapters[i];
                if (tmpInterface.interfaceId === tmp.adapts.interfaceId) {
                    // If we got the adaptsInterface parameter we need to check that it matches otherwise
                    // keep on looking
                    if (adaptsInterface && adaptsInterface.interfaceId !== tmp.adapts.interfaceId) {
                        continue
                    }
                    // Found the adapter, instantiate and return (adapter should set obj as context on creation)
                    var Adapter = new tmp.adapter(obj);
                    return Adapter;
                }

            }
        }
    };

    // TODO: Figure out how to throw nice custom errors
    // If we get this far, throws error if no adapter is found
    var adaptsInterfaces = adapters.interfaceAdapters.map(function (adapter) {
        return "[" + adapter.adapts.name + "] " + adapter.adapts.interfaceId;
    });
    var contextInterfaces = obj._implements.map(function (intrfc) {
        return "[" + intrfc.name + "] " + intrfc.interfaceId;
    })

    var message = "No registered adapter implementing [" + implementsInterface.name + "] found that adapts: " + (obj._iname || obj);
    throw new AdapterRegistryException(message, obj);
}

module.exports = AdapterRegistry;


/**
 *  Modified from the jquery cookie plugin.
 *
 *  https://github.com/carhartl/jquery-cookie
 */
'use strict';

var pluses = /\+/g;

/**
 *  Create a cookie instance.
 *
 *  @param options.raw Do not URI encode the cookie value.
 *  @param options.json Store the cookie value as JSON.
 *  @param options.expires Define lifetime of the cookie in days, default is 30.
 *  @param options.path Define the path where the cookie is valid, default is /.
 *  @param options.domain Define the domain where the cookie is valid.
 *  @param options.secure If true, the cookie transmission requires a
 *  secure protocol (https).
 */
function Cookie(options) {
  this.options = options || {};
  this.options.expires =
    typeof this.options.expires === 'number' ? this.options.expires : 30;
  this.options.path =
    this.options.path !== undefined ? this.options.path : '/';
  this.options.secure = typeof this.options.secure === 'boolean'
    ? this.options.secure : false;
}

var proto = Cookie.prototype;

/**
 *  Set a cookie value.
 *
 *  @param key The cookie key name.
 *  @param value The value for the cookie.
 *  @param options Options to use when setting the cookie.
 */
function set(key, value, options) {
  options = options || this.options;
  var days = parseInt(options.expires || -1);
  if(value !== undefined && typeof value !== 'function') {
    var t = new Date();
    t.setDate((t.getDate() + days));
    var res = (document.cookie = [
      this.encode(key), '=', this.stringify(value),
      // use expires attribute, max-age is not supported by IE
      options.expires ? '; expires=' + t.toUTCString() : '',
      options.path ? '; path=' + options.path : '',
      options.domain ? '; domain=' + options.domain : '',
      options.secure ? '; secure' : ''
    ].join(''));
    return res;
  }
}

/**
 *  Get a cookie value.
 *
 *  @param key The cookie key name.
 *  @param value A converter function used when reading the cookie value.
 */
function get(key, value) {
  var i, parts, name, cookie;
  var result = key ? undefined : {};
  /* istanbul ignore next */
  var cookies = (document.cookie || '').split('; ');
  for (i = 0; i < cookies.length; i++) {
    parts = cookies[i].split('=');
    name = this.decode(parts.shift());
    cookie = parts.join('=');
    if (key && key === name) {
      // if second argument (value) is a function it's a converter
      result = this.read(cookie, value);
      break;
    }
    // prevent storing a cookie that we couldn't decode
    if (!key && (cookie = this.read(cookie)) !== undefined) {
      result[name] = cookie;
    }
  }
  return result;
}

/**
 *  Delete a cookie value.
 *
 *  @param key The cookie key name.
 */
function del(key, options) {
  if(!options) {
    options = {};
    for(var z in this.options) {
      options[z] = this.options[z];
    }
  }
  options.expires = -1;
  this.set(key, '', options);
}

/**
 *  Clear all stored cookies, optionally keeping the
 *  keys in the except array.
 *
 *  @param except Array of keys that should not be deleted.
 *  @param options The cookie options.
 */
function clear(except, options) {
  var keys = this.get(), z;
  except = except || [];
  for(z in keys) {
    if(~except.indexOf(z)) {
      continue;
    }
    this.del(z, options);
  }
}

/**
 *  @private
 */
function encode(s) {
  return this.options.raw ? s : encodeURIComponent(s);
}

/**
 *  @private
 */
function decode(s) {
  return this.options.raw ? s : decodeURIComponent(s);
}

/**
 *  @private
 */
function read(s, converter) {
  var value = this.options.raw ? s : this.parse(s);
  return typeof converter === 'function' ? converter(value) : value;
}

/**
 *  @private
 */
function stringify(value) {
  return this.encode(
    this.options.json ? JSON.stringify(value) : String(value));
}

/**
 *  @private
 */
function parse(s) {
  // this is a quoted cookie as according to RFC2068, unescape
  if(s.indexOf('"') === 0) {
    s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
  }

  // decode and parse
  try {
    s = decodeURIComponent(s.replace(pluses, ' '));
    return this.options.json ? JSON.parse(s) : s;
  // if we can't decode or parse the cookie it is unusable
  }catch(e) {}
}

// public methods
proto.set = set;
proto.get = get;
proto.del = del;
proto.clear = clear;

// private methods
proto.encode = encode;
proto.decode = decode;
proto.read = read;
proto.stringify = stringify;
proto.parse = parse;

module.exports = Cookie;


'use strict'

/*!
 * imports.
 */

var bind = Function.prototype.bind || require('fast-bind')

/*!
 * exports.
 */

module.exports = curry2

/**
 * Curry a binary function.
 *
 * @param {Function} fn
 * Binary function to curry.
 *
 * @param {Object} [self]
 * Function `this` context.
 *
 * @return {Function|*}
 * If partially applied, return unary function, otherwise, return result of full application.
 */

function curry2 (fn, self) {
  var out = function () {
    if (arguments.length === 0) return out

    return arguments.length > 1
      ? fn.apply(self, arguments)
      : bind.call(fn, self, arguments[0])
  }

  out.uncurry = function uncurry () {
    return fn
  }

  return out
}


function computedStyle(el, prop) {
    return (
        window.getComputedStyle ? window.getComputedStyle(el) : el.currentStyle
    )[prop.replace(/-(\w)/gi, function (word, letter) {
        return letter.toUpperCase();
    })];
}

function getChildrenSize(container) {
    if (!container) {
        return;
    }
    var children = [].slice.call(container.children, 0).filter(function (el) {
        var pos = computedStyle(el, 'position');
        el.rect = el.getBoundingClientRect(); // store rect for later
        return !(
            (pos === 'absolute' || pos === 'fixed') ||
            (el.rect.width === 0 && el.rect.height === 0)
        );
    });
    if (children.length === 0) {
        return {
            width: 0,
            height: 0
        };
    }

    var totRect = children.reduce(function (tot, el) {
        return (!tot ?
            el.rect : {
                top: Math.min(tot.top, el.rect.top),
                left: Math.min(tot.left, el.rect.left),
                right: Math.max(tot.right, el.rect.right),
                bottom: Math.max(tot.bottom, el.rect.bottom)
            });
    }, null);

    return {
        width: totRect.right - totRect.left,
        height: totRect.bottom - totRect.top
    };
}

/*
    list can be either [[x, y], [x, y]] or [x, y]
*/
function createTouchList(target, list) {
    if (Array.isArray(list) && list[0] && !Array.isArray(list[0])) {
        list = [list];
    }
    list = list.map(function (entry, index) {
        var x = entry[0];
        var y = entry[1];
        var id = entry[2] || index;
        return createTouch(x, y, target, id);
    });
    return document.createTouchList.apply(document, list);
}

function createTouch(x, y, target, id) {
    return document.createTouch(window, target,
        //identifier
        id || 0,
        //pageX / clientX
        x,
        //pageY / clientY
        y,
        //screenX
        x,
        //screenY
        y
    );
}

//http://stackoverflow.com/questions/7056026/variation-of-e-touches-e-targettouches-and-e-changedtouches
function initTouchEvent(touchEvent, type, touches) {
    var touch1 = touches[0];
    return touchEvent.initTouchEvent(
        //touches
        touches,
        //targetTouches
        touches,
        //changedTouches
        touches,
        //type
        type,
        //view
        window,
        //screenX
        touch1.screenX,
        //screenY
        touch1.screenY,
        //clientX
        touch1.clientX,
        //clientY
        touch1.clientY,
        //ctrlKey
        false,
        //altKey
        false,
        //shiftKey
        false,
        //metaKey
        false
    );
}

function createTouchEvent(elem, type, touches) {
    var touchEvent = document.createEvent('TouchEvent');
    if (Array.isArray(touches)) {
        touches = createTouchList(elem, touches);
    }

    function dispatch(getEvent) {
        initTouchEvent(touchEvent, type, touches);
        if (typeof getEvent === 'function'){
            getEvent.call(elem, touchEvent, elem);
        }
        elem.dispatchEvent(touchEvent);
    }
    dispatch.event = touchEvent;
    return dispatch;
}

function apply(fn, arg, args) {
    return fn.apply(null, [arg].concat(Array.prototype.slice.call(args)));
}

function swipeLeft() {
    return apply(swipe, 'left', arguments);
}

function swipeRight() {
    return apply(swipe, 'right', arguments);
}

function swipeTop(){
    return apply(swipe, 'top', arguments);
}

function swipeBottom(){
    return apply(swipe, 'bottom', arguments);
}

function round(num){
    return Math.round(num);
}

var HORIZONTAL_OFFSET = 45;
var VERTICAL_OFFSET = 10;
function swipe(direction, elem, ms, frames, getEvent) {
    var elemSize = getChildrenSize(elem.parentNode);

    var x;
    var y;
    var from;
    var to;
    var isVertical = direction === 'top' || direction === 'bottom';
    if (isVertical){
        y = elemSize.height;
        x = elemSize.width / 2;

        from = [x*0.95, VERTICAL_OFFSET].map(round);
        to   = [x*1.01, y-VERTICAL_OFFSET].map(round);
    } else {
        // horizontal
        x = elemSize.width;
        y = elemSize.height / 2;
        from = [HORIZONTAL_OFFSET, y*0.98].map(round);
        to   = [x - HORIZONTAL_OFFSET, y*1.01].map(round);
    }

    if (direction === 'right' || direction === 'top') {
        touchActionSequence(elem, from, to, ms, frames, getEvent);
    } else {
        touchActionSequence(elem, to, from, ms, frames, getEvent);
    }
}

function getDiff(fromList, toList){
    return [
        toList[0] - fromList[0],
        toList[1] - fromList[1]
    ];
}

function getXandYFrame(startPoint, diffToWalk, currentProgress){
    return [
        Math.round(
            Math.abs(
                startPoint[0] + (diffToWalk[0] * currentProgress))),
        Math.round(
            Math.abs(
                startPoint[1] + (diffToWalk[1] * currentProgress)))
    ];
}

function touchActionSequence(elem, fromXandY, toXandY, ms, frames, getEvent) {
    frames       = frames || 10;
    ms           = Math.round((ms||1000) / frames);
    // lets find difference from start to end and divide on frames
    var diff     = getDiff(fromXandY, toXandY);
    var counter  = frames;
    var pos             = getXandYFrame(fromXandY, diff, counter/frames);
    var targetElement;

    targetElement   = document.elementFromPoint(pos[0], pos[1]);

    setTimeout(function handler() {
        counter--;
        if (counter) {
            pos = getXandYFrame(fromXandY, diff, counter/frames);
            targetElement = document.elementFromPoint(pos[0], pos[1]);
            createTouchEvent(targetElement||elem, 'touchmove', pos)(getEvent);
            setTimeout(handler, ms);
        } else {
            createTouchEvent(targetElement||elem, 'touchend', [[0, 0]])(getEvent);
        }
    }, ms);
    createTouchEvent(targetElement||elem, 'touchstart', pos)(getEvent);
}

function factory(){
    return {
        _apply: apply,
        _getXandYFrame: getXandYFrame,
        _getDiff: getDiff,
        swipeLeft: swipeLeft,
        swipeRight: swipeRight,
        swipeTop: swipeTop,
        swipeBottom: swipeBottom,
        touchActionSequence: touchActionSequence,
        createTouchEvent: createTouchEvent
    };
}

if (typeof module !== 'undefined' && module.exports){
    module.exports = factory();
} else {
    window.mockPhantomTouchEvents = factory();
}


'use strict';

var Unit = /** @type Unit */ require('./unit');

var _ = require('lodash-node');
var errors = require('./errors');
var f = require('util').format;
var inherit = require('inherit');

function init(app) {

    /**
     * default cache interface "local"
     *
     * @public
     * @memberOf app.caches
     * @property
     * @type {LRUDictTtlAsync}
     * */
    app.caches.local = Unit.prototype.cache;

    /**
     * @class app.Unit
     * @extends Unit
     * */
    app.Unit = inherit(Unit, /** @lends app.Unit.prototype */ {

        /**
         * @public
         * @memberOf {app.Unit}
         * @method
         * */
        __constructor: function () {
            // Close `app` in constructor to allow to simplify base calls in subclasses
            this.__base(app);

            /**
             * @public
             * @memberOf {app.Unit}
             * @property
             * @type {Object}
             * */
            this.cache = this.createCache();
        },

        /**
         * @public
         * @memberOf {app.Unit}
         * @method
         *
         * @returns {Object}
         * @throw {FistError}
         * */
        createCache: function () {
            if (_.isObject(this.cache)) {
                return this.cache;
            }

            if (_.has(this.app.caches, this.cache)) {
                return this.app.caches[this.cache];
            }

            throw new errors.NoSuchCacheError(f('You should define app.caches[%j] interface', this.cache));
        }

    });
}

module.exports = init;


import * as $ from 'manhattan-essentials'

// -- Utils --

/**
 * Shortcut for creating an icon button within a viewer.
 */
function createIconButton(viewerElm, css, eventType) {
    const buttonElm = $.create('div', {'class': css})
    $.listen(
        buttonElm,
        {
            'click': (event) => {
                event.preventDefault()
                if (event.buttons === 0) {
                    $.dispatch(viewerElm, eventType)
                }
            }
        }
    )
    return buttonElm
}

// -- Class definition --

/**
 * File viewer UI component for form fields.
 */
export class FileViewer {

    constructor(
        container,
        filename,
        fileSize,
        buttons={
            'download': true,
            'metadata': true,
            'remove': true
        }
    ) {

        // The file's filename
        this._filename = filename

        // The size of the file (in bytes)
        this._fileSize = fileSize

        // A set of flags which indicate which buttons should be displayed in
        // the viewer.
        this._buttons = buttons

        // Domain for related DOM elements
        this._dom = {
            'container': null,
            'handle': null,
            'viewer': null
        }

        // Store a reference to the container element
        this._dom.container = container
    }

    // -- Getters & Setters --

    get viewer() {
        return this._dom.viewer
    }

    // -- Public methods --

    /**
     * Remove the file viewer.
     */
    destroy() {

        // Remove the viewer element
        if (this.viewer !== null) {
            this.viewer.parentNode.removeChild(this.viewer)
        }

        // Clear DOM element references
        this._dom.container = null
        this._dom.viewer = null
    }

    /**
     * Initialize the file viewer.
     */
    init() {
        const cls = this.constructor

        // Create the viewer element
        this._dom.viewer = $.create('div', {'class': cls.css['viewer']})

        // Create the file info element
        const infoElm = $.create('div', {'class': cls.css['info']})
        this.viewer.appendChild(infoElm)

        const filenameElm = $.create('div', {'class': cls.css['filename']})
        filenameElm.textContent = this._filename
        infoElm.appendChild(filenameElm)

        const fileSizeElm = $.create('div', {'class': cls.css['fileSize']})
        fileSizeElm.textContent = this._fileSize
        infoElm.appendChild(fileSizeElm)

        // Create the handle
        const handleElm = $.create('div', {'class': cls.css['handle']})
        this.viewer.appendChild(handleElm)

        // Create the buttons
        const buttonsElm = $.create('div', {'class': cls.css['buttons']})
        for (let button of ['download', 'metadata', 'remove']) {
            if (this._buttons[button]) {
                let buttonElm = createIconButton(
                    this.viewer,
                    cls.css[button],
                    button
                )
                buttonsElm.appendChild(buttonElm)
            }
        }

        // If there are buttons add the buttons element to the viewer
        if (buttonsElm.children.length > 0) {
            this.viewer.appendChild(buttonsElm)
        }

        // Add the viewer element to the container
        this._dom.container.appendChild(this.viewer)
    }
}

// -- CSS classes --

FileViewer.css = {

    /**
     * Applied to the buttons component within the viewer.
     */
    'buttons': 'mh-file-viewer__buttons',

    /**
     * Applied to the download button within the buttons component.
     */
    'download': 'mh-file-viewer__download',

    /**
     * Applied to the element that displays the file's filename within the
     * information component.
     */
    'filename': 'mh-file-viewer__filename',

    /**
     * Applied to the element that displays the file's size within the
     * information component.
     */
    'fileSize': 'mh-file-viewer__file-size',

    /**
     * Applied to the handle element which is used to enable drag/sort when
     * the viewer is displayed within a gallery item.
     */
    'handle': 'mh-file-viewer__handle',

    /**
     * Applied to the file information component within the viewer.
     */
    'info': 'mh-file-viewer__info',

    /**
     * Applied to the metadata button within the buttons component.
     */
    'metadata': 'mh-file-viewer__metadata',

    /**
     * Applied to the remove button within the buttons component.
     */
    'remove': 'mh-file-viewer__remove',

    /**
     * Applied to the file viewer.
     */
    'viewer': 'mh-file-viewer'

}

/**
 * Image viewer UI component for form fields.
 */
export class ImageViewer {

    constructor(
        container,
        imageURL,
        buttons={
            'download': true,
            'edit': true,
            'metadata': true,
            'remove': true
        }
    ) {

        // The image to view
        this._imageURL = imageURL

        // A set of flags which indicate which buttons should be displayed in
        // the viewer.
        this._buttons = buttons

        // Domain for related DOM elements
        this._dom = {
            'container': null,
            'image': null,
            'viewer': null
        }

        // Store a reference to the container element
        this._dom.container = container
    }

    // -- Getters & Setters --

    get imageURL() {
        return this._imageURL
    }

    set imageURL(value) {
        this._imageURL = value
        if (this._dom.image) {
            this._dom.image.style.backgroundImage = `url('${this._imageURL}')`
        }
    }

    get viewer() {
        return this._dom.viewer
    }

    // -- Public methods --

    /**
     * Remove the image viewer.
     */
    destroy() {

        // Remove the viewer element
        if (this.viewer !== null) {
            this.viewer.parentNode.removeChild(this.viewer)
        }

        // Clear DOM element references
        this._dom.container = null
        this._dom.viewer = null
    }

    /**
     * Initialize the image viewer.
     */
    init() {
        const cls = this.constructor

        // Create the viewer element
        this._dom.viewer = $.create('div', {'class': cls.css['viewer']})

        // Create the image element
        this._dom.image = $.create('div', {'class': cls.css['image']})
        this._dom.image.style.backgroundImage = `url('${this._imageURL}')`
        this.viewer.appendChild(this._dom.image)

        // Create the handle
        const handleElm = $.create('div', {'class': cls.css['handle']})
        this.viewer.appendChild(handleElm)

        // Create the buttons
        const buttonsElm = $.create('div', {'class': cls.css['buttons']})
        for (let button of ['download', 'edit', 'metadata', 'remove']) {
            if (this._buttons[button]) {
                let buttonElm = createIconButton(
                    this.viewer,
                    cls.css[button],
                    button
                )
                buttonsElm.appendChild(buttonElm)
            }
        }

        // If there are buttons add the buttons element to the viewer
        if (buttonsElm.children.length > 0) {
            this.viewer.appendChild(buttonsElm)
        }

        // Add the viewer element to the container
        this._dom.container.appendChild(this.viewer)
    }
}

// -- CSS classes --

ImageViewer.css = {

    /**
     * Applied to the buttons component within the viewer.
     */
    'buttons': 'mh-image-viewer__buttons',

    /**
     * Applied to the download button within the viewer.
     */
    'download': 'mh-image-viewer__download',

    /**
     * Applied to the edit button within the viewer.
     */
    'edit': 'mh-image-viewer__edit',

    /**
     * Applied to the handle element which is used to enable drag/sort when
     * the viewer is displayed within a gallery item.
     */
    'handle': 'mh-image-viewer__handle',

    /**
     * Applied to image component within the viewer.
     */
    'image': 'mh-image-viewer__image',

    /**
     * Applied to the properties button within the viewer.
     */
    'metadata': 'mh-image-viewer__metadata',

    /**
     * Applied to the remove button within the viewer.
     */
    'remove': 'mh-image-viewer__remove',

    /**
     * Applied to the image viewer.
     */
    'viewer': 'mh-image-viewer'

}


'use strict';

var Obus = /** @type Obus */ require('obus');

/**
 * @class ContextLite
 * */
function ContextLite() {

    /**
     * @public
     * @memberOf {ContextLite}
     * @property
     * @type {Obus}
     * */
    this.params = new Obus();
}

/**
 * @public
 * @memberOf {ContextLite}
 * @method
 *
 * @param {String} path
 * @param {*} [def]
 *
 * @returns {*}
 * */
ContextLite.prototype.p = function (path, def) {
    return Obus.get(this.params, path, def);
};

/**
 * @public
 * @memberOf {Context}
 * @method
 *
 * @param {Object} params
 *
 * @returns {ContextLite}
 * */
ContextLite.prototype.addParams = function (params) {
    $Context$extendParams(this.params, params);
    return this;
};

/**
 * @class Context
 *
 * @param {Obus} params
 * @param {Logger} logger
 * */
function Context(params, logger) {

    /**
     * @public
     * @memberOf {Context}
     * @property
     * @type {Object}
     * */
    this.params = params;

    /**
     * @public
     * @memberOf {Context}
     * @property
     * @type {Object}
     * */
    this.result = new Obus();

    /**
     * @public
     * @memberOf {Context}
     * @property
     * @type {Object}
     * */
    this.errors = new Obus();

    /**
     * @public
     * @memberOf {Context}
     * @property
     * @type {Logger}
     * */
    this.logger = logger;
}

Context.prototype = Object.create(ContextLite.prototype);

/**
 * @public
 * @memberOf {Context}
 * @method
 * @constructs
 * */
Context.prototype.constructor = Context;

/**
 * @public
 * @memberOf {Context}
 * @method
 *
 * @param {String} path
 * @param {*} [def]
 *
 * @returns {*}
 * */
Context.prototype.r = function (path, def) {
    return Obus.get(this.result, path, def);
};

/**
 * @public
 * @memberOf {Context}
 * @method
 *
 * @param {String} path
 * @param {*} [def]
 *
 * @returns {*}
 * */
Context.prototype.e = function (path, def) {
    return Obus.get(this.errors, path, def);
};

/**
 * @public
 * @memberOf {Context}
 * @method
 *
 * @returns {Object}
 * */
Context.prototype.toJSON = function () {
    return {
        params: this.params,
        errors: this.errors,
        result: this.result
    };
};

function $Context$extendParams(obj, src) {
    var k;
    var keys = Object.keys(Object(src));
    var l = keys.length;

    while (l) {
        l -= 1;
        k = keys[l];
        if (src[k] !== void 0) {
            obj[k] = src[k];
        }
    }

    return obj;
}

Context.Lite = ContextLite;

module.exports = Context;


	var U, DEBUG = 'DEBUG', RESERVED = '__ASSERT__ __CONTEXT__ __FILTER_ __OUTPUT__ __UTIL__ $_ document false global instanceof null true typeof undefined window'.split( ' ' ).reduce( function( o, k ) {
			o[k] = true; return o;
		}, util.obj() ),
		ba          = {
			blank      : function( o ) { return util.empty( o ) || ( typeof o == 'string' && !o.trim() ); },
			contains   : contains,
			endsWith   : function( s, str ) {
				s = String( s );
				var n = s.length - str.length;
				return n >= 0 && s.lastIndexOf( str ) == n;
			},
			empty      : util.empty,
			equals     : function( o, v )   { return o == v },
			exists     : util.exists,
			is         : function( o, v )   { return o === v },
			isEven     : function( i )      { return  !( parseInt( i, 10 ) & 1 ); },
			isOdd      : function( i )      { return !!( parseInt( i, 10 ) & 1 ); },
			isTPL      : function( id )     { return !!getTPL( id, this ); },
			iterable   : function( o )      { return util.iter( o ); },
			notEmpty   : not_empty,
			startsWith : function( s, str ) { return String( s ).indexOf( str ) === 0; }
		},
		bf = {}, bu = {
			inspect    : function( v ) {
				switch ( util.ntype( v ) ) {
					case 'object' :
					case 'array'  : console.dir( v ); break;
					default       : console.log( v );
				}
				return '';
			},
			objectify  : function( v, k ) { var o = {}; o[k] = v; return o; },
			parse      : function( o, id, tpl ) {
				var e, p, s, t; o = Object( o ); // this fixes an issue in WebKit nightly — 6.0.2 (8536.26.17, 537+) —
										   // which does not allow you to set a property on a primitive value

				if ( id instanceof __Class__ )
					t  = id;
				else {
					id = String( id ).trim();
					t  = getTPL( id, this );
				}

				if ( !t ) return this.fallback;

				p = this[fn_var.dict];

				e = fn_var.parent in p;

				while( p && p === p[fn_var.parent] )
					p = p[fn_var.parent];

				if ( o !== p )
					o[fn_var.parent] = p;

				s = t.parse( o );

				if ( e )
					o[fn_var.parent] = p[fn_var.parent];
				else
					delete o[fn_var.parent];

				return s;
			},
			stop       : function( iter ) { iter.stop(); },
			stringify  : stringify,
			type       : function( o, match ) {
				var type = util.type( o );
				return typeof match == 'string' ? type == match : type;
			},
			value      : function( o, key ) { return Object.value( o, key ); }
		},
		cache_key = '__tpl_cs_cached_keys',                         cache_stack = '__tpl_cs_stack',
		defaults  = 'compiled debug dict fallback id sourceURL'.split( ' ' ), delim       = '<~>',
		esc_chars = /([-\*\+\?\.\|\^\$\/\\\(\)[\]\{\}])/g,          esc_val     = '\\$1',

		fn_var    = { assert : '__ASSERT__', ctx : '__CONTEXT__', dict : '__dict__', filter : '__FILTER__', output : '__OUTPUT__', parent : '__PARENT__', util : '__UTIL__' },
		fn_end    = util.format( 'return {0};\n ', fn_var.output ),
		fn_start  = '\n"use strict";\n' + util.format( 'var $C = new ContextStack( {0}, this ), $_ = $C.current(), iter = new Iter( null ), {1} = "", U;', fn_var.ctx, fn_var.output ),

		id_count  = 999, internals, logger = 'console', // <= gets around jsLint

		re_br              = /[\n\r]/gm,                      re_esc                = /(['"])/g,
		re_fix_jscomments  = /(\*)(\/)/gm,                    re_format_delim       = new RegExp( delim, 'gm' ),
		re_new_line        = /[\r\n]+/g,                      re_space              = /\s+/g,
		re_special_char    = /[\(\)\[\]\{\}\?\*\+\/<>%&=!-]/, re_split_tpl,
		re_statement_fix   = /\.(\d+)(\.?)/g,                 re_statement_replacer = "['$1']$2",
		re_statement_split = new RegExp( '\\s*([^\\|]+(?:\\|[^\\|]+?)){0,}' + delim, 'g' ),

		split_token        = '<__SPLIT__TEMPLATE__HERE__>',     split_replace         = ['', '$1', '$2', ''].join( split_token ),

		tpl           = {}, tpl_compiled  = '\/{0}{0} original template string:\n\n{1}\n\n{0}\/\n\n\/\/ compiled template code: \n\n{2}\n\n\/\/@ sourceURL={3}\n',
		tpl_debug     = 'WARNING: NO VALUE FOUND FOR => {0}',   tpl_id  = 't8-anon-{0}', tpl_srcurl = '/Templ8/{0}\.tpl',
		tpl_statement = '{0}["{1}"].call( this, {2}{3}, {4} )', tpl_sub = '{0}.{1}';

/*** START: Utility Functions ***/
	function contains( o, k ) { return o && ( typeof o.indexOf == 'function' && !!~o.indexOf( k ) ) || util.got( o, k ) ; }

	function escapeRE( s ) { return String( s ).replace( esc_chars, esc_val ); }

	function getTPL( id, ref_tpl ) {
		if ( !ref_tpl )
			return tpl[id] || null;

		var _id;

		do {
			_id = util.format( tpl_sub, ref_tpl.id, id );

			if ( _id in tpl )
				return tpl[_id];

		} while( ref_tpl = ref_tpl.parentTemplate );

		return tpl[id] || null;
	}

	function is_obj( o ) { return typeof o == 'object' && ( o.constructor === Object || o.constructor === U ); }

	function mapc( a, fn, ctx ) {
		fn || ( fn = util ); ctx || ( ctx = a );
		var i = -1, l = a.length, res = [], v;
		while ( ++i < l ) {
			v = fn.call( ctx, a[i], i, a );
			switch ( v ) {
				case null : case U : break;
				default   : switch ( typeof v ) {
					case 'string' :    v.trim() === '' || res.push( v );                    break;
					case 'number' :    isNaN( v )      || res.push( v );                    break;
					default       : ( !util.iter( v )  || util.len( v ) ) || res.push( v ); break;
				}
			}
		}
		return res;
	}

	function not_empty( o ) { return !util.empty( o ); }
/*** END:   Utility Functions ***/

/*** START: Classes used by compiled templates ***/
	function ContextStack( dict, tpl ) {
		this[cache_stack] = [];
		this.tpl = tpl;
		this.push( util.global );

		if ( tpl.fallback !== U ) {
			this.hasFallback = true;
			this.fallback    = tpl.fallback;
		}

		switch( util.ntype( dict ) ) {
			case 'object' : this.push( dict );
							break;
			case 'array'  : dict[fn_var.dict]
						  ? dict.map( this.push, this ) : this.push( dict );
						  	break;
			default       : !util.exists( dict ) || this.push( dict );
		}
	}

	ContextStack.prototype = {
		current : function ContextStack_current() { return ( this.top || this[cache_stack][0] ).dict; },
		get     : function ContextStack_get( key ) {
			var ctx, fb = this.fallback, stack = this[cache_stack], l = stack.length, val;

			while ( l-- ) {
				ctx = stack[l];
				if ( key in ctx.cache ) return ctx.cache[key];
				if ( ( val = ctx.dict[key] ) !== U || ( val = Object.value( ctx.dict, key ) ) !== U )
					return ctx.cache[key] = val;
			}

			if ( this.hasFallback ) switch ( util.ntype( fb ) ) {
				case 'string'   : return fb === DEBUG ? util.format( tpl_debug, key ) : fb;
				case 'function' : return fb( key, ctx.dict );
			}

			return U;
		},
		pop     : function ContextStack_pop() {
			var dict = this[cache_stack].pop().dict;
			this.top = this[cache_stack][this[cache_stack].length - 1];
			return dict;
		},
		push    : function ContextStack_push( dict ) {
			this[cache_stack].push( this.top = { cache : util.obj(), dict : dict } );
			return this;
		}
	};

	function Iter( iter, parent, start, count ) {
		var keys = Object.keys( iter = this._ = Object( iter ) ),
			len  = keys.length;

		if ( !len ) return this.stop();

		util.tostr( iter ) == '[object Object]' || ( keys = keys.map( Number ) );

		this.empty     = false;
		this.count     = isNaN( count ) ? len : count < 0 ? len + count : count > len ? len : count;
		if ( start == 0 || isNaN( start ) ) {
			this.firstIndex =  0;
			this.index      = -1;
		}
		else {
			this.firstIndex = start;
			this.index      = start - 2;
		}
		this.index1    = this.index + 1;
		this.lastIndex = this.count === len ? this.count - 1 : this.count;
		this.keys      = keys;

		!( parent instanceof Iter ) || ( this.parent = parent );
	}

	Iter.prototype = {
		empty   : true,

		hasNext : function Iter_hasNext() {
			if ( this.stopped || this.empty ) return false;
			++this.index < this.lastIndex || ( this.stop().isLast = true );

			this.key     = this.keys[this.index1++];
			this.current = this.val = this._[this.key];

			return this;
		},
		stop    : function Iter_stop() {
			this.stopped = true;
			return this;
		}
	};

	util.defs( Iter.prototype, { // todo: these aren't tested yet!
		first     : { get : function() { return this._[this.keys[this.firstKey]]; } },
		last      : { get : function() { return this._[this.keys[this.lastKey]];  } },
		next      : { get : function() { return this._[this.keys[this.nextKey]];  } },
		prev      : { get : function() { return this._[this.keys[this.prevKey]];  } },

		nextIndex : { get : function() {
			var i = this.index + 1;
			return i <= this.lastIndex  ? i : U;
		} },
		prevIndex : { get : function() {
			var i = this.index - 1;
			return i >= this.firstIndex ? i : U;
		} },

		firstKey  : { get : function() { return this.keys[this.firstIndex]; } },
		lastKey   : { get : function() { return this.keys[this.lastIndex];  } },
		nextKey   : { get : function() { return this.keys[this.nextIndex];  } },
		prevKey   : { get : function() { return this.keys[this.prevIndex];  } }
	}, 'r' );

/*** END:   Classes used by compiled templates ***/

/*** START: create template methods ***/

	function aggregatetNonEmpty( res, str ) {
		util.empty( str ) || res.push( str );
		return res;
	}

	function aggregateStatement( ctx, s ) {
		return s.reduce( function( res, v, i, parts ) {
			if ( i == 0 ) return wrapGetter( ctx, v );
			var args = '', fn, j = v.indexOf( ':' );
			if ( !!~j ) {
				fn   = v.substring( 0,  j );
				args = v.substring( j + 1 );
			}
			else fn = v;
			!args || ( args = ', ' + args.split( ',' ).map( function( o ) { return wrapGetter( this, o ); }, ctx ).join( ', ' ) );
			return util.format( tpl_statement, getFnParent( fn ), fn, wrapGetter( ctx, res ), args, fn_var.ctx );
		}, '' );
	}

	function assembleParts( ctx, parts ) {
		var fn = [fn_start], part;

		while ( part = parts.shift() ) fn.push( emitTag( ctx, part, parts ) );

		fn.push( fn_end );

		return fn.join( '\r\n' );
	}

	function clean( str ) { return str.replace( re_format_delim, '' ).replace( re_new_line, '\n' ).replace( re_space, ' ' ).trim(); }

	function compileTemplate( ctx, fn ) {
		fn = util.format( tpl_compiled, '*', ctx.__tpl__.replace( re_fix_jscomments, '$1 \\$2' ), fn, ctx.sourceURL ? ctx.sourceURL : util.format( tpl_srcurl, ctx.id ) );

		var func = ( new Function( 'root', 'ContextStack', 'Iter', fn_var.filter, fn_var.assert, fn_var.util, fn_var.ctx, fn ) );

		return func.bind( ctx, util.global, ContextStack, Iter, util.copy( ctx.filters, __Class__.Filter.all(), true ), ba, bu );
	}

	function createTemplate( ctx ) {
		ctx.currentIterKeys = [];
		var fn = compileTemplate( ctx, assembleParts( ctx, splitStr( ctx.__tpl__ ) ) );
		delete ctx.currentIterKeys;
		return fn;
	}

	function emitTag( ctx, part, parts ) {
		var tag;
		if ( tag = __Class__.Tag.get( part ) ) {
			part = parts.shift();
			return tag.emit( internals, ctx, part, parts );
		}
		return wrapStr( util.format( '"{0}"', part.replace( re_esc, "\\$1" ) ) );
	}

 	function formatStatement( ctx, str ) {
		str = clean( str );
		switch ( str ) {
			case 'AND' : return ' && ';
			case 'OR'  : return ' || ';
		}
		return contains( str, '|' ) || contains( str, delim ) ? ( ' ' + str + delim ).replace( re_statement_split, function( m ) {
			return ba.blank( m ) || m == delim ? '' : aggregateStatement( ctx, clean( m ).split( '|' ) );
		} ) : wrapGetter( ctx, str );
	}

	function getFnParent( fn ) { return ( ba[fn] ? fn_var.assert : bu[fn] ? fn_var.util : fn_var.filter ); }

	function splitStr( str ) {
		return str.replace( re_split_tpl, split_replace )
				  .split( split_token )
				  .reduce( aggregatetNonEmpty, [] );
	}

	function stringify( o, str ) {
		switch ( typeof o ) {
			case 'boolean' : case 'number' : case 'string' : return String( o );
			default        : switch ( util.ntype( o ) ) {
				case 'date'   : return o.toDateString();
				case 'array'  : return mapc( o, stringify ).join( ', ' );
				case 'object' : return cache_key in o
							  ? stringify( o.dict ) : ( ( str = o.toString() ) != '[object Object]' )
							  ? str : mapc( Object.values( o ), stringify ).join( ', ' );
				default       : switch ( util.type( o ) ) {
					case 'htmlelement'    : return o.outerHTML; //o.textContent || o.text || o.innerText;
					case 'htmlcollection' : return mapc( Array.coerce( o ), function( el ) { return stringify( el ); } ).join( '\n' );
				}
			}
		}
		return '';
	}

	function usingIterKey( key ) { return this == key || ba.startsWith( this, key + '.' ); }
	function usingIterKeys( keys, o ) { return keys.length ? keys.some( function( k ) { return k.some( usingIterKey, o ); } ) : 0; }

	function wrapGetter( ctx, o ) {
		var k = ctx.currentIterKeys || []; o = clean( o );
		return ( contains( o, '.call(' )
			|| re_special_char.test( o )
			|| ( ba.startsWith( o, '"' ) && ba.endsWith( o, '"' ) )
			|| ( ba.startsWith( o, "'" ) && ba.endsWith( o, "'" ) )
			|| !isNaN( o ) )
		? o : ( ba.startsWith( o, '$_.' ) || ba.startsWith( o, 'iter.' ) || ( k.length && usingIterKeys( k, o ) ) || o in RESERVED )
		? o.replace( re_statement_fix, re_statement_replacer ) : util.format( '$C.get( "{0}" )', o );
	}

	function wrapStr( str ) { return util.format( '{0} += {1};', fn_var.output, str.replace( re_br, '\\n' ) ); }

// these will be passed to tags & statements for internal usage
	internals = {
		assembleparts   : assembleParts,   clean   : clean,      compiletpl : compileTemplate,
		createtpl       : createTemplate,  emittag : emitTag,    fnvar      : fn_var,
		formatstatement : formatStatement, get     : wrapGetter, util       : bu,
		wrap            : wrapStr
	};
/*** END:   create template methods ***/

/*** START: Templ8 constructor and prototype ***/
	function __Class__() {
		var a = Array.coerce( arguments ),
			f = is_obj( a[a.length - 1] ) ? a.pop() : is_obj( a[0] ) ? a.shift() : null,
			fb;

// take care of peeps who are too lazy or too ©ººL to use the "new" constructor...
		if ( !( this instanceof __Class__ ) )
			return is_obj( f ) ? new __Class__( a.join( '' ), f ) : new __Class__( a.join( '' ) );

		!f || defaults.forEach( function( k ) {
			if ( k in f ) { this[k] = f[k]; delete f[k]; }
		}, this );

		fb = this.fallback;
		if ( ( !fb && this.debug ) || ( typeof fb == 'string' && fb.toUpperCase() === DEBUG ) )
			this.fallback = DEBUG;

		this.filters = f || {};

		this.__tpl__ = a.join( '\n' );

		tpl[$id( this )] = this;

		if ( this.compiled ) {
			this.compiled = false;
			compile( this );
		}
	}

	function $id( ctx ) {
		ctx.id || ( ctx.id = util.format( tpl_id, ++id_count ) );
		return ctx.id;
	}

	function parse( dict ) {
		this.compiled || compile( this );
		this[fn_var.dict] = dict;
		var s = this._parse( dict );
		delete this[fn_var.dict];
		return s;
	}

	function compile( ctx ) {
		if ( !ctx.compiled ) {
			ctx.compiled = true;
			ctx._parse   = createTemplate( ctx );
		}
		return ctx;
	}

	__Class__.prototype = {
		compiled : false, debug : false, dict : null, fallback : '',
		parse    : parse
	};
/*** END:   Templ8 constructor and prototype ***/

/*** START: Templ8 functionality packages ***/
// exposed for general usage
	util.defs( __Class__, {             // store a reference to m8 in Templ8 so we can do fun stuff in commonjs
		m8       : { value : util }, // modules without having to re-request m8 as well as Templ8 each time.
		escapeRE : escapeRE,  format    : util.format,  get : getTPL,
		gsub     : util.gsub, stringify : stringify
	}, 'r' );

	function Mgr( o ) {
		var cache = {};

		!is_obj( o ) || util.copy( cache, o );

		function _add( id, fn, replace ) { ( !replace && id in cache ) || ( cache[id] = fn ); }
		function  add( replace, o ) {
			switch( typeof o ) {
				case 'string' : _add( o, arguments[2], replace );            break;
				case 'object' : for ( var k in o ) _add( k, o[k], replace ); break;
			} return this;
		}

		this.all     = function()     { return util.copy( cache ); };
		this.add     = function()     { return add.call( this, false, arguments[0], arguments[1] ); };
		this.get     = function( id ) { return cache[id]; };
		this.replace = function()     { return add.call( this, true, arguments[0], arguments[1] ); };
	}

	__Class__.Assert    = new Mgr( ba );
	__Class__.Filter    = new Mgr( bf );
	__Class__.Statement = new Mgr;
	__Class__.Tag       = new function() {
		var KEYS   = 'emit end start'.split( ' ' ),
			ERRORS = {
				emit  : 'emit function',
				end   : 'end tag definition',
				start : 'start tag definition'
			},
			tag    = {};

		function Tag( config ) {
			KEYS.forEach( assert_exists, config );
			util.copy( this, config );
			tag[this.start] = this;
		}

		function assert_exists( k ) { if ( !( k in this ) ) { throw new TypeError( util.format( 'A ' + Name + ' Tag requires an {0}', ERRORS[k] ) ); } }

		this.all = function() { return util.copy( tag ); };

		this.compileRegExp = function() {
			var end = [], start = [], t;
			for ( t in tag ) {
				end.push( escapeRE( tag[t].end.substring( 0, 1 ) ) );
				start.push( escapeRE( tag[t].start.substring( 1 ) ) );
			}
			return ( re_split_tpl = new RegExp( '(\\{[' + start.join( '' ) + '])\\s*(.+?)\\s*([' + end.join( '' ) + ']\\})', 'gm' ) );
		};

		this.create = function( o, dont_compile ) {
			new Tag( o ); dont_compile === true || this.compileRegExp();
			return this;
		};

		this.get = function( id ) { return tag[id]; };
	};

/*** END:   Templ8 functionality packages ***/


'use strict';

var Bluebird = /** @type Promise */ require('bluebird');

var errors = require('./errors');
var f = require('util').format;
var uniqueId = require('unique-id');

/**
 * @class Track
 *
 * @param {Core} app
 * @param {Logger} logger
 * */
function Track(app, logger) {

    /**
     * @public
     * @memberOf {Track}
     * @property
     * @type {String}
     * */
    this.id = this._createId();

    /**
     * TODO Deprecate?
     *
     * @public
     * @memberOf {Track}
     * @property
     * @type {Logger}
     * */
    this.logger = logger;

    /**
     * @public
     * @memberOf {Track}
     * @property
     * @type {Object}
     * */
    this.params = {};

    /**
     * @protected
     * @memberOf {Track}
     * @property
     * @type {Core}
     * */
    this._app = app;

    /**
     * @public
     * @memberOf {Track}
     * @property
     * @type {Object}
     * */
    this.calls = {};

    /**
     * @protected
     * @memberOf {Track}
     * @property
     * @type {Boolean}
     * */
    this._isFlushed = false;
}

/**
 * @public
 * @memberOf {Track}
 * @method
 *
 * @constructs
 * */
Track.prototype.constructor = Track;

/**
 * @protected
 * @memberOf {Connect}
 * @method
 *
 * @returns {String}
 * */
Track.prototype._createId = function () {
    return uniqueId();
};

/**
 * @public
 * @memberOf {Track}
 * @method
 *
 * @param {String} name
 * @param {*} [args]
 *
 * @returns {Promise}
 * */
Track.prototype.invoke = function (name, args) {
    var defer = Bluebird.defer();
    var unit = this._app.getUnit(name);

    if (!(unit instanceof this._app.Unit)) {
        return Bluebird.reject(new errors.NoSuchUnitError(f('Can not invoke unknown unit %j', name)));
    }

    unit.run(this, args, function (runtime) {
        if (runtime.isRejected()) {
            defer.reject(runtime.valueOf());
        } else {
            defer.resolve(runtime.valueOf());
        }
    });

    return defer.promise;
};

/**
 * @public
 * @memberOf {Track}
 * @method
 *
 * @returns {Boolean}
 * */
Track.prototype.isFlushed = function () {
    return this._isFlushed;
};

module.exports = Track;


import {Sortable} from 'manhattan-sortable'

/**
 * Clone behaviour for gallery items used when creating a helper for the
 * purpose of sorting.
 */
function cloneGalleryItem(inst, element) {

    // Clone the element
    const clone = element.cloneNode(true)

    // Remove id attribute to avoid unwanted duplicates
    clone.removeAttribute('id')

    // Add a helper class
    clone.classList.add('mh-gallery-item--sort-helper')

    return clone
}

Sortable.behaviours.helper['cloneGalleryItem'] = cloneGalleryItem


// TODO make errors subclasses of FistError
'use strict';

var f = require('util').format;

/**
 * @class FistError
 * @extends Error
 *
 * @param {String} code
 * @param {String} msg
 * */
function FistError(code, msg) {
    var err = new Error(f('(%s) %s', code, msg));

    err.name = this.name;

    Error.captureStackTrace(err, this.constructor);

    /**
     * @public
     * @memberOf {FistError}
     * @property
     * @type {String}
     * */
    this.code = code;

    /**
     * @public
     * @memberOf {FistError}
     * @property
     * @type {String}
     * */
    this.message = err.message;

    /**
     * @public
     * @memberOf {FistError}
     * @property
     * @type {String}
     * */
    this.stack = err.stack;
}

FistError.prototype = Object.create(Error.prototype);

/**
 * @public
 * @memberOf {FistError}
 * @method
 *
 * @constructs
 * */
FistError.prototype.constructor = FistError;

/**
 * @public
 * @memberOf {FistError}
 * @property
 * @type {String}
 * */
FistError.prototype.name = 'FistError';

/**
 * @class BadUnitError
 * @extends FistError
 *
 * @param {String} msg
 * */
function BadUnitError(msg) {
    FistError.call(this, 'BAD_UNIT', msg);
}

BadUnitError.prototype = Object.create(FistError.prototype);

/**
 * @public
 * @memberOf {BadUnitError}
 * @method
 *
 * @constructs
 * */
BadUnitError.prototype.constructor = BadUnitError;

/**
 * @public
 * @memberOf {BadUnitError}
 * @property
 * @type {String}
 * */
BadUnitError.prototype.name = 'BadUnitError';

/**
 * @class NoSuchUnitError
 * @extends FistError
 *
 * @param {String} msg
 * */
function NoSuchUnitError(msg) {
    FistError.call(this, 'NO_SUCH_UNIT', msg);
}

NoSuchUnitError.prototype = Object.create(FistError.prototype);

/**
 * @public
 * @memberOf {NoSuchUnitError}
 * @method
 *
 * @constructs
 * */
NoSuchUnitError.prototype.constructor = NoSuchUnitError;

/**
 * @public
 * @memberOf {NoSuchUnitError}
 * @property
 * @type {String}
 * */
NoSuchUnitError.prototype.name = 'NoSuchUnitError';

/**
 * @class DepsConflictError
 * @extends FistError
 *
 * @param {String} msg
 * */
function DepsConflictError(msg) {
    FistError.call(this, 'DEPS_CONFLICT', msg);
}

DepsConflictError.prototype = Object.create(FistError.prototype);

/**
 * @public
 * @memberOf {BadUnitError}
 * @method
 *
 * @constructs
 * */
DepsConflictError.prototype.constructor = DepsConflictError;

/**
 * @public
 * @memberOf {DepsConflictError}
 * @property
 * @type {String}
 * */
DepsConflictError.prototype.name = 'DepsConflictError';

/**
 * @class NoSuchCache
 * @extends FistError
 *
 * @param {String} msg
 * */
function NoSuchCacheError(msg) {
    FistError.call(this, 'NO_SUCH_CACHE', msg);
}

NoSuchCacheError.prototype = Object.create(FistError.prototype);

/**
 * @public
 * @memberOf {NoSuchCacheError}
 * @method
 * @constructs
 * */
NoSuchCacheError.prototype.constructor = NoSuchCacheError;

/**
 * @public
 * @memberOf {NoSuchCacheError}
 * @property
 * @type {String}
 * */
NoSuchCacheError.prototype.name = 'NoSuchCacheError';

exports.FistError = FistError;

exports.BadUnitError = BadUnitError;

exports.NoSuchUnitError = NoSuchUnitError;

exports.DepsConflictError = DepsConflictError;

exports.NoSuchCacheError = NoSuchCacheError;

Object.freeze(exports);