// define accessable functions
exports = {
  // define en-/disable function, restarts the service to make changes take effect
  setEnabled: function(enable) {
    // stop if enabled
    if (global.sleeplog && sleeplog.enabled) sleeplog.stop();

    // define settings filename
    var settings = "sleeplog.json";

    // change enabled value in settings
    require("Storage").writeJSON(settings, Object.assign(
      require("Storage").readJSON(settings, true) || {}, {
        enabled: enable
      }
    ));

    // force changes to take effect by executing the boot script
    eval(require("Storage").read("sleeplog.boot.js"));

    return true;
  },

  // define read log function, returns log array
  // sorting: ascending (latest first), format:
  // [[number, int, int], [...], ... ]
  // - number // timestamp in 10min
  // - int    // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = light sleep, 4 = deep sleep
  // - int    // consecutive: 0 = unknown, 1 = no consecutive sleep, 2 = consecutive sleep
  readLog: function(since, until) {
    // set now and check if now is before since
    var now = Date.now();
    if (now < since) return [];

    // set defaults and convert since, until and now to 10min steps
    since = Math.floor((since || 0) / 6E5);
    until = Math.ceil((until || now) / 6E5);
    now = Math.ceil(now / 6E5);

    // define output log
    var log = [];

    // open StorageFile
    var file = require("Storage").open("sleeplog.log", "r");
    // cache StorageFile size
    var storageFileSize = file.getLength();
    // check if a Storage File needs to be read
    if (storageFileSize) {
      // define previous line cache
      var prevLine;
      // loop through StorageFile entries
      while (true) {
        // cache new line
        var line = file.readLine();
        // exit loop if all lines are read
        if (!line) break;
        // skip lines starting with ","
        if (line.startsWith(",")) continue;
        // parse line
        line = line.trim().split(",").map(e => parseInt(e));
        // exit loop if new line timestamp is not before until
        if (line[0] >= until) break;
        // check if new line timestamp is 24h before since or not after since
        if (line[0] + 144 < since) {
          // skip roughly the next 10 lines
          file.read(118);
          file.readLine();
        } else if (line[0] <= since) {
          // cache line for next cycle
          prevLine = line;
        } else {
          // add previous line if it was cached
          if (prevLine) log.push(prevLine);
          // add new line at the end of log
          log.push(line);
          // clear previous line cache
          prevLine = undefined;
        }
      }
      // add previous line if it was cached
      if (prevLine) log.push(prevLine);
      // set unknown consecutive statuses
      log = log.reverse().map((entry, index) => {
        if (entry[2] === 0) entry[2] = (log[index - 1] || [])[2] || 0;
        return entry;
      }).reverse();
      // remove duplicates
      log = log.filter((entry, index) =>
        !(index > 0 && entry[1] === log[index - 1][1] && entry[2] === log[index - 1][2])
      );
    }

    // check if log empty or first entry is after since
    if (!log[0] || log[0][0] > since) {
      // look for all needed storage files
      var files = require("Storage").list(/^sleeplog_\d\d\d\d\.log$/, {
        sf: false
      });

      // check if any file available
      if (files.length) {
        // generate start and end times in 10min steps
        files = files.map(file => {
          var start = this.fnToMs(parseInt(file.substr(9, 4))) / 6E5;
          return {
            name: file,
            start: start,
            end: start + 2016
          };
        }).sort((a, b) => b.start - a.start);

        // read all neccessary files
        var filesLog = [];
        files.some(file => {
          // exit loop if since after end
          if (since >= file.end) return true;
          // read file if until after start and since before end
          if (until > file.start || since < file.end) {
            var thisLog = require("Storage").readJSON(file.name, 1) || [];
            if (thisLog.length) filesLog = thisLog.concat(filesLog);
          }
        });
        // free ram
        files = undefined;

        // check if log from files is available
        if (filesLog.length) {
          // remove unwanted entries
          filesLog = filesLog.filter((entry, index, filesLog) => (
            (filesLog[index + 1] || [now])[0] >= since && entry[0] <= until
          ));
          // add to log as previous entries
          log = filesLog.concat(log);
        }
        // free ram
        filesLog = undefined;
      }
    }

    // define last index
    var lastIndex = log.length - 1;
    // set timestamp of first entry to since if first entry before since
    if (log[0] && log[0][0] < since) log[0][0] = since;
    // add timestamp at now with unknown status if until after now
    if (until > now) log.push([now, 0, 0]);

    return log;
  },

  // define move log function, move StorageFile content into files seperated by fortnights
  moveLog: function(force) {
    // first day of this fortnight period
    var thisFirstDay = this.fnToMs(this.msToFn(Date.now()));

    // read timestamp of the first StorageFile entry
    var firstDay = (require("Storage").open("sleeplog.log", "r").read(47) || "").match(/\n\d*/);
    // calculate the first day of the fortnight period
    if (firstDay) firstDay = this.fnToMs(this.msToFn(parseInt(firstDay[0].trim()) * 6E5));

    // check if moving is neccessary or forced
    if (force || firstDay && firstDay < thisFirstDay) {
      // read log for each fortnight period
      while (firstDay) {
        // calculate last day
        var lastDay = firstDay + 12096E5;
        // read log of the fortnight period
        var log = require("sleeplog").readLog(firstDay, lastDay);

        // check if before this fortnight period
        if (firstDay < thisFirstDay) {
          // write log in seperate file
          require("Storage").writeJSON("sleeplog_" + this.msToFn(firstDay) + ".log", log);
          // set last day as first
          firstDay = lastDay;
        } else {
          // rewrite StorageFile
          require("Storage").open("sleeplog.log", "w").write(log.map(e => e.join(",")).join("\n"));
          // clear first day to exit loop
          firstDay = undefined;
        }

        // free ram
        log = undefined;
      }
    }
  },

  // define function to return stats from the last date [ms] for a specific duration [ms] or for the complete log
  getStats: function(until, duration, log) {
    // define stats variable
    var stats = {
      calculatedAt: //   [date] timestamp of the calculation
        Math.round(Date.now()),
      deepSleep: 0, //   [min] deep sleep duration
      lightSleep: 0, //  [min] light sleep duration
      awakeSleep: 0, //  [min] awake duration inside consecutive sleep
      consecSleep: 0, // [min] consecutive sleep duration
      awakeTime: 0, //   [min] awake duration outside consecutive sleep
      notWornTime: 0, // [min] duration of not worn status
      unknownTime: 0, // [min] duration of unknown status
      logDuration: 0, // [min] duration of all entries taken into account
      firstDate: undefined, // [date] first entry taken into account
      lastDate: undefined // [date] last entry taken into account
    };

    // set default inputs
    until = until || stats.calculatedAt;
    if (!duration) duration = 864E5;

    // read log for the specified duration or complete log if not handed over
    if (!log) log = this.readLog(duration ? until - duration : 0, until);

    // check if log not empty or corrupted
    if (log && log.length && log[0] && log[0].length === 3) {
      // calculate and set first log date from 10min steps
      stats.firstDate = log[0][0] * 6E5;
      stats.lastDate = log[log.length - 1][0] * 6E5;

      // cycle through log to calculate sums til end or duration is exceeded
      log.forEach((entry, index, log) => {
        // calculate duration of this entry from 10min steps to minutes
        var duration = ((log[index + 1] || [until / 6E5 | 0])[0] - entry[0]) * 10;

        // check if duration greater 0
        if (duration) {
          // calculate sums
          if (entry[1] === 4) stats.deepSleep += duration;
          else if (entry[1] === 3) stats.lightSleep += duration;
          else if (entry[1] === 2) {
            if (entry[2] === 2) stats.awakeSleep += duration;
            else if (entry[2] === 1) stats.awakeTime += duration;
          }
          if (entry[2] === 2) stats.consecSleep += duration;
          if (entry[1] === 1) stats.notWornTime += duration;
          if (entry[1] === 0) stats.unknownTime += duration;
          stats.logDuration += duration;
        }
      });
    }

    // free ram
    log = undefined;

    // return stats of the last day
    return stats;
  },

  // define function to return last break time of day from date or now (default: 12 o'clock)
  getLastBreak: function(date, ToD) {
    // set default date or correct date type if needed
    if (!date || !date.getDay) date = date ? new Date(date) : new Date();
    // set default ToD as set in sleeplog.conf or settings if available
    if (ToD === undefined) ToD = (global.sleeplog && sleeplog.conf ? sleeplog.conf.breakToD :
      (require("Storage").readJSON("sleeplog.json", true) || {}).breakToD) || 12;
    // calculate last break time and return
    return new Date(date.getFullYear(), date.getMonth(), date.getDate(), ToD);
  },

  // define functions to convert ms to the number of fortnights since the first Sunday at noon: 1970-01-04T12:00
  fnToMs: function(no) {
    return (no + 0.25) * 12096E5;
  },
  msToFn: function(ms) {
    return (ms / 12096E5 - 0.25) | 0;
  },

  // define set debug function, options:
  //  enable as boolean, start/stop debugging
  //  duration in hours, generate csv log if set, max: 96h
  setDebug: function(enable, duration) {
    // check if global variable accessable
    if (!global.sleeplog) return new Error("sleeplog: Can't set debugging, global object missing!");

    // check if nothing has to be changed
    if (!duration &&
      (enable && sleeplog.debug === true) ||
      (!enable && !sleeplog.debug)) return;

    // check if en- or disable debugging
    if (enable) {
      // define debug object
      sleeplog.debug = {};

      // check if a file should be generated
      if (typeof duration === "number") {
        // check duration boundaries, 0 => 8
        duration = duration > 96 ? 96 : duration || 12;
        // calculate and set writeUntil in 10min steps
        sleeplog.debug.writeUntil = ((Date.now() / 6E5 | 0) + duration * 6) * 6E5;
        // set fileid to "{hours since 1970}"
        sleeplog.debug.fileid = Date.now() / 36E5 | 0;
        // write csv header on empty file
        var file = require("Storage").open("sleeplog_" + sleeplog.debug.fileid + ".csv", "a");
        if (!file.getLength()) file.write(
          "timestamp,movement,status,consecutive,asleepSince,awakeSince,bpm,bpmConfidence\n"
        );
        // free ram
        file = undefined;
      } else {
        // set debug as active
        sleeplog.debug = true;
      }
    } else {
      // disable debugging
      delete sleeplog.debug;
    }

    // save status forced
    sleeplog.saveStatus(true);
  },

  // define debugging function, called after logging if debug is set
  debug: function(data) {
    // check if global variable accessable and debug active
    if (!global.sleeplog || !sleeplog.debug) return;

    // set functions to convert timestamps
    function localTime(timestamp) {
      return timestamp ? Date(timestamp).toString().split(" ")[4].substr(0, 5) : "- - -";
    }
    function officeTime(timestamp) {
      // days since 30.12.1899
      return timestamp / 864E5 + 25569;
    }

    // generate console output
    var console = "sleeplog: " +
      localTime(data.timestamp) + " > " +
      "movement: " + ("" + data.movement).padStart(4) + ", " +
      "unknown    ,non consec.,consecutive".split(",")[sleeplog.consecutive] + " " +
      "unknown,not worn,awake,light sleep,deep sleep".split(",")[data.status].padEnd(12) + ", " +
      "asleep since: " + localTime(sleeplog.info.asleepSince) + ", " +
      "awake since: " + localTime(sleeplog.info.awakeSince);
    // add bpm if set
    if (data.bpm) console += ", " +
      "bpm: " + ("" + data.bpm).padStart(3) + ", " +
      "confidence: " + data.bpmConfidence;
    // output to console
    print(console);

    // check if debug is set as object with a file id and it is not past writeUntil
    if (typeof sleeplog.debug === "object" && sleeplog.debug.fileid &&
      Date.now() < sleeplog.debug.writeUntil) {
      // generate next csv line
      var csv = [
        officeTime(data.timestamp),
        data.movement,
        data.status,
        sleeplog.consecutive,
        sleeplog.info.asleepSince ? officeTime(sleeplog.info.asleepSince) : "",
        sleeplog.info.awakeSince ? officeTime(sleeplog.info.awakeSince) : "",
        data.bpm || "",
        data.bpmConfidence || ""
      ].join(",");
      // write next line to log if set
      require("Storage").open("sleeplog_" + sleeplog.debug.fileid + ".csv", "a").write(csv + "\n");
    } else {
      // clear file setting in debug
      sleeplog.debug = true;
    }

  },

  // print log as humanreadable output similar to debug output
  printLog: function(since, until) {
    // set default until
    until = until || Date.now();
    // print each entry inside log
    this.readLog(since, until).forEach((entry, index, log) => {
      // calculate duration of this entry from 10min steps to minutes
      var duration = ((log[index + 1] || [until / 6E5 | 0])[0] - entry[0]) * 10;
      // print this entry
      print((index + ")").padStart(4) + " " +
        Date(entry[0] * 6E5).toString().substr(0, 21) + " > " +
        "unknown    ,non consec.,consecutive".split(",")[entry[2]] + " " +
        "unknown,not worn,awake,light sleep,deep sleep".split(",")[entry[1]].padEnd(12) +
        "for" + (duration + "min").padStart(8));
    });
  }
};
