const
    util = require('util'),
    EventEmitter = require('events').EventEmitter,
    //Logger = require('../logs/clsLogger').clsLogger,
    DownloadManager = require('./DownloadManager'),
    //smgUtils = require('../utils/smgutils'),
    //AnnouncesDB = require('../db/announcesDB'),
    moment = require('moment'),
    chalk = require('chalk'),
    _ = require('lodash');

const announcesDownloader = class announcesDownloader {
    constructor({ db, config, utils,announcesdb, logger } = {}) {
        this.announcesDB = announcesdb;
        this.db = db;
        this.logger = logger;
        this.config = config;
       //utils.AnnouncesSaveDir
        this.dm = new DownloadManager({ options: { directory: utils.AnnouncesSaveDir, speed:0 }, config, utils, logger });
        //start download manager
        this.dm.start();
        this.sutils = utils;
        this.dm.AnnounceDownloader = true;

        this.eventTypes = {
            onProgress: 'onProgress',
            onDownloadCompleted: 'onDownloadCompleted',
            onError: 'onError',
            onListCompleted: 'onListCompleted'
        };
        //events
        this.db.on(this.db.eventTypes.Announces_Details_bulkInsert, async (arr) => {
            //this.logger.DownloadAnnounces('Bulk insert occured, we have ' + arr.length + ' announces');
            await this.start();
        });
        this.dm.on(this.dm.eventTypes.DownloadCompleted, async (pageStatus, pageMessage, filename) => {
            //console.log('File Download Finished');
            if(pageStatus == 200) {
                this.emit(this.eventTypes.onDownloadCompleted, pageStatus, pageMessage, filename);
                await this.announcesDB.updateAnnounceFileDownloadedStatus(true, filename);
              this.logger.DownloadAnnounces(chalk.bgGreen(`${filename} is set Downloaded in signage.db`));
            } else {
                await this.announcesDB.updateAnnounceFileDownloadedStatus(false, filename);
            }
        });
        this.dm.on(this.dm.eventTypes.Error, (file) => {
            this.emit(this.eventTypes.onError, file);
        });
        this.dm.on(this.dm.eventTypes.Progress, (filename, percentage, currentStr, durationStr, speed, current, length, totalDownloaded, totalFiles, FilesSizeDownloaded, FilesSize) => {
            this.emit(this.eventTypes.onProgress, filename, percentage, currentStr, durationStr, speed, current, length, totalDownloaded, totalFiles, FilesSizeDownloaded, FilesSize);
        });
        this.dm.on(this.dm.eventTypes.QueueIsEmpty, () => {
            this.emit(this.eventTypes.onListCompleted);
        });
        this.dm.on(this.dm.eventTypes.SetAnnounceDownloadedStatus, async (status, file) => {
            await this.announcesDB.updateAnnounceFileDownloadedStatus(status, file);
        })
    }

    async start() {
        this.dm.clearQue();
        await this.checkFileStatus();
        await this.announcesToDownload();
    }

    async checkDownloads() {
        let now = new moment().utc(true);
        let today = Math.pow(2, now.weekday());
        let arrFiles = [];
        try {
            let announces = await this.announcesDB.getAnnounces(); // get all announces that are on db.
            if(announces == undefined) return [];
            //take announces where today is between start and end date of that announce, then prioritize opening/closing, then next announce supposed to play in that day, then by start date.
            let openingClosing = await announces.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Announces_Details.isDownloaded && (c.isOpeningAnnounce || c.isClosingAnnounce) &&
                (c.daysEffected & today) == today);
            let afterNow = await announces.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Announces_Details.isDownloaded && (c.daysEffected & today) == today && c.startTime.isAfter(now))
                .orderBy(c => c.startTime).reverse();
            let activeCurrent = await announces.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Announces_Details.isDownloaded && c.startTime.isAfter(now))
                .orderBy(c => c.startTime).reverse();
            let beforeNow = await announces.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Announces_Details.isDownloaded && (c.daysEffected & today) == today && c.startTime.isBefore(now))
                .orderBy(c => c.startTime).reverse();
            let activeFuture = await announces.where(c => now.isBefore(c.startDate) && c.isActive && !c.Announces_Details.isDownloaded).orderBy(c => c.startDate).reverse();
            //take inactive announces where today is before the initial start date. sort them by start date and push them into array
            let inactiveFuture = await announces.where(c => !c.isActive && now.isBefore(c.startDate) && !c.Announces_Details.isDownloaded).orderBy(c => c.startDate);
            let past = await announces.where(c => now.isAfter(c.endDate) && !c.Announces_Details.isDownloaded).orderBy(c => c.startDate);
            announces = null;
            // push or unshift each announce to array unless its details are null.
            past.takeWhile(c => c.Announces_Details != null).forEach(c => c.Announces_Details.forEach(d => arrFiles.unshift(d)));
            past = null;
            inactiveFuture.takeWhile(c => c.Announces_Details != null).forEach(c => c.Announces_Details.forEach(d => arrFiles.unshift(d)));
            inactiveFuture = null;
            activeFuture.takeWhile(c => c.Announces_Details != null).forEach(c => c.Announces_Details.forEach(d => arrFiles.unshift(d)));
            activeFuture = null;
            beforeNow.takeWhile(c => c.Announces_Details != null).forEach(c => c.Announces_Details.forEach(d => arrFiles.unshift(d)));
            beforeNow = null;
            activeCurrent.takeWhile(c => c.Announces_Details != null).forEach(c => c.Announces_Details.forEach(d => arrFiles.unshift(d)));
            activeCurrent = null;
            afterNow.takeWhile(c => c.Announces_Details != null).forEach(c => c.Announces_Details.forEach(d => arrFiles.unshift(d)));
            afterNow = null;
            openingClosing.takeWhile(c => c.Announces_Details != null).forEach(c => c.Announces_Details.forEach(d => arrFiles.unshift(d)));
            openingClosing = null;
            if (arrFiles != null && arrFiles.length > 0) {
                arrFiles = await arrFiles.orderBy(c=> c.orderID).groupBy(c => c.FileName).select(c => c.first())
                    .select(c => {
                        return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName}
                    });
                this.logger.DownloadAnnounces(`Found ${arrFiles.length} announce files`);
                return arrFiles;
            }else{
                return [];
            }
        }
        catch (e) {
            this.logger.DownloadAnnounces(chalk.bgRed('Error fetching announces: ' + e.message));
            return [];
        }
    }


    // async checkDownloadsOld() {
    //     //console.log('announcelar basladi ' + new moment().utc(true).toJSON().substr(11, 19));
    //     let now = new moment().utc(true);
    //     let today = Math.pow(2, now.weekday());
    //     let arr = [];
    //     let announces = await (await this.announcesDB.getAnnounces());
    //     // first push opening and closing announces to array.
    //     let openingClosing = await announces.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Announces_Details.isDownloaded &&
    //         (c.isOpeningAnnounce || c.isClosingAnnounce)).orderBy(c => c.startTime).forEach(c => c.Announces_Details.forEach(d => arr.push(d)));
    //     openingClosing = null; //garbage collection
    //     if (arr != undefined && arr.length > 0) {
    //         arr = await arr.groupBy(c => c.FileName).select(c => c.first()).select(c => {
    //             return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName}
    //         });
    //         //console.log('opening closing bitti ' + new moment().utc(true).toJSON().substr(11, 19) + ' ' + arr.length + ' anons buldum');
    //         return arr;
    //     }
    //     // then push announces that are supposed to play today and after now.
    //     arr = [];
    //     let afterNow = await announces.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Announces_Details.isDownloaded &&
    //         (c.daysEffected & today) == today && c.startTime.isAfter(now)).orderBy(c => c.startTime).forEach(c => c.Announces_Details.forEach(d => arr.push(d)));
    //     afterNow = null; //garbage collection
    //     if (arr != undefined && arr.length > 0) {
    //         arr = await arr.groupBy(c => c.FileName).select(c => c.first()).select(c => {
    //             return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName}
    //         });
    //         return arr;
    //     }
    //     // push active future announces
    //     arr = [];
    //     let activeFuture = await announces.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Announces_Details.isDownloaded &&
    //         c.startTime.isAfter(now)).orderBy(c => c.startTime).forEach(c => c.Announces_Details.forEach(d => arr.push(d)));
    //     activeFuture = null; //garbage collection
    //     if (arr != undefined && arr.length > 0) {
    //         arr = await arr.groupBy(c => c.FileName).select(c => c.first()).select(c => {
    //             return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName}
    //         });
    //         return arr;
    //     }
    //     // then push announces that are supposed to play today and now is after play time.
    //     arr = [];
    //     let beforeNow = await announces.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Announces_Details.isDownloaded &&
    //         c.startTime.isBefore(now)).orderBy(c => c.startTime).forEach(c => c.Announces_Details.forEach(d => arr.push(d)));
    //     beforeNow = null; //garbage collection
    //     if (arr != undefined && arr.length > 0) {
    //         arr = await arr.groupBy(c => c.FileName).select(c => c.first()).select(c => {
    //             return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName}
    //         });
    //         return arr;
    //     }
    //
    //     // finally push future inactive announces
    //     arr = [];
    //     let inactiveFuture = await announces.where(c => now.isBefore(c.startDate) && !c.isActive && !c.Announces_Details.isDownloaded).orderBy(c.startDate)
    //         .forEach(c => c.Announces_Details.forEach(d => arr.push(d)));
    //     inactiveFuture = null; // garbage collection
    //     if (arr != undefined && arr.length > 0) {
    //         arr = await arr.groupBy(c => c.FileName).select(c => c.first()).select(c => {
    //             return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName}
    //         });
    //         return arr;
    //     }
    // }

    async announcesToDownload() {
        let newAnnounces = await this.checkDownloads();
        if (!newAnnounces)return [];
        if ( newAnnounces.length > 0) {
            let uniqueArray = await this.checkAlreadyHave(newAnnounces);
            if (uniqueArray != null && uniqueArray.length > 0) {
                newAnnounces = null;
                let downloadSize = uniqueArray.select(c => c.FileSize).sum();
                this.dm.clearQue();
                this.dm.setTotalSize(downloadSize);
                /*
                    uncomment these if we decide on que rather than array
                 */
                // await uniqueArray.forEach(announce => {
                //     let fname = announce.FileName;
                //     this.dm.addFileToQue(fname)
                // });
                await this.dm.addArrayToQue(uniqueArray);
                this.logger.DownloadAnnounces('We have ' + uniqueArray.length + ' unique announces to download.')
            }
        } else {
            return newAnnounces;
        }
    }

    async checkAlreadyHave(filesToCheck) {
        //get local files
        let fpath = this.sutils.AnnouncesSaveDir;
        let localFiles = await (await this.sutils.getFilesInfo(fpath));
        //remove the files we have from download list
        if (localFiles == undefined || localFiles == null || localFiles.length == 0) {
            this.logger.DownloadAnnounces('There are no announce files in drive. Adding ' + filesToCheck.length + ' files to announces downloader que.');
            return filesToCheck;
        }
        localFiles = await localFiles.select(c => {
            return { FileName: c.file, FileSize: c.size }
        });
        filesToCheck = await filesToCheck.select(c => {
            return { FileName: c.FileName, FileSize: c.FileSize }
        });
        //join files on db and files on local storage
        let join = filesToCheck.innerJoin(localFiles,
            (_filesToCheck) => {
                return _filesToCheck.FileName
            },
            (_localFiles) => {
                return _localFiles.FileName
            },
            (fc, lf) => {
                return {
                    dbFileName: fc.FileName,
                    dbFileSize: fc.FileSize,
                    localFileName: lf.FileName,
                    localFileSize: lf.FileSize
                }
            });
        //size and names are equal files, update db that they are isDownloaded=true
        let FilesWeAlreadyHave = join.where(c => c.dbFileSize == c.localFileSize);
        if (FilesWeAlreadyHave.length > 0) {
            FilesWeAlreadyHave.forEach(async c => {
                await this.announcesDB.updateAnnounceFileDownloadedStatus(true, c.dbFileName);
            });
        }
        //size not matches, find and delete them and set them isDownloaded=false
        let falseSize = join.where(c => c.dbFileSize != c.localFileSize);
        if (falseSize.length > 0) {
            falseSize = await falseSize.select(c => {
                return { FileName: c.localFileName }
            });
            falseSize.forEach(async c => {
                await this.announcesDB.updateAnnounceFileDownloadedStatus(false, c.FileName);
            });
            falseSize = _.remove(falseSize, ((c) => {
                return c.FileName != this.dm.CurrentFile
            }));
            if(falseSize.length > 0) {
                this.sutils.deleteFilesArray(falseSize, fpath);
                this.logger.DownloadAnnounces(chalk.red(`${falseSize.length} incomplete downloaded announces were deleted from drive`));
            }
        }
        //now get the filenames only from FilesWeAlreadyHave and remove them from original list and set them isDownloaded=false
        let fileNamesOnly = FilesWeAlreadyHave.select(c => c.dbFileName);
        let filesToDownload = filesToCheck.where(c => !fileNamesOnly.contains(c.FileName));
        if (filesToDownload != undefined && filesToDownload.length > 0) {
            filesToDownload.forEach(async c => {
                    await this.announcesDB.updateAnnounceFileDownloadedStatus(false, c.FileName);

            });
            return filesToDownload;
        } else {
            return [];
        }
    }

    // check local and db files, requeue files that arent downloaded.
    // async downloadSkippedFiles() {
    //     this.dm.on(this.dm.eventTypes.QueueIsEmpty, () => {
    //         let arr = [];
    //         let fpath = this.sutils.AnnouncesSaveDir;
    //         let databaseFiles = await(await(this.announcesDB.getAnnounces()));
    //         let localFiles = this.sutils.getFilesInfo(fpath);
    //         let remoteNames = databaseFiles.select((f) => {
    //             return f.FileName;
    //         });
    //         databaseFiles = null;
    //         let localNames = localFiles.select((f) => {
    //             return f.file;
    //         });
    //         localFiles = null;
    //         arr = remoteNames.where((c) => {
    //             localNames.contains(c) == false
    //         });
    //         if (arr.length > 0) {
    //             arr.forEach(file => {
    //                 this.dm.addFileToQue(file)
    //             });
    //             arr = null;
    //         }
    //     });
    // }

    async checkFileStatus() {
        let fpath = this.sutils.AnnouncesSaveDir;
        let remoteFiles = await this.announcesDB.getAnnounces();
        if (remoteFiles == undefined || remoteFiles.length == 0 || remoteFiles == null) return [];
        let localFiles = await this.sutils.getFilesInfo(fpath);
        if (localFiles == undefined || localFiles.length == 0) return;
        let databaseFiles = [];
        await remoteFiles.select(c => c.Announces_Details.forEach(d => databaseFiles.push(d)));
        databaseFiles = await databaseFiles.groupBy(c => c.FileName).select(c => c.first());
        remoteFiles = null;
        localFiles = await localFiles.select(c => {
            return { FileName: c.file, FileSize: c.size };
        });
        // join two tables for comparision
        let join = await databaseFiles.innerJoin(localFiles,
            (_databaseFiles) => {
                return _databaseFiles.FileName
            },
            (_localFiles) => {
                return _localFiles.FileName
            },
            (df, lf) => {
                return {
                    dbFileName: df.FileName,
                    dbFileSize: df.FileSize,
                    localFileName: lf.FileName,
                    localFileSize: lf.FileSize
                }
            });
        // filter, delete and set isDownloaded false files where sizes doesnt match with db.
        let falseSize = await join.where(c => c.dbFileSize != c.localFileSize);
        if(falseSize.length > 0) {
            falseSize = await falseSize.select(c => {
                return { FileName: c.localFileName }
            });
            falseSize.forEach(async c => {
                await this.announcesDB.updateAnnounceFileDownloadedStatus(false, c.FileName);
            });
            falseSize = _.remove(falseSize, ((c) => {
                return c.FileName != this.dm.CurrentFile
            }));
            if(falseSize.length > 0) {
                this.sutils.deleteFilesArray(falseSize, fpath);
                this.logger.DownloadAnnounces(chalk.red(`${falseSize.length} incomplete downloaded announces were deleted from drive`));
            }
        }
        // filtering and deleting files that are in drive but not on db.
        // push files directly into array, not as objects
        let localNames = await localFiles.select(c => {
            return c.FileName
        });
        // push db files names directly into array, not as objects
        let dbFileNames = await databaseFiles.select(c => {
            return c.FileName
        });
        //compare 2 arrays and push local files where it doesnt appear in db file names.
        let noShow = await localNames.filter(c => !dbFileNames.contains(c));
        // convert filenames to objects as deleteFilesArray method works with objects.
        if (noShow.length > 0) {
            noShow = await noShow.select(c => {
                return { FileName: c }
            });
            this.sutils.deleteFilesArray(noShow, fpath);
            this.logger.DownloadAnnounces(`${noShow.length} announces were deleted from drive as they were removed from newmanager.`)
        }
        // garbage collection
        noShow = null;
        falseSize = null;
        localFiles = null;
        databaseFiles = null;
    }
};

util.inherits(announcesDownloader, EventEmitter);
module.exports = announcesDownloader;
