//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');
const chalk = require('chalk');
let pl = null;
require('linqjs');

const announcePlayer = class announcePlayer {
    constructor({ db, moment, config, utils, logger, schedulesdb, announcesdb, decodeOnly = true, logdb } = {}) {
        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;
        this.isPlaying = false;
        pl = new PlaylistManager({
            moment, utils, config, isLooping: false, shuffle: false, autoStart: false,
            minFileCountToPlay: 0, fadeInEnabled: false, musicVolume: 100, decodeOnly: this.decodeOnly, logger
        });
        this.pl = pl;
        this.isCheckingNextAnnounce = false;
        this.checkNextPlayInterval = 10000;
        this.activeAnnounces = null;
        this.announceTimeOut = 2000;
        this.eventTypes = {
            announcesFinished: 'announcesFinished',
            announcesStarted: 'announcesStarted',
            stateChanged: 'stateChanged',
            playlistFinished: 'playlistFinished',
            fileChanged: 'fileChanged',
            positionChange: 'positionChange',
            vumeter: 'vumeter',
            playbackFinished: 'playbackFinished'
        };
        this.pl.path = this.utils.AnnouncesSaveDir;
        this.nowPlaying = {
            FileName: '',
            OriginalName: '',
            Duration: 0,
            Position: 0,
            TotalFiles: 0,
            FilePosition: 0,
            vumeterLeft: 0,
            vumeterRight: 0
        };

        //events
        pl.on(pl.playerEvents.fileChanged, async (chan, FileName, activeFile, duration, index) => {
            this.nowPlaying.FileName = activeFile.FileName;
            this.nowPlaying.OriginalName = activeFile.OriginalName;
            this.nowPlaying.FilePosition = this.nowPlaying.TotalFiles - this.pl.announcesArray.length;
            this.emit(this.eventTypes.fileChanged, 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
            };
            this.logger.PlayingAnnounces(`${activeFile.FileName} - ${activeFile.OriginalName} - ${this.moment({
                hour: 0, minute: 0, second: 0
            }).second(duration).format('mm:ss')}`);
            await this.logDb.insertLog(obj);
        });

        pl.on(pl.playerEvents.positionChange, (activeFile, pos, dur, position, duration) => {
            this.emit(this.eventTypes.positionChange, activeFile, pos, dur, position, duration);
            this.nowPlaying.Duration = duration;
            this.nowPlaying.Position = position;

        });
        pl.on(pl.playerEvents.stateChanged, (state) => {
            this.emit(this.eventTypes.stateChanged, state);
        });
        pl.on(pl.playerEvents.vumeter, (left, right) => {
            if(this.isPlaying){
                this.nowPlaying.vumeterLeft = left;
                this.nowPlaying.vumeterRight = right;
                this.emit(this.eventTypes.vumeter, left, right);
            }
        });
        pl.on(pl.playerEvents.announcesFinished, async () => {
            this.emit(this.eventTypes.announcesFinished);
            this.isCheckingNextAnnounce = false;
            this.isPlaying = false;
            this.nowPlaying.TotalFiles = 0;
            this.nowPlaying.OriginalName = '';
            this.nowPlaying.FileName = '';
            this.nowPlaying.Duration = 0;
            this.nowPlaying.Position = 0;
            this.nowPlaying.FilePosition = 0;
        });
        pl.on(pl.playerEvents.playbackFinished, (chan) => {
            this.emit(this.eventTypes.playbackFinished, chan);
            this.isCheckingNextAnnounce = false;
        });
        pl.on(pl.playerEvents.Error, (filename, err) => {
            this.logger.PlayerEventsAnnounces(chalk.red(`Error opening ${filename}, err: ${err}`));
        });
        this.on(this.eventTypes.announcesStarted, () => {
            this.isPlaying = true;
            this.nowPlaying.TotalFiles = this.pl.announcesArray.length;
        });

        this._initSystem();
    }

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

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

    async _initSystem() {
        this.recurrentNightAnnounce();

        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 = [];
        allAnnounces.forEach((c) => {
            if (!c.isOpeningAnnounce && !c.isClosingAnnounce) {
                regularAnnounces.push(c);
            }
        });
        if (regularAnnounces.length == 0) return;
        //let q = await allAnnounces.where(c => c.isActive && c.isOpeningAnnounce == 0 && c.isClosingAnnounce == 0);
        this.logger.PlayerEventsAnnounces(`we have ${regularAnnounces.length} active announces, creating jobs for them`);
        // if start time and announce time is exactly same, event doesnt fire on first day,
        // so we set it back 30 seconds to make sure it fires on the very same day.
        await regularAnnounces.forEach(c => this.setAnnouce(c.announceID, c.startDate.subtract(5, 'seconds'), 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 = this.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.isCheckingNextAnnounce = false;
            this.logger.PlayerEventsAnnounces(`opening/closing announce event fired for ${announceID}, is checking next announce: ${this.isCheckingNextAnnounce}, `);
            this.checkNextPlay();
        });
    }

    recurrentNightAnnounce() {
        this.clearEx('nightAnnounce');
        let rule = new schedule.RecurrenceRule();
        rule.hour = 1;
        rule.minute = 0;
        let openingClosingRecurrent = schedule.scheduleJob('nightAnnounce', {
            rule: rule
        }, () => {
            this.logger.PlayerEventsAnnounces(`Setting announce jobs for today`);
            this.setTodayOpeningClosing();
            this.setAnnounces();
            this.isCheckingNextAnnounce = false;
            this.logger.PlayerEventsAnnounces(`checking next announce set to false`);
        });
    }

    setAnnouce(announceID, startDate, endDate, time, daysEffected) {

        this.logger.PlayerEventsAnnounces(`setting cron for ${announceID}: ${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.toJSON().substr(0, 19),
            end: endDate.toJSON().substr(0, 19),
            rule: rule
        }, async () => {
            this.logger.PlayerEventsAnnounces(`announce event fired for ${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) {
            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.logger.PlayerEventsAnnounces(chalk.blue(`global exception event fired`));
            this.checkNextPlay();
        });
    }

    async setTimeExceptions(delays) {
        //get all time exceptions and create recurrence jobs for them.
        let now = this.moment().utc(true);
        this.logger.PlayerEventsAnnounces('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) {
            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.logger.PlayerEventsAnnounces(chalk.blue(`time exception event fired`));
            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() {
        this.logger.PlayerEventsAnnounces('started checking next announce');
        if (this.isCheckingNextAnnounce) {
            this.logger.PlayerEventsAnnounces('already checking next announce, returning');
            return;
        }
        this.isCheckingNextAnnounce = true;

        this.logger.PlayerEventsAnnounces(chalk.blue('checking next announce to play'));
        let announcesToPlay = await this.announcesdb.getAnnouncesToPlay();
        if (!announcesToPlay) {
            this.logger.PlayerEventsAnnounces(chalk.blue('no announce to play at this time'));
            this.isPlaying = false;
            this.isCheckingNextAnnounce = false;
            return;
        }
        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.isPlaying = true;
            this.emit(this.eventTypes.announcesStarted);
            this.nowPlaying.TotalFiles = q1.length;
            this.pl.announcesArray.push(q1);
            setTimeout(() => {
                this.pl.PlayAnnounces();
            }, this.announceTimeOut);

            //this.isPlaying = false;
            this.isCheckingNextAnnounce = false;
            closingOpening = null;
            q1 = null;
            return;
        }
        let availablePlayTime = await this.db.getAvailablePlayTime();
        // if time isnt appropriate return
        if (!availablePlayTime) {
            this.isCheckingNextAnnounce = false;
            this.pl.isAnnouncePlaying = false;
            this.isPlaying = false;
            setTimeout(() => {
                this.checkNextPlay();
            }, this.checkNextPlayInterval);
            return;
        }
        let IsNowOkForPlayback = this.db.IsNowOkForPlayback(availablePlayTime.startTime, availablePlayTime.endTime);
        if (!IsNowOkForPlayback) {
            this.isPlaying = false;
            this.pl.isAnnouncePlaying = false;
            this.isCheckingNextAnnounce = false;
            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 announce to play');
            this.isPlaying = false;
            this.isCheckingNextAnnounce = false;
            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) {
            this.isCheckingNextAnnounce = false;
            this.isPlaying = false;
            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.isPlaying = true;
            this.emit(this.eventTypes.announcesStarted);
            this.nowPlaying.TotalFiles = closingAnnounce.length;
            this.pl.announcesArray.push(closingAnnounce);
            setTimeout(() => {
                this.pl.PlayAnnounces();
            }, this.announceTimeOut);
            //this.isPlaying = false;
            this.isCheckingNextAnnounce = false;
            closingAnnounce = null;
            q = null;
            return;
        }
        this.isPlaying = true;
        this.emit(this.eventTypes.announcesStarted);
        this.pl.announcesArray.push(q);
        this.nowPlaying.TotalFiles = q.length;
        //if we want to fade out song before announce, we need timeout to achieve it.
        setTimeout(() => {
            this.pl.PlayAnnounces();
        }, this.announceTimeOut);
        //this.isPlaying = false;
        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;