// Dependencies
var CliFrames = require("cli-frames")
  , ImageToAscii = require("image-to-ascii")
  , GifExplode = require("gif-explode")
  , OneByOne = require("one-by-one")
  , Fs = require("fs")
  , Tmp = require("tmp")
  ;

/**
 * GifCli
 * Converts the gif file into ASCII frames.
 *
 * @name GifCli
 * @function
 * @param {String} path The input gif file path.
 * @param {Function} callback The callback function.
 */
function GifCli(path, callback) {
    var frames = [];

    OneByOne([
        Tmp.dir
      , function (next, tmpDir) {
            var str = Fs.createReadStream(path)
              , isFinished = false
              , complete = []
              , i = 0
              ;

            str.on("end", function () {
                isFinished = true;
            });

            str.pipe(
                GifExplode(function (frame) {
                    Tmp.file({ postfix: ".gif", }, function (err, cImg) {
                        (function (i, cImg) {
                            if (err) { return next(err); }
                            var wStr = Fs.createWriteStream(cImg);
                            frame.pipe(wStr);
                            complete[i] = false;
                            wStr.on("close", function () {
                                // TODO Allow passing options
                                ImageToAscii(cImg, function (err, asciified) {
                                    complete[i] = true;
                                    frames[i] = asciified || "";
                                    // TODO https://github.com/hughsk/gif-explode/issues/4
                                    //if (err) { return next(err); }
                                    if (!isFinished) { return; }
                                    if (!complete.filter(function (c) {
                                        return c !== true
                                    }).length) {
                                        next();
                                    }
                                });
                            });
                        })(i++, cImg);
                    });
                })
            );
        }
      , function (next) {
            frames = frames.filter(Boolean);
            next();
        }
    ], function (err) {
        if (err) { return callback(err); }
        callback(null, frames);
    });
}

module.exports = GifCli;


/*global define */
(function (global, factory) {
  'use strict';
  if (typeof module === 'object' && typeof module.exports === 'object') {
    module.exports = global.document
      ? factory(global, true)
      : function (w) {
        if (!w.document) {
          throw new Error('jQuery requires a window with a document');
        }
        return factory(w);
      };
  } else {
    factory(global);
  }
})(typeof window !== 'undefined' ? window : this, function (window, noGlobal) {

  function postMessage(action, payload) {
    var message = JSON.stringify({
      action: action,
      payload: payload
    });
    if (window.isPhone) {
      window.postMessage(message, '');
    } else {
      console.log(message);
    }
  }

  function getAsyncData(action, payload) {
    return new Promise(function (resolve, reject) {
      function listener(event) {
        event.target.removeEventListener('message', listener, false);
        resolve(JSON.parse(event.data));
      }

      window.document.addEventListener('message', listener, false);
      window.postMessage(
        JSON.stringify({
          action: action,
          payload: payload
        })
      );

      setTimeout(function () {
        reject('timeout');
      }, 3000);
    });
  }

  function invoke(action, payload) {
    postMessage(action, payload);
  }

  var DoreClient = {
    invoke: function (options) {
      invoke(options.action, options.payload);
    },
    getAsyncData: function (options, cb) {
      getAsyncData(options.action, options.payload).then(function (results) {
        cb(results);
      })
    },
    //TODO: Test funcationl
    SQLite: {
      action: "SQLite",
      open: function (name, version, displayName, databaseSize, operationDataBase, errorCB) {
        var options = {
          type: 'OPEN',
          name: name,
          version: version,
          displayName: displayName,
          databaseSize: databaseSize,
          operationDataBase: operationDataBase.toString(),
          errorCB: errorCB.toString()
        };
        return getAsyncData(this.action, options);
      },
    },
    getAppVersion: function () {
      return getAsyncData('DEVICE_INFO', {type: 'APP_VERSION'});
    },
    getUniqueID: function () {
      return getAsyncData('DEVICE_INFO', {type: 'DEVICE_ID'});
    },
    getBrand: function () {
      return getAsyncData('DEVICE_INFO', {type: 'BRAND'});
    },
    getModel: function () {
      return getAsyncData('DEVICE_INFO', {type: 'MODEL'});
    },
    getSystemName: function () {
      return getAsyncData('DEVICE_INFO', {type: 'SYSTEM_NAME'});
    },
    isEmulator: function () {
      return getAsyncData('DEVICE_INFO', {type: 'IS_EMULATOR'});
    },
    isTablet: function () {
      return getAsyncData('DEVICE_INFO', {type: 'IS_TABLET'});
    },
    showToast: function (message, duration, position) {
      return invoke('TOAST', {
        options: {duration: duration, position: position},
        message: message
      });
    },
    showDatePicker: function (options) {
      options.TYPE = 'DATE_PICKER';
      return getAsyncData('DATE_PICKER', options);
    },
    setBadge: function (badgeNumber) {
      return invoke('BADGE', {type: 'SET_BADGE', badgeNumber: badgeNumber});
    },
    getBadge: function (badgeNumber) {
      return getAsyncData('BADGE', {
        type: 'GET_BADGE',
        badgeNumber: badgeNumber
      });
    },
    clearBadge: function () {
      return invoke('BADGE', {type: 'CLEAR_BADGE'});
    },
    hideKeyboard: function () {
      return invoke('KEYBOARD', {type: 'DISMISS'});
    },
    getCurrentPosition: function (text) {
      return getAsyncData('GEOLOCATION', {
        type: 'CURRENT_POSITION',
        text: text
      });
    },
    watchPosition: function (text) {
      return invoke('GEOLOCATION', {type: 'WATCH_POSITION', text: text});
    },
    clearWatch: function (text) {
      return invoke('GEOLOCATION', {type: 'CLEAR_WATCH', text: text});
    },
    stopObserving: function (text) {
      return invoke('GEOLOCATION', {type: 'STOP_OBSERVING', text: text});
    },
    copy: function (text) {
      return invoke('CLIPBOARD', {type: 'COPY', text: text});
    },
    paste: function () {
      return invoke('CLIPBOARD', {type: 'PASTE'});
    },
    open: function (text) {
      return invoke('OPEN_LINK', text);
    },
    getOrientation: function () {
      return getAsyncData('ORIENTATION', {type: 'ORIENTATION'});
    },
    lockToPortrait: function () {
      return invoke('ORIENTATION', {type: 'LOCK_PORTRAIT'});
    },
    lockToLandscape: function () {
      return invoke('ORIENTATION', {type: 'LOCK_LANDSCAPE'});
    },
    getConnectionInfo: function () {
      return getAsyncData('NET_INFO', {type: 'CONNECTION_INFO'});
    },
    addNetInfoEventListener: function () {
      return invoke('NET_INFO', {type: 'ADD_LISTENER'});
    },
    removeNetInfoEventListener: function () {
      return invoke('NET_INFO', {type: 'REMOVE_LISTENER'});
    },
    hideStatusBar: function () {
      return invoke('STATUS_BAR', {type: 'HIDE'});
    },
    showStatusBar: function () {
      return invoke('STATUS_BAR', {type: 'SHOW'});
    },
    addStateListener: function () {
      return invoke('STATE', {type: 'ADD_LISTENER'});
    },
    removeStateListener: function () {
      return invoke('STATE', {type: 'REMOVE_LISTENER'});
    },
    vibrationVibrate: function (duration) {
      return invoke('VIBRATION', {type: 'VIBRATE', duration: duration});
    },
    vibrationCancel: function () {
      return invoke('VIBRATION', {type: 'CANCEL'});
    },
    setBrightnessLevel: function (luminous) {
      return invoke('BRIGHTNESS', {type: 'SET', luminous: luminous});
    },
    getBrightnessLevel: function () {
      return getAsyncData('BRIGHTNESS', {type: 'GET'});
    },
    checkPermissions: function (permission, options) {
      return getAsyncData('PERMISSIONS', {type: 'CHECK', permission: permission, options: options});
    },
    requestPermissions: function (permission, options) {
      return getAsyncData('PERMISSIONS', {type: 'REQUEST', permission: permission, options: options});
    },
    checkMultiple: function (permission, options) {
      return getAsyncData('PERMISSIONS', {type: 'CHECK_MULTIPLE', permission: permission, options: options});
    },
    captureScreen: function (options) {
      return getAsyncData('SCREENSHOT', {type: 'CAPTURE', options: options});
    },
    setAsyncStorage: function (key, data) {
      return invoke('ASYNC_STORAGE', {type: 'SET', key: key, data: data});
    },
    getAsyncStorage: function (key) {
      return getAsyncData('ASYNC_STORAGE', {type: 'GET', key: key});
    },
    getAppList: function () {
      return getAsyncData('AVAILABILITY', {type: 'GET_LIST'});
    },
    isAppInstalled: function (key) {
      return getAsyncData('AVAILABILITY', {type: 'IS_INSTALLED', key: key});
    },
    isAppInstalledAndroid: function (key) {
      return getAsyncData('AVAILABILITY', {type: 'IS_INSTALLED_ANDROID', key: key});
    },
    checkURLScheme: function (key) {
      return getAsyncData('AVAILABILITY', {type: 'URL_SCHEME', key: key});
    },
    addShakeListener: function () {
      return invoke('SHAKE', {type: 'ADD_LISTENER'});
    },
    onFlashlight: function () {
      return invoke('FLASHLIGHT', {type: 'ON'});
    },
    offFlashlight: function () {
      return invoke('FLASHLIGHT', {type: 'OFF'});
    },
    sendSMS: function (options) {
      return invoke('SMS', {type: 'send', options: options});
    },
    calendarAuthorizationStatus: function () {
      return getAsyncData('CALENDAR', {type: 'AUTHORIZATION_STATUS'});
    },
    requestCalendarAuthorize: function () {
      return invoke('CALENDAR', {type: 'REQUEST_AUTHORIZE'});
    },
    findEventById: function (id) {
      return getAsyncData('CALENDAR', {type: 'FIND_EVENT_BY_ID', id: id});
    },
    fetchAllCalendar: function (startDate, endDate, calendars) {
      return getAsyncData('CALENDAR', {
        type: 'FETCH_ALL_CALENDAR',
        startDate: startDate,
        endDate: endDate,
        calendars: calendars,
      });
    },
    saveCalendar: function (title, settings) {
      return getAsyncData('CALENDAR', {type: 'SAVE_EVENT', title: title, settings: settings});
    },
    removeCalendar: function (id) {
      return getAsyncData('CALENDAR', {type: 'REMOVE_EVENT_BY_ID', id: id});
    },
    findCalendars: function () {
      return getAsyncData('CALENDAR', {type: 'FIND_CALENDARS'});
    },
    console: {
      // assert(test?: boolean, message?: string, optionalParams: any[]): void;
      assert: function (test, message, optionalParams) {
        return invoke('CONSOLE', {
          type: 'assert',
          test: test,
          message: message,
          optionalParams: optionalParams || []
        });
      },
      // clear(): void;
      clear: function () {
        return invoke('CONSOLE', {
          type: 'clear'
        });
      },
      // count(countTitle?: string): void;
      count: function (countTitle) {
        return invoke('CONSOLE', {
          type: 'count',
          countTitle: countTitle
        });
      },
      // debug(message?: any, optionalParams: any[]): void;
      debug: function (message, optionalParams) {
        return invoke('CONSOLE', {
          type: 'debug',
          message: message,
          optionalParams: optionalParams || []
        });
      },
      // dir(value?: any, optionalParams: any[]): void;
      dir: function (value, optionalParams) {
        return invoke('CONSOLE', {
          type: 'dir',
          value: value,
          optionalParams: optionalParams || []
        });
      },
      // dirxml(value: any): void;
      dirxml: function (value) {
        return invoke('CONSOLE', {
          type: 'dirxml',
          value: value
        });
      },
      // error(message?: any, optionalParams: any[]): void;
      error: function (message, optionalParams) {
        return invoke('CONSOLE', {
          type: 'error',
          message: message,
          optionalParams: optionalParams || []
        });
      },
      // exception(message?: string, optionalParams: any[]): void;
      exception: function (message, optionalParams) {
        return invoke('CONSOLE', {
          type: 'exception',
          message: message,
          optionalParams: optionalParams || []
        });
      },
      // group(groupTitle?: string, optionalParams: any[]): void;
      group: function (groupTitle, optionalParams) {
        return invoke('CONSOLE', {
          type: 'group',
          groupTitle: groupTitle,
          optionalParams: optionalParams || []
        });
      },
      // groupCollapsed(groupTitle?: string, optionalParams: any[]): void;
      groupCollapsed: function (groupTitle, optionalParams) {
        return invoke('CONSOLE', {
          type: 'groupCollapsed',
          groupTitle: groupTitle,
          optionalParams: optionalParams || []
        });
      },
      // groupEnd(): void;
      groupEnd: function () {
        return invoke('CONSOLE', {
          type: 'groupEnd'
        });
      },
      // info(message?: any, optionalParams: any[]): void;
      info: function (message, optionalParams) {
        return invoke('CONSOLE', {
          type: 'info',
          message: message,
          optionalParams: optionalParams || []
        });
      },
      // log(message?: any, optionalParams: any[]): void;
      log: function (message, optionalParams) {
        return invoke('CONSOLE', {
          type: 'log',
          message: message,
          optionalParams: optionalParams || []
        });
      },
      // msIsIndependentlyComposed(element: Element): boolean;
      msIsIndependentlyComposed: function (element) {
        return invoke('CONSOLE', {
          type: 'msIsIndependentlyComposed',
          element: element
        });
      },
      // profile(reportName?: string): void;
      profile: function (reportName) {
        return invoke('CONSOLE', {
          type: 'profile',
          reportName: reportName
        });
      },
      // profileEnd(): void;
      profileEnd: function () {
        return invoke('CONSOLE', {
          type: 'profileEnd'
        });
      },
      // select(element: Element): void;
      select: function (element) {
        return invoke('CONSOLE', {
          type: 'select',
          element: element
        });
      },
      // table(data: any[]): void;
      table: function (data) {
        return invoke('CONSOLE', {
          type: 'table',
          data: data
        });
      },
      // time(timerName?: string): void;
      time: function (timerName) {
        return invoke('CONSOLE', {
          type: 'time',
          timerName: timerName
        });
      },
      // timeEnd(timerName?: string): void;
      timeEnd: function (timerName) {
        return invoke('CONSOLE', {
          type: 'timeEnd',
          timerName: timerName
        });
      },
      // trace(message?: any, optionalParams: any[]): void;
      trace: function (message, optionalParams) {
        return invoke('CONSOLE', {
          type: 'trace',
          message: message,
          optionalParams: optionalParams || []
        });
      },
      // warn(message?: any, optionalParams: any[]): void;
      warn: function (message, optionalParams) {
        return invoke('CONSOLE', {
          type: 'warn',
          message: message,
          optionalParams: optionalParams || []
        });
      }
    }
  };

  function awaitPostMessage() {
    var isReactNativePostMessageReady = !!window.originalPostMessage;
    var queue = [];
    var currentPostMessageFn = function store(message) {
      if (queue.length > 100) queue.shift();
      queue.push(message);
    };
    if (!isReactNativePostMessageReady) {
      var originalPostMessage = window.postMessage;
      Object.defineProperty(window, 'postMessage', {
        configurable: true,
        enumerable: true,
        get: function () {
          return currentPostMessageFn;
        },
        set: function (fn) {
          currentPostMessageFn = fn;
          isReactNativePostMessageReady = true;
          setTimeout(sendQueue, 0);
        }
      });
      window.postMessage.toString = function () {
        return String(originalPostMessage);
      };
    }

    function sendQueue() {
      while (queue.length > 0) window.postMessage(queue.shift());
    }
  }

  if (!!window.isPhone) {
    awaitPostMessage(); // Call this only once in your Web Code.
    // TODO: fix console issue
    window.console = DoreClient.console
  }
  if (typeof define === 'function' && define.amd) {
    define('DoreClient', [], function () {
      return DoreClient;
    });
  }
  var strundefined = typeof undefined;
  if (typeof noGlobal === strundefined) {
    window.DoreClient = DoreClient;
  }

  return DoreClient;
});


angular
  .module('starter.controllers', [])
  .controller('DashCtrl', function ($scope, $ionicPlatform) {
    $scope.version = null;
    $scope.text = 'Text Copy';
    $scope.copyText = '';
    DoreClient.addStateListener();
    $ionicPlatform.on('STATE', function (event) {
      $scope.state = event.detail.data;
      $scope.$apply();
    });

    $ionicPlatform.on('ANDROID_BACK', function (event) {
      DoreClient.showToast('ANDROID_BACK');
    });

    DoreClient.getAppVersion().then(function (data) {
      $scope.version = data.version;
      $scope.$apply();
    });
    $scope.showToast = function () {
      DoreClient.showToast('this is a toast');
    };
    $scope.showToastCenter = function () {
      DoreClient.showToast('this is a toast', 'long', 'center');
    };
    $scope.open = function () {
      DoreClient.open('https://www.phodal.com/');
    };
    $scope.copy = function () {
      DoreClient.copy($scope.text);
    };
    $scope.paste = function () {
      $ionicPlatform.on('PASTE', function (event) {
        $scope.copyText = event.detail.data;
        $scope.$apply();
      });
      DoreClient.paste();
    };

    $scope.getBadge = function () {
      DoreClient.getBadge().then(function (data) {
        $scope.badge = data.badge;
        $scope.$apply();
      })
    };
    $scope.setBadge = function () {
      DoreClient.setBadge(19);
    };
    $scope.clearBadge = function () {
      DoreClient.clearBadge();
    };
    $scope.showDatePicker = function () {
      var options = {
        date: '2017-10-22 12:12:12',
        maxDate: '2022-10-22 12:12:12'
      };
      DoreClient.showDatePicker(options).then(function (data) {
        $scope.date = data.date;
        $scope.$apply();
      });
    };

    $scope.getCurrentPosition = function () {
      DoreClient.getCurrentPosition().then(function (data) {
        $scope.location = data;
        $scope.$apply();
      });
    };
    $scope.watchPosition = function () {
      $ionicPlatform.on('WATCH_POSITION', function (event) {
        $scope.wPosition = event.detail.data;
        $scope.$apply();
      });
      DoreClient.watchPosition();
    };
    $scope.clearWatch = function () {
      DoreClient.clearWatch();
    };
    $scope.stopObserving = function () {
      DoreClient.stopObserving();
    };
    $scope.getOrientation = function () {
      DoreClient.getOrientation().then(function (data) {
        $scope.orientation = data;
        $scope.$apply();
      });
    };
    $scope.lockToLandscape = function () {
      DoreClient.lockToLandscape();
    };
    $scope.lockToPortrait = function () {
      DoreClient.lockToPortrait();
    };
    $scope.getConnectionInfo = function () {
      DoreClient.getConnectionInfo().then(function (data) {
        $scope.connectionInfo = data;
        $scope.$apply();
      });
    };
    $scope.addEventListener = function () {
      $ionicPlatform.on('CONNECTION_CHANGE', function (event) {
        $scope.wConnectionInfo = event.detail.data;
        $scope.$apply();
      });
      DoreClient.addNetInfoEventListener();
    };
    $scope.removeEventListener = function () {
      DoreClient.removeNetInfoEventListener();
    };
    $scope.hideStatusBar = function () {
      DoreClient.hideStatusBar();
    };
    $scope.showStatusBar = function () {
      DoreClient.showStatusBar();
    };
    $scope.vibrationVibrate = function () {
      DoreClient.vibrationVibrate([1000, 2000, 3000]);
    };
    $scope.vibrationCancel = function () {
      DoreClient.vibrationCancel();
    };
    $scope.getBrightness = function () {
      DoreClient.getBrightnessLevel().then(function (brightness) {
        $scope.brightness = brightness;
        $scope.$apply();
      })
    };
    $scope.setBrightness = function () {
      DoreClient.setBrightnessLevel(0.2);
    };
    $scope.checkPermissions = function () {
      DoreClient.checkPermissions('camera').then(function (response) {
        DoreClient.showToast(JSON.stringify(response));
      })
    };
    $scope.requestPermissions = function () {
      DoreClient.requestPermissions('camera').then(function (response) {
        DoreClient.showToast(JSON.stringify(response));
      })
    };
    $scope.checkMultiple = function () {
      DoreClient.checkMultiple(['camera', 'photo']).then(function (response) {
        DoreClient.showToast(JSON.stringify(response));
      })
    };
    $scope.captureScreen = function () {
      DoreClient.captureScreen().then(function (response) {
        DoreClient.showToast(JSON.stringify(response));
      })
    };
    $scope.console = console; //  can use inline console function after register
    $scope.outputSingleObject = function () {
      var someObject = {str: "Some text", id: 5};
      console.log(someObject);
    };
    $scope.outputMultipleObjects = function () {
      var car = "Dodge Charger";
      var someObject = {str: "Some text", id: 5};
      var optionalParams = [car, ". The object is:", someObject];
      console.info("My first car was a", optionalParams);
    };
    $scope.stringSubstitutions = function () {
      for (var i = 0; i < 5; i++) {
        var optionalParams = ["Bob", i + 1];
        console.log("Hello, %s. You've called me %d times.", optionalParams);
      }
    };
    $scope.stylingConsoleOutput = function () {
      var optionalParams = ["color: yellow; font-style: italic; background-color: blue;padding: 2px"];
      console.log("This is %cMy stylish message", optionalParams);
    };
    $scope.groupInTheConsole = function () {
      console.log("This is the outer level");
      console.group();
      console.log("Level 2");
      console.group();
      console.log("Level 3");
      console.warn("More of level 3");
      console.groupEnd();
      console.log("Back to level 2");
      console.groupEnd();
      console.debug("Back to the outer level");
    };
    $scope.timers = function () {
      console.time("answer time");
      alert("Click to continue");
      console.timeEnd("answer time");
    };
    $scope.stackTraces = function () {
      function foo() {
        function bar() {
          console.trace();
        }

        bar();
      }

      foo();
    };
    $scope.setAsyncStorage = function (key, data) {
      DoreClient.setAsyncStorage(key, data);
    };

    $scope.getAsyncStorage = function (key) {
      DoreClient.getAsyncStorage(key).then(function (response) {
        $scope.storage = response.data;
        $scope.$apply();
      })
    };
    $scope.checkURLScheme = function () {
      DoreClient.checkURLScheme('whatsapp').then(function (response) {
        $scope.isAppInstalled = response;
        $scope.$apply();
      })
    };
    $scope.addShakeListener = function () {
      $ionicPlatform.on('SHAKE', function (response) {
        $scope.shake = response.detail.data;
        $scope.$apply();
      });
      DoreClient.addShakeListener();
    };
    $scope.removeShakeListener = function () {
      DoreClient.removeShakeListener();
    };
    $scope.onFlashlight = function () {
      DoreClient.onFlashlight();
    };
    $scope.offFlashlight = function () {
      DoreClient.offFlashlight();
    };
    $scope.sendSMS = function () {
      DoreClient.sendSMS({
        body: 'Hello, world',
        recipients: ['10086']
      });
    };
    $scope.calendarAuthorizationStatus = function () {
      DoreClient.calendarAuthorizationStatus().then(function (response) {
        $scope.authStatus = response;
        $scope.$apply();
      });
    };
    $scope.requestCalendarAuthorize = function () {
      DoreClient.requestCalendarAuthorize();
    };
    $scope.saveCalendar = function () {
      DoreClient.saveCalendar("title",
        {
          location: 'location',
          notes: 'notes',
          startDate: new Date().toISOString(),
          endDate: new Date().toISOString()
        }).then(function (response) {
        console.log(response);
      })
    };
    $scope.findCalendars = function () {
      DoreClient.findCalendars().then(function (response) {
        console.log(response);
        $scope.calendars = response;
        $scope.$apply();
      })
    };
    $scope.fetchAllCalendar = function () {
      DoreClient.fetchAllCalendar('2017-12-26T19:26:00.000Z',
        '2018-01-1T19:26:00.000Z', ['1', '2']).then(function (response) {
        console.log(response);
        $scope.fetchCalendars = response;
        $scope.$apply();
      })
    };
    $scope.removeFirstCalendar = function () {
      DoreClient.fetchAllCalendar('2017-12-26T19:26:00.000Z',
        '2018-01-1T19:26:00.000Z', ['1', '2']).then(function (response) {
        console.log(response);
        if (response.length < 1) {
          return DoreClient.showToast("请先创建日历");
        }
        var lastCalendar = response[0];
        DoreClient.removeCalendar(lastCalendar.id)
      })
    };
    $scope.findEventById = function () {
      DoreClient.findEventById("297D3B27-4070-49A4-8BF9-1E7631727B4A").then(function (response) {
        console.log(response);
        $scope.savedCalendar = response;
        $scope.$apply();
      })
    };
    $scope._test_sqlite_functional = function () { // Test functional
      function operationDataBase(webview, db) {
        console.log('open then');
        db.executeSql('SELECT 1 FROM Version LIMIT 1')
          .then(function (result) {
            console.log('executeSql then');
            console.log(result);

            function queryEmployees(tx) {
              console.log("Executing employee query");
              tx.executeSql('SELECT a.name, b.name as deptName FROM Employees a, Departments b WHERE a.department = b.department_id').then(([tx, results]) => {

                var payload = {}
                payload.result = []
                payload.type = 'OPERATION_DATABASEE_DONE_QUERY_DB';
                var len = results.rows.length;
                for (let i = 0; i < len; i++) {
                  let row = results.rows.item(i);
                  payload.result.push(row)
                }

                webview.postMessage(JSON.stringify(payload));
              }).catch((error) => {
                console.log(error);
              });
            }

            db.transaction(queryEmployees).then(() => {
              console.log('query done.')
            });
          })
          .catch(function (err) {
            console.log('executeSql catch');
            console.log(err);
            db.transaction(function (tx) {
              tx.executeSql('DROP TABLE IF EXISTS Employees;');
              tx.executeSql('DROP TABLE IF EXISTS Offices;');
              tx.executeSql('DROP TABLE IF EXISTS Departments;');

              tx.executeSql('CREATE TABLE IF NOT EXISTS Version( '
                + 'version_id INTEGER PRIMARY KEY NOT NULL); ').catch((error) => {
                console.log(error);
              });

              tx.executeSql('CREATE TABLE IF NOT EXISTS Departments( '
                + 'department_id INTEGER PRIMARY KEY NOT NULL, '
                + 'name VARCHAR(30) ); ').catch((error) => {
                console.log(error)
              });

              tx.executeSql('CREATE TABLE IF NOT EXISTS Offices( '
                + 'office_id INTEGER PRIMARY KEY NOT NULL, '
                + 'name VARCHAR(20), '
                + 'longtitude FLOAT, '
                + 'latitude FLOAT ) ; ').catch((error) => {
                console.log(error)
              });

              tx.executeSql('CREATE TABLE IF NOT EXISTS Employees( '
                + 'employe_id INTEGER PRIMARY KEY NOT NULL, '
                + 'name VARCHAR(55), '
                + 'office INTEGER, '
                + 'department INTEGER, '
                + 'FOREIGN KEY ( office ) REFERENCES Offices ( office_id ) '
                + 'FOREIGN KEY ( department ) REFERENCES Departments ( department_id ));').catch((error) => {
                console.log(error)
              });

              tx.executeSql('INSERT INTO Departments (name) VALUES ("Client Services");');
              tx.executeSql('INSERT INTO Departments (name) VALUES ("Investor Services");');
              tx.executeSql('INSERT INTO Departments (name) VALUES ("Shipping");');
              tx.executeSql('INSERT INTO Departments (name) VALUES ("Direct Sales");');

              tx.executeSql('INSERT INTO Offices (name, longtitude, latitude) VALUES ("Denver", 59.8,  34.1);');
              tx.executeSql('INSERT INTO Offices (name, longtitude, latitude) VALUES ("Warsaw", 15.7, 54.1);');
              tx.executeSql('INSERT INTO Offices (name, longtitude, latitude) VALUES ("Berlin", 35.3, 12.1);');
              tx.executeSql('INSERT INTO Offices (name, longtitude, latitude) VALUES ("Paris", 10.7, 14.1);');

              tx.executeSql('INSERT INTO Employees (name, office, department) VALUES ("Sylvester Stallone", 2,  4);');
              tx.executeSql('INSERT INTO Employees (name, office, department) VALUES ("Elvis Presley", 2, 4);');
              tx.executeSql('INSERT INTO Employees (name, office, department) VALUES ("Leslie Nelson", 3,  4);');
              tx.executeSql('INSERT INTO Employees (name, office, department) VALUES ("Fidel Castro", 3, 3);');
              tx.executeSql('INSERT INTO Employees (name, office, department) VALUES ("Bill Clinton", 1, 3);');
              tx.executeSql('INSERT INTO Employees (name, office, department) VALUES ("Margaret Thatcher", 1, 3);');
              tx.executeSql('INSERT INTO Employees (name, office, department) VALUES ("Donald Trump", 1, 3);');
              tx.executeSql('INSERT INTO Employees (name, office, department) VALUES ("Dr DRE", 2, 2);');
              tx.executeSql('INSERT INTO Employees (name, office, department) VALUES ("Samantha Fox", 2, 1);');
              console.log("all executed SQL done");
              webview.postMessage(JSON.stringify({
                type: "OPERATION_DATABASEE_DONE_CREATE_TABLE_AND_INSERT_DATA",
                result: 'init table done.'
              }));
            })
          })
      }

      function errorCB(error) {
        console.log(error)
      }

      var SQLite = DoreClient.SQLite;
      SQLite.open(
        'test.db',
        '1.0',
        'SQLite test databases',
        200000,
        operationDataBase,
        errorCB)
        .then(function (payload) {
          console.log('open then');
          console.log(payload);
          alert(JSON.stringify(payload))
        })
        .catch(function (err) {
          console.log(err)
        })
    };
  })

  .controller('DeviceCtrl', function ($scope) {
    $scope.getAppVersion = function () {
      DoreClient.getAppVersion().then(function (data) {
        $scope.version = data.version;
        $scope.$apply();
      });
    };
    $scope.getUniqueID = function () {
      DoreClient.getUniqueID().then(function (data) {
        $scope.uniqueID = data.uniqueID;
        $scope.$apply();
      });
    };
    $scope.getBrand = function () {
      DoreClient.getBrand().then(function (data) {
        $scope.brand = data.brand;
        $scope.$apply();
      });
    };
    $scope.getModel = function () {
      DoreClient.getModel().then(function (data) {
        $scope.model = data.model;
        $scope.$apply();
      });
    };
    $scope.getSystemName = function () {
      DoreClient.getSystemName().then(function (data) {
        $scope.systemName = data.systemName;
        $scope.$apply();
      });
    };
    $scope.isEmulator = function () {
      DoreClient.isEmulator().then(function (data) {
        $scope.isEmulator = data;
        $scope.$apply();
      });
    };
    $scope.isTablet = function () {
      DoreClient.isTablet().then(function (data) {
        $scope.isTablet = data;
        $scope.$apply();
      });
    };
  });


/**
 * Loggly implementation for metalogger
 *
 * @type {Loggly|exports|*}
 */
var optional = require('optional')
  , loggly   = optional('loggly')
  , util     = require('util')
  , os       = require('os')
  , client   = {}
  , options  = {
    subdomain: 'test',
    auth:      null,
    json:      true
  };

var metalogger = require('../metalogger');

/**
 * Default log level if non provided
 * @type {string}
 * @private
 */
var _level      = 'debug';

var levels = {'debug':7, 'info':6, 'notice':5, 'warning':4, 'error':3, 'critical':2, 'alert':1, 'emergency':0};

exports = module.exports = function(level) {
  if (!loggly) {
    throw new Error('Installation required: in order to use loggly bridge, you need to: npm install loggly. Aborting.');
  }

  var logger = {};

  if ('NODE_LOGGER_LOGGLY_SUBDOMAIN' in process.env && process.env.NODE_LOGGER_LOGGLY_SUBDOMAIN) {
    options.subdomain = process.env.NODE_LOGGER_LOGGLY_SUBDOMAIN;
  }

  if ('NODE_LOGGER_LOGGLY_TOKEN' in process.env && process.env.NODE_LOGGER_LOGGLY_TOKEN) {
    options.token = process.env.NODE_LOGGER_LOGGLY_TOKEN;
  }

  if (level in levels) { _level = level; }

  client = loggly.createClient(options);

  logger.debug      = function() { delegate('debug', arguments); };
  logger.info       = function() { delegate('info', arguments); };
  logger.notice     = function() { delegate('notice', arguments); };
  logger.warning    = function() { delegate('warning', arguments); };
  logger.error      = function() { delegate('error', arguments); };
  logger.critical   = function() { delegate('critical', arguments); };
  logger.alert      = function() { delegate('alert', arguments); };
  logger.emergency  = function() { delegate('emergency', arguments); };

  return logger;
};

/**
 * Delegate function for each log level
 *
 * @param method
 * @param _args
 */
function delegate(method, _args) {

  var callPosition = metalogger.callposition()
    , file
    , line;

  if (!metalogger.shouldLog(method, _level)) {
    return;
  }

  var args = Array.prototype.slice.call(_args);

  var message = [];
  if(args.length === 1) {
    message.push(util.inspect(args[0], { showHidden: true, depth: null }));
  }

  if(args.length === 2) {
    message.push(args.shift());
    message.push(util.inspect(args[0], { showHidden: true, depth: null }));
  }

  if(args.length > 2) {
    message.push(args.shift());
    message.push(util.format.apply(null, args));
  }

  try{
    file = callPosition.split(':')[0].replace(' [','');
    line = callPosition.split(':')[1].replace('] ','');
  } catch(e) {
    // something went wrong with stack trace
  }

  var jsonFormat = {
    timestamp: (new Date()).toISOString(),
    hostname: os.hostname(),
    level: method,
    file: file,
    line: line,
    message: message.join(' ')
  };

  if(options.token) {
    client.log(jsonFormat, function(err){
      if(err) {
        console.error('error occured while logging to loggly', err);
      }
    });
  } else {
    console.error('Loggly auth token not provided');
  }
}


var util       = require('util')
  , os         = require('os')
  , client     = {}
  , optional   = require('optional')
  , S3Logger   = optional('s3-streamlogger')
  , metalogger = require('../metalogger');

var bucketDetails = {
  bucket: '',
  access_key_id: '',
  secret_access_key: '',
  server_side_encryption: false,
  name_format: '%Y-%m-%d-%H-%M-%S-%L-' + (process.env.NODE_ENV || 'development') + '-' + os.hostname() + '.log'
};

var _level = 'debug';

exports = module.exports = function (level) {
  if (!S3Logger) {
    throw new Error('Installation required: in order to use the s3/"sumologic" bridge, you need to: npm install s3-streamlogger. Aborting.');
  }

  var levels = {'debug':7, 'info':6, 'notice':5, 'warning':4, 'error':3, 'critical':2, 'alert':1, 'emergency':0};

  if ('NODE_LOGGER_S3_BUCKET' in process.env && process.env.NODE_LOGGER_S3_BUCKET) {
    bucketDetails.bucket = process.env.NODE_LOGGER_S3_BUCKET.replace(/\/$/, '');
  }

  if ('NODE_LOGGER_S3_KEY_ID' in process.env && process.env.NODE_LOGGER_S3_KEY_ID) {
    bucketDetails.access_key_id = process.env.NODE_LOGGER_S3_KEY_ID;
  }

  if ('NODE_LOGGER_S3_KEY_SECRET' in process.env && process.env.NODE_LOGGER_S3_KEY_SECRET) {
    bucketDetails.secret_access_key = process.env.NODE_LOGGER_S3_KEY_SECRET;
  }

  if ('NODE_LOGGER_S3_USE_SSE' in process.env && process.env.NODE_LOGGER_S3_USE_SSE) {
    bucketDetails.server_side_encryption = true;
  }

  if (level in levels) { _level = level; }

  if (bucketDetails.bucket && bucketDetails.access_key_id && bucketDetails.secret_access_key) {
    client = new S3Logger.S3StreamLogger(bucketDetails);
  } else {
    throw new Error('Configuration required: in order to use the s3/"sumologic" bridge, you need to: ' +
      'provide NODE_LOGGER_S3_BUCKET, NODE_LOGGER_S3_KEY_ID, and NODE_LOGGER_S3_KEY_SECRET in the process environment.');
  }

  var logger = {};

  logger.debug      = function() { delegate('debug', arguments); };
  logger.info       = function() { delegate('info', arguments); };
  logger.notice     = function() { delegate('notice', arguments); };
  logger.warning    = function() { delegate('warning', arguments); };
  logger.error      = function() { delegate('error', arguments); };
  logger.critical   = function() { delegate('critical', arguments); };
  logger.alert      = function() { delegate('alert', arguments); };
  logger.emergency  = function() { delegate('emergency', arguments); };

  return logger;
};

/**
 * Delegate function for each log level
 *
 * @param method
 * @param _args
 */
function delegate(method, _args) {

  var callPosition = metalogger.callposition()
    , file
    , line;

  if (!metalogger.shouldLog(method, _level)) {
    return;
  }

  var args = Array.prototype.slice.call(_args);

  var message = ''
    , inspect = null;

  message = util.inspect(args.shift(), { showHidden: true, depth: null }).replace(/\n/g, ' ');

  inspect = args[0];

  if (args.length > 2) {
    inspect = util.format.apply(null, args);
  }

  try{
    file = callPosition.split(':')[0].replace(' [','');
    line = callPosition.split(':')[1].replace('] ','');
  } catch(e) {
    // something went wrong with stack trace
  }

  var jsonFormat = {
    timestamp: (new Date()).toISOString(),
    hostname: os.hostname(),
    level: method,
    file: file,
    line: line,
    debug: inspect
  };

  if(bucketDetails.bucket.length !== 0 && bucketDetails.access_key_id.length !== 0 && bucketDetails.secret_access_key.length !== 0) {
    client.write(util.format("%s %s [host=%s] [message=%s] [message.stream] %s\n", jsonFormat.timestamp, method.toUpperCase(), jsonFormat.hostname, message, JSON.stringify(jsonFormat)));
  } else {
    console.error('Configuration: Sumologic is not configured correctly, please provide `NODE_LOGGER_S3_BUCKET`, `NODE_LOGGER_S3_KEY_ID`, and `NODE_LOGGER_S3_KEY_SECRET` environment variables.');
  }
}


var callsite = require('callsite')
  , path     = require('path');

// defaults
var _level          = 'info'
  , _loggermodule   = 'util'
  , _granularlevels = true
  , _showlines      = true;

exports = module.exports = function(loggermodule, level, loggerInstance) {

  if ('NODE_LOGGER_GRANULARLEVELS' in process.env && process.env.NODE_LOGGER_GRANULARLEVELS === 0) {
    _granularlevels = false;
  }

  if ('NODE_LOGGER_SHOWLINES' in process.env && process.env.NODE_LOGGER_SHOWLINES === 0) {
    _showlines = false;
  }

  if ('NODE_LOGGER_PLUGIN' in process.env && process.env.NODE_LOGGER_PLUGIN) {
    _loggermodule = process.env.NODE_LOGGER_PLUGIN;
  }
  if (loggermodule) { _loggermodule = loggermodule; }

  if ('NODE_LOGGER_LEVEL' in process.env && process.env.NODE_LOGGER_LEVEL) {
    _level = process.env.NODE_LOGGER_LEVEL;
  }
  if (level) { _level = level; }

  _level = _level.toLowerCase(); // crush case before any comparisons

  checkLevelAndPluginValidity(_level, _loggermodule);

  var concreteLogger;
  if (typeof loggerInstance === 'undefined') {
    concreteLogger = require('./bridges/' + _loggermodule)(_level);
  } else {
    concreteLogger = require('./bridges/' + _loggermodule)(_level, loggerInstance);
  }
  concreteLogger.loggers            = availableLoggers;
  concreteLogger.levels             = logLevels;
  concreteLogger.thresholdLevel     = _level;

  return concreteLogger;
};

exports.logLevels               = logLevels;
exports.shouldLog               = shouldLog;
exports.logLevelAllowed         = logLevelAllowed;
exports.logLevelAllowedGranular = logLevelAllowedGranular;
exports.callposition            = callposition;

function checkLevelAndPluginValidity(level, logger) {

  var loggers = availableLoggers();
  var foundLogger = false;
  loggers.forEach(function checkLogger(currLogger) {
    if (logger in currLogger) {
      foundLogger = true;
    }
  });

  var availableLevels = logLevels();
  var foundLevel = false;
  availableLevels.forEach(function checkLevel(currLevel) {
    if (level === currLevel) {
      foundLevel = true;
    }
  });

  if (!foundLevel)  { throw new Error("Level " + level + " is not allowed. Cannot initialize metalogger."); }
  if (!foundLogger) { throw new Error("Logger " + logger + " is not implemented. Cannot initialize metalogger."); }

}

function availableLoggers() {
  return [ { "log"       : "Tiny logger with streaming reader (visionmedia)" }
    , { "winston"   : "Winston transports" }
    , { "npmlog"    : "Logger for npm (isaacs)" }
    , { "util"      : "Native, Node.js' util module-based logging" }
    , { "ain2"      : "Syslog logging for node.js. Continuation of ain" }
    , { "sumologic" : "Sumologic logging for node.js, with storage in s3" }
    , { "loggly"    : "Agent-supported logging to Loggly" }
  ]
  ;
}

var logLevelsObj = {
  "emergency" : 0
  , "alert"     : 1
  , "critical"  : 2
  , "error"     : 3
  , "warning"   : 4
  , "notice"    : 5
  , "info"      : 6
  , "debug"     : 7
};

/**
* Syslog levels:
*
* 0 EMERGENCY system is unusable
* 1 ALERT action must be taken immediately
* 2 CRITICAL the system is in critical condition
* 3 ERROR error condition
* 4 WARNING warning condition
* 5 NOTICE a normal but significant condition
* 6 INFO a purely informational message
* 7 DEBUG messages to debug an application
*/
function logLevels() {
  return Object.keys(logLevelsObj);
}

/**
 * Should I log?
 *
 * Use this instead of older pattern of granular check then global check.
 */
function shouldLog(testlevel, thresholdLevel) {
  var allowed = logLevelAllowedGranular(testlevel);
  if (allowed) {
    return true;
  }

  return logLevelAllowed(testlevel, thresholdLevel);
}

function logLevelAllowed(testlevel, thresholdLevel) {
  return logLevelsObj[testlevel] <= logLevelsObj[thresholdLevel];
}

/**
* As opposed to logLevelAllowed() this one doesn't take testlevel, since that
* needs to be figured out in-function.
*
* We take care not to return an opinion if we don't know.
*/
function logLevelAllowedGranular(testlevel) {
  if(!_granularlevels) {
    return;
  }
  var pos = callpositionObj();
  if (pos) {
    var key = 'NODE_LOGGER_LEVEL_' + pos.filename.replace(/[\.\/]/gi, '_');

    if (key in process.env && process.env[key]) {
      var thresholdLevel = process.env[key].toLowerCase();
      return logLevelsObj[testlevel] <= logLevelsObj[thresholdLevel];
    }
  }

}

function callposition() {
  if (!_showlines) { return ''; }

  var position = callpositionObj();
  var callpos = ' [' + position.filename + ':' + position.lineno + '] ';

  return callpos;
}

function callpositionObj() {
  var stack = callsite();
  var call      = stack[4];
  var filename  = call.getFileName() || "";
  var lineno    = call.getLineNumber() || "";
  filename      = filename.replace(process.env.PWD, '');
  filename      = (filename[0] === path.sep) ? filename.substr(1) : filename;

  return {filename: filename, lineno: lineno};
}


/**
 * angular-bootstrap-switch
 * @version v0.5.2 - 2017-04-19
 * @author Francesco Pontillo (francescopontillo@gmail.com)
 * @link https://github.com/frapontillo/angular-bootstrap-switch
 * @license Apache License 2.0(http://www.apache.org/licenses/LICENSE-2.0.html)
**/

(function() {
'use strict';

// Source: common/module.js
angular.module('frapontillo.bootstrap-switch', []);

// Source: dist/.temp/directives/bsSwitch.js
angular.module('frapontillo.bootstrap-switch')
  .directive('bsSwitch', ["$parse", "$timeout", function ($parse, $timeout) {
    return {
      restrict: 'A',
      require: 'ngModel',
      link: function link(scope, element, attrs, controller) {
        var isInit = false;

        /**
         * Return the true value for this specific checkbox.
         * @returns {Object} representing the true view value; if undefined, returns true.
         */
        var getTrueValue = function() {
          if (attrs.type === 'radio') {
            return attrs.value || $parse(attrs.ngValue)(scope) || true;
          }
          var trueValue = ($parse(attrs.ngTrueValue)(scope));
          if (angular.isUndefined(trueValue)) {
            trueValue = true;
          }
          return trueValue;
        };

        /**
         * Get a boolean value from a boolean-like string, evaluating it on the current scope.
         * @param value The input object
         * @returns {boolean} A boolean value
         */
        var getBooleanFromString = function(value) {
          return scope.$eval(value) === true;
        };

        /**
         * Get a boolean value from a boolean-like string, defaulting to true if undefined.
         * @param value The input object
         * @returns {boolean} A boolean value
         */
        var getBooleanFromStringDefTrue = function(value) {
          return (value === true || value === 'true' || !value);
        };

        /**
         * Returns the value if it is truthy, or undefined.
         *
         * @param value The value to check.
         * @returns the original value if it is truthy, {@link undefined} otherwise.
         */
        var getValueOrUndefined = function (value) {
          return (value ? value : undefined);
        };

        /**
         * Returns a function that executes the provided expression
         *
         * @param value The string expression
         * @return a function that evaluates the expression
         */
        var getExprFromString = function (value) {
          if (angular.isUndefined(value)) {
            return angular.noop;
          }
          return function () {
            scope.$evalAsync(value);
          };
        };

        /**
         * Get the value of the angular-bound attribute, given its name.
         * The returned value may or may not equal the attribute value, as it may be transformed by a function.
         *
         * @param attrName  The angular-bound attribute name to get the value for
         * @returns {*}     The attribute value
         */
        var getSwitchAttrValue = function(attrName) {
          var map = {
            'switchRadioOff': getBooleanFromStringDefTrue,
            'switchActive': function(value) {
              return !getBooleanFromStringDefTrue(value);
            },
            'switchAnimate': getBooleanFromStringDefTrue,
            'switchLabel': function(value) {
              return value ? value : ' ';
            },
            'switchIcon': function(value) {
              if (value) {
                return '';
              }
            },
            'switchWrapper': function(value) {
              return value || 'wrapper';
            },
            'switchInverse': getBooleanFromString,
            'switchReadonly': getBooleanFromString,
            'switchChange': getExprFromString
          };
          var transFn = map[attrName] || getValueOrUndefined;
          return transFn(attrs[attrName]);
        };

        /**
         * Set a bootstrapSwitch parameter according to the angular-bound attribute.
         * The parameter will be changed only if the switch has already been initialized
         * (to avoid creating it before the model is ready).
         *
         * @param element   The switch to apply the parameter modification to
         * @param attr      The name of the switch parameter
         * @param modelAttr The name of the angular-bound parameter
         */
        var setSwitchParamMaybe = function(element, attr, modelAttr) {
          if (!isInit) {
            return;
          }
          var newValue = getSwitchAttrValue(modelAttr);
          element.bootstrapSwitch(attr, newValue);
        };

        var setActive = function() {
          setSwitchParamMaybe(element, 'disabled', 'switchActive');
        };

        /**
         * If the directive has not been initialized yet, do so.
         */
        var initMaybe = function() {
          // if it's the first initialization
          if (!isInit) {
            var viewValue = (controller.$modelValue === getTrueValue());
            isInit = !isInit;
            // Bootstrap the switch plugin
            element.bootstrapSwitch({
              radioAllOff: getSwitchAttrValue('switchRadioOff'),
              disabled: getSwitchAttrValue('switchActive'),
              state: viewValue,
              onText: getSwitchAttrValue('switchOnText'),
              offText: getSwitchAttrValue('switchOffText'),
              onColor: getSwitchAttrValue('switchOnColor'),
              offColor: getSwitchAttrValue('switchOffColor'),
              animate: getSwitchAttrValue('switchAnimate'),
              size: getSwitchAttrValue('switchSize'),
              labelText: attrs.switchLabel ? getSwitchAttrValue('switchLabel') : getSwitchAttrValue('switchIcon'),
              wrapperClass: getSwitchAttrValue('switchWrapper'),
              handleWidth: getSwitchAttrValue('switchHandleWidth'),
              labelWidth: getSwitchAttrValue('switchLabelWidth'),
              inverse: getSwitchAttrValue('switchInverse'),
              readonly: getSwitchAttrValue('switchReadonly')
            });
            if (attrs.type === 'radio') {
              controller.$setViewValue(controller.$modelValue);
            } else {
              controller.$setViewValue(viewValue);
            }
          }
        };

        var switchChange = getSwitchAttrValue('switchChange');

        /**
         * Listen to model changes.
         */
        var listenToModel = function () {

          attrs.$observe('switchActive', function (newValue) {

            var active = getBooleanFromStringDefTrue(newValue);
            // if we are disabling the switch, delay the deactivation so that the toggle can be switched
            if (!active) {
              $timeout(setActive);
            } else {
              // if we are enabling the switch, set active right away
              setActive();
            }
          });

          // When the model changes
          controller.$render = function () {
            initMaybe();
            var newValue = controller.$modelValue;
            if (newValue !== undefined && newValue !== null) {
              element.bootstrapSwitch('state', newValue === getTrueValue(), true);
            } else {
              element.bootstrapSwitch('indeterminate', true, true);
              controller.$setViewValue(undefined);
            }
            switchChange();
          };

          // angular attribute to switch property bindings
          var bindings = {
            'switchRadioOff': 'radioAllOff',
            'switchOnText': 'onText',
            'switchOffText': 'offText',
            'switchOnColor': 'onColor',
            'switchOffColor': 'offColor',
            'switchAnimate': 'animate',
            'switchSize': 'size',
            'switchLabel': 'labelText',
            'switchIcon': 'labelText',
            'switchWrapper': 'wrapperClass',
            'switchHandleWidth': 'handleWidth',
            'switchLabelWidth': 'labelWidth',
            'switchInverse': 'inverse',
            'switchReadonly': 'readonly'
          };

          var observeProp = function(prop, bindings) {
            return function() {
              attrs.$observe(prop, function () {
                setSwitchParamMaybe(element, bindings[prop], prop);
              });
            };
          };

          // for every angular-bound attribute, observe it and trigger the appropriate switch function
          for (var prop in bindings) {
            attrs.$observe(prop, observeProp(prop, bindings));
          }
        };

        /**
         * Listen to view changes.
         */
        var listenToView = function () {

          if (attrs.type === 'radio') {
            // when the switch is clicked
            element.on('change.bootstrapSwitch', function (e) {
              // discard not real change events
              if ((controller.$modelValue === controller.$viewValue) && (e.target.checked !== $(e.target).bootstrapSwitch('state'))) {
                // $setViewValue --> $viewValue --> $parsers --> $modelValue
                // if the switch is indeed selected
                if (e.target.checked) {
                  // set its value into the view
                  controller.$setViewValue(getTrueValue());
                } else if (getTrueValue() === controller.$viewValue) {
                  // otherwise if it's been deselected, delete the view value
                  controller.$setViewValue(undefined);
                }
                switchChange();
              }
            });
          } else {
            // When the checkbox switch is clicked, set its value into the ngModel
            element.on('switchChange.bootstrapSwitch', function (e) {
              // $setViewValue --> $viewValue --> $parsers --> $modelValue
              controller.$setViewValue(e.target.checked);
              switchChange();
            });
          }
        };

        // Listen and respond to view changes
        listenToView();

        // Listen and respond to model changes
        listenToModel();

        // On destroy, collect ya garbage
        scope.$on('$destroy', function () {
          element.bootstrapSwitch('destroy');
        });
      }
    };
  }])
  .directive('bsSwitch', function () {
    return {
      restrict: 'E',
      require: 'ngModel',
      template: '<input bs-switch>',
      replace: true
    };
  });

// Source: bsSwitch.suffix
})();


/*jslint */
/*global module: true, exports: true, define: false */

/**
 * jaaulde-cookies.js
 *
 * Copyright (c) 2005-2015, Jim Auldridge MIT License
 *
 */

/**
 *
 * @param {object} scope - a reference to the call scope
 * @param {undefined} undef - an undefined variable for comparison checks
 * @returns {void}
 */
(function (scope, undef) {
    'use strict';

    /**
     * IIFE for injecting cookies module into whichever require/scope is in use
     *
     * @param {string} name - the name of module being created
     * @param {function} definition - function which produces and returns the created module
     * @returns {void}
     */
    (function (name, definition) {
        if (typeof module === 'object' && module !== null && module.exports) {
            module.exports = exports = definition();
        } else if (typeof define === 'function' && define.amd) {
            define(definition);
        } else {
            scope[name] = definition();
        }
    }('cookies', function () {
            /* localize natives */
        var document = scope.document,
            /* opts and support */
            default_options = {
                expires: null,
                path: '/',
                domain: null,
                secure: false
            },
            /**
             *
             * @access private
             * @param {string} msg
             * @returns {void}
             */
            warn = function (msg) {
                if (typeof scope.console === 'object' && scope.console !== null && typeof scope.console.warn === 'function') {
                    warn = function (msg) {
                        scope.console.warn(msg);
                    };
                    warn(msg);
                }
            },
            /**
             *
             * @param {object} o
             * @returns {object}
             */
            resolveOptions = function (o) {
                var r,
                    e;

                if (typeof o !== 'object' || o === null) {
                    r = default_options;
                } else {
                    r = {
                        expires: default_options.expires,
                        path: default_options.path,
                        domain: default_options.domain,
                        secure: default_options.secure
                    };

                    /*
                     * I've been very finicky about the name and format of the expiration option over time,
                     * so I'm accounting for older styles to maintain backwards compatibility. Preferably it
                     * will be called "expires" and will be an instance of Date
                     */
                    if (typeof o.expires === 'object' && o.expires instanceof Date) {
                        r.expires = o.expires;
                    } else if (typeof o.expires_at === 'object' && o.expires_at instanceof Date) {
                        r.expires = o.expires_at;
                        warn('Cookie option "expires_at" has been deprecated. Rename to "expires". Support for "expires_at" will be removed in a version to come.');
                    } else if (typeof o.expiresAt === 'object' && o.expiresAt instanceof Date) {
                        r.expires = o.expiresAt;
                        warn('Cookie option "expiresAt" has been deprecated. Rename to "expires". Support for "expiresAt" will be removed in a version to come.');
                    } else if (typeof o.hoursToLive === 'number' && o.hoursToLive !== 0) {
                        e = new Date();
                        e.setTime(e.getTime() + (o.hoursToLive * 60 * 60 * 1000));
                        r.expires = e;
                        warn('Cookie option "hoursToLive" has been deprecated. Rename to "expires" and prodvide a Date instance (see documentation). Support for "hoursToLive" will be removed in a version to come.');
                    }

                    if (typeof o.path === 'string' && o.path !== '') {
                        r.path = o.path;
                    }

                    if (typeof o.domain === 'string' && o.domain !== '') {
                        r.domain = o.domain;
                    }

                    if (o.secure === true) {
                        r.secure = o.secure;
                    }
                }

                return r;
            },
            /**
             *
             * @access private
             * @param {object} o
             * @returns {string}
             */
            cookieOptions = function (o) {
                o = resolveOptions(o);

                return ([
                    (typeof o.expires === 'object' && o.expires instanceof Date ? '; expires=' + o.expires.toGMTString() : ''),
                    ('; path=' + o.path),
                    (typeof o.domain === 'string' ? '; domain=' + o.domain : ''),
                    (o.secure === true ? '; secure' : '')
                ].join(''));
            },
            /**
             *
             * @access private
             * @param {string} s
             * @returns {string}
             */
            trim = (function () {
                var trim_def;

                /* Some logic for `trim` and `isNaN` borrowed from http://jquery.com/ */
                if (String.prototype.trim) {
                    trim_def = function (s) {
                        return String.prototype.trim.call(s);
                    };
                } else {
                    trim_def = (function () {
                        var l,
                            r;

                        l = /^\s+/;
                        r = /\s+$/;

                        return function (s) {
                            return s.replace(l, '').replace(r, '');
                        };
                    }());
                }

                return trim_def;
            }()),
            /**
             *
             * @access private
             * @param {mixed} v
             * @returns {boolean}
             */
            isNaN = (function () {
                var p = /\d/,
                    native_isNaN = scope.isNaN;

                return function (v) {
                    return (v === null || !p.test(v) || native_isNaN(v));
                };
            }()),
            /**
             *
             * @access private
             * @returns {object}
             */
            parseCookies = (function () {
                var parseJSON,
                    p;

                if (JSON && typeof JSON.parse === 'function') {
                    parseJSON = function (s) {
                        var r = null;

                        if (typeof s === 'string' && s !== '') {
                            s = trim(s);

                            if (s !== '') {
                                try {
                                    r = JSON.parse(s);
                                } catch (e1) {
                                    r = null;
                                }
                            }
                        }

                        return r;
                    };
                } else {
                    parseJSON = function () {
                        return null;
                    };
                }

                p = new RegExp('^(?:\\{.*\\}|\\[.*\\])$');

                return function () {
                    var c = {},
                        s1 = document.cookie.split(';'),
                        q = s1.length,
                        i,
                        s2,
                        n,
                        v,
                        vv;

                    for (i = 0; i < q; i += 1) {
                        s2 = s1[i].split('=');

                        n = trim(s2.shift());
                        if (s2.length >= 1) {
                            v = s2.join('=');
                        } else {
                            v = '';
                        }

                        try {
                            vv = decodeURIComponent(v);
                        } catch (e2) {
                            vv = v;
                        }

                        /* Logic borrowed from http://jquery.com/ dataAttr method */
                        try {
                            vv = (vv === 'true')
                                ? true : (vv === 'false')
                                    ? false : !isNaN(vv)
                                        ? parseFloat(vv) : p.test(vv)
                                            ? parseJSON(vv) : vv;
                        } catch (ignore) {}

                        c[n] = vv;
                    }

                    return c;
                };
            }());

        return {
            /**
             * get - get one, several, or all cookies
             *
             * @access public
             * @static
             * @param {mixed} n {string} name of single cookie
             *                  {array} list of multiple cookie names
             *                  {void} if you want all cookies
             * @return {mixed} type/value of cookie as set
             *                 {null} if only one cookie is requested and is not found
             *                 {object} hash of multiple or all cookies (if multiple or all requested)
             */
            get: function (n) {
                var r,
                    i,
                    c = parseCookies();

                if (typeof n === 'string') {
                    r = (c[n] !== undef) ? c[n] : null;
                } else if (typeof n === 'object' && n !== null) {
                    r = {};

                    for (i in n) {
                        if (Object.prototype.hasOwnProperty.call(n, i)) {
                            if (c[n[i]] !== undef) {
                                r[n[i]] = c[n[i]];
                            } else {
                                r[n[i]] = null;
                            }
                        }
                    }
                } else {
                    r = c;
                }

                return r;
            },
            /**
             * filter - get array of cookies whose names match the provided RegExp
             *
             * @access public
             * @static
             * @param {RegExp} p The regular expression to match against cookie names
             * @return {object} hash of cookies whose names match the RegExp
             */
            filter: function (p) {
                var n,
                    r = {},
                    c = parseCookies();

                if (typeof p === 'string') {
                    p = new RegExp(p);
                }

                for (n in c) {
                    if (Object.prototype.hasOwnProperty.call(c, n) && n.match(p)) {
                        r[n] = c[n];
                    }
                }

                return r;
            },
            /**
             * set - set or delete a cookie with desired options
             *
             * @access public
             * @static
             * @param {string} n name of cookie to set
             * @param {mixed} v Any JS value. If not a string, will be JSON encoded (http://code.google.com/p/cookies/wiki/JSON)
             *                  {null} to delete
             * @param {object} o optional list of cookie options to specify
             * @return {void}
             */
            set: function (n, v, o) {
                if (typeof o !== 'object' || o === null) {
                    o = {};
                }

                if (v === undef || v === null) {
                    v = '';
                    o.expires = new Date();
                    o.expires.setFullYear(1978);
                } else {
                    /* Logic borrowed from http://jquery.com/ dataAttr method and reversed */
                    v = (v === true)
                        ? 'true' : (v === false)
                            ? 'false' : !isNaN(v)
                                ? String(v) : v;

                    if (typeof v !== 'string') {
                        if (typeof JSON === 'object' && JSON !== null && typeof JSON.stringify === 'function') {
                            v = JSON.stringify(v);
                        } else {
                            throw new Error('cookies.set() could not be serialize the value');
                        }
                    }
                }

                document.cookie = n + '=' + encodeURIComponent(v) + cookieOptions(o);
            },
            /**
             * del - delete a cookie (domain and path options must match those with which the cookie was set; this is really an alias for set() with parameters simplified for this use)
             *
             * @access public
             * @static
             * @param {mixed} n {string} name of cookie to delete
             *                  {boolean} true to delete all
             * @param {object} o optional list of cookie options to specify (path, domain)
             * @return {void}
             */
            del: function (n, o) {
                var d = {},
                    i;

                if (typeof o !== 'object' || o === null) {
                    o = {};
                }

                if (typeof n === 'boolean' && n === true) {
                    d = this.get();
                } else if (typeof n === 'string') {
                    d[n] = true;
                }

                for (i in d) {
                    if (Object.prototype.hasOwnProperty.call(d, i) && typeof i === 'string' && i !== '') {
                        this.set(i, null, o);
                    }
                }
            },
            /**
             * test - test whether the browser is accepting cookies
             *
             * @access public
             * @static
             * @return {boolean}
             */
            test: function () {
                var r = false,
                    n = 'test_cookies_jaaulde_js',
                    v = 'data';

                this.set(n, v);

                if (this.get(n) === v) {
                    this.del(n);
                    r = true;
                }

                return r;
            },
            /**
             * setOptions - set default options for calls to cookie methods
             *
             * @access public
             * @static
             * @param {object} o list of cookie options to specify
             * @return {void}
             */
            setOptions: function (o) {
                if (typeof o !== 'object') {
                    o = null;
                }

                default_options = resolveOptions(o);
            }
        };
    }));
}(this));


/*
 * grunt-contrib-qunit
 * https://gruntjs.com/
 *
 * Copyright (c) 2016 "Cowboy" Ben Alman, contributors
 * Licensed under the MIT license.
 */

'use strict';

// Nodejs libs.
var fs = require('fs');
var path = require('path');
var url = require('url');
var EventEmitter = require('eventemitter2');
// NPM libs.
var pEachSeries = require('p-each-series');
var puppeteer = require('puppeteer');

var Promise = global.Promise;

// Shared functions

// Allow an error message to retain its color when split across multiple lines.
function formatMessage (str) {
  return String(str).split('\n')
    .map(function(s) {
      return s.magenta;
    })
    .join('\n');
}

function createStatus () {
  return {
    passed: 0,
    failed: 0,
    skipped: 0,
    todo: 0,
    runtime: 0,
    assertions: {
      passed: 0,
      failed: 0
    }
  };
}

function mergeStatus(statusA, statusB) {
  statusA.passed += statusB.passed;
  statusA.failed += statusB.failed;
  statusA.skipped += statusB.skipped;
  statusA.todo += statusB.todo;
  statusA.runtime += statusB.runtime;
  statusA.assertions.passed += statusB.assertions.passed;
  statusA.assertions.failed += statusB.assertions.failed;
}

function generateMessage(status) {
  var totalTests = status.passed + status.failed + status.skipped + status.todo;
  var totalAssertions = status.assertions.passed + status.assertions.failed;

  return [
    totalTests,
    ' tests completed with ',
    status.failed,
    ' failed, ' +
    status.skipped,
    ' skipped, and ',
    status.todo,
    ' todo. \n' +
    totalAssertions,
    ' assertions (in ',
    status.runtime,
    'ms), passed: ' +
    status.assertions.passed,
    ', failed: ',
    status.assertions.failed
  ].join('');
}

// Copied from QUnit source code
function generateHash (module) {
  var hex;
  var i = 0;
  var hash = 0;
  var str = module + '\x1C' + undefined;
  var len = str.length;

  for (; i < len; i++) {
    hash = ((hash << 5) - hash) + str.charCodeAt(i);
    hash |= 0;
  }

  // Convert the possibly negative integer hash code into an 8 character
  // hex string, which isn't strictly necessary but increases user understanding
  // that the id is a SHA-like hash
  hex = (0x100000000 + hash).toString(16);
  if (hex.length < 8) {
    hex = '0000000' + hex;
  }

  return hex.slice(-8);
}

function getPath(url) {
  if (url.substr( 0, 7 ) === 'http://' || url.substr( 0, 8 ) === 'https://') {
    return url;
  }

  return 'file://' + path.resolve(process.cwd(), url);
}

module.exports = function(grunt) {

  var eventBus = new EventEmitter({wildcard: true, maxListeners: 0});

  // Keep track of the last-started module and test. Additionally, keep track
  // of status for individual test files and the entire test suite.
  var options;
  var currentModule;
  var currentTest;
  var currentStatus;
  var status;
  var browser;
  var page;

  // Keep track of the last-started test(s).
  var unfinished = {};

  // Get an asset file, local to the root of the project.
  var asset = path.join.bind(null, __dirname, '..');

  // If options.force then log an error, otherwise exit with a warning
  function warnUnlessForced (message) {
    if (options && options.force) {
      grunt.log.error(message);
    } else {
      grunt.warn(message);
    }
  }

  // Keep track of failed assertions for pretty-printing.
  var failedAssertions = [];
  function logFailedAssertions () {
    var assertion;

    if (options && options.summaryOnly) {
      return;
    }

    // Print each assertion error.
    while (assertion = failedAssertions.shift()) {
      grunt.verbose.or.error(assertion.testName);
      grunt.log.error('Message: ' + formatMessage(assertion.message));
      if (assertion.actual !== assertion.expected) {
        grunt.log.error('Actual: ' + formatMessage(assertion.actual));
        grunt.log.error('Expected: ' + formatMessage(assertion.expected));
      }
      if (assertion.source) {
        grunt.log.error(assertion.source.replace(/ {4}(at)/g, '  $1'));
      }
      grunt.log.writeln();
    }
  }

  // QUnit hooks.
  eventBus.on('qunit.begin', function() {
    currentStatus = createStatus();
  });

  eventBus.on('qunit.moduleStart', function(name) {
    unfinished[name] = true;
    currentModule = name;
  });

  eventBus.on('qunit.moduleDone', function(name) {
    delete unfinished[name];
  });

  eventBus.on('qunit.log', function(result, actual, expected, message, source, todo) {
    if (!result && !todo) {
      failedAssertions.push({
        actual: actual,
        expected: expected,
        message: message,
        source: source,
        testName: currentTest
      });
    }
  });

  eventBus.on('qunit.testStart', function(name) {
    currentTest = (currentModule ? currentModule + ' - ' : '') + name;
    grunt.verbose.write(currentTest + '...');
  });

  eventBus.on('qunit.testDone', function(name, failed, passed, total, runtime, skipped, todo) {
    var testPassed = failed > 0 ? todo : !todo;

    if (skipped) {
      currentStatus.skipped++;
    } else if (!testPassed) {
      currentStatus.failed++;
    } else if (todo) {
      currentStatus.todo++;
    } else {
      currentStatus.passed++;
    }

    // Log errors if necessary, otherwise success.
    if (testPassed) {
      grunt.verbose.ok().or.write('.');
      // list assertions or message about todo failure
    } else if (grunt.option('verbose')) {
      grunt.log.error();

      if (todo) {
        grunt.log.error('Expected at least one failing assertion in todo test:' + name);
      } else {
        logFailedAssertions();
      }
    } else {
      grunt.log.write('F'.red);
    }
  });

  eventBus.on('qunit.done', function(failed, passed, total, runtime) {
    currentStatus.runtime += runtime;
    currentStatus.assertions.passed += passed;
    currentStatus.assertions.failed += failed;

    // Print assertion errors here, if verbose mode is disabled.
    if (!grunt.option('verbose')) {
      if (currentStatus.failed > 0) {
        grunt.log.writeln();
        logFailedAssertions();
      } else {
        grunt.log.ok();
      }
    }

    mergeStatus(status, currentStatus);
  });

  // Re-broadcast qunit events on grunt.event.
  eventBus.on('qunit.*', function() {
    var args = [this.event].concat(grunt.util.toArray(arguments));
    grunt.event.emit.apply(grunt.event, args);
  });

  // Built-in error handlers.
  eventBus.on('fail.load', function(url) {
    grunt.verbose.write('...');
    grunt.event.emit('qunit.fail.load', url);
    grunt.log.error('Chrome unable to load \'' + url + '\' URI.');

    status.failed += 1;
  });

  eventBus.on('fail.timeout', function() {
    grunt.log.writeln();
    grunt.event.emit('qunit.fail.timeout');
    grunt.log.error('Chrome timed out, possibly due to:\n' +
        '- QUnit is not loaded correctly.\n- A missing QUnit start() call.\n' +
        '- Or, a misconfiguration of this task.');

    status.failed += 1;
  });

  eventBus.on('error.onError', function (msg, stackTrace) {
    grunt.event.emit('qunit.error.onError', msg, stackTrace);
  });

  grunt.registerMultiTask('qunit', 'Run QUnit unit tests in a headless Chrome instance.', function() {
    // Merge task-specific and/or target-specific options with these defaults.
    options = this.options({
      // Default Chrome timeout.
      timeout: 5000,
      // QUnit-Chrome bridge file to be injected.
      inject: asset('chrome/bridge.js'),
      // Explicit non-file URLs to test.
      urls: [],
      force: false,
      // Connect Chrome console output to Grunt output
      console: true,
      // Do not use an HTTP base by default
      httpBase: false,
      summaryOnly: false
    });
    var puppeteerLaunchOptions = Object.assign({headless: true}, options.puppeteer);

    // This task is asynchronous.
    var done = this.async();
    var urls;

    // Read the content of the specified bridge files
    var bridgeFiles = Array.isArray(options.inject) ? options.inject : [options.inject];
    var bridgContents = [];

    for (var i = 0; i < bridgeFiles.length; i++) {
      try {
        bridgContents.push(fs.readFileSync(bridgeFiles[i], 'utf8'));
      } catch (err) {
        grunt.fail.fatal('Could not load the specified Chrome/QUnit bridge file: ' + bridgeFiles[i]);
      }
    }

    if (options.httpBase) {
      // If URLs are explicitly referenced, use them still
      urls = options.urls;
      // Then create URLs for the src files
      this.filesSrc.forEach(function(testFile) {
        urls.push(options.httpBase + '/' + testFile);
      });
    } else {
      // Combine any specified URLs with src files.
      urls = options.urls.concat(this.filesSrc);
    }

    // The final tasks to run before terminating the task
    function finishTask(success) {
      // Close the puppeteer browser
      if (browser) {
        browser.close();
      }
      // Finish the task
      done(success);
    }

    function appendToUrls (queryParam, value) {
      // Append the query param to all urls
      urls = urls.map(function(testUrl) {
        var parsed = url.parse(testUrl, true);
        parsed.query[queryParam] = value;
        delete parsed.search;
        return url.format(parsed);
      });
    }

    if (options.noGlobals) {
      // Append a noglobal query string param to all urls
      appendToUrls('noglobals', 'true');
    }

    if (grunt.option('modules')) {
      var modules = grunt.option('modules').split(',');
      var hashes = modules.map(function(module) {
        return generateHash(module.trim());
      });
      // Append moduleId to all urls
      appendToUrls('moduleId', hashes);
    }

    if (grunt.option('seed')) {
      // Append seed to all urls
      appendToUrls('seed', grunt.option('seed'));
    }

    // Reset status.
    status = createStatus();

    // Instantiate headless browser
    puppeteer.launch(puppeteerLaunchOptions)
      .then(function(b) {
        browser = b;
        return b.newPage();
      })
      .then(function(p) {
        page = p;
        // emit events published in bridge.js.
        // This function exposure survives url navigations.
        return page.exposeFunction('__grunt_contrib_qunit__', function() {
          eventBus.emit.apply(eventBus, [].slice.call(arguments));
        });
      })
      .then(function() {
        // Pass through the console logs if instructed
        if (options.console) {
          page.on('console', function() {
            var args = [].slice.apply(arguments);
            var colors = {
              'error': 'red',
              'warning': 'yellow'
            };
            for (var i = 0; i < args.length; ++i) {
              var txt = args[i].text();
              var color = colors[args[i].type()];
              grunt.log.writeln(color ? txt[color] : txt);
            }
          });
        }

        // Surface uncaught exceptions
        page.on('pageerror', function() {
          var args = [].slice.apply(arguments);
          for (var i = 0; i < args.length; i++) {
            eventBus.emit('error.onError', args[i]);
          }
        });

        // Whenever a page is loaded with a new document, before scripts execute, inject the bridge file.
        // Tell the client that when DOMContentLoaded fires, it needs to tell this
        // script to inject the bridge. This should ensure that the bridge gets
        // injected before any other DOMContentLoaded or window.load event handler.
        page.evaluateOnNewDocument('if (window.QUnit) {\n' + bridgContents.join(";") + '\n} else {\n' + 'document.addEventListener("DOMContentLoaded", function() {\n' + bridgContents.join(";") + '\n});\n}\n');

        return pEachSeries(urls, function(url) {
          // Reset current module.
          currentModule = null;
          grunt.event.emit('qunit.spawn', url);
          grunt.verbose.subhead('Testing ' + url + ' ').or.write('Testing ' + url + ' ');

          return Promise.all([
            // Setup listeners for qunit.done / fail events
            new Promise(function(resolve, reject) {
              eventBus.once('qunit.done', function() { resolve(); });
              eventBus.once('fail.*', function() { reject(url); });
            }),

            // Navigate to the url to be tested
            page.goto(getPath(url), { timeout: options.timeout })
          ]);
        });
      })
      .then(function() {
        // All tests have been run.
        var message = generateMessage(status);
        var success;

        // Log results.
        if (status.failed > 0) {
          warnUnlessForced(message);
        } else {
          grunt.verbose.writeln();
          grunt.log.ok(message);
        }

        if (options && options.force) {
          success = true;
        } else {
          success = status.failed === 0;
        }
        // All done!
        finishTask(success);
      })
      .catch(function(err) {
        // If anything goes wrong, terminate the grunt task
        grunt.log.error("There was an error with headless chrome");
        grunt.fail.fatal(err);
        finishTask(false);
      });
  });

};


(function () {

  'use strict';

  /* istanbul ignore next */
  var angular = window.angular ? window.angular : 'undefined' !== typeof require ? require('angular') : undefined;

  function AngularChartState(AngularChartWatcher) {
    var service = {
      disableSelectionListener: false,
      synchronizeZoom: synchronizeZoom,
      applyZoom: applyZoom,
      synchronizeSelection: synchronizeSelection,
      applySelection: applySelection
    };

    return service;

    ////////////

    /**
     * Apply earlier zoom
     */
    function applyZoom(options, chart) {
      if (angular.isObject(options.state) && angular.isObject(options.state) && angular.isArray(options.state.range)) {
        chart.zoom(options.state.range);
      } else {
        chart.unzoom();
      }
    }

    /**
     * Create nested options objects.
     */
    function createZoomRangePath(options) {
      if (!angular.isObject(options.state)) {
        options.state = {};
      }
      if (!angular.isObject(options.state.range)) {
        options.state.range = [];
      }
    }

    /**
     * Setup zoom event listeners which update the state
     */
    function synchronizeZoom(options, configuration, watcher) {
      if (angular.isObject(options.chart) && angular.isObject(options.chart.zoom) && options.chart.zoom.enabled === true) {

        // setup onzoomend listener
        configuration.zoom.onzoomend = function (domain) {

          // update state
          AngularChartWatcher.updateState(watcher, function () {
            createZoomRangePath(options);
            options.state.range = domain;
          });

          // call user defined callback
          if (angular.isFunction(options.chart.zoom.onzoomend)) {
            AngularChartWatcher.applyFunction(watcher, function () {
              options.chart.zoom.onzoomend(domain);
            });
          }
        };
      }

      if (angular.isObject(options.chart) && angular.isObject(options.chart.subchart) && options.chart.subchart.show === true) {
        // setup onbrush listener
        configuration.subchart.onbrush = function (domain) {

          // update state
          AngularChartWatcher.updateState(watcher, function () {
            createZoomRangePath(options);
            options.state.range = domain;
          });

          // call user defined callback
          if (angular.isFunction(options.chart.subchart.onbrush)) {
            AngularChartWatcher.applyFunction(watcher, function () {
              options.chart.subchart.onbrush(domain);
            });
          }
        };
      }
    }

    /**
     * Add passed selection to the chart.
     */
    function addSelections(chart, selections) {
      service.disableSelectionListener = true;
      selections.forEach(function (selection) {
        chart.select([selection.id], [selection.index]);
      });
      service.disableSelectionListener = false;
    }

    /**
     * Remove passed selections from the chart.
     */
    //function removeSelections(chart, selections) {
    //  disableSelectionListener = true;
    //  selections.forEach(function (selection) {
    //    chart.unselect([selection.id], [selection.index]);
    //  });
    //  disableSelectionListener = false;
    //}

    /**
     * Remove all selections present in the chart.
     */
    function removeAllSelections(chart) {
      service.disableSelectionListener = true;
      chart.unselect();
      service.disableSelectionListener = false;
    }

    /**
     * Apply earlier selections.
     */
    function applySelection(options, chart) {
      if (angular.isObject(options.state) && angular.isArray(options.state.selected)) {
        // TODO: get new selections
        // TODO: get removed selections
        // var chartSelections = chart.selected();
        //    // addedSelections
        //    var addedSelections = newSelections.filter(function (elm) {
        //      var isNew = true;
        //      oldSelections.forEach(function (old) {
        //        if (old.id === elm.id && old.index === elm.index) {
        //          isNew = false;
        //          return isNew;
        //        }
        //      });
        //      return isNew;
        //    });
        //
        //    // removedSelections
        //    var removedSelections = oldSelections.filter(function (elm) {
        //      var isOld = true;
        //      newSelections.forEach(function (old) {
        //        if (old.id === elm.id && old.index === elm.index) {
        //          isOld = false;
        //          return isOld;
        //        }
        //      });
        //      return isOld;
        //    });

        // alternative: deselect all and select again
        //removeAllSelections(chart);
        addSelections(chart, options.state.selected);

      } else {
        removeAllSelections(chart);
      }
    }

    /**
     * Create nested options object.
     */
    function createSelectionsPath(options) {
      if (!angular.isObject(options.state)) {
        options.state = {};
      }
      if (!angular.isArray(options.state.selected)) {
        options.state.selected = [];
      }
    }

    /**
     * Listen to chart events to save selections into to state object.
     */
    function synchronizeSelection(options, configuration, watcher) {
      if (angular.isObject(options.chart) && angular.isObject(options.chart.data) && angular.isObject(options.chart.data.selection) && options.chart.data.selection.enabled === true) {

        // add onselected listener
        configuration.data.onselected = function (data, element) {

          // check if listener is disabled currently
          if (service.disableSelectionListener) {
            return;
          }

          // update state
          AngularChartWatcher.updateState(watcher, function () {
            createSelectionsPath(options);
            options.state.selected.push(data);
          });

          // call user defined callback
          if (angular.isFunction(options.chart.data.onselected)) {
            AngularChartWatcher.applyFunction(watcher, function () {
              options.chart.data.onselected(data, element);
            });
          }

        };

        // add onunselection listener
        configuration.data.onunselected = function (data, element) {

          // check if listener is disabled currently
          if (service.disableSelectionListener) {
            return;
          }

          // update state
          AngularChartWatcher.updateState(watcher, function () {
            createSelectionsPath(options);
            options.state.selected = options.state.selected.filter(function (selected) {
              return selected.id !== data.id || selected.index !== data.index;
            });
          });

          // call user defined callback
          if (angular.isFunction(options.chart.data.onunselected)) {
            AngularChartWatcher.applyFunction(watcher, function () {
              options.chart.data.onunselected(data, element);
            });
          }

        };

      }
    }

  }

  angular
    .module('angularChart')
    .service('AngularChartState', AngularChartState);

})();