const appRoot = process.cwd();
const moment = require('moment');
const path = require('path');
const clsLogger = require('../logs/clsLogger').clsLogger;
//const logger = new clsLogger();
const schedule = require('node-schedule');
const PlaylistManager = require('../playerCan/PlaylistManager');
const LogDb = require('../db/logDB');
const _ = require('lodash');
let pl = null;
require('linqjs');

const announcePlayer = class announcePlayer {
    constructor({ db, moment, config, utils, logger, schedulesdb, announcesdb, decodeOnly = true } = {}) {
        this.db = db;
        this.moment = moment;
        this.config = config;
        this.utils = utils;
        this.logger = new clsLogger();
        this.schedulesdb = schedulesdb;
        this.announcesdb = announcesdb;
        this.decodeOnly = decodeOnly;
        this.logDb = new LogDb();
        pl = new PlaylistManager({
            moment, utils, config, isLooping: false, shuffle: false, autoStart: false,
            minFileCountToPlay: 0, fadeInEnabled: false, musicVolume: 100, decodeOnly: this.decodeOnly
        });
        this.pl = pl;
        this.isCheckingNextAnnounce = false;
        this.checkNextPlayInterval = 10000;
        this.activeAnnounces = null;
        this.announceTimeOut = 3000;
        this.eventTypes = { announcesFinished: 'announcesFinished', announcesStarted: 'announcesStarted' };
        this.pl.path = this.utils.AnnouncesSaveDir;
        this.pl.on(this.pl.playerEvents.fileChanged, async(chan, FileName, activeFile, duration, index) => {
            let obj = {
                FileID: activeFile.FileID, FileName: FileName, OriginalName: activeFile.OriginalName,
                PlayDate: new moment().utc(true).toJSON().substr(0, 19), MusicType: 1, Duration: duration
            };
            await this.logDb.insertLog(obj);
        });
        this.pl.on(this.pl.playerEvents.announcesFinished, async() => {
            this.emit(this.eventTypes.announcesFinished);
        });

        this._initSystem();
    }

    get mixer() {
        return this.pl.mixer;
    }

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

    async _initSystem() {
        let openingClosingRecurrent = schedule.scheduleJob('todaysOpeningClosing','10 0 * * *', async () => {
          await this.setTodayOpeningClosing();
        });

      this.db.on(this.db.eventTypes.TimeExceptions_bulkInsert, async (fileArray) => {
        await this.setTodayOpeningClosing();
        await this.setAnnounces();
        //this.checkNextPlay();
      });
      this.db.on(this.db.eventTypes.GlobalTimeExceptions_bulkInsert, async (fileArray) => {
        await this.setTodayOpeningClosing();
        await this.setAnnounces();
        //this.checkNextPlay();
      });
        this.db.on(this.db.eventTypes.Announces_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.on(this.db.eventTypes.Announces_Details_bulkInsert, async() => {
            await this.setTimeExceptions();
            await this.setGlobalExceptions();
            await this.setTodayOpeningClosing();
            await this.setAnnounces();
            //this.checkNextPlay();
        });

        await this.setTodayOpeningClosing();
        await this.setAnnounces();
        this.logger.PlayerEventsAnnounces('announcePlayer started');
    }

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

    async setAnnounces() {
        /*
        instead of checking next play, we get all active announces and we create jobs for each of them,
        these jobs will fire events between start and end dates on selected time and day.
        when these events fire, we're going to pass the array of to be played announces to playlist manager.
        After announcePlaylist finishes, we'll fire another event which will set music volume back to original.
         */
        this.logger.PlayerEventsAnnounces('creating recurrence jobs for announces');
        this.clearAnnounces();
        let allAnnounces = await this.announcesdb.getAnnounces();
        if (!allAnnounces) return;
        let regularAnnounces = await allAnnounces.where(c => !c.isOpeningAnnounce || !c.isClosingAnnounce);
        if(!regularAnnounces) return;
        //let q = await allAnnounces.where(c => c.isActive && c.isOpeningAnnounce == 0 && c.isClosingAnnounce == 0);
        this.logger.PlayerEventsAnnounces(`we have ${allAnnounces.length} active announces, creating jobs for them`);
        await regularAnnounces.forEach(c => this.setAnnouce(c.announceID, c.startDate, c.endDate, c.startTime, c.daysEffected));

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

    async setTodayOpeningClosing() {
      this.clearEx('openingClosing');
      let allAnnounces = await this.announcesdb.getAnnounces();
      let today = new moment().utc(true).weekday();
      let todayP=Math.pow(2,today);
      let todayOpeningClosing = await allAnnounces.where(c => (c.isOpeningAnnounce || c.isClosingAnnounce) && (c.daysEffected & todayP) == todayP);
      if(todayOpeningClosing.length > 0) {
        await todayOpeningClosing.forEach( c =>  this.todaysOpeningClosingEvents(c.announceID, c.startTime));
      }
      this.logger.PlayerEventsAnnounces('Creating jobs for opening and closing announces completed');
      let tmp = schedule.scheduledJobs;
      console.log(tmp);
    }

   todaysOpeningClosingEvents(announceID, date) {
    this.logger.PlayerEventsAnnounces(`setting cron for opening/closing announce: ${announceID} - start: ${date}`);


      let arr = date.toObject();
    let q = schedule.scheduleJob('openingClosing' + announceID, {hour: arr.hours, minute: arr.minutes, second: arr.seconds}, (y) => {
          this.logger.PlayerEventsAnnounces(`Running opening/closing announce: ${announceID}`);
          this.checkNextPlay();
        });
  }

    setAnnouce(announceID, startDate, endDate, time, daysEffected) {
        this.logger.PlayerEventsAnnounces(`setting cron for announce: ${startDate} - ${endDate}, time:${time}, days:${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 = time.toObject();
        let rule = new schedule.RecurrenceRule();
        rule.dayOfWeek = arr;
        rule.hour = start.hours;
        rule.minute = start.minutes;
        rule.second = start.seconds;
        let job = schedule.scheduleJob('announce' + announceID, {
            start: startDate,
            end: endDate,
            rule: rule
        }, async() => {
            this.logger.PlayerEventsAnnounces(`running announce ${announceID}`);
            await this.checkNextPlay();
        });
    }

    async setGlobalExceptions(delays) {
        let now = new moment().utc(true);
        //clear old jobs first
        this.logger.PlayerEventsAnnounces('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.PlayerEventsAnnounces(`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'));

        if (delays.length > 0) {
            this.logger.PlayerEventsAnnounces(`now setting globalEx for ${delays.length} delays`);
            //now set crons for the delays
            for(let i = 0; i < delays.length; i++) {
                await globalEx.forEach(c => this.setGlobalEx(c.ID, c.startDate, c.endDate, c.startTime.add(delays[ i ], 'minutes'), c.daynumber, 'globalEx'));
                await globalEx.forEach(c => this.setGlobalEx(c.ID, c.startDate, c.endDate, c.endTime.add(delays[ i ], 'minutes'), c.daynumber, 'globalEx'));
            }
        }
        let tmp = schedule.scheduledJobs;
        this.logger.PlayerEventsAnnounces('creating globalEx jobs completed');

    }

    setGlobalEx(ID, startDate, endDate, startTime, daysEffected, exception) {
        this.logger.PlayerEventsAnnounces(`setting cron for announce global exception: ${startDate.toJSON()} - ${endDate.toJSON()}, time:${startTime.toJSON().substr(11, 8)}, days:${daysEffected}`);

        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.checkNextPlay();
        });
    }

    async setTimeExceptions(delays) {
        //get all time exceptions and create recurrence jobs for them.
        let now = new moment().utc(true);
        this.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));
        this.logger.PlayerEventsAnnounces(`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'));

        if (delays.length > 0) {
            this.logger.PlayerEventsAnnounces(`now setting delays for ${delays.length} delays`);
            //now set crons for the delays
            for(let i = 0; i < delays.length; i++) {
                await timeExceptions.forEach(c => this.setTimeEx(c.ID, c.startDate, c.endDate, c.startTime.add(delays[ i ], 'minutes'), 'timeEx'));
                await timeExceptions.forEach(c => this.setTimeEx(c.ID, c.startDate, c.endDate, c.endTime.add(delays[ i ], 'minutes'), 'timeEx'));
            }
        }
        this.logger.PlayerEventsAnnounces('creating time exception jobs completed.');
    }

    setTimeEx(ID, startDate, endDate, startTime, exception) {
        this.logger.PlayerEventsAnnounces(`setting cron for announce TimeExceptions:${startDate.toJSON()} - ${endDate.toJSON()}, time:${startTime.toJSON().substr(11, 8)}`);

        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.checkNextPlay();
        });
    }

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

  async checkNextPlay() {
    if (this.isCheckingNextAnnounce) return;
    this.isCheckingNextAnnounce = true;

    this.logger.PlayerEventsAnnounces('checking next announce to play');
    let announcesToPlay = await this.announcesdb.getAnnouncesToPlay();
    if (!announcesToPlay) {
      this.logger.PlayerEventsAnnounces('no announce to play at this time');
      return;
    }
    let availablePlayTime = await this.db.getAvailablePlayTime();
    // if time isnt appropriate return
    if (!availablePlayTime) {

      this.isCheckingNextAnnounce = false;
      return;
    }
    let IsNowOkForPlayback = this.db.IsNowOkForPlayback(availablePlayTime.startTime, availablePlayTime.endTime);
    if (!IsNowOkForPlayback) {
      //if we have an opening or closing announce bypass exceptions
      let closingOpening = announcesToPlay.where(c => c.isOpeningAnnounce || c.isClosingAnnounce);
      if(closingOpening && closingOpening.length > 0) {
        let volume = closingOpening[ 0 ].Volume;
        this.pl.setMusicVolume(volume);
        let q1 = [];
        await closingOpening.forEach(c => c.Announces_Details.forEach(d => q1.push(d)));
        this.emit(this.eventTypes.announcesStarted);
        this.pl.announcesArray.push(q1);
        this.pl.PlayAnnounces();
        this.isCheckingNextAnnounce = false;
        closingOpening = null;
        q1 = null;
        return;
      }
      this.logger.PlayerEventsAnnounces('not a good time to play announces, time must be between ' + availablePlayTime.startTime.toJSON() + ' and ' + availablePlayTime.endTime.toJSON());
      return;
    }

    let filesToPlay = [];
    await announcesToPlay.forEach(c => c.Announces_Details.forEach(d => filesToPlay.push(d)));
    // check if announces to be played has details
    if (!filesToPlay) {
      this.logger.PlayerEventsAnnounces('no details in files to play');
      return;
    }
    // if(this.activeAnnounces != null) {
    //   this.logger.PlayerEventsAnnounces('checking list equity');
    //   if(!await this.checkIsEqual(this.activeAnnounces, announcesToPlay)) {
    //     pl.clearFiles();
    //   } else {
    //     return;
    //   }
    // }
    this.activeAnnounces = announcesToPlay;
    if (announcesToPlay[ 0 ] == undefined) return;
    let volume = announcesToPlay[ 0 ].Volume;
    this.pl.setMusicVolume(volume);
    // set start times of announces according to time exceptions
    let startTime = await announcesToPlay.forEach((c) => c.startTime.isAfter(availablePlayTime.startTime) ?
        c.startTime : availablePlayTime.startTime);

    let q = [];
    await announcesToPlay.forEach(c => c.Announces_Details.forEach(d => q.push(d)));
    // if there's an opening announce, play it first.
    q = await q.orderByDescending(c => c.isOpeningAnnounce == 1).thenBy(c => c.orderID);
    // check if there's a closing announce in announces to play, if there is,
    // then discard all other but closing announce.
    let closingAnnounce = _.find(q, (c) => c.isClosingAnnounce);
    if (closingAnnounce) {
      this.emit(this.eventTypes.announcesStarted);
      this.pl.announcesArray.push(closingAnnounce);
      this.pl.PlayAnnounces();
      this.isCheckingNextAnnounce = false;
      closingAnnounce = null;
      q = null;
      return;
    }
    this.emit(this.eventTypes.announcesStarted);
    this.pl.announcesArray.push(q);
    //if we want to fade out song before announce, we need timeout to achieve it.
    setTimeout(() => {
      this.pl.PlayAnnounces();
    }, this.announceTimeOut);

    this.isCheckingNextAnnounce = false;
    q = null;
  }

    async checkFileExists(files) {
        let fpath = this.utils.AnnouncesSaveDir;
        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'), EventEmitter = require('events').EventEmitter;
util.inherits(announcePlayer, EventEmitter);
module.exports = announcePlayer;