/*
 * OfflineMusicService.kt
 * Copyright (C) 2009-2021 Ultrasonic developers
 *
 * Distributed under terms of the GNU GPLv3 license.
 */
package org.moire.ultrasonic.service

import android.media.MediaMetadataRetriever
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import java.io.InputStream
import java.io.Reader
import java.lang.Math.min
import java.util.ArrayList
import java.util.HashSet
import java.util.LinkedList
import java.util.Locale
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Bookmark
import org.moire.ultrasonic.domain.ChatMessage
import org.moire.ultrasonic.domain.Genre
import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.domain.JukeboxStatus
import org.moire.ultrasonic.domain.Lyrics
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.domain.Playlist
import org.moire.ultrasonic.domain.PodcastsChannel
import org.moire.ultrasonic.domain.SearchCriteria
import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.domain.Share
import org.moire.ultrasonic.domain.UserInfo
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util
import timber.log.Timber

// TODO: There are quite a number of deeply nested and complicated functions in this class..
// Simplify them :)
@Suppress("TooManyFunctions")
class OfflineMusicService : MusicService, KoinComponent {
    private val activeServerProvider: ActiveServerProvider by inject()

    override fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index> {
        val indexes: MutableList<Index> = ArrayList()
        val root = FileUtil.musicDirectory
        for (file in FileUtil.listFiles(root)) {
            if (file.isDirectory) {
                val index = Index(file.path)
                index.id = file.path
                index.index = file.name.substring(0, 1)
                index.name = file.name
                indexes.add(index)
            }
        }
        val ignoredArticlesString = "The El La Los Las Le Les"
        val ignoredArticles = COMPILE.split(ignoredArticlesString)
        indexes.sortWith { lhsArtist, rhsArtist ->
            var lhs = lhsArtist.name!!.lowercase(Locale.ROOT)
            var rhs = rhsArtist.name!!.lowercase(Locale.ROOT)
            val lhs1 = lhs[0]
            val rhs1 = rhs[0]
            if (Character.isDigit(lhs1) && !Character.isDigit(rhs1)) {
                return@sortWith 1
            }
            if (Character.isDigit(rhs1) && !Character.isDigit(lhs1)) {
                return@sortWith -1
            }
            for (article in ignoredArticles) {
                var index = lhs.indexOf(
                    String.format(Locale.ROOT, "%s ", article.lowercase(Locale.ROOT))
                )
                if (index == 0) {
                    lhs = lhs.substring(article.length + 1)
                }
                index = rhs.indexOf(
                    String.format(Locale.ROOT, "%s ", article.lowercase(Locale.ROOT))
                )
                if (index == 0) {
                    rhs = rhs.substring(article.length + 1)
                }
            }
            lhs.compareTo(rhs)
        }

        return indexes
    }

    override fun getMusicDirectory(
        id: String,
        name: String?,
        refresh: Boolean
    ): MusicDirectory {
        val dir = File(id)
        val result = MusicDirectory()
        result.name = dir.name

        val seen: MutableCollection<String?> = HashSet()

        for (file in FileUtil.listMediaFiles(dir)) {
            val filename = getName(file)
            if (filename != null && !seen.contains(filename)) {
                seen.add(filename)
                result.addChild(createEntry(file, filename))
            }
        }

        return result
    }

    override fun search(criteria: SearchCriteria): SearchResult {
        val artists: MutableList<Artist> = ArrayList()
        val albums: MutableList<MusicDirectory.Entry> = ArrayList()
        val songs: MutableList<MusicDirectory.Entry> = ArrayList()
        val root = FileUtil.musicDirectory
        var closeness: Int
        for (artistFile in FileUtil.listFiles(root)) {
            val artistName = artistFile.name
            if (artistFile.isDirectory) {
                if (matchCriteria(criteria, artistName).also { closeness = it } > 0) {
                    val artist = Artist(artistFile.path)
                    artist.index = artistFile.name.substring(0, 1)
                    artist.name = artistName
                    artist.closeness = closeness
                    artists.add(artist)
                }
                recursiveAlbumSearch(artistName, artistFile, criteria, albums, songs)
            }
        }

        artists.sort()
        albums.sort()
        songs.sort()

        return SearchResult(artists, albums, songs)
    }

    @Suppress("NestedBlockDepth", "TooGenericExceptionCaught")
    override fun getPlaylists(refresh: Boolean): List<Playlist> {
        val playlists: MutableList<Playlist> = ArrayList()
        val root = FileUtil.getPlaylistDirectory()
        var lastServer: String? = null
        var removeServer = true
        for (folder in FileUtil.listFiles(root)) {
            if (folder.isDirectory) {
                val server = folder.name
                val fileList = FileUtil.listFiles(folder)
                for (file in fileList) {
                    if (FileUtil.isPlaylistFile(file)) {
                        val id = file.name
                        val filename = server + ": " + FileUtil.getBaseName(id)
                        val playlist = Playlist(server, filename)
                        playlists.add(playlist)
                    }
                }
                if (server != lastServer && !fileList.isEmpty()) {
                    if (lastServer != null) {
                        removeServer = false
                    }
                    lastServer = server
                }
            } else {
                // Delete legacy playlist files
                try {
                    if (!folder.delete()) {
                        Timber.w("Failed to delete old playlist file: %s", folder.name)
                    }
                } catch (e: Exception) {
                    Timber.w(e, "Failed to delete old playlist file: %s", folder.name)
                }
            }
        }
        if (removeServer) {
            for (playlist in playlists) {
                playlist.name = playlist.name.substring(playlist.id.length + 2)
            }
        }
        return playlists
    }

    @Throws(Exception::class)
    override fun getPlaylist(id: String, name: String): MusicDirectory {
        var playlistName = name
        var reader: Reader? = null
        var buffer: BufferedReader? = null

        return try {
            val firstIndex = playlistName.indexOf(id)
            if (firstIndex != -1) {
                playlistName = playlistName.substring(id.length + 2)
            }
            val playlistFile = FileUtil.getPlaylistFile(id, playlistName)
            reader = FileReader(playlistFile)
            buffer = BufferedReader(reader)
            val playlist = MusicDirectory()
            var line = buffer.readLine()
            if ("#EXTM3U" != line) return playlist
            while (buffer.readLine().also { line = it } != null) {
                val entryFile = File(line)
                val entryName = getName(entryFile)
                if (entryFile.exists() && entryName != null) {
                    playlist.addChild(createEntry(entryFile, entryName))
                }
            }
            playlist
        } finally {
            Util.close(buffer)
            Util.close(reader)
        }
    }

    @Suppress("TooGenericExceptionCaught")
    @Throws(Exception::class)
    override fun createPlaylist(id: String?, name: String?, entries: List<MusicDirectory.Entry>) {
        val playlistFile =
            FileUtil.getPlaylistFile(activeServerProvider.getActiveServer().name, name)
        val fw = FileWriter(playlistFile)
        val bw = BufferedWriter(fw)
        try {
            fw.write("#EXTM3U\n")
            for (e in entries) {
                var filePath = FileUtil.getSongFile(e).absolutePath
                if (!File(filePath).exists()) {
                    val ext = FileUtil.getExtension(filePath)
                    val base = FileUtil.getBaseName(filePath)
                    filePath = "$base.complete.$ext"
                }
                fw.write(
                    """
    $filePath
    
                    """.trimIndent()
                )
            }
        } catch (ignored: Exception) {
            Timber.w("Failed to save playlist: %s", name)
        } finally {
            bw.close()
            fw.close()
        }
    }

    override fun getRandomSongs(size: Int): MusicDirectory {
        val root = FileUtil.musicDirectory
        val children: MutableList<File> = LinkedList()
        listFilesRecursively(root, children)
        val result = MusicDirectory()
        if (children.isEmpty()) {
            return result
        }
        children.shuffle()
        val finalSize: Int = min(children.size, size)
        for (i in 0 until finalSize) {
            val file = children[i % children.size]
            result.addChild(createEntry(file, getName(file)))
        }
        return result
    }

    @Throws(Exception::class)
    override fun deletePlaylist(id: String) {
        throw OfflineException("Playlists not available in offline mode")
    }

    @Throws(Exception::class)
    override fun updatePlaylist(id: String, name: String?, comment: String?, pub: Boolean) {
        throw OfflineException("Updating playlist not available in offline mode")
    }

    @Throws(Exception::class)
    override fun getLyrics(artist: String, title: String): Lyrics? {
        throw OfflineException("Lyrics not available in offline mode")
    }

    @Throws(Exception::class)
    override fun scrobble(id: String, submission: Boolean) {
        throw OfflineException("Scrobbling not available in offline mode")
    }

    @Throws(Exception::class)
    override fun getAlbumList(
        type: String,
        size: Int,
        offset: Int,
        musicFolderId: String?
    ): MusicDirectory {
        throw OfflineException("Album lists not available in offline mode")
    }

    @Throws(Exception::class)
    override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
        throw OfflineException("Jukebox not available in offline mode")
    }

    @Throws(Exception::class)
    override fun skipJukebox(index: Int, offsetSeconds: Int): JukeboxStatus {
        throw OfflineException("Jukebox not available in offline mode")
    }

    @Throws(Exception::class)
    override fun stopJukebox(): JukeboxStatus {
        throw OfflineException("Jukebox not available in offline mode")
    }

    @Throws(Exception::class)
    override fun startJukebox(): JukeboxStatus {
        throw OfflineException("Jukebox not available in offline mode")
    }

    @Throws(Exception::class)
    override fun getJukeboxStatus(): JukeboxStatus {
        throw OfflineException("Jukebox not available in offline mode")
    }

    @Throws(Exception::class)
    override fun setJukeboxGain(gain: Float): JukeboxStatus {
        throw OfflineException("Jukebox not available in offline mode")
    }

    @Throws(Exception::class)
    override fun getStarred(): SearchResult {
        throw OfflineException("Starred not available in offline mode")
    }

    @Throws(Exception::class)
    override fun getSongsByGenre(genre: String, count: Int, offset: Int): MusicDirectory {
        throw OfflineException("Getting Songs By Genre not available in offline mode")
    }

    @Throws(Exception::class)
    override fun getGenres(refresh: Boolean): List<Genre>? {
        throw OfflineException("Getting Genres not available in offline mode")
    }

    @Throws(Exception::class)
    override fun getUser(username: String): UserInfo {
        throw OfflineException("Getting user info not available in offline mode")
    }

    @Throws(Exception::class)
    override fun createShare(
        ids: List<String>,
        description: String?,
        expires: Long?
    ): List<Share> {
        throw OfflineException("Creating shares not available in offline mode")
    }

    @Throws(Exception::class)
    override fun getShares(refresh: Boolean): List<Share> {
        throw OfflineException("Getting shares not available in offline mode")
    }

    @Throws(Exception::class)
    override fun deleteShare(id: String) {
        throw OfflineException("Deleting shares not available in offline mode")
    }

    @Throws(Exception::class)
    override fun updateShare(id: String, description: String?, expires: Long?) {
        throw OfflineException("Updating shares not available in offline mode")
    }

    @Throws(Exception::class)
    override fun star(id: String?, albumId: String?, artistId: String?) {
        throw OfflineException("Star not available in offline mode")
    }

    @Throws(Exception::class)
    override fun unstar(id: String?, albumId: String?, artistId: String?) {
        throw OfflineException("UnStar not available in offline mode")
    }

    @Throws(Exception::class)
    override fun getMusicFolders(refresh: Boolean): List<MusicFolder> {
        throw OfflineException("Music folders not available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun getAlbumList2(
        type: String,
        size: Int,
        offset: Int,
        musicFolderId: String?
    ): MusicDirectory {
        throw OfflineException("getAlbumList2 isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun getVideoUrl(id: String): String? {
        throw OfflineException("getVideoUrl isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun getChatMessages(since: Long?): List<ChatMessage?>? {
        throw OfflineException("getChatMessages isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun addChatMessage(message: String) {
        throw OfflineException("addChatMessage isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun getBookmarks(): List<Bookmark?>? {
        throw OfflineException("getBookmarks isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun deleteBookmark(id: String) {
        throw OfflineException("deleteBookmark isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun createBookmark(id: String, position: Int) {
        throw OfflineException("createBookmark isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun getVideos(refresh: Boolean): MusicDirectory? {
        throw OfflineException("getVideos isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun getStarred2(): SearchResult {
        throw OfflineException("getStarred2 isn't available in offline mode")
    }

    override fun ping() {
        // Void
    }

    override fun isLicenseValid(): Boolean = true

    @Throws(OfflineException::class)
    override fun getArtists(refresh: Boolean): List<Artist> {
        throw OfflineException("getArtists isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory {
        throw OfflineException("getArtist isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory {
        throw OfflineException("getAlbum isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun getPodcastEpisodes(podcastChannelId: String?): MusicDirectory? {
        throw OfflineException("getPodcastEpisodes isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun getDownloadInputStream(
        song: MusicDirectory.Entry,
        offset: Long,
        maxBitrate: Int,
        save: Boolean
    ): Pair<InputStream, Boolean> {
        throw OfflineException("getDownloadInputStream isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun setRating(id: String, rating: Int) {
        throw OfflineException("setRating isn't available in offline mode")
    }

    @Throws(OfflineException::class)
    override fun getPodcastsChannels(refresh: Boolean): List<PodcastsChannel> {
        throw OfflineException("getPodcastsChannels isn't available in offline mode")
    }

    companion object {
        private val COMPILE = Pattern.compile(" ")
        private fun getName(file: File): String? {
            var name = file.name
            if (file.isDirectory) {
                return name
            }
            if (name.endsWith(".partial") || name.contains(".partial.") ||
                name == Constants.ALBUM_ART_FILE
            ) {
                return null
            }
            name = name.replace(".complete", "")
            return FileUtil.getBaseName(name)
        }

        @Suppress("TooGenericExceptionCaught", "ComplexMethod", "LongMethod", "NestedBlockDepth")
        private fun createEntry(file: File, name: String?): MusicDirectory.Entry {
            val entry = MusicDirectory.Entry(file.path)
            entry.isDirectory = file.isDirectory
            entry.parent = file.parent
            entry.size = file.length()
            val root = FileUtil.musicDirectory.path
            entry.path = file.path.replaceFirst(
                String.format(Locale.ROOT, "^%s/", root).toRegex(), ""
            )
            entry.title = name
            if (file.isFile) {
                var artist: String? = null
                var album: String? = null
                var title: String? = null
                var track: String? = null
                var disc: String? = null
                var year: String? = null
                var genre: String? = null
                var duration: String? = null
                var hasVideo: String? = null
                try {
                    val mmr = MediaMetadataRetriever()
                    mmr.setDataSource(file.path)
                    artist = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
                    album = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM)
                    title = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
                    track = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER)
                    disc = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER)
                    year = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR)
                    genre = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE)
                    duration = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
                    hasVideo = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
                    mmr.release()
                } catch (ignored: Exception) {
                }
                entry.artist = artist ?: file.parentFile!!.parentFile!!.name
                entry.album = album ?: file.parentFile!!.name
                if (title != null) {
                    entry.title = title
                }
                entry.isVideo = hasVideo != null
                Timber.i("Offline Stuff: %s", track)
                if (track != null) {
                    var trackValue = 0
                    try {
                        val slashIndex = track.indexOf('/')
                        if (slashIndex > 0) {
                            track = track.substring(0, slashIndex)
                        }
                        trackValue = track.toInt()
                    } catch (ex: Exception) {
                        Timber.e(ex, "Offline Stuff")
                    }
                    Timber.i("Offline Stuff: Setting Track: %d", trackValue)
                    entry.track = trackValue
                }
                if (disc != null) {
                    var discValue = 0
                    try {
                        val slashIndex = disc.indexOf('/')
                        if (slashIndex > 0) {
                            disc = disc.substring(0, slashIndex)
                        }
                        discValue = disc.toInt()
                    } catch (ignored: Exception) {
                    }
                    entry.discNumber = discValue
                }
                if (year != null) {
                    var yearValue = 0
                    try {
                        yearValue = year.toInt()
                    } catch (ignored: Exception) {
                    }
                    entry.year = yearValue
                }
                if (genre != null) {
                    entry.genre = genre
                }
                if (duration != null) {
                    var durationValue: Long = 0
                    try {
                        durationValue = duration.toLong()
                        durationValue = TimeUnit.MILLISECONDS.toSeconds(durationValue)
                    } catch (ignored: Exception) {
                    }
                    entry.setDuration(durationValue)
                }
            }
            entry.suffix = FileUtil.getExtension(file.name.replace(".complete", ""))
            val albumArt = FileUtil.getAlbumArtFile(entry)
            if (albumArt.exists()) {
                entry.coverArt = albumArt.path
            }
            return entry
        }

        @Suppress("NestedBlockDepth")
        private fun recursiveAlbumSearch(
            artistName: String,
            file: File,
            criteria: SearchCriteria,
            albums: MutableList<MusicDirectory.Entry>,
            songs: MutableList<MusicDirectory.Entry>
        ) {
            var closeness: Int
            for (albumFile in FileUtil.listMediaFiles(file)) {
                if (albumFile.isDirectory) {
                    val albumName = getName(albumFile)
                    if (matchCriteria(criteria, albumName).also { closeness = it } > 0) {
                        val album = createEntry(albumFile, albumName)
                        album.artist = artistName
                        album.closeness = closeness
                        albums.add(album)
                    }
                    for (songFile in FileUtil.listMediaFiles(albumFile)) {
                        val songName = getName(songFile)
                        if (songFile.isDirectory) {
                            recursiveAlbumSearch(artistName, songFile, criteria, albums, songs)
                        } else if (matchCriteria(criteria, songName).also { closeness = it } > 0) {
                            val song = createEntry(albumFile, songName)
                            song.artist = artistName
                            song.album = albumName
                            song.closeness = closeness
                            songs.add(song)
                        }
                    }
                } else {
                    val songName = getName(albumFile)
                    if (matchCriteria(criteria, songName).also { closeness = it } > 0) {
                        val song = createEntry(albumFile, songName)
                        song.artist = artistName
                        song.album = songName
                        song.closeness = closeness
                        songs.add(song)
                    }
                }
            }
        }

        private fun matchCriteria(criteria: SearchCriteria, name: String?): Int {
            val query = criteria.query.lowercase(Locale.ROOT)
            val queryParts = COMPILE.split(query)
            val nameParts = COMPILE.split(
                name!!.lowercase(Locale.ROOT)
            )
            var closeness = 0
            for (queryPart in queryParts) {
                for (namePart in nameParts) {
                    if (namePart == queryPart) {
                        closeness++
                    }
                }
            }
            return closeness
        }

        private fun listFilesRecursively(parent: File, children: MutableList<File>) {
            for (file in FileUtil.listMediaFiles(parent)) {
                if (file.isFile) {
                    children.add(file)
                } else {
                    listFilesRecursively(file, children)
                }
            }
        }
    }
}
