/**
 * ProjectLaogai
 *
 * Copyright 2019-2020  <seil0@mosad.xyz>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 */

package org.mosad.seil0.projectlaogai.controller

import android.content.Context
import android.util.Log
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonParser
import kotlinx.coroutines.*
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cCourse
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.coursesCacheTime
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.mensaCacheTime
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.timetableCacheTime
import org.mosad.seil0.projectlaogai.util.*
import java.io.*
import java.util.*
import kotlin.Exception
import kotlin.collections.ArrayList

/**
 * TODO rework
 *  * move all write functions from TCorController to CacheController
 *  * init: check for blocking updates, read all data from cache, check for non blocking update
 *      * [x] save needed data to validate cache in separate file
 *  * new functions:
 *      * [x] updateTimetable(): get Timetable object from TCoRController, write object to file
 *      * [x] updateMensa(): get MensaMenu object from TCoRController, write object to file
 *  * [ ] save additional subject and lessons
 */
class CacheController(cont: Context) {

    private val className = "CacheController"
    private val context = cont

    init {
        val cal = Calendar.getInstance()
        val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_WEEK)
        val currentTime = System.currentTimeMillis() / 1000

        // check if we need to update the mensa data before displaying it
        cal.time = Date(mensaCacheTime * 1000)

        // if a) it's monday and the last cache update was on sunday or b) the cache is older than 24hr, update blocking
        if ((currentDay == Calendar.MONDAY && cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) || (currentTime - mensaMenu.meta.updateTime) > 86400) {
           Log.i(className, "update mensa blocking")
           GlobalScope.launch(Dispatchers.Default) {  updateMensaMenu(context).join() }
        }

        cal.time = Date(timetableCacheTime * 1000)

        // if a) it`s monday and the last cache update was not on a sunday or b) the cache is older than 24hr, update blocking
        if ((currentDay == Calendar.MONDAY && cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) || (currentTime - timetableCacheTime) > 86400) {
            Log.i(className, "updating timetable after sunday!")

            GlobalScope.launch(Dispatchers.Default) {
                val threads = listOf(
                    updateTimetable(cCourse.courseName, 0, context),
                    updateTimetable(cCourse.courseName, 1, context)
                )
                threads.joinAll()
            }
        }

        updateCourseList(context)

        readStartCache(cCourse.courseName) // initially read values from cache

        // check if an update is necessary, not blocking
        if (currentTime - coursesCacheTime > 86400)
            updateCourseList(context)

        if (currentTime - mensaCacheTime > 10800)
            updateMensaMenu(context)

        if (currentTime - timetableCacheTime > 10800) {
            updateTimetable(cCourse.courseName, 0, context)
            updateTimetable(cCourse.courseName, 1, context)
        }

    }

    companion object {
        private const val className = "CacheController"
        var coursesList = ArrayList<Course>()
        var mensaMenu = MensaMenu()

        /**
         * update the course list, async
         */
        fun updateCourseList(context: Context): Job {
            val file = File(context.filesDir, "courses.json")
            var courseListUp = CoursesList()

            return GlobalScope.launch(Dispatchers.IO) {
                try {
                    courseListUp = TCoRAPIController.getCourseListNEW()
                } catch (ex: Exception) {
                    Log.e(className, "could not load course list from tcor", ex)
                }

                // update coursesList array list
                coursesList = courseListUp.courses

                // save cache file and update time
                save(file, Gson().toJson(courseListUp))
                coursesCacheTime = System.currentTimeMillis() / 1000
                PreferencesController.save(context)
            }
        }

        /**
         * update the mensa menu, async
         */
        fun updateMensaMenu(context: Context): Job {
            val file = File(context.filesDir, "mensa.json")

            return GlobalScope.launch(Dispatchers.IO) {
                try {
                    mensaMenu = TCoRAPIController.getMensaMenu()
                } catch (ex: Exception) {
                    Log.e(className, "could not load mensa menu from tcor", ex)
                }

                // save cache file and update time
                save(file, Gson().toJson(mensaMenu))
                mensaCacheTime = System.currentTimeMillis() / 1000
                PreferencesController.save(context)
            }
        }

        /**
         * update the timetable for a week, async
         * @param courseName the course name (e.g AI1)
         * @param week the week to update (0 for the current and so on)
         */
        fun updateTimetable(courseName: String, week: Int, context: Context): Job {
            val file = File(context.filesDir, "timetable-$courseName-$week.json")
            var timetable = TimetableWeek()

            // try to update timetable from tcor, async
            return GlobalScope.launch(Dispatchers.IO) {
                try {
                    timetable = TCoRAPIController.getTimetable(courseName, week)
                } catch (ex: Exception) {
                    Log.e(className, "could not load timetable from tcor", ex)
                }

                // update timetable in TTC
                if (TimetableController.timetable.size > week) {
                    TimetableController.timetable[week] = timetable
                } else {
                    TimetableController.timetable.add(timetable)
                }

                // save cache file and update time
                save(file, Gson().toJson(timetable))
                timetableCacheTime = System.currentTimeMillis() / 1000
                PreferencesController.save(context)
            }
        }

        private fun save(file: File, text: String) {
            try {
                val writer = BufferedWriter(FileWriter(file))
                writer.write(text)
                writer.close()
            } catch (ex: Exception) {
                Log.e(className, "failed to write file \"${file.absoluteFile}\"", ex)
            }

        }

    }

    /**
     * read coursesList, mensa (current and next week), timetable (current and next week)
     * @param courseName the course name (e.g AI1)
     */
    private fun readStartCache(courseName: String) {
        try {
            readCoursesList(context)
        } catch (ex : Exception) {
            Log.e(className, "Error while reading the course list", ex)
        }

        try {
            readMensa(context)
        } catch (ex : Exception) {
            Log.e(className, "Error while reading the mensa menu", ex)
        }

        try {
            readTimetable(courseName, 0, context)
        } catch (ex : Exception) {
            Log.e(className, "Error while reading timetable week 0", ex)
        }

        try {
            readTimetable(courseName, 1, context)
        } catch (ex : Exception) {
            Log.e(className, "Error while reading timetable week 1", ex)
        }

    }

    /**
     * read the courses list from the cached file
     * add them to the coursesList object
     */
    private fun readCoursesList(context: Context) {
        val file = File(context.filesDir, "courses.json")

        // make sure the file exists
        if (!file.exists()) {
            runBlocking { updateCourseList(context).join() }
            return
        }

        coursesList = FileReader(file).use {
            GsonBuilder().create().fromJson(BufferedReader(it).readLine(), CoursesList().javaClass).courses
        }
    }

    /**
     * get the MensaMenu object from the cached json,
     * if cache is empty create the cache file
     */
    private fun readMensa(context: Context) {
        val file = File(context.filesDir, "mensa.json")

        // make sure the file exists
        if (!file.exists()) {
            runBlocking { updateMensaMenu(context).join() }
            return
        }

        mensaMenu = FileReader(file).use {
            GsonBuilder().create().fromJson(BufferedReader(it).readLine(), MensaMenu().javaClass)
        }
    }

    /**
     * read the weeks timetable from the cached file
     * @param courseName the course name (e.g AI1)
     * @param week the week to read (0 for the current and so on)
     */
    private fun readTimetable(courseName: String, week: Int, context: Context) {
        val file = File(context.filesDir, "timetable-$courseName-$week.json")

        // if the file does not exist, call updateTimetable blocking and return
        if (!file.exists()) {
            runBlocking { updateTimetable(courseName, week, context).join() }
            return
        }

        val timetableObject = FileReader(file).use {
            JsonParser.parseString(BufferedReader(it).readLine()).asJsonObject
        }

        // if its a TimetableCourseWeek object migrate to TimetableWeek TODO remove converting at version 0.8
        val timetable = if(timetableObject.has("meta")) {
            Log.i(Companion.className, "trying to migrate TimetableCourseWeek to TimetableWeek")
            val timetableWC = Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass)
            save(file, Gson().toJson(TimetableWeek(
                timetableWC.meta.weekIndex,
                timetableWC.meta.weekNumberYear,
                timetableWC.timetable.days
            )))
            TimetableWeek(timetableWC.meta.weekIndex, timetableWC.meta.weekNumberYear, timetableWC.timetable.days)
        } else {
            Gson().fromJson(timetableObject, TimetableWeek().javaClass)
        }

        // update timetable in TTC
        if (TimetableController.timetable.size > week) {
            TimetableController.timetable[week] = timetable
        } else {
            TimetableController.timetable.add(timetable)
        }
    }

}