//let moment = require('moment');
let schedule = require('node-schedule');
//let path = require('path');
let PlaylistManager = require('./PlaylistManager');
const chalk = require('chalk');
let pl = null;

require('linqjs');
let _ = require('lodash');

class schedulePlayer {
  constructor({ db, moment, config, utils, schedulesdb, announcesdb, decodeOnly = true, logdb, logger }) {

    this.db = db;
    this.moment = moment;
    this.config = config;
    this.utils = utils;
    this.logger = logger;
    this.schedulesdb = schedulesdb;
    this.announcesdb = announcesdb;
    this.decodeOnly = decodeOnly;
    this.logDB = logdb;
    pl = new PlaylistManager({
      moment, utils, config,
      isLooping: true,
      shuffle: true,
      autoStart: true,
      minFileCountToPlay: 3,
      fadeInEnabled: true,
      musicVolume: 100,
      decodeOnly: this.decodeOnly,
      logger
    });

    this.activePlaylist = null;
    this.activeFile = null;
    this.isPlaying = false;
    this.pl = pl;
    this.eventTypes = {
      checkingNextPlay: 'checkingNextPlay',
      stateChanged: 'stateChanged',
      playlistFinished: 'playlistFinished',
      fileChanged: 'fileChanged',
      positionChange: 'positionChange',
      vumeter: 'vumeter',
      fadeOutStarted: 'fadeOutStarted',
      fadeInStarted: 'fadeInStarted',
      playbackFinished: 'playbackFinished',
    };

    //mutex
    this.isCheckingNextPlay = false;

    //prepare currentSong
    this.currentSong = {
      FilePosition: 0,
      OriginalName: '',
      FileName: '',
      TotalFiles: 0,
      Duration: 0,
      Position: 0,
      vumeterLeft: 0,
      vumeterRight: 0
    };

    //events
    pl.on(pl.playerEvents.fileChanged,
      async (chan, FileName, activeFile, duration, index) => {
        this.currentSong.TotalFiles = this.pl.files.length;
        this.currentSong.FileName = activeFile.FileName;
        this.currentSong.OriginalName = activeFile.OriginalName;
        this.currentSong.Duration = duration;
        this.currentSong.FilePosition = index;


        this.activeFile = pl.activeFile;
        this.emit(this.eventTypes.fileChanged, chan, FileName, activeFile, duration, index);
        await this.schedulesdb.setLastPlayedSong(activeFile.scheduleID,
          activeFile.FileName, duration, index);
        let obj = {
          FileID: activeFile.FileID,
          FileName: FileName,
          OriginalName: activeFile.OriginalName,
          PlayDate: new moment().utc(true).toJSON().substr(0, 19),
          MusicType: 0,
          Duration: duration,
        };
        await this.logDB.insertLog(obj);
        this.logger.PlayingSchedules(`${activeFile.FileName} - ${activeFile.OriginalName} - ${this.moment({
          hour: 0, minute: 0, second: 0
        }).second(duration).format('mm:ss')
          }`);
      });
    pl.on(pl.playerEvents.playlistFinished, () => {
      this.emit(this.eventTypes.playlistFinished);
    });
    pl.on(pl.playerEvents.positionChange, (activeFile, pos, dur, position, duration) => {
      this.emit(this.eventTypes.positionChange, activeFile, pos, dur, position, duration);


      this.currentSong.FilePosition = this.pl.lastSongIndex;
      this.currentSong.Position = position;
      this.currentSong.Duration = duration;

    });
    pl.on(pl.playerEvents.stateChanged, (state) => {
      this.emit(this.eventTypes.stateChanged, state);
    });
    pl.on(pl.playerEvents.vumeter, (left, right) => {
      if(this.isPlaying) {
          this.emit(this.eventTypes.vumeter, left, right);
          this.currentSong.vumeterLeft = left;
          this.currentSong.vumeterRight = right;
      }
    });

    pl.on(pl.playerEvents.fadeOutStarted, (chan) => {
      this.emit(this.eventTypes.fadeOutStarted, chan);
    });

    pl.on(pl.playerEvents.fadeInStarted, (chan) => {
      this.emit(this.eventTypes.fadeInStarted, chan);
    });

    pl.on(pl.playerEvents.playbackFinished, (chan) => {
      this.emit(this.eventTypes.playbackFinished, chan);
    });
    pl.on(pl.playerEvents.Error, (filename, err) => {
      this.logger.PlayerEventsSchedules(chalk.red(`Error opening ${filename}, err: ${err}`));
    });
    //new parameters
    this._initSystem();
  }

  async _initSystem() {
    this.setSchedulesCronJob();

    //if there is a change on db, then reschedule recurrent jobs.
    this.db.on(this.db.eventTypes.TimeExceptions_bulkInsert,
      async (fileArray) => {
        await this.setTimeExceptions();
          await this.setScheduleTimes();
        await this.setSchedules();
        pl.setTimes();
        this.checkNextPlay();
      });
    this.db.on(this.db.eventTypes.GlobalTimeExceptions_bulkInsert,
      async (fileArray) => {
        await this.setGlobalExceptions();
          await this.setScheduleTimes();
        await this.setSchedules();

        pl.setTimes();
        this.checkNextPlay();
      });
    this.db.on(this.db.eventTypes.Schedules_Details_bulkInsert,
      async (fileArray) => {
        await this.setSchedules();
        this.checkNextPlay();
      });

    this.db.on(this.db.eventTypes.Schedules_Details_bulkUpdate,
      async (fileArray) => {
        //received update of file
        for (let i = 0; i < fileArray.length; i++) {
          pl.setFileStatus(fileArray[i].FileName, fileArray[i].isDownloaded);
        }

      });

    //this.db=global.db;
    this.checkNextPlayInterval = 2000;
    await this.setSchedules();
    await this.setGlobalExceptions();
    await this.setTimeExceptions();
    this.setScheduleTimes();


    this.logger.PlayerEventsSchedules('schedulePlayer started');

  }

  get mixer() {
    return pl.mixer;
  }

  async initdb() {
    await this.db.initsync();
  }

  //------------------ scheduling---------------------------------------------------
  setSchedulesCronJob() {
    let rule = new schedule.RecurrenceRule();
    rule.hour = 1;
    rule.minute = 5;
    let schedulesStartEndTimes = schedule.scheduleJob('setPlJobs', {
      rule: rule
    }, async () => {
      await this.setScheduleTimes();
      await this.setSchedules();
    });
  }

  async setSchedules() {
    //instead of checking next play, we get all active schedules and we create recurrence jobs for each.
    //this jobs will fire between start and end dates on selected time and 'days'

    //first lets clear previous jobs if we have
    this.logger.PlayerEventsSchedules('creating recurrence jobs for schedules');
    this.clearSchedules();
    //get all active schedules
    let q1 = await this.schedulesdb.getSchedules();
    if (!q1) return;
    let q = (q1).where(c => c.isActive);
    this.logger.PlayerEventsSchedules(
      ` we have ${q.length} active Schedules..creating jobs for them.. `);
    //now schedule each of them
    //first for the startTimes
    await q.forEach(
      c => this.setSchedule(c.scheduleID, c.startDate, c.endDate, c.startTime,
        c.daysEffected));
    //then for the endTimes, it would be nice to check for the end times
    await q.forEach(
      c => this.setSchedule(c.scheduleID, c.startDate, c.endDate, c.endTime,
        c.daysEffected));

    // this is only for test of setschedule and calling the method twice.. remove on production
    //this.setSchedule(12345,new moment(),new moment('2018-08-30T23:59:59'),new moment().add(5,'seconds'),127)

    let tmp = schedule.scheduledJobs;
    this.logger.PlayerEventsSchedules('creating jobs completed');
  }

  async setGlobalExceptions() {
    let now = this.moment().utc(true);
    //clear old jobs first
    this.logger.PlayerEventsSchedules('creating recurrence jobs for global ex');
    this.clearEx('globalEx');
    //get all global exceptions
    let globalEx = await this.db.getGlobalExceptions();
    if (!globalEx) return;
    // globalEx = await globalEx.where(c => now.isBefore(c.endDate));
    this.logger.PlayerEventsSchedules(
      ` we have ${globalEx.length} global exceptions creating jobs for them`);
    await globalEx.forEach(
      c => this.setGlobalEx(c.ID, c.startDate, c.endDate, c.startTime,
        c.daynumber, 'globalEx'));
    await globalEx.forEach(
      c => this.setGlobalEx(c.ID, c.startDate, c.endDate, c.endTime,
        c.daynumber, 'globalEx'));
    let tmp = schedule.scheduledJobs;
    this.logger.PlayerEventsSchedules('creating globalEx jobs completed');

  }

  setGlobalEx(ID, startDate, endDate, startTime, daysEffected, exception) {
    let arr = [];
    // for(let i = 0; i < 7; i++){ if((daysEffected & Math.pow(2, i)) == Math.pow(2, i)) arr.push(i); }
    arr.push(daysEffected);
    let start = startTime.toObject();
    let rule = new schedule.RecurrenceRule();
    rule.dayOfWeek = arr;
    rule.hour = start.hours;
    rule.minute = start.minutes;
    rule.second = start.seconds;
    let j = schedule.scheduleJob(exception + ID,
      { start: startDate, end: endDate, rule: rule }, async () => {
        this.logger.PlayerEventsSchedules(`global exception event fired for ${j.name}`);
        this.setScheduleTimes();
        this.checkNextPlay();
      });
  }

  async setTimeExceptions() {
    //get all time exceptions and create recurrence jobs for them.
    let now = this.moment().utc(true);
    this.logger.PlayerEventsSchedules('creating recurrence jobs for time exceptions');
    // first clear old exceptions
    this.clearEx('timeEx');
    // get all time exceptions
    let timeExceptions = await this.db.getTimeExceptions();
    if (!timeExceptions) return;
    timeExceptions = await timeExceptions.where(c => now.isBefore(c.endDate));
    this.logger.PlayerEventsSchedules(
      `we have ${timeExceptions.length} time exceptions, creating jobs for them`);
    await timeExceptions.forEach(
      c => this.setTimeEx(c.ID, c.startDate, c.endDate, c.startTime,
        'timeEx'));
    await timeExceptions.forEach(
      c => this.setTimeEx(c.ID, c.startDate, c.endDate, c.endTime, 'timeEx'));
    let tmp = schedule.scheduledJobs;
    this.logger.PlayerEventsSchedules('creating time exception jobs completed.');
  }

  setTimeEx(ID, startDate, endDate, startTime, exception) {
    let start = startTime.toObject();
    let rule = new schedule.RecurrenceRule();
    rule.hour = start.hours;
    rule.minute = start.minutes;
    rule.second = start.seconds;
    let j = schedule.scheduleJob(exception + ID,
      { start: startDate, end: endDate, rule: rule }, async () => {
        this.logger.PlayerEventsSchedules(`time exception event fired for ${j.name}`);
        this.setScheduleTimes();
        this.checkNextPlay();
      });
  }

  clearEx(exception) {
    this.logger.System(`Clearing old ${exception} jobs`);
    let cnt = 0;
    for (let key in schedule.scheduledJobs) {
      if (key.startsWith(exception)) {
        schedule.scheduledJobs[key].cancel();
        cnt++;
      }
    }
    this.logger.PlayerEventsSchedules(`Cleared ${cnt} old ${exception} jobs`);
  }

  setSchedule(scheduleID, startDate, endDate, startTime, daysEffected) {
    let arr = [];
    for (let i = 0; i < 7; i++) {
      if ((daysEffected & Math.pow(2, i)) == Math.pow(2, i)) arr.push(i);
    }
    let start = startTime.toObject();
    let rule = new schedule.RecurrenceRule();
    rule.dayOfWeek = arr;
    rule.hour = start.hours;
    rule.minute = start.minutes;
    rule.second = start.seconds;
    let j = schedule.scheduleJob('schedule' + scheduleID,
      { start: startDate, end: endDate, rule: rule }, async () => {
        this.logger.PlayerEventsSchedules(`schedule event fired for ${scheduleID}`);
        await this.checkNextPlay();
      });
  }

  clearSchedules() {
    this.logger.System('Clearing old jobs');
    let cnt = 0;
    for (let key in schedule.scheduledJobs) {
      if (key.startsWith('schedule')) {
        schedule.scheduledJobs[key].cancel();
        cnt++;
      }
    }
    this.logger.PlayerEventsSchedules(`Cleared ${cnt} old jobs`);
  }

  //------------------ scheduling---------------------------------------------------

  async setScheduleTimes() {
    let availablePlayTime = await this.db.getAvailablePlayTime();
    if (!availablePlayTime) {
      setTimeout(() => {
        this.setScheduleTimes();
      }, 1000);
      return;
    }
    this.pl.startTime = availablePlayTime.startTime;
    this.pl.endTime = availablePlayTime.endTime;
    this.logger.PlayerEventsSchedules(chalk.blue(`Set playlist manager startTime to ${availablePlayTime.startTime}, endTime to ${availablePlayTime.endTime}`));

  }

  async checkNextPlay() {
    if (this.isCheckingNextPlay) return;
    this.isPlaying = true;
    this.isCheckingNextPlay = true;
    // if this method called again, just find the difference and set it.
    // otherwise let it play

    this.logger.PlayerEventsSchedules('checking next playlist');
    let availablePlayTime = await this.db.getAvailablePlayTime();
    //if there is no data then return
    if (!availablePlayTime) {
      this.isCheckingNextPlay = false;
      this.isPlaying = false;
      this.emit(this.eventTypes.playbackFinished);
      setTimeout(() => {
        this.checkNextPlay();
      }, this.checkNextPlayInterval);
      return;
    }
    //this.setScheduleTimes();
    let IsNowOkForPlayback = this.db.IsNowOkForPlayback(availablePlayTime.startTime, availablePlayTime.endTime);
    if (!IsNowOkForPlayback) {
      this.logger.PlayerEventsSchedules('not a good time to play, time must be between' + availablePlayTime.startTime.toJSON() + ' and ' + availablePlayTime.endTime.toJSON());
      this.isPlaying = false;
      pl.clearFiles();

      setTimeout(() => {
        this.isCheckingNextPlay = false;
        this.checkNextPlay();
      }, this.checkNextPlayInterval);
      return;
    }

    let schedulesToPlay = await this.schedulesdb.getSchedulesToPlay();
    if (schedulesToPlay == null || !schedulesToPlay.startTime) {
      this.logger.PlayerEventsSchedules(chalk.red('no files to play'));
      this.isPlaying = false;
      // clear playlist, there is no file to play
      pl.clearFiles();

      setTimeout(() => {
        this.isCheckingNextPlay = false;
        this.checkNextPlay();
      }, this.checkNextPlayInterval);
      return;
    }
    let filesToPlay = await schedulesToPlay.Schedules_Details;
    //if there is no file to play, then checkNextPlay again.
    if (filesToPlay == null || filesToPlay.length == 0) {
      this.logger.PlayerEventsSchedules('no files in details to play');
      this.isPlaying = false;
      this.emit(this.eventTypes.playbackFinished);
      pl.clearFiles();
      setTimeout(() => {
        this.isCheckingNextPlay = false;
        this.checkNextPlay();
      }, this.checkNextPlayInterval);
      return;
    }
    if (this.activePlaylist != null) {
      //check new playlist and activePlaylist equality
      this.logger.PlayerEventsSchedules('checking list equality');
      if (!await this.checkIsEqual(this.activePlaylist, schedulesToPlay)) {
        //these lists are different
        pl.clearFiles();
      } else {
        this.isCheckingNextPlay = false;
        //this.isPlaying = false;
        return;
      }
    }
    this.activePlaylist = schedulesToPlay;
    //get the volume of the playlist
    let volume = schedulesToPlay.Volume;
    pl.setMusicVolume(volume);
    let startTime = schedulesToPlay.startTime.isAfter(availablePlayTime.startTime) ? schedulesToPlay.startTime : availablePlayTime.startTime;
    let endTime = schedulesToPlay.endTime.isBefore(availablePlayTime.endTime) ? schedulesToPlay.endTime : availablePlayTime.endTime;
    //this.logger.PlayerEventsSchedules('dur');

    //let playableFirstSong = await filesToPlay.where(c => c.isDownloaded).first();
    // if(playableFirstSong != null || playableFirstSong != undefined){
    //   let lastSong = await this.schedulesdb.getLastPlayedSong(playableFirstSong.scheduleID);
    //   pl.addFiles(filesToPlay, lastSong);
    // } else{
    //   let lastSong = {  };
    //   pl.addFiles(filesToPlay, lastSong);
    // }

    let playableFirstSong = await filesToPlay.where(c => c.isDownloaded).first();
    // if there no downloaded files from files to play, return and check back again.
    if (!playableFirstSong && (filesToPlay.length == 0)) {
      setTimeout(() => {
        this.isCheckingNextPlay = false;
        this.isPlaying = false;
        this.checkNextPlay();
      }, this.checkNextPlayInterval);
      return;
    }
    let lastSong = null;
    if (playableFirstSong) {
      lastSong = await this.schedulesdb.getLastPlayedSong(playableFirstSong.scheduleID);
    }
    //now send this list (schedulesToPlay)  to playlist player

    pl.addFiles(filesToPlay, lastSong);
    this.isPlaying = true;
    if(lastSong) {
      this.logger.PlayerEventsSchedules(chalk.blue(`Added ${filesToPlay.length} schedules to playlist manager, last played song playlistID: ${lastSong.playlistID} file: ${lastSong.FileName}`));
    } else {
      this.logger.PlayerEventsSchedules(chalk.blue(`Added ${filesToPlay.length} schedules to playlist manager, no last played song`));
    }

    this.isCheckingNextPlay = false;
    this.emit(this.eventTypes.checkingNextPlay);
  }

  playNext() {
    this.logger.PlayerEventsSchedules('next button clicked');
    if (this.pl.activeChannel && this.pl.activeChannel != 0) {
      this.pl.activeScheduleChannels.remove(this.pl.activeChannel);
      this.pl.player.StopChannel(this.pl.activeChannel);
      this.pl.Play();
    }
  }

  async playNextButton() {

    try {
      let all = await this.schedulesdb.getSchedules();

      if(all && this.pl.files.length > 0) {
        let currentSchedule = this.pl.files[0].scheduleID;
        let index = _.findIndex(all, (c)  => {
          return c.scheduleID == currentSchedule
        });

        if(index != -1) {
          let indexToPlay = index + 1;
          indexToPlay = indexToPlay % all.length;
          this.pl.clearFiles();
          let lastPlayed = await this.schedulesdb.getLastPlayedSong(all[indexToPlay].scheduleID);
          let songsToPlay = all[indexToPlay].Schedules_Details;

          if(songsToPlay.length == 0) {
            this.logger.PlayerEventsSchedules('This playlist is empty, skipping');
            return;
          }

          this.pl.addFiles(songsToPlay, lastPlayed);
          this.logger.PlayerEventsSchedules(chalk.blue(`Play next button clicked, ${all[indexToPlay].scheduleID} schedule id added to play list`));
          if (this.pl.activeChannel && this.pl.activeChannel != 0) {
            this.pl.activeScheduleChannels.remove(this.pl.activeChannel);
            this.pl.player.StopChannel(this.pl.activeChannel);
            this.pl.Play();
          }
        }

        this.logger.PlayerEventsSchedules(chalk.blue(`Play next button clicked, but scheduleID could not be found in db`));
      }

    } catch (e) {
      this.logger.PlayerEventsSchedules(`Error on playing next schedule from button, err: ${e.message}`);
    }
  }
  async checkIsEqual(activeList, newlist) {

    //first check if schedules are identical
    if (activeList.Volume == newlist.Volume &&
      activeList.startTime.toJSON().substr(0, 19) ==
      newlist.startTime.toJSON().substr(0, 19) &&
      activeList.endTime.toJSON().substr(0, 19) ==
      newlist.endTime.toJSON().substr(0, 19) &&
      activeList.isShuffleEnabled == newlist.isShuffleEnabled &&
      activeList.hasPriority == newlist.hasPriority
    ) {
      //now check files
      if (_.differenceWith(activeList.Schedules_Details,
        activeList.Schedules_Details, (f1, f2) => {
          return f1.scheduleID == f2.scheduleID;
        }).length == 0) {
        return true;
      } else {
        return false;
      }
    }
    else {
      return false;
    }
  }


  async checkFileExists(files) {
    let fpath = this.utils.SchedulesSaveDir;
    let localFiles = await this.utils.getFilesInfo(fpath);
    if (localFiles == undefined || localFiles == null) return [];
    let filesToPlay = await files.select(c => {
      return { FileName: c.FileName, FileSize: c.FileSize };
    });
    localFiles = await localFiles.select(c => {
      return { FileName: c.file, FileSize: c.size };
    });
    // take difference of local files and files to play by file size and see if they're fully downloaded.
    let filesWeHave = await _.intersectionBy(localFiles, filesToPlay,
      'FileName');
    if (filesWeHave.length > 0) {
      //if there're files that arent downloaded fully, remove them from files to play and return what we already have, else return files to play.
      return filesWeHave;
    }//else {
    //   return localFiles;
    // }
  }
}

const util = require('util');
const EventEmitter = require('events').EventEmitter;
util.inherits(schedulePlayer, EventEmitter);
module.exports = schedulePlayer;