let moment=require('moment');
let clsLogger=require('../logs/clsLogger').clsLogger;
let logger=new clsLogger();
let schedule = require('node-schedule');
let path=require('path');
let PlaylistManager=require('./PlaylistManager')
let pl=null;
const LogDB = require('../db/logDB');
require('linqjs');
let _ = require('lodash');
class schedulePlayer{
  constructor({db,moment,config,utils,logger,schedulesdb,announcesdb,decodeOnly=true}){
    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 = new LogDB;
    pl=new PlaylistManager({moment,utils,config,
      isLooping:true,
      shuffle:true,
      autoStart:true,
      minFileCountToPlay:3,
      fadeInEnabled:true,
      musicVolume:100,
      decodeOnly:this.decodeOnly})
    this.activePlaylist=null;
    this.pl=pl;

    //mutex
    this.isCheckingNextPlay=false;
    pl.on(pl.playerEvents.fileChanged,async (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);
      //console.log('file changed:' + JSON.stringify(activeFile))
    });
    //new parameters
    this._initSystem()
  }

  async _initSystem(){




    //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.setSchedules();
     await this.checkNextPlay()
    });
    this.db.on(this.db.eventTypes.GlobalTimeExceptions_bulkInsert,async (fileArray)=>{
      await this.setGlobalExceptions();
      await  this.setSchedules();
     await this.checkNextPlay()
    });
    this.db.on(this.db.eventTypes.Schedules_Details_bulkInsert,async (fileArray)=>{
      await  this.setSchedules();
        await  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();

    logger.PlayerEventsSchedules('schedulePlayer started');


  }

  get mixer(){return pl.mixer;}
  async initdb(){await  this.db.initsync()}

  //------------------ scheduling---------------------------------------------------
  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
    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);
    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)

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

  async setGlobalExceptions() {
    let now = new moment().utc(true);
    //clear old jobs first
    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));
    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;
    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 () => {
      console.log(`created globalex: ${j.name}`);
    });
  }

  async setTimeExceptions() {
    //get all time exceptions and create recurrence jobs for them.
    let now = new moment().utc(true);
    logger.PlayingSchedules('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));
    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;
    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 () => {
      console.log(`created time ex: ${j.name}`)
    });
  }

  clearEx(exception) {
    logger.PlayingSchedules('Clearing old time exception jobs');
    let cnt = 0;
    for(let key in schedule.scheduledJobs) {
      if(key.startsWith(exception)) {
        schedule.scheduledJobs[key].cancel();
        cnt++
      }
    }
    logger.PlayerEventsSchedules(`Cleared ${cnt} old time 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 ()=>{
      logger.PlayerEventsSchedules(`running schedule ${scheduleID}`)
      await this.checkNextPlay()
    });
  }

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


  async checkNextPlay(){

    if(this.isCheckingNextPlay) {
        logger.PlayerEventsSchedules('Currently checking next play, skipping')
        return;
    }

    this.isCheckingNextPlay=true;
    // if this method called again, just find the difference and set it.
    // otherwise let it play

    logger.PlayerEventsSchedules('checking next playlist');
    let availablePlayTime=await this.db.getAvailablePlayTime();
    //if there is no data then return
    if (!availablePlayTime){
        logger.PlayerEventsSchedules('there is no data in exceptions table')
        this.isCheckingNextPlay=false; return;
    }
    let IsNowOkForPlayback=this.db.IsNowOkForPlayback(availablePlayTime.startTime,availablePlayTime.endTime);
    if(!IsNowOkForPlayback){
      logger.PlayerEventsSchedules('not a good time to play, time must be between' + availablePlayTime.startTime.toJSON() + ' and ' + availablePlayTime.endTime.toJSON());

      pl.clearFiles();

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

    let schedulesToPlay=await this.schedulesdb.getSchedulesToPlay();
    if (schedulesToPlay==null || !schedulesToPlay.startTime){
      logger.PlayerEventsSchedules('no files to play.. ');
      // 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){
      logger.PlayerEventsSchedules('no files in details to play.. ');
      pl.clearFiles();
      setTimeout( ()=>{this.isCheckingNextPlay=false; this.checkNextPlay()},this.checkNextPlayInterval);
      return;
    }
    if(this.activePlaylist!=null){
      //check new playlist and activePlaylist equality
      logger.PlayerEventsSchedules('checking list equality')
      if(!await this.checkIsEqual(this.activePlaylist,schedulesToPlay)){
        //these lists are different
        pl.clearFiles();
      }else{
          this.isCheckingNextPlay=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;
    logger.PlayerEventsSchedules('dur');
    // get the last played song from db and pass it to playlistManager
    //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 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) {
      //setTimeout( ()=>{this.isCheckingNextPlay=false; this.checkNextPlay()},this.checkNextPlayInterval);
      //return;
    }else{
        lastSong=await this.schedulesdb.getLastPlayedSong(playableFirstSong.scheduleID);
    }


      //now send this list (schedulesToPlay)  to playlist player..
      pl.addFiles(filesToPlay,lastSong);


    this.isCheckingNextPlay=false;
  }
  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
    ){
      //now check files
      // second one should be newList, comparing same array with itself :)
      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;
    }
  }

  // differenceBy returns all database files ignoring what we have. find a fix using lodash
  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;