(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.main = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
'use strict'


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


module.exports = {
  template: '#admin-view-template',
  props: ['config'],


  data: function () {
    return {
      configRestored: false,
      confirmReset: false,
      configReset: false,
      firmwareUpgrading: false,
      hostnameSet: false,
      usernameSet: false,
      passwordSet: false,
      latest: '',
      hostname: '',
      username: '',
      current: '',
      password: '',
      password2: ''
    }
  },


  events: {
    connected: function () {
      if (this.firmwareUpgrading) location.reload(true);
    }
  },


  ready: function () {
    api.get('hostname').done(function (hostname) {
      this.hostname = hostname;
    }.bind(this));
    api.get('remote/username').done(function (username) {
      this.username = username;
    }.bind(this));
  },


  methods: {
    set_hostname: function () {
      api.put('hostname', {hostname: this.hostname}).done(function () {
        this.hostnameSet = true;
      }.bind(this)).fail(function (error) {
        alert('Set hostname failed: ' + JSON.stringify(error));
      })
    },


    set_username: function () {
      api.put('remote/username', {username: this.username}).done(function () {
        this.usernameSet = true;
      }.bind(this)).fail(function (error) {
        alert('Set username failed: ' + JSON.stringify(error));
      })
    },


    set_password: function () {
      if (this.password != this.password2) {
        alert('Passwords to not match');
        return;
      }

      if (this.password.length < 6) {
        alert('Password too short');
        return;
      }

      api.put('remote/password', {
        current: this.current,
        password: this.password
      }).done(function () {
        this.passwordSet = true;
      }.bind(this)).fail(function (error) {
        alert('Set password failed: ' + JSON.stringify(error));
      })
    },


    backup: function () {
      document.getElementById('download-target').src = '/api/config/download';
    },


    restore: function (e) {
      var files = e.target.files || e.dataTransfer.files;
      if (!files.length) return;

      var fr = new FileReader();
      fr.onload = function (e) {
        var config;

        try {
          config = JSON.parse(e.target.result);
        } catch (e) {
          alert("Invalid config file");
          return;
        }

        api.put('config/save', config).done(function (data) {
          this.$dispatch('update');
          this.configRestored = true;

        }.bind(this)).fail(function (error) {
          alert('Restore failed: ' + error);
        })
      }.bind(this);

      fr.readAsText(files[0]);
    },


    reset: function () {
      this.confirmReset = false;
      api.put('config/reset').done(function () {
        this.$dispatch('update');
        this.configReset = true;

      }.bind(this)).fail(function (error) {
        alert('Reset failed: ' + error);
      });
    },


    check: function () {
      $.ajax({
        type: 'GET',
        url: 'https://buildbotics.com/bbctrl/latest.txt',
        cache: false

      }).done(function (data) {
        this.latest = data;

      }.bind(this)).fail(function (error) {
        alert('Failed to get latest version information');
      });
    },


    upgrade: function () {
      this.firmwareUpgrading = true;
      api.put('upgrade');
    }
  }
}

},{"./api":2}],2:[function(require,module,exports){
'use strict'


function api_cb(method, url, data, config) {
  config = $.extend({
    type: method,
    url: '/api/' + url,
    dataType: 'json',
    cache: false
  }, config);

  if (typeof data == 'object') {
    config.data = JSON.stringify(data);
    config.contentType = 'application/json; charset=utf-8';
  }

  var d = $.Deferred();

  $.ajax(config).success(function (data, status, xhr) {
    d.resolve(data, status, xhr);

  }).error(function (xhr, status, error) {
    var text = xhr.responseText;
    try {text = $.parseJSON(xhr.responseText)} catch(e) {}
    d.reject(text, xhr, status, error);
    console.debug('API Error: ' + url + ': ' + xhr.responseText);
  })

  return d.promise();
}


module.exports = {
  get: function (url, config) {
    return api_cb('GET', url, undefined, config);
  },


  put: function(url, data, config) {
    return api_cb('PUT', url, data, config);
  },


  post: function(url, data, config) {
    return api_cb('POST', url, data, config);
  },


  upload: function(url, data, config) {
    config = $.extend({
      processData: false,
      contentType: false,
      cache: false,
      data: data
    }, config);

    return api_cb('PUT', url, undefined, config);
  },


  'delete': function (url, config) {
    return api_cb('DELETE', url, undefined, config);
  }
}

},{}],3:[function(require,module,exports){
'use strict'

var api = require('./api');
var Sock = require('./sock');


module.exports = new Vue({
  el: 'body',


  data: function () {
    return {
      status: 'connecting',
      currentView: 'loading',
      index: -1,
      modified: false,
      template: {motors: {}, axes: {}},
      config: {motors: [{}]},
      state: {}
    }
  },


  components: {
    'estop': {template: '#estop-template'},
    'loading-view': {template: '<h1>Loading...</h1>'},
    'control-view': require('./control-view'),
    'motor-view': require('./motor-view'),
    'tool-view': require('./tool-view'),
    'io-view': require('./io-view'),
    'gcode-view': require('./gcode-view'),
    'admin-view': require('./admin-view')
  },


  events: {
    'config-changed': function () {this.modified = true;},


    send: function (msg) {
      if (this.status == 'connected') {
        console.debug('>', msg);
        this.sock.send(msg)
      }
    },


    connected: function () {this.update()},
    update: function () {this.update()}
  },


  ready: function () {
    $(window).on('hashchange', this.parse_hash);
    this.connect();
  },


  methods: {
    estop: function () {
      if (this.state.x == 'ESTOPPED') api.put('clear');
      else api.put('estop');
    },


    update: function () {
      $.ajax({type: 'GET', url: '/config-template.json', cache: false})
        .success(function (data, status, xhr) {
          this.template = data;

          api.get('config/load').done(function (data) {
            this.config = data;
            this.parse_hash();
          }.bind(this))
        }.bind(this))
    },


    connect: function () {
      this.sock = new Sock('//' + window.location.host + '/sockjs');

      this.sock.onmessage = function (e) {
        var msg = e.data;

        if (typeof msg == 'object') {
          for (var key in msg)
            Vue.set(this.state, key, msg[key]);

          if ('msg' in msg) this.$broadcast('message', msg);
        }
      }.bind(this);

      this.sock.onopen = function (e) {
        this.status = 'connected';
        this.$emit(this.status);
        this.$broadcast(this.status);
      }.bind(this);

      this.sock.onclose = function (e) {
        this.status = 'disconnected';
        this.$broadcast(this.status);
      }.bind(this);

      console.debug('Hello');
    },


    parse_hash: function () {
      var hash = location.hash.substr(1);

      if (!hash.trim().length) {
        location.hash = 'control';
        return;
      }

      var parts = hash.split(':');

      if (parts.length == 2) this.index = parts[1];

      this.currentView = parts[0];
    },


    save: function () {
      api.put('config/save', this.config).done(function (data) {
        this.modified = false;
      }.bind(this)).fail(function (error) {
        alert('Save failed: ' + error);
      });
    }
  }
})

},{"./admin-view":1,"./api":2,"./control-view":5,"./gcode-view":6,"./io-view":8,"./motor-view":11,"./sock":12,"./tool-view":14}],4:[function(require,module,exports){
'use strict'


module.exports = {
  template: '#axis-control-template',
  props: ['axes', 'colors', 'enabled'],


  methods: {
    jog: function (axis, move) {
      this.$dispatch('jog', this.axes[axis], move)
    }
  }
}

},{}],5:[function(require,module,exports){
'use strict'

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


function _is_array(x) {
  return Object.prototype.toString.call(x) === '[object Array]';
}


function _msg_equal(a, b) {
  return a.level == b.level && a.location == b.location && a.code == b.code &&
    a.msg == b.msg;
}


function escapeHTML(s) {
  var entityMap = {'&': '&amp;', '<': '&lt;', '>': '&gt;'};
  return String(s).replace(/[&<>]/g, function (s) {return entityMap[s];});
}


module.exports = {
  template: '#control-view-template',
  props: ['config', 'template', 'state'],


  data: function () {
    return {
      mdi: '',
      file: '',
      last_file: '',
      files: [],
      axes: 'xyzabc',
      gcode: [],
      history: [],
      console: [],
      speed_override: 1,
      feed_override: 1,
      manual_home: {x: false, y: false, z: false, a: false, b: false, c: false},
      position_msg:
      {x: false, y: false, z: false, a: false, b: false, c: false},
      axis_position: 0,
      video_url: '',
      deleteGCode: false
    }
  },


  components: {
    'axis-control': require('./axis-control')
  },


  watch: {
    'state.ln': function () {this.update_gcode_line();}
  },


  events: {
    // TODO These should all be implemented via the API
    jog: function (axis, move) {this.send('g91 g0' + axis + move)},


    message: function (msg) {
      if (this.console.length &&
          _msg_equal(msg, this.console[this.console.length - 1]))
        this.console[this.console.length - 1].repeat++;

      else {
        msg.repeat = 1;
        this.console.push(msg);
      }
    }
  },


  ready: function () {
    this.update();
  },


  methods: {
    get_state: function () {return this.state.x || ''},


    get_reason: function () {
      if (this.state.x == 'ESTOPPED') return this.state.er;
      if (this.state.x == 'HOLDING') return this.state.pr;
      return '';
    },


    highlight_reason: function () {return this.get_reason() != ''},
    send: function (msg) {this.$dispatch('send', msg)},


    get_axis_motor_id: function (axis) {
      var axis = axis.toLowerCase();

      for (var i = 0; i < this.config.motors.length; i++) {
        var motor = this.config.motors[i];
        if (motor.axis.toLowerCase() == axis) return i;
      }

      return -1;
    },


    get_axis_motor: function (axis) {
      var motor = this.get_axis_motor_id(axis);
      if (motor != -1) return this.config.motors[motor];
    },


    get_axis_motor_param: function (axis, name) {
      var motor = this.get_axis_motor(axis);
      if (typeof motor == 'undefined') return;
      if (typeof motor[name] != 'undefined') return motor[name];

      for (var section in this.template['motors']) {
        var sec = this.template['motors'][section];
        if (typeof sec[name] != 'undefined') return sec[name]['default'];
      }
    },


    enabled: function (axis) {
      var pm = this.get_axis_motor_param(axis, 'power-mode')
      return typeof pm != 'undefined' && pm != 'disabled';
    },


    is_homed: function (axis) {
      var motor = this.get_axis_motor_id(axis);
      return motor != -1 && this.state[motor + 'homed'];
    },


    update_gcode_line: function () {
      if (typeof this.last_line != 'undefined') {
        $('#gcode-line-' + this.last_line).removeClass('highlight');
        this.last_line = undefined;
      }

      if (0 <= this.state.ln) {
        var line = this.state.ln - 1;
        var e = $('#gcode-line-' + line);
        if (e.length)
          e.addClass('highlight')[0]
          .scrollIntoView({behavior: 'smooth'});

        this.last_line = line;
      }
    },


    update: function () {
      // Update file list
      api.get('file')
        .done(function (files) {
          var index = files.indexOf(this.file);
          if (index == -1 && files.length) this.file = files[0];

          this.files = files;

          this.load()
        }.bind(this))
    },


    submit_mdi: function () {
      this.send(this.mdi);
      if (!this.history.length || this.history[0] != this.mdi)
        this.history.unshift(this.mdi);
      this.mdi = '';
    },


    load_history: function (index) {this.mdi = this.history[index];},


    open: function (e) {
      $('.gcode-file-input').click();
    },


    upload: function (e) {
      var files = e.target.files || e.dataTransfer.files;
      if (!files.length) return;

      var file = files[0];
      var fd = new FormData();

      fd.append('gcode', file);

      api.upload('file', fd)
        .done(function () {
          this.file = file.name;
          if (file.name == this.last_file) this.last_file = '';
          this.update();
        }.bind(this));
    },


    load: function () {
      var file = this.file;

      if (!file || this.files.indexOf(file) == -1) {
        this.file = '';
        this.gcode = [];
        return;
      }

      if (file == this.last_file) return;

      api.get('file/' + file)
        .done(function (data) {
          this.gcode = data.trimRight().split(/\r?\n/);
          this.last_file = file;

          Vue.nextTick(this.update_gcode_line);
        }.bind(this));
    },


    deleteCurrent: function () {
      if (this.file) api.delete('file/' + this.file).done(this.update);
      this.deleteGCode = false;
    },


    deleteAll: function () {
      api.delete('file').done(this.update);
      this.deleteGCode = false;
    },


    home: function (axis) {
      if (typeof axis == 'undefined') api.put('home');

      else {
        if (this.get_axis_motor_param(axis, 'homing-mode') != 'manual')
          api.put('home/' + axis);
        else this.manual_home[axis] = true;
      }
    },


    set_home: function (axis, position) {
      this.manual_home[axis] = false;
      api.put('home/' + axis + '/set', {position: parseFloat(position)});
    },


    show_set_position: function (axis) {
      this.axis_position = 0;
      this.position_msg[axis] = true;
    },


    get_offset: function (axis) {return this.state['offset_' + axis] || 0},


    set_position: function (axis, position) {
      this.position_msg[axis] = false;
      api.put('position/' + axis, {'position': parseFloat(position)});
    },


    zero_axis: function (axis) {this.set_position(axis, 0)},


    start_pause: function () {
      if (this.state.x == 'RUNNING') this.pause();

      else if (this.state.x == 'STOPPING' || this.state.x == 'HOLDING')
        this.unpause();

      else this.start();
    },


    start: function () {api.put('start/' + this.file).done(this.update)},
    pause: function () {api.put('pause')},
    unpause: function () {api.put('unpause')},
    optional_pause: function () {api.put('pause/optional')},
    stop: function () {api.put('stop')},
    step: function () {api.put('step/' + this.file).done(this.update)},


    override_feed: function () {api.put('override/feed/' + this.feed_override)},


    override_speed: function () {
      api.put('override/speed/' + this.speed_override)
    },


    current: function (axis, value) {
      var x = value / 32.0;
      if (this.state[axis + 'pl'] == x) return;

      var data = {};
      data[axis + 'pl'] = x;
      this.send(JSON.stringify(data));
    },


    clear_console: function () {this.console = [];},


    load_video: function () {
      this.video_url = '//' + document.location.hostname + ':8000/stream/0?=' +
        Math.random();
    }
  }
}

},{"./api":2,"./axis-control":4}],6:[function(require,module,exports){
'use strict'


module.exports = {
  template: '#gcode-view-template',
  props: ['config', 'template'],


  data: function () {
    return {
      gcode: {}
    }
  },


  events: {
    'input-changed': function() {
      this.$dispatch('config-changed');
      return false;
    }
  },


  ready: function () {
    this.update();
  },


  methods: {
    update: function () {
      Vue.nextTick(function () {
        if (this.config.hasOwnProperty('gcode'))
          this.gcode = this.config.gcode;

        var template = this.template.gcode;
        for (var key in template)
          if (!this.gcode.hasOwnProperty(key))
            this.$set('gcode["' + key + '"]', template[key].default);
      }.bind(this));
    }
  }
}

},{}],7:[function(require,module,exports){
'use strict'


module.exports = {
  template: '#indicators-template',
  props: ['state'],


  methods: {
    is_motor_enabled: function (motor) {
      return typeof this.state[motor + 'pm'] != 'undefined' &&
        this.state[motor + 'pm'];
    },


    get_min_pin: function (motor) {
      switch (motor) {
      case 0: return 3;
      case 1: return 5;
      case 2: return 9;
      case 3: return 11;
      }
    },


    get_max_pin: function (motor) {
      switch (motor) {
      case 0: return 4;
      case 1: return 8;
      case 2: return 10;
      case 3: return 12;
      }
    },


    get_class: function (code) {
      if (typeof this.state[code] != 'undefined') {
        var state = this.state[code];

        if (state == 0 || state === false) return 'logic-lo fa-circle';
        if (state == 1 || state === true) return 'logic-hi fa-circle';
        if (state == 2) return 'logic-tri fa-circle-o';
      }

      return 'fa-exclamation-triangle warn';
    }
  }
}

},{}],8:[function(require,module,exports){
'use strict'


module.exports = {
  template: '#io-view-template',
  props: ['config', 'template'],


  data: function () {
    return {
      switches: {},
      outputs: {}
    }
  },


  events: {
    'input-changed': function() {
      this.$dispatch('config-changed');
      return false;
    }
  },


  ready: function () {this.update()},


  methods: {
    update: function () {
      Vue.nextTick(function () {
        // Switches
        if (this.config.hasOwnProperty('switches'))
          this.switches = this.config.switches;
        else this.switches = {};

        var template = this.template.switches;
        for (var key in template)
          if (!this.switches.hasOwnProperty(key))
            this.$set('switches["' + key + '"]', template[key].default);

        // Outputs
        if (this.config.hasOwnProperty('outputs'))
          this.outputs = this.config.outputs;
        else this.outputs = {};

        var template = this.template.outputs;
        for (var key in template)
          if (!this.outputs.hasOwnProperty(key))
            this.$set('outputs["' + key + '"]', template[key].default);
      }.bind(this));
    }
  }
}

},{}],9:[function(require,module,exports){
$(function() {
  // Vue debugging
  Vue.config.debug = true;
  Vue.util.warn = function (msg) {console.debug('[Vue warn]: ' + msg)}

  // Register global components
  Vue.component('templated-input', require('./templated-input'));
  Vue.component('message', require('./message'));
  Vue.component('indicators', require('./indicators'));

  Vue.filter('percent', function (value, precision) {
    if (typeof precision == 'undefined') precision = 2;
    return (value * 100.0).toFixed(precision) + '%';
  });

  Vue.filter('fixed', function (value, precision) {
    if (typeof value == 'undefined') return '';
    return parseFloat(value).toFixed(precision)
  });

  Vue.filter('upper', function (value) {
    if (typeof value == 'undefined') return '';
    return value.toUpperCase()
  });

  // Vue app
  require('./app');
});

},{"./app":3,"./indicators":7,"./message":10,"./templated-input":13}],10:[function(require,module,exports){
'use strict'


module.exports = {
  template: '#message-template',

  props: {
    show: {
      type: Boolean,
      required: true,
      twoWay: true
    }
  }
}

},{}],11:[function(require,module,exports){
'use strict'


module.exports = {
  template: '#motor-view-template',
  props: ['index', 'config', 'template'],


  data: function () {
    return {
      active: false,
      motor: {}
    }
  },


  watch: {
    index: function() {this.update();}
  },


  events: {
    'input-changed': function() {
      this.slave_update();
      this.$dispatch('config-changed');
      return false;
    }
  },


  attached: function () {this.active = true; this.update()},
  detached: function () {this.active = false},


  methods: {
    slave_update: function () {
      var slave = false;
      for (var i = 0; i < this.index; i++)
        if (this.motor['axis'] == this.config.motors[i]['axis'])
          slave = true;

      var el = $(this.$el);

      if (slave) {
        el.find('.axis .units').text('(slave motor)');
        el.find('.limits, .homing, .motion').find('input, select')
          .attr('disabled', 1);
        el.find('.power-mode select').attr('disabled', 1);
        el.find('.motion .reverse input').removeAttr('disabled');

      } else {
        el.find('.axis .units').text('');
        el.find('input,select').removeAttr('disabled');
      }
    },


    update: function () {
      if (!this.active) return;

      Vue.nextTick(function () {
        if (this.config.hasOwnProperty('motors'))
          this.motor = this.config.motors[this.index];
        else this.motor = {};

        var template = this.template.motors;
        for (var category in template)
          for (var key in template[category])
            if (!this.motor.hasOwnProperty(key))
              this.$set('motor["' + key + '"]',
                        template[category][key].default);

        this.slave_update();
      }.bind(this));
    }
  }
}

},{}],12:[function(require,module,exports){
'use strict'


var Sock = function (url, retry, timeout) {
  if (!(this instanceof Sock)) return new Sock(url, retry);

  if (typeof retry == 'undefined') retry = 2000;
  if (typeof timeout == 'undefined') timeout = 8000;

  this.url = url;
  this.retry = retry;
  this.timeout = timeout;

  this.connect();
}


Sock.prototype.onmessage = function () {}
Sock.prototype.onopen = function () {}
Sock.prototype.onclose = function () {}


Sock.prototype.connect = function () {
  console.debug('connecting to', this.url);
  this.close();

  this._sock = new SockJS(this.url);

  this._sock.onmessage = function (e) {
    console.debug('msg:', e.data);
    this._set_timeout();
    this.onmessage(e);
  }.bind(this);


  this._sock.onopen = function () {
    console.debug('connected');
    this._set_timeout();
    this.onopen();
  }.bind(this);


  this._sock.onclose = function () {
    console.debug('disconnected');
    this._cancel_timeout();

    this.onclose();
    if (typeof this._sock != 'undefined')
      setTimeout(this.connect.bind(this), this.retry);
  }.bind(this);
}


Sock.prototype._set_timeout = function () {
  this._cancel_timeout();
  this._timeout = setTimeout(this._timedout.bind(this), this.timeout);
}


Sock.prototype._timedout = function () {
  console.debug('connection timedout');
  this._timeout = undefined;
  this._sock.close();
}


Sock.prototype._cancel_timeout = function () {
  clearTimeout(this._timeout);
  this._timeout = undefined;
}


Sock.prototype.close = function () {
  if (typeof this._sock != 'undefined') {
    var sock = this._sock;
    this._sock = undefined;
    sock.close();
  }
}


Sock.prototype.send = function (msg) {
  this._sock.send(msg);
}


module.exports = Sock

},{}],13:[function(require,module,exports){
'use strict'


module.exports = {
  replace: true,
  template: '#templated-input-template',
  props: ['name', 'model', 'template'],


  methods: {
    change: function () {
      this.$dispatch('input-changed');
    }
  }
}

},{}],14:[function(require,module,exports){
'use strict'


module.exports = {
  template: '#tool-view-template',
  props: ['config', 'template'],


  data: function () {
    return {
      tool: {}
    }
  },


  events: {
    'input-changed': function() {
      this.$dispatch('config-changed');
      return false;
    }
  },


  ready: function () {
    this.update();
  },


  methods: {
    update: function () {
      Vue.nextTick(function () {
        if (this.config.hasOwnProperty('tool'))
          this.tool = this.config.tool;

        var template = this.template.tool;
        for (var key in template)
          if (!this.tool.hasOwnProperty(key))
            this.$set('tool["' + key + '"]', template[key].default);
      }.bind(this));
    }
  }
}

},{}]},{},[9])(9)
});