function startApp()
{
  Vue.component('check', {
    template: '<div class="check" :class="{ checked: value, disabled: disabled }" @keydown="handleKeyDown" @click="handleClick" tabindex="0"><div class="control"><div class="inner"></div></div><div class="label">{{ title }}</div></div>',
    props: {
      title: String,
      value: {
        type: Boolean,
        default: false
      },
      disabled: {
        type: Boolean,
        default: false
      }
    },

    methods: {
      handleClick: function()
      {
        if (this.disabled) return;
        this.value = !this.value;
        this.$emit('input', this.value);
      },

      handleKeyDown: function(event)
      {
        if (event.keyCode == 32)
        {
          this.handleClick();
          event.preventDefault();
        }
      }
    }
  });


  Vue.component('radio', {
    template: '<div class="radio" :class="{ checked: value == id, disabled: disabled }" @keydown="handleKeyDown" @click="handleClick" tabindex="0"><div class="control"><div class="inner"></div></div><div class="label">{{ title }}</div></div>',
    props: {
      title: String,
      value: null,
      id: null,
      disabled: {
        type: Boolean,
        default: false
      }
    },

    methods: {
      handleClick: function()
      {
        if (this.disabled) return;
        this.value = this.id;
        this.$emit('input', this.value);
      },

      handleKeyDown: function(event)
      {
        if (event.keyCode == 32)
        {
          this.handleClick();
          event.preventDefault();
        }
      }
    }
  });


  Vue.component('range', {
    template: '<div>' +
                '<div class="start">' +
                  '<span class="value">{{ value.start }}</span>' +
                  '<div class="slidercontainer">' +
                    '<input type="range" min="0" max="4094" class="slider" v-model.number="value.start">' +
                  '</div>' +
                '</div>' +

                '<div class="end">' +
                  '<span class="value">{{ value.end }}</span>' +
                  '<div class="slidercontainer">' +
                    '<input type="range" min="1" max="4095" class="slider" v-model.number="value.end">' +
                  '</div>' +
                '</div>' +
              '</div>',
    props: ['value'],

    mounted: function()
    {
      this.oldValue = { start: this.value.start, end: this.value.end };
    },

    watch: {
      value: {
        handler: function(newValue)
        {
          if (newValue.start != this.oldValue.start)
          {
            if (newValue.start > newValue.end)
            {
              newValue.end = newValue.start + 1;
              this.$emit('input', newValue);
            }
          }
          else if (newValue.end != this.oldValue.end)
          {
            if (newValue.end < newValue.start)
            {
              newValue.start = newValue.end - 1;
              this.$emit('input', newValue);
            }
          }

          this.oldValue.start = newValue.start;
          this.oldValue.end = newValue.end;
        },
        deep: true
      }
    }
  });

  var i18n = new VueI18n({
    locale: navigator.language,
    fallbackLocale: 'en',
    messages: messages
  });

  var app = new Vue({
    el: '#app',

    i18n: i18n,

    data: {
      notification: null,

      loading: true,
      saving: false,
      savingSteps: false,
      loadingIndicator: '|',
      uploadProgress: false,
      savingCalibration: false,

      activeTab: 'status',

      status: {
        systemID: 'loading...',
        version: 'loading...',
        time: null,
        timeOffset: 0,
        resetReason: null,
        stackTrace: false
      },

      wifiStatus: {
        ap: {
          enabled: false,
          ip: '0.0.0.0'
        },
        station: {
          enabled: false,
          status: 0,
          ip: '0.0.0.0'
        }
      },

      searchingLocation: false,

      triggers: {
        time: {
          enabled: false,
          transitionTime: null,
          triggers: []
        },

        motion: {
          enabled: false,
          enabledDuringTimeTrigger: 0,
          enabledDuringDay: false,
          transitionTime: null,
          delay: null,
          triggers: []
        }
      },

      connection: {
        hostname: null,
        accesspoint: true,
        station: false,
        ssid: null,
        password: null,
        dhcp: true,
        ip: null,
        subnetmask: null,
        gateway: null
      },

      system: {
        ntpServer: null,
        ntpInterval: 5,
        lat: null,
        lng: null,
        pins: {
          ledAP: null,
          ledSTA: null,
          apButton: null,
          pwmSDA: null,
          pwmSCL: null
        },
        pwmAddress: null,
        pwmFrequency: null,
        mapsAPIKey: ''
      },

      allSteps: true,
      allStepsValue: 0,
      steps: [],

      location: '',

      fixedTimes: [],
      relativeTimes: [],

      calibration: null
    },

    created: function()
    {
      var self = this;

      self.notificationTimer = null;

      document.title = i18n.t('title');
      var hash = window.location.hash.substr(1);
      if (hash)
        self.activeTab = hash;

      self.startLoadingIndicator();
      self.updateWiFiStatus();

      self.disableStepsChanged = false;
      self.savingStepsTimer = false;
      self.savingCalibrationTimer = false;

      var fixedTimes = [];
      var relativeTimes = [];

      for (var minutes = 0; minutes < 60 * 24; minutes += 15)
        fixedTimes.push(minutes);

      for (var minutes = -(4 * 60); minutes <= 4 * 60; minutes += 15)
        relativeTimes.push(minutes);

      self.fixedTimes = fixedTimes;
      self.relativeTimes = relativeTimes;


      // Sequential loading of all the settings makes sure
      // we don't overload the ESP8266 with requests, as that
      // can cause it to run out of memory easily.
      // This is a horrible way to implement it, but I don't feel like
      // including a big library or working out a clean short solution
      // at the moment, and it works :)
      self.loadStatus().then(function()
      {
        self.loadConnection().then(function()
        {
          self.loadSystem().then(function()
          {
            self.loadTimeTriggers().then(function()
            {
              self.loadMotionTriggers().then(function()
              {
                self.loadSteps().then(function()
                {
                  self.stopLoadingIndicator();
                  self.loading = false;
                });
              });
            });
          });
        });
      });
    },

    methods: {
      showNotification: function(message, error)
      {
        var self = this;
        self.notification = {
          message: message,
          error: error
        };

        if (self.notificationTimer != null)
          clearTimeout(self.notificationTimer);

        self.notificationTimer = setTimeout(function()
        {
          self.notification = null;
          self.notificationTimer = null;
        }, 5000);
      },

      hideNotification: function()
      {
        var self = this;
        self.notification = null;

        if (self.notificationTimer != null)
        {
          clearTimeout(self.notificationTimer);
          self.notificationTimer = null;
        }
      },

      handleAPIError: function(messageId, error)
      {
        var self = this;

        console.log(error);
        var errorMessage = '';

        if (error.response)
        {
          errorMessage = 'HTTP response code ' + error.response.status;
        }
        else if (error.request)
        {
          errorMessage = 'No response';
        }
        else
        {
          errorMessage = error.message;
        }

        self.showNotification(i18n.t(messageId) + '\n\n' + errorMessage, true);
      },

      loadStatus: function()
      {
        var self = this;
        return axios.get('/api/status')
          .then(function(response)
          {
            if (typeof response.data == 'object')
              self.status = response.data;
          })
          .catch(self.handleAPIError.bind(self, 'error.loadStatus'));
      },

      loadConnection: function()
      {
        var self = this;
        return axios.get('/api/connection')
          .then(function(response)
          {
            if (typeof response.data == 'object')
              self.connection = response.data;
          })
          .catch(self.handleAPIError.bind(self, 'error.loadConnection'));
      },

      loadSystem: function()
      {
        var self = this;
        return axios.get('/api/system')
          .then(function(response)
          {
            if (typeof response.data == 'object')
              self.system = response.data;
          })
          .catch(self.handleAPIError.bind(self, 'error.loadSystem'));
      },

      loadTimeTriggers: function()
      {
        var self = this;
        return axios.get('/api/triggers/time')
          .then(function(response)
          {
            if (typeof response.data == 'object')
            {
              var timeSettings = {
                enabled: response.data.enabled || false,
                transitionTime: response.data.transitionTime || 0,
                triggers: []
              };

              if (Array.isArray(response.data.triggers))
              {
                for (var i = 0; i < response.data.triggers.length; i++)
                {
                  var trigger = response.data.triggers[i];

                  timeSettings.triggers.push({
                    brightness: trigger.brightness || 0,
                    triggerType: trigger.triggerType || 0,
                    enabled: trigger.enabled || false,

                    fixedTime: trigger.triggerType > 0 ? 0 : trigger.time || 0,
                    relativeTime: trigger.triggerType > 0 ? trigger.time || 0 : 0,

                    monday: (trigger.daysOfWeek & 1) > 0,
                    tuesday: (trigger.daysOfWeek & 2) > 0,
                    wednesday: (trigger.daysOfWeek & 4) > 0,
                    thursday: (trigger.daysOfWeek & 8) > 0,
                    friday: (trigger.daysOfWeek & 16) > 0,
                    saturday: (trigger.daysOfWeek & 32) > 0,
                    sunday: (trigger.daysOfWeek & 64) > 0
                  });
                }
              }

              self.triggers.time = timeSettings;
            }
          })
          .catch(self.handleAPIError.bind(self, 'error.loadTimeTriggers'));
      },

      loadMotionTriggers: function()
      {
        var self = this;
        return axios.get('/api/triggers/motion')
          .then(function(response)
          {
            if (typeof response.data == 'object')
              self.triggers.motion = response.data;
          })
          .catch(self.handleAPIError.bind(self, 'error.loadMotionTriggers'));
      },

      loadSteps: function()
      {
        var self = this;
        return axios.get('/api/steps/values')
          .then(function(response)
          {
            if (Array.isArray(response.data))
            {
              var allSteps = true;
              var allStepsValue = false;

              var total = 0;
              var steps = [];


              for (var i = 0; i < response.data.length; i++)
              {
                var value = response.data[i];

                if (allStepsValue === false)
                  allStepsValue = value;
                else if (value !== allStepsValue)
                  allSteps = false;

                steps.push({ value: value });
                total += value;
              }

              self.steps = steps;
              self.allStepsValue = Math.floor(total / steps.length);
              self.allSteps = allSteps;
            }
          });
      },


      applyConnection: function()
      {
        var self = this;
        if (self.saving) return;

        self.saving = true;

        axios.post('/api/connection', {
            hostname: self.connection.hostname,
            accesspoint: self.connection.accesspoint,
            station: self.connection.station,
            ssid: self.connection.ssid,
            password: self.connection.password,
            dhcp: self.connection.dhcp,
            ip: self.connection.ip,
            subnetmask: self.connection.subnetmask,
            gateway: self.connection.gateway,
        })
          .then(function(response)
          {
          })
          .catch(self.handleAPIError.bind(self, 'error.applyConnection'))
          .then(function()
          {
            self.saving = false;
          });
      },

      applySystem: function()
      {
        var self = this;
        if (self.saving) return;

        self.saving = true;

        axios.post('/api/system', self.system)
          .then(function(response)
          {
            self.showNotification(i18n.t('rebootPending'));
          })
          .catch(self.handleAPIError.bind(self, 'error.applySystem'))
          .then(function()
          {
            self.saving = false;
          });
      },

      startLoadingIndicator: function()
      {
        var self = this;

        self.loadingStage = 0;
        self.loadingTimer = setInterval(function()
        {
          self.loadingStage++;
          switch (self.loadingStage)
          {
            case 1: self.loadingIndicator = '/'; break;
            case 2: self.loadingIndicator = '-'; break;
            case 3: self.loadingIndicator = '\\'; break;
            case 4: self.loadingIndicator = '|'; self.loadingStage = 0; break;
          }
        }, 250);
      },

      stopLoadingIndicator: function()
      {
        clearInterval(this.loadingTimer);
      },

      getWiFiStationStatus: function()
      {
        if (!this.wifiStatus.station.enabled)
          return 'disconnected';

        switch (this.wifiStatus.station.status)
        {
          case 0: // WL_IDLE_STATUS
          case 2: // WL_SCAN_COMPLETED
            return 'connecting';

          case 1: // WL_NO_SSID_AVAIL
          case 4: // WL_CONNECT_FAILED
          case 5: // WL_CONNECTION_LOST
            return 'error';

          case 3: // WL_CONNECTED
            return 'connected';

          case 6: // WL_DISCONNECTED
          default:
            return 'disconnected';
        }
      },

      getWiFiStationStatusText: function()
      {
        if (!this.wifiStatus.station.enabled)
          return i18n.t('wifiStatus.stationmode.disabled');

        switch (this.wifiStatus.station.status)
        {
          case 0: // WL_IDLE_STATUS
            return i18n.t('wifiStatus.stationmode.idle');

          case 1: // WL_NO_SSID_AVAIL
            return i18n.t('wifiStatus.stationmode.noSSID');

          case 2: // WL_SCAN_COMPLETED
            return i18n.t('wifiStatus.stationmode.scanCompleted');

          case 3: // WL_CONNECTED
            return this.wifiStatus.station.ip;

          case 4: // WL_CONNECT_FAILED
            return i18n.t('wifiStatus.stationmode.connectFailed');

          case 5: // WL_CONNECTION_LOST
            return i18n.t('wifiStatus.stationmode.connectionLost');

          case 6: // WL_DISCONNECTED
          default:
            return i18n.t('wifiStatus.stationmode.disconnected');
        }
      },

      updateWiFiStatus: function()
      {
        var self = this;
        if (!self.saving)
        {
          axios.get('/api/connection/status')
            .then(function(response)
            {
              if (typeof response.data == 'object')
                self.wifiStatus = response.data;
            })
            .catch(self.handleAPIError.bind(self, 'error.updateWiFiStatus'))
            .then(function()
            {
              setTimeout(self.updateWiFiStatus, 5000);
            });
        }
        else
          setTimeout(self.updateWiFiStatus, 5000);
      },

      uploadFirmware: function()
      {
        var self = this;
        if (self.saving) return;

        self.saving = true;
        self.uploadProgress = 0;


        var data = new FormData();
        data.append('file', document.getElementById('firmwareFile').files[0]);

        var config = {
          timeout: 360000,
          onUploadProgress: function(progressEvent)
          {
            self.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
          }
        };

        axios.post('/api/firmware', data, config)
          .then(function(response)
          {
            self.showNotification(i18n.t('rebootPending'));
          })
          .catch(self.handleAPIError.bind(self, 'error.uploadFirmware'))
          .then(function()
          {
            self.uploadProgress = false;
            self.saving = false;

            document.getElementById('firmware').reset();
          });
      },

      stepsChanged: function()
      {
        var self = this;
        if (self.loading || self.disableStepsChanged) return;

        if (self.savingStepsTimer === false)
          self.savingStepsTimer = setTimeout(function() { self.updateSteps(); }, 200);
      },

      updateSteps: function()
      {
        var self = this;

        if (self.savingSteps)
          self.savingStepsTimer = setTimeout(function() { self.updateSteps(); }, 200);

        self.savingSteps = true;
        self.savingStepsTimer = false;


        self.disableStepsChanged = true;

        if (self.allSteps)
        {
          var newSteps = [];
          for (var i = 0; i < self.steps.length; i++)
            newSteps.push({ value: self.allStepsValue });

          self.steps = newSteps;
        }
        else
        {
          var total = 0;
          for (var i = 0; i < self.steps.length; i++)
            total += self.steps[i].value;

          self.allStepsValue = Math.floor(total / self.steps.length);
        }
        var steps = [];
        for (var i = 0; i < self.steps.length; i++)
          steps.push(self.steps[i].value);

        self.disableStepsChanged = false;


        axios.post('/api/steps/values', {
          transitionTime: 1000,
          values: steps
        })
          .then(function(response)
          {
          })
          .catch(self.handleAPIError.bind(self, 'error.updateSteps'))
          .then(function()
          {
            self.savingSteps = false;
          });
      },

      searchLocation: function()
      {
        var self = this;
        if (!self.location) return;

        self.searchingLocation = true;

        axios.get('https://maps.googleapis.com/maps/api/geocode/json', { params: { address: self.location, key: self.system.mapsAPIKey }})
          .then(function(response)
          {
            if (Array.isArray(response.data.results) && response.data.results.length > 0)
            {
              var location = response.data.results[0].geometry.location;
              self.system.lat = location.lat;
              self.system.lng = location.lng;
            }
          })
          .catch(self.handleAPIError.bind(self, 'error.searchLocation'))
          .then(function()
          {
            self.searchingLocation = false;
          });
      },

      applyTimeTriggers: function()
      {
        var self = this;
        if (self.saving) return;

        self.saving = true;

        var timeSettings = {
          enabled: self.triggers.time.enabled,
          transitionTime: self.triggers.time.transitionTime,
          triggers: []
        };

        for (var i = 0; i < self.triggers.time.triggers.length; i++)
        {
          var trigger = self.triggers.time.triggers[i];

          timeSettings.triggers.push({
            brightness: trigger.brightness,
            triggerType: trigger.triggerType,
            enabled: trigger.enabled,
            time: trigger.triggerType > 0 ? trigger.relativeTime : trigger.fixedTime,
            daysOfWeek: (trigger.monday ? 1 : 0) |
                        (trigger.tuesday ? 2 : 0) |
                        (trigger.wednesday ? 4 : 0) |
                        (trigger.thursday ? 8 : 0) |
                        (trigger.friday ? 16 : 0) |
                        (trigger.saturday ? 32 : 0) |
                        (trigger.sunday ? 64 : 0)
          });
        }

        axios.post('/api/triggers/time', timeSettings)
          .then(function(response)
          {
          })
          .catch(self.handleAPIError.bind(self, 'error.applyTimeTriggers'))
          .then(function()
          {
            self.saving = false;
          });
      },

      addTimeTrigger: function()
      {
        var self = this;
        self.triggers.time.triggers.push({
          brightness: 0,
          triggerType: 0,
          enabled: true,

          fixedTime: 9 * 60,
          relativeTime: 0,

          monday: true,
          tuesday: true,
          wednesday: true,
          thursday: true,
          friday: true,
          saturday: true,
          sunday: true
        });
      },

      deleteTimeTrigger: function(index)
      {
        var self = this;
        self.triggers.time.triggers.splice(index, 1);
      },

      applyMotionTriggers: function()
      {
        var self = this;
        if (self.saving) return;

        self.saving = true;

        axios.post('/api/triggers/motion', self.triggers.motion)
          .then(function(response)
          {
          })
          .catch(self.handleAPIError.bind(self, 'error.applyMotionTriggers'))
          .then(function()
          {
            self.saving = false;
          });
      },

      addMotionTrigger: function()
      {
        var self = this;
        self.triggers.motion.triggers.push({
          brightness: 0,
          enabled: true,
          pin: 2,
          direction: 1
        });
      },

      deleteMotionTrigger: function(index)
      {
        var self = this;
        self.triggers.motion.triggers.splice(index, 1);
      },

      getDisplayTime: function(time, isRelative)
      {
        var result = '';

        if (isRelative)
          result += time >= 0 ? '+' : '-';

        time = Math.abs(time);
        var hours = Math.floor(time / 60);
        var minutes = time % 60;

        result += hours + ':';
        if (minutes < 10)
          result += '0';

        result += minutes;
        return result;
      },

      startCalibration: function()
      {
        var self = this;

        axios.get('/api/steps')
          .then(function(response)
          {
            if (typeof response.data == 'object')
            {
              self.calibration = {
                wizardStep: 0,
                count: response.data.count,
                useCurve: response.data.useCurve,
                ranges: response.data.ranges
              };
            }
          })
          .catch(self.handleAPIError.bind(self, 'error.loadSteps'));
      },

      stopCalibration: function()
      {
        this.calibration = null;
      },

      applyCalibration: function()
      {
        // All the changes are already applied
        this.stopCalibration();
      },

      hasNextCalibrationStep: function()
      {
        return this.calibration.wizardStep < 1;
      },

      nextCalibrationStep: function()
      {
        var self = this;

        if (self.calibration.wizardStep == 0)
        {
          if (self.calibration.count < 1)
            self.calibration.count = 1;
          else if (self.calibration.count > 16)
            self.calibration.count = 16;

          // Update ranges with possible new count
          if (self.calibration.ranges.length > self.calibration.count)
          {
            self.calibration.ranges.splice(self.calibration.count);
          }
          else
          {
            while (self.calibration.ranges.length < self.calibration.count)
              self.calibration.ranges.push({ start: 0, end: 4095 });
          }
        }

        self.calibration.wizardStep++;
        if (self.calibration.wizardStep == 2)
          self.applyCalibration();
      },

      calibrationChanged: function()
      {
        var self = this;
        if (self.loadingCalibration || self.calibration === null) return;

        if (self.savingCalibrationTimer === false)
          self.savingCalibrationTimer = setTimeout(function() { self.updateCalibration(); }, 200);
      },

      updateCalibration: function()
      {
        var self = this;

        if (self.calibration === null)
          return;

        if (self.savingCalibration)
          self.savingStepsTimer = setTimeout(function() { self.updateCalibration(); }, 200);


        self.savingCalibration = true;
        self.savingCalibrationTimer = false;


        axios.post('/api/steps', {
          count: self.calibration.count,
          useCurve: self.calibration.useCurve,
          ranges: self.calibration.ranges
        })
          .then(function(response)
          {
          })
          .catch(self.handleAPIError.bind(self, 'error.updateCalibration'))
          .then(function()
          {
            self.savingCalibration = false;
          });
      },

      deleteStackTrace: function()
      {
        var self = this;

        return axios.get('/api/stacktrace/delete')
          .then(function(response)
          {
            self.status.resetReason = 0;
            self.status.stackTrace = false;
          })
          .catch(self.handleAPIError.bind(self, 'error.stackTraceDeleteError'));
      }
    },

    computed: {
      hasResetError: function()
      {
        var self = this;

        /*
          REASON_DEFAULT_RST      = 0  normal startup by power on
          REASON_WDT_RST          = 1  hardware watch dog reset
          REASON_EXCEPTION_RST    = 2  exception reset, GPIO status won’t change
          REASON_SOFT_WDT_RST     = 3  software watch dog reset, GPIO status won’t change
          REASON_SOFT_RESTART     = 4  software restart ,system_restart , GPIO status won’t change
          REASON_DEEP_SLEEP_AWAKE = 5  wake up from deep-sleep
          REASON_EXT_SYS_RST      = 6   system reset
        */
        return (self.status.resetReason === 1 ||
                self.status.resetReason === 2 ||
                self.status.resetReason === 3 ||
                self.status.stackTrace);
      },

      getDeviceTime: function()
      {
        var self = this;
        if (self.status.time === false)
          return '';

        var date = new Date(self.status.time * 1000);
        var hours = date.getHours();
        var minutes = '0' + date.getMinutes();

        var offsetHours = '0' + Math.floor(Math.abs(self.status.timeOffset / 60) / 60);
        var offsetMinutes = '0' + Math.abs(self.status.timeOffset / 60) % 60;
        var offset = (self.status.timeOffset >= 0 ? '+' : '-') + offsetHours.substr(-2) + ':' + offsetMinutes.substr(-2);

        return hours + ':' + minutes.substr(-2) + ' (' + offset + ')';
      }
    },

    watch: {
      // The sync: true ensures we can wrap the internal updates with disableStepsChanged
      allSteps: {
        handler: function() { this.stepsChanged(); },
        sync: true
      },

      allStepsValue: {
        handler: function() { this.stepsChanged(); },
        sync: true
      },

      steps: {
        handler: function() { this.stepsChanged(); },
        deep: true,
        sync: true
      },

      activeTab: function(newValue)
      {
        window.location.hash = '#' + newValue;
      },

      calibration: {
        handler: function() { this.calibrationChanged(); },
        deep: true
      }
    }
  });
}