const
    util = require('util'),
    EventEmitter = require('events').EventEmitter,
    moment = require('moment'),
    DownloadManager = require('./DownloadManager'),
    chalk = require('chalk'),
    _ = require('lodash');

const schedulesDownloader = class schedulesDownloader {
    constructor({db, config, utils,schedulesdb, logger} = {}) {
        this.schedulesDB = schedulesdb;
        this.db = db;
        this.sutils = utils;
        this.logger = logger;
        this.dm = new DownloadManager({options: {directory: utils.SchedulesSaveDir, speed: 0}, config, utils, logger});
        this.dm.start();
        this.config = config;

        this.eventTypes = {
            onProgress: 'onProgress',
            onDownloadCompleted: 'onDownloadCompleted',
            onError: 'onError',
            onListCompleted: 'onListCompleted'
        };

        this.db.on(this.db.eventTypes.Schedules_Details_bulkInsert, async (arr) => {
            this.logger.DownloadSchedules('Bulk insert occured, we have ' + arr.length + ' schedule files');
            await  this.start();
        });
        this.dm.on(this.dm.eventTypes.DownloadCompleted, async (pageStatus, pageMessage, filename)=>{
            //console.log('File Download finished')
            if(pageStatus==200) {
                await this.schedulesDB.updateScheduleFileDownloadedStatus(1, filename);
                this.logger.DownloadSchedules(chalk.bgGreen(`${filename} is set Downloaded in signage.db`));
                // v8.7.9 send Download file status
                this.emit(this.eventTypes.onDownloadCompleted, pageStatus, pageMessage, filename);
            }else{
                await this.schedulesDB.updateScheduleFileDownloadedStatus(0, 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.SetScheduleDownloadedStatus, async (status, file) => {
            await this.schedulesDB.updateScheduleFileDownloadedStatus(status, file);
        });


    }




     async start() {
        if (!this.dm.checkTime()){
            this.logger.DownloadSchedules('Not a good time to download.');
            return;
        }
        this.dm.clearQue();
        await this.checkFiles();
        await this.downloadSchedules();
    }

    async checkDownloads() {
        let availableDownloadTime = await this.db.getAvailableDownloadTime();
        if (availableDownloadTime.length==0){
            this.logger.DownloadSchedules('database is empty and cannot take available download times, skipping');
            return;
        }

        let arrFiles = [];
        // let stt = availableDownloadTime[0].startTime.toJSON().substr(12,7);
        // console.log(stt);
        this.dm.startTime = availableDownloadTime[0].startTime.toJSON().substr(11,8);
        this.dm.endTime = availableDownloadTime[0].endTime.toJSON().substr(11,8);
        this.dm.options.speed = availableDownloadTime[0].speedLimit;
        let now = moment().utc(true);
        let today = Math.pow(2, now.weekday());
        try {
            let schedules = await this.schedulesDB.getSchedules();
            if (schedules == null || schedules.length == 0){
                this.logger.DownloadSchedules('schedules is empty');
                return [];
            }
            schedules = schedules.where(c => !c.Schedules_Details.isDownloaded);

            //let hasPriorityToday = schedules.where(c => c.isActive && (c.daysEffected & today) == today && now.isBetween(c.startDate, c.endDate, null, '[]') && now.isBetween(c.startTime, c.endTime, null, '[]') ).orderBy(c => c.hasPriority).thenBy(c => c.orderID);
            let hasPriorityToday = _.remove(schedules, (c) => {
                return c.isActive &&  (c.daysEffected & today) == today && now.isBetween(c.startDate, c.endDate, null, '[]') && now.isBetween(c.startTime, c.endTime, null, '[]')
            });
            if(hasPriorityToday != undefined) {
                hasPriorityToday = _.orderBy(hasPriorityToday, ['hasPriority'], ['desc']);
                arrFiles.push(hasPriorityToday);
                hasPriorityToday = null;
            }
            //let afterNowToday = schedules.where(c => c.isActive && (c.daysEffected & today) == today && now.isBetween(c.startDate, c.endDate, null, '[]') && );
            let afterNowToday = _.remove(schedules, (c) => {
                return c.isActive && (c.daysEffected & today) == today && now.isBefore(c.startTime, 'hour')
            });
            if(afterNowToday != undefined) {
                arrFiles = _.orderBy(arrFiles, ['hasPriority'], ['desc']);
                arrFiles.push(afterNowToday);
                afterNowToday = null;
            }
            //let current = schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive).orderBy(c => c.hasPriority).thenByDescending(c => c.startTime).thenBy(c => c.orderID);
            let current = _.remove(schedules, (c) => {
                return now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive
            });
            if(current != undefined) {
                current = _.orderBy(current, ['hasPriority', 'startTime'], ['desc', 'asc']);
                arrFiles.push(current);
                current = null;
            }
            //let future = schedules.where(c => now.isBefore(c.startDate)).orderBy(c => c.isActive).thenByDescending(c => c.hasPriority).thenBy(c => c.orderID);
            let future = _.remove(schedules, (c) => {
                return now.isSameOrBefore(c.startDate, 'day')
            });
            if(future != undefined) {
                future = _.orderBy(future, ['hasPriority'], ['desc']);
                arrFiles.push(future);
                future = null;
            }
            //let before = schedules.where(c => now.isAfter(c.endDate)).orderBy(c => c.isActive).thenBy(c => c.hasPriority).thenBy(c => c.orderID);
            let before = _.remove(schedules, (c) => {
                return  now.isAfter(c.endDate, 'day')
            });
            if(before != undefined) {
                before = _.orderBy(before, ['hasPriority'], ['desc']);
                arrFiles.push(before);
                before = null;
            }
            if(schedules.length > 0) {
                arrFiles.push(schedules);
            }

            if (arrFiles != undefined && arrFiles != null && arrFiles.length > 0) {
                arrFiles = _.flattenDeep(arrFiles);
                let q  = [];
                await arrFiles.forEach(c => c.Schedules_Details.forEach(d => q.push(d)));
                q = await q.groupBy(c => c.FileName).select(c => c.first()).select(c => {
                    return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName, orderID: c.orderID}
                });
                this.logger.DownloadSchedules(`Found ${q.length} schedule files`);
                return q;
            } else {
                return [];
            }
        } catch(ex) {
            this.logger.DownloadSchedules(chalk.bgRed('Error fetching schedules files: ' + ex.message));
            return [];
        }
    }

    // async checkDownloadsOld() {
    //     //console.log('schedulelar 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 schedules = await (await this.schedulesDB.getSchedules());
    //     // push files that are supposed to today, has priority and is after current time.
    //     let hasPriorityTodayAfter = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Schedules_Details.isDownloaded && c.hasPriority &&
    //         (c.daysEffected & today) == today && c.startTime.isAfter(now)).orderBy(c => c.startTime).forEach(c => c.Schedules_Details.forEach(d => arr.push(d)));
    //     hasPriorityTodayAfter = null;
    //     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('priority bitti ' + new moment().utc(true).toJSON().substr(11, 19) + ' ' + arr.length + ' dosya buldum.');
    //         return arr;
    //     }
    //     // push schedules that are supposed to play today and after current time
    //     arr = [];
    //     let afterNow = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Schedules_Details.isDownloaded &&
    //         (c.daysEffected & today) == today && c.startTime.isAfter(now)).orderBy(c => c.startTime).forEach(c => c.Schedules_Details.forEach(d => arr.push(d)));
    //     afterNow = null;
    //     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 schedules with priority
    //     arr = [];
    //     let hasPriority = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Schedules_Details.isDownloaded && c.hasPriority)
    //         .orderBy(c => c.startDate).forEach(c => c.Schedules_Details.forEach(d => arr.push(d)));
    //     hasPriority = null;
    //     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 schedules where we are between start and end dates.
    //     arr = [];
    //     let activeFuture = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Schedules_Details.isDownloaded)
    //         .orderBy(c => c.startTime).forEach(c => c.Schedules_Details.forEach(d => arr.push(d)));
    //     activeFuture = null;
    //     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 schedules wjere we are between start and end dates but play time already passed.
    //     arr = [];
    //     let beforeNow = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Schedules_Details.isDownloaded && c.startTime.isBefore(now))
    //         .orderBy(c => c.startTime).forEach(c => c.Schedules_Details.forEach(d => arr.push(d)));
    //     beforeNow = null;
    //     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 inactive future schedules
    //     arr = [];
    //     let inactiveFuture = await schedules.where(c => now.isBefore(c.startDate) && !c.isActive && !c.Schedules_Details.isDownloaded).orderBy(c => c.hasPriority).thenBy(c => c.startDate)
    //         .forEach(c => c.Schedules_Details.forEach(d => arr.push(d)));
    //     inactiveFuture = null;
    //     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 downloadSchedules() {
        let newSchedules = await this.checkDownloads();
        if (!newSchedules) {
            this.logger.DownloadSchedules('seems there is nothing to download');
            return;
        }
        let uniqueArray = await this.checkAlreadyHave(newSchedules);
        if (uniqueArray == undefined || uniqueArray.length == 0 || uniqueArray == null) return newSchedules;
        newSchedules = null;
        let downloadSize = uniqueArray.select(c => c.FileSize).sum();
        //clear que before sending new list
        this.dm.clearQue();
        this.dm.setTotalSize(downloadSize);
        // uncomment below if we decide on queue rather than array
        // await uniqueArray.forEach(file => {
        //     let fname = file.FileName;
        //     this.dm.addFileToQue(fname)
        // });
        await this.dm.addArrayToQue(uniqueArray);
        this.logger.DownloadSchedules('We have ' + uniqueArray.length + ' unique schedule files to download.');
    }

    async checkAlreadyHave(filesToCheck) {
        //get local files
        let fpath = this.sutils.SchedulesSaveDir;
        let localFiles = await this.sutils.getFilesInfo(fpath);
        //remove the files we have from download list
        if (localFiles == undefined || localFiles == null || localFiles.length == 0) {
            this.logger.DownloadSchedules('There are no schedule files in drive. Adding ' + filesToCheck.length + ' files to schedules 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.schedulesDB.updateScheduleFileDownloadedStatus(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.forEach(async c => {
                // added c.FileName because falseSize didnt have 'FileName' and update file download status and deleteFilesArray uses FileName.
                c.FileName = c.localFileName;
                await this.schedulesDB.updateScheduleFileDownloadedStatus(false, c.FileName);
            });
            falseSize = _.remove(falseSize, ((c) => {
                return c.FileName != this.dm.CurrentFile
            }));
            if(falseSize.length > 0) {
                this.sutils.deleteFilesArray(falseSize, fpath);
                this.logger.DownloadSchedules(`${falseSize.length} incomplete downloaded files 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.schedulesDB.updateScheduleFileDownloadedStatus(false, c.FileName)
            });
            return filesToDownload;
        } else {
            return [];
        }
    }

    async checkFiles() {
        let fpath = this.sutils.SchedulesSaveDir;
        let remoteFiles = await this.schedulesDB.getSchedules();
        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.Schedules_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 = _.remove(falseSize, ((c) => {
                return c.FileName != this.dm.CurrentFile
            }));
            if(falseSize.length > 0) {
                this.sutils.deleteFilesArray(falseSize, fpath);
                this.logger.DownloadSchedules(`${falseSize.length} incomplete downloaded files were deleted from drive`);
            }
        }

        // after deleting false size files, we only have correct size files, set them isDownloaded incase its cold start
        let correctSize = await this.sutils.getFilesInfo(fpath);
        correctSize.forEach( (c) => {
            this.schedulesDB.updateScheduleFileDownloadedStatus(true, c.file);
        });
        correctSize = null;

        // filtering and deleting files that are in drive but not on db.
        // push db and local file names directly into array, not as objects
        let localNames = await localFiles.select(c => c.FileName);
        let dbFileNames = await databaseFiles.select(c => 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.System(chalk.blue(`${noShow.length} schedule files were deleted from drive as they were removed from newmanager.`));
        }
        // garbage collection
        noShow = null;
        falseSize = null;
        localFiles = null;
        databaseFiles = null;
    }
};

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