package app.furmeet.events.system.schedule.managers

import app.furmeet.events.system.helper.*
import app.furmeet.events.system.schedule.abstractDataTypes.GregorianCalendarRange
import app.furmeet.events.system.schedule.abstractDataTypes.Icons
import app.furmeet.events.system.schedule.abstractDataTypes.convention.*
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.io.InputStream
import java.util.*


object ConventionJsonReader {
    internal class UuidHashResolver {
        val conventionSeries = mutableMapOf<String, ConventionSeries>()
        val conventions = mutableMapOf<String, Convention>()
        val events = mutableMapOf<String, Event>()
        val places = mutableMapOf<String, Place>()
        val registrationTiers = mutableMapOf<String, RegistrationTier>()
        val socialMedias = mutableMapOf<String, SocialMedia>()
        val tags = mutableMapOf<String, Tag>()
        val languageStrings = mutableMapOf<String, Map<Language, String>>()
        val additionalRules = mutableMapOf<String, AddtionalRules>()
        val banners = mutableMapOf<String, Banner>()
        val maps = mutableMapOf<String, MapImage>()
        val registrationLinks = mutableMapOf<String, RegistrationLink>()
        val announcements = mutableMapOf<String, Announcement>()
        val urgentMessages = mutableMapOf<String, UrgentMessage>()
        val conventionColorSchemes = mutableMapOf<String, ConventionColorScheme>()
    }

    fun readFromFile(file: String) =
            readFromFile(File(file))

    fun readFromFile(file: File) =
            readFromJson(file.readText(Charsets.UTF_8))

    fun readFromInputStream(inputStream: InputStream): ConventionSeries =
            readFromJson(inputStream.bufferedReader(Charsets.UTF_8).readText())

    fun readFromByteArray(byteArray: ByteArray): ConventionSeries =
            readFromInputStream(byteArray.inputStream())

    fun readFromJson(json: String) =
            readFromJson(JSONObject(json))

    fun readFromJson(json: JSONObject) =
            getConventionSeries(json, UuidHashResolver())

    private fun getConventionSeries(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver): ConventionSeries {
        val uuid = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.conventionSeries[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val name = parseLanguageString(jsonObject.getJSONObject("name"), uuidHashResolver)
        val language = parseLanguage(jsonObject.getString("language"))
        val timezone = parseTimeZone(jsonObject.getJSONObject("timezone"))
        val statute = parseLanguageString(jsonObject.getJSONObject("statute"), uuidHashResolver)
        val startupTerms = parseLanguageString(jsonObject.getJSONObject("startup_terms"), uuidHashResolver).let { map ->
            if (map.values.filterNot { it.isBlank() }.isEmpty()) null else map
        }
        val socialMedias = parseSocialMedias(jsonObject.getJSONArray("social_medias"), uuidHashResolver)
        val editions = parseConventions(jsonObject.getJSONArray("editions"), uuidHashResolver, statute, socialMedias, name)
        val featured = uuidHashResolver.conventions[jsonObject.getString("featured")]
                ?: uuidHashResolver.conventions.values.first()
        val defaultBanner = parseLanguageImage(jsonObject.getJSONObject("default_banner"), uuidHashResolver)!!
        val banners = parseBanners(jsonObject.getJSONArray("banners"), uuidHashResolver)
        val announcements = parseAnnouncements(jsonObject.getJSONArray("announcements"), uuidHashResolver, timezone)
        val urgentMessage = jsonObject.optJSONObject("urgent_message")?.let { parseUrgentMessage(it, uuidHashResolver) }
        val hasCountdownOnSplash = jsonObject.getBoolean("countdown_on_splash")
        return ConventionSeries(
                name,
                featured,
                editions,
                timezone,
                socialMedias,
                language,
                statute,
                startupTerms,
                defaultBanner,
                banners,
                1.0 / 6,
                announcements,
                urgentMessage,
                hasCountdownOnSplash
        ).apply {
            uuidHashResolver.conventionSeries[uuid] = this
        }
    }

    private fun parseAnnouncements(jsonArray: JSONArray, uuidHashResolver: UuidHashResolver, timezone: TimeZone): List<Announcement> =
            jsonArray.asSequence<JSONObject>().map { parseAnnouncement(it, uuidHashResolver, timezone) }.toList()

    private fun parseAnnouncement(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver, timezone: TimeZone): Announcement {
        val uuid = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.announcements[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val title = parseLanguageString(jsonObject.getJSONObject("title"), uuidHashResolver)
        val body = parseLanguageString(jsonObject.getJSONObject("body"), uuidHashResolver)
        val link = parseLanguageString(jsonObject.getJSONObject("link"), uuidHashResolver)
        val created = parseGregCal(jsonObject.getJSONObject("created"))
        val modified = parseGregCal(jsonObject.getJSONObject("modified"))
        created.timeInMillis += timezone.rawOffset
        modified.timeInMillis += timezone.rawOffset
        return Announcement(
                uuid,
                title,
                body,
                link,
                created,
                modified
        ).apply {
            uuidHashResolver.announcements[uuid] = this
        }
    }

    private fun parseBanners(jsonArray: JSONArray, uuidHashResolver: UuidHashResolver) =
            jsonArray.asSequence<JSONObject>().map { parseBanner(it, uuidHashResolver) }.toList()

    private fun parseBanner(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver): Banner {
        val uuid = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.banners[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val alias = jsonObject.getString("alias")
        val image = parseLanguageImage(jsonObject.getJSONObject("banner"), uuidHashResolver)!!
        val lifetime = GregorianCalendarRange(
                parseGregCal(jsonObject.getJSONObject("show_up_after")),
                parseGregCal(jsonObject.getJSONObject("hide_after"))
        )
        return Banner(
                alias,
                lifetime,
                image
        ).apply {
            uuidHashResolver.banners[uuid] = this
        }
    }

    private fun parseConventions(jsonArray: JSONArray, uuidHashResolver: UuidHashResolver, statute: Map<Language, String>, socialMedias: List<SocialMedia>, name: Map<Language, String>): List<Convention> =
            jsonArray.asSequence<JSONObject>().map {
                parseConvention(it, uuidHashResolver, statute, socialMedias, name)
            }.toList()

    private fun parseConvention(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver, rules: Map<Language, String>, socialMedias: List<SocialMedia>, genericName: Map<Language, String>): Convention {
        val uuid = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.conventions[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val notificationFireMinutesBefore = jsonObject.getInt("fire_notifications_n_minutes_before")
        val paddingStartOfDayMillis = parseGregCal(jsonObject.getJSONObject("convention_day_start_time")).let {
            val hr = it.get(GregorianCalendar.HOUR_OF_DAY)
            val mn = it.get(GregorianCalendar.MINUTE)
            val sc = it.get(GregorianCalendar.SECOND)
            ((hr * 3600L) + (mn * 60L) + sc) * 1000L
        }
        val paddingEndOfDayMillis = parseGregCal(jsonObject.getJSONObject("convention_day_end_time")).let {
            val hr = it.get(GregorianCalendar.HOUR_OF_DAY)
            val mn = it.get(GregorianCalendar.MINUTE)
            val sc = it.get(GregorianCalendar.SECOND)
            ((hr * 3600L) + (mn * 60L) + sc) * 1000L
        }.let {
            if (it == 0L)
                24 * 3600 * 1000L
            else
                it
        }
        val name = parseLanguageString(jsonObject.getJSONObject("name"), uuidHashResolver)
        val places = parsePlaces(jsonObject.getJSONArray("places"), uuidHashResolver)
        val tags = parseTags(jsonObject.getJSONArray("tags"), uuidHashResolver)
        val hashtagReminder = jsonObject.getString("hashtag_reminder")
        val theme = parseLanguageString(jsonObject.getJSONObject("theme"), uuidHashResolver)
        val registrationTiers = parseRegistrationTiers(jsonObject.getJSONArray("registration_tiers"), uuidHashResolver)
        val lowestRegistrationTier = uuidHashResolver.registrationTiers[jsonObject.getString("lowest_registration_tier")]!!
        val language = parseLanguage(jsonObject.getString("language"))
        val r621checker = jsonObject.getBoolean("rule_621_checker")
        val skipRegistrationTier = jsonObject.getBoolean("do_not_ask_registration_tier")
        val maps = parseMaps(jsonObject.getJSONArray("maps"), uuidHashResolver)
        val imageDefaultEvent = parseLanguageImage(jsonObject.getJSONObject("image_default_event"), uuidHashResolver)
        val imageFavorites = parseLanguageImage(jsonObject.getJSONObject("image_favorites"), uuidHashResolver)
        val splitDayIn = jsonObject.getInt("split_day_into_n_parts")
        val timeZone = parseTimeZone(jsonObject.getJSONObject("timezone"))
        val nextEdition = parseGregCal(jsonObject.getJSONObject("next_edition"))
        val officialLifespan = GregorianCalendarRange(
                parseGregCal(jsonObject.getJSONObject("ceremony_opening_time")),
                parseGregCal(jsonObject.getJSONObject("ceremony_closing_time"))
        )
        val registrationLinks = parseRegistrationLinks(jsonObject.getJSONArray("registration_links"), uuidHashResolver)
        val additionalRules = parseAdditionalRulesPlural(jsonObject.getJSONArray("additional_rules"), uuidHashResolver)
        val events = parseEvents(jsonObject.getJSONArray("events"), uuidHashResolver, language)
        val eventDates = events.flatMap {
            Pair(it.timeRange.start, it.timeRange.finish).toList()
        }.distinctBy { it.timeInMillis }.sortedBy { it.timeInMillis }
        val startCalendarOn = eventDates.first()
        val endCalendarOn = eventDates.last()
        val days = events.map {
            it.timeRange.start.dayRange.start
        }.distinctBy { it.timeInMillis }.sortedBy { it.timeInMillis }
        val colorScheme = parseConventionColorSchemeOpt(
            jsonObject.getJSONObject("color_scheme"),
            uuidHashResolver
        )
        return Convention(
                events,
                places,
                registrationTiers,
                lowestRegistrationTier,
                tags,
                uuid,
                name[language]!!,
                genericName[language]!!,
                theme,
                rules,
                additionalRules,
                officialLifespan,
                startCalendarOn,
                endCalendarOn,
                paddingStartOfDayMillis,
                paddingEndOfDayMillis,
                days,
                splitDayIn,
                timeZone,
                socialMedias,
                notificationFireMinutesBefore,
                r621checker,
                skipRegistrationTier,
                maps,
                registrationLinks,
                imageDefaultEvent,
                imageFavorites,
                hashtagReminder,
                nextEdition,
                colorScheme,
        ).apply { uuidHashResolver.conventions[uuid] = this }
    }

    private fun parseConventionColorSchemeOpt(
        jsonObject: JSONObject?,
        uuidHashResolver: UuidHashResolver
    ): ConventionColorScheme? =
        jsonObject?.let { parseConventionColorScheme(it, uuidHashResolver) }

    private fun parseConventionColorScheme(
        jsonObject: JSONObject,
        uuidHashResolver: UuidHashResolver
    ): ConventionColorScheme {
        val uuid = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.conventionColorSchemes[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val alias = jsonObject.getString("alias")
        val primaryDarker = jsonObject.getString("primary_darker").toColor()
        val primaryRegular = jsonObject.getString("primary_regular").toColor()
        val primaryLighter = jsonObject.getString("primary_lighter").toColor()
        val accentColor = jsonObject.getString("accent_color").toColor()
        return ConventionColorScheme(
            alias,
            primaryDarker,
            primaryRegular,
            primaryLighter,
            accentColor,
        ).apply { uuidHashResolver.conventionColorSchemes[uuid] = this }
    }

    private fun parseRegistrationLinks(jsonArray: JSONArray, uuidHashResolver: UuidHashResolver): List<RegistrationLink> =
            jsonArray.asSequence<JSONObject>().map { parseRegistrationLink(it, uuidHashResolver) }.toList()

    private fun parseRegistrationLink(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver): RegistrationLink {
        val uuid = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.registrationLinks[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val ordering = jsonObject.getInt("ordering")
        val alias = jsonObject.getString("alias")
        val name = parseLanguageString(jsonObject.getJSONObject("name"), uuidHashResolver)
        val url = parseLanguageString(jsonObject.getJSONObject("url"), uuidHashResolver)
        val color = parseColorNullable(jsonObject.getJSONObject("color"))
        val lifespan = GregorianCalendarRange(
                parseGregCal(jsonObject.getJSONObject("appears")),
                parseGregCal(jsonObject.getJSONObject("vanishes"))
        )
        return RegistrationLink(
                alias,
                ordering,
                name,
                url,
                color,
                lifespan
        ).apply { uuidHashResolver.registrationLinks[uuid] = this }
    }

    private fun parseColorNullable(jsonObject: JSONObject?): Int? =
            nullAndLogIfException { parseColor(jsonObject!!) }


    private fun parseMaps(jsonArray: JSONArray, uuidHashResolver: UuidHashResolver): List<MapImage> =
            jsonArray.asSequence<JSONObject>().map { parseMap(it, uuidHashResolver) }.toList()

    private fun parseMap(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver): MapImage {
        val uuid = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.maps[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val alias = jsonObject.getString("alias")
        val name = parseLanguageString(jsonObject.getJSONObject("name"), uuidHashResolver)
        val image = parseLanguageImage(jsonObject.getJSONObject("image"), uuidHashResolver)!!
        return MapImage(
                alias,
                name,
                image
        ).apply { uuidHashResolver.maps[uuid] = this }
    }

    private fun parseAdditionalRulesPlural(jsonArray: JSONArray, uuidHashResolver: UuidHashResolver): List<AddtionalRules> =
            jsonArray.asSequence<JSONObject>().map {
                parseAdditionalRules(it, uuidHashResolver)
            }.toList()

    private fun parseAdditionalRules(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver): AddtionalRules {
        val uuid = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.additionalRules[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val names = parseLanguageString(jsonObject.getJSONObject("name"), uuidHashResolver)
        val rules = parseLanguageString(jsonObject.getJSONObject("rules"), uuidHashResolver)
        return AddtionalRules(
                names,
                rules
        ).apply { uuidHashResolver.additionalRules[uuid] = this }
    }

    private fun parseEvents(jsonArray: JSONArray, uuidHashResolver: UuidHashResolver, conventionLanguage: Language): List<Event> =
            jsonArray.asSequence<JSONObject>().map {
                parseEvent(it, uuidHashResolver, conventionLanguage)
            }.toList()

    private fun parseEvent(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver, conventionLanguage: Language): Event {
        val uuid = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.events[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val conbookId = jsonObject.getString("conbook_key")
        val conbookPages = jsonObject.getJSONArray("conbook_pages").toList<Int>()
        val titles = parseLanguageString(jsonObject.getJSONObject("title"), uuidHashResolver)
        val subtitles = parseLanguageString(jsonObject.getJSONObject("subtitle"), uuidHashResolver)
        val descriptions = parseLanguageString(jsonObject.getJSONObject("description"), uuidHashResolver)
        val timeRange = GregorianCalendarRange(
                parseGregCal(jsonObject.getJSONObject("time_start")),
                parseGregCal(jsonObject.getJSONObject("time_end"))
        )
        val places = jsonObject.getJSONArray("places").toList<String>().map { uuidHashResolver.places[it]!! }
        val tags = jsonObject.getJSONArray("categories").toList<String>().map { uuidHashResolver.tags[it]!! }
        val attendableBy = jsonObject.getJSONArray("attendable_by").toList<String>().map { uuidHashResolver.registrationTiers[it]!! }
        val language = parseLanguage(jsonObject.getString("language")).let {
            if (it == conventionLanguage) null else it
        }
        val hiddenFromTimeTable = jsonObject.getBoolean("hidden_from_time_table")
        val assetImage = parseLanguageImage(jsonObject.getJSONObject("picture"), uuidHashResolver)
        return Event(
                uuid,
                conbookId,
                conbookPages,
                titles,
                subtitles,
                descriptions,
                timeRange,
                places,
                attendableBy,
                tags,
                language,
                hiddenFromTimeTable,
                assetImage
        ).apply { uuidHashResolver.events[uuid] = this }
    }

    private fun parseGregCal(jsonObject: JSONObject): GregorianCalendar =
            GregorianCalendar(
                    jsonObject.getInt("yr"),
                    jsonObject.getInt("mo").minus(1),
                    jsonObject.getInt("dy"),
                    jsonObject.getInt("hr"),
                    jsonObject.getInt("mn"),
                    jsonObject.getInt("sc")
            )

    private fun parseRegistrationTiers(jsonArray: JSONArray, uuidHashResolver: UuidHashResolver): List<RegistrationTier> =
            jsonArray.asSequence<JSONObject>().map {
                parseRegistrationTier(it, uuidHashResolver)
            }.toList()

    private fun parseRegistrationTier(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver): RegistrationTier {
        val uuid = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.registrationTiers[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val level = jsonObject.getInt("level")
        val name = parseLanguageString(jsonObject.getJSONObject("tier"), uuidHashResolver)
        val benefits = jsonObject.getJSONArray("benefits").asSequence<JSONObject>().map {
            parseLanguageString(it, uuidHashResolver)
        }.toList()
        return RegistrationTier(
                level,
                name,
                benefits
        ).apply { uuidHashResolver.registrationTiers[uuid] = this }
    }

    private fun parseTags(jsonArray: JSONArray, uuidHashResolver: UuidHashResolver): List<Tag> =
            jsonArray.asSequence<JSONObject>().map {
                parseTag(it, uuidHashResolver)
            }.toList()

    private fun parseTag(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver): Tag {
        val uuid = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.tags[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val label = parseLanguageString(jsonObject.getJSONObject("label"), uuidHashResolver)
        val color = parseColor(jsonObject.getJSONObject("color"))
        return Tag(label, color).apply { uuidHashResolver.tags[uuid] = this }
    }

    private fun parsePlaces(jsonArray: JSONArray, uuidHashResolver: UuidHashResolver): List<Place> =
            jsonArray.asSequence<JSONObject>().map {
                parsePlace(it, uuidHashResolver)
            }.toList()

    private fun parsePlace(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver): Place {
        val uuid = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.places[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val label = parseLanguageString(jsonObject.getJSONObject("label"), uuidHashResolver)
        val color = parseColor(jsonObject.getJSONObject("color"))
        return Place(label, color).apply { uuidHashResolver.places[uuid] = this }
    }

    private fun parseSocialMedias(jsonArray: JSONArray, uuidHashResolver: UuidHashResolver): List<SocialMedia> =
            jsonArray.asSequence<JSONObject>().map {
                parseSocialMedia(it, uuidHashResolver)
            }.toList()

    private fun parseSocialMedia(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver): SocialMedia {
        val uuid = jsonObject.getString("alias")
        val previouslyKnown = uuidHashResolver.socialMedias[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val name = parseLanguageString(jsonObject.getJSONObject("name"), uuidHashResolver)
        val url = parseLanguageString(jsonObject.getJSONObject("url"), uuidHashResolver)
        val color = parseColor(jsonObject.getJSONObject("color"))
        val icon = jsonObject.getString("icon").let { icon -> Icons.values().firstOrNull { it.name == icon } }?.drawableId
                ?: Icons.ic_help_outline_black_24dp.drawableId
        val sm = SocialMedia(
                name,
                url,
                icon,
                color
        ).apply { uuidHashResolver.socialMedias[uuid] = this }
        return sm
    }

    private fun parseUrgentMessage(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver): UrgentMessage {
        val uuid = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.urgentMessages[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val joCard = jsonObject.getJSONObject("card")
        val joTab = jsonObject.getJSONObject("tab")
        return UrgentMessage(
                uuid,
                CustomCard(
                        parseLanguageString(joCard.getJSONObject("name"), uuidHashResolver),
                        parseLanguageString(joCard.getJSONObject("description"), uuidHashResolver),
                        joCard.getString("icon").let { icon -> Icons.values().firstOrNull { it.name == icon } }
                                ?: Icons.ic_warning
                ),
                CustomTab(
                        parseLanguageString(joTab.getJSONObject("name"), uuidHashResolver),
                        parseLanguageString(joTab.getJSONObject("description"), uuidHashResolver),
                        parseLanguageString(joTab.getJSONObject("text"), uuidHashResolver)
                )
        ).apply { uuidHashResolver.urgentMessages[uuid] = this }
    }

    private fun parseColor(jsonObject: JSONObject): Int =
            jsonObject
                    .getString("color")
                    .toColor()

    private fun parseTimeZone(jsonObject: JSONObject): TimeZone {
        val name = jsonObject.getString("name")
        val hr = jsonObject.getInt("hr")
        val mn = jsonObject.getInt("mn")
        val offset = hr * 3600 * 1000 + (mn * 60 * 1000 * (if (hr >= 0) 1 else -1))
        return SimpleTimeZone(offset, name)
    }

    private fun parseLanguage(string: String): Language {
        return Language.values().firstOrNull { it.locale.language == string } ?: Language.EN
    }

    private fun parseLanguageImage(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver): Map<Language, String>? =
            parseLanguageString(jsonObject, uuidHashResolver, true)
                    .filterNot {
                        it.value.isBlank()
                    }.let {
                        if (it.isEmpty()) null else it
                    }

    private fun parseLanguageString(jsonObject: JSONObject, uuidHashResolver: UuidHashResolver, isPath: Boolean = false): Map<Language, String> {
        val uuid: String? = jsonObject.getString("uuid")
        val previouslyKnown = uuidHashResolver.languageStrings[uuid]
        if (previouslyKnown != null) return previouslyKnown
        val langString = Language.values().map {
            it to
                    (if (jsonObject.has(it.locale.language)) jsonObject.getString(it.locale.language) else "").let {
                        if (isPath)
                            it.trimStart('/').trim()
                        else
                            it
                    }
        }.toMap()
        uuid?.let { uuidHashResolver.languageStrings[it] = langString }
        return langString
    }
}