import groovy.json.JsonSlurper
import org.apache.commons.io.IOUtils
import org.apache.http.client.utils.URLEncodedUtils
import org.apache.http.NameValuePair
import org.apache.http.message.BasicNameValuePair
import java.util.regex.Matcher
import java.util.zip.GZIPInputStream
import java.util.zip.InflaterInputStream

import org.serviio.library.metadata.MediaFileType
import org.serviio.library.online.ContentURLContainer
import org.serviio.library.online.PreferredQuality
import org.serviio.library.online.WebResourceContainer
import org.serviio.library.online.WebResourceItem
import org.serviio.library.online.WebResourceUrlExtractor


class YetAnotherSeasonVar extends WebResourceUrlExtractor {
    static final String baseURL = "seasonvar.ru"
    private static final Map<String, String> headers = [
        "User-Agent"     : "Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) \
AppleWebKit/601.1.46 (KHTML, like Gecko) \
Version/9.0 Mobile/13B143 Safari/601.1",
        "Accept"         : "*/*",
        "Accept-Language": "ru-RU",
    ]
    private static final Map<String, String> cookies = [
        "playerHtml": "true",
        "svid": "1428617_bdaa2d32cd6d1f864cba566da5949672"
    ]
    private HTTP connection = new HTTP(headers, [(baseURL): cookies])

    /**
     * Called once for the whole feed.
     * For each feed which needs a plugin Serviio tries to match all available plugins to the feed's URL by
     * calling this method and uses the first plugin that returns true. Use of regular expression is
     * recommended.
     *
     * @param url URL of the whole feed, as entered by the user
     * @return true if the feed's items can be processed by this plugin
     */
    @Override
    boolean extractorMatches(URL url) {
        return url ==~ /^(?:http?:\\/\\/)?(?:www\.)?seasonvar\.ru\\/.*/
    }

    /**
     * @return the version of this plugin. Defaults to “1” if the method is not implemented.
     */
    @Override
    int getVersion() {
        return 4
    }

    /**
     * @return the name of this plugin. Is mostly used for logging and reporting purposes.
     */
    @Override
    String getExtractorName() {
        return "${this.getClass().getName()} v${getVersion()}"
    }

    private Map<String, String> extractSeasonInfo(URL url, String preferredTranslation = null, String alreadyLoadedHTML = null) {
        String indexHTML = alreadyLoadedHTML ?: this.connection.GET(url).text
        Map<String, String> seasonInfo = ["type": "html5"]
        seasonInfo["title"] = Parser.title(indexHTML)
        seasonInfo["seasonId"] = Parser.currentSeasonNumber(indexHTML)
        seasonInfo["id"] = Parser.id(indexHTML)
        seasonInfo["serial"] = Parser.serial(indexHTML)
        seasonInfo["secure"] = Parser.secure(indexHTML)
        seasonInfo["time"] = Parser.time(indexHTML)
        String playerHTML = this.connection.POST("http://seasonvar.ru/player.php", seasonInfo).text
        Map<String, String> availableTranslations = Parser.translations(playerHTML)
        seasonInfo["playlist"] = availableTranslations.containsKey(preferredTranslation) ?
                availableTranslations.get(preferredTranslation) : Parser.playlist(playerHTML) // else default translation
        return seasonInfo
    }

    /**
     * Performs the extraction of basic information about the resource.
     * If the object cannot be constructed the method should return null or throw an exception.
     *
     * @param url URL of the resource to be extracted. The plugin will have to get the contents on the URL itself.
     * @param i Max. number of items the user prefers to get back or -1 for unlimited. It is
     * up to the plugin designer to decide how to limit the results (if at all).
     * @return an instance of org.serviio.library.online.WebResourceContainer.
     * These are the properties of the class:
     * • String title – title of the web resource; optional
     * • String thumbnailUrl – URL of the resource's thumbnail; optional
     * • List<org.serviio.library.online.WebResourceItem> items – list of extracted content items
     * WebResourceItem represents basic information about an item and has these properties:
     * • String title – title of the content item; mandatory
     * • Date releaseDate – release date of the content; optional
     * • Map<String,String> additionalInfo – a map of key – value pairs that can include
     * information needed to retrieve content URL of the item (see extractUrl() method)
     * • String cacheKey – if present, the URL extracted on Serviio startup will be cached, and
     * reused rather than re-extracted on subsequent feed refreshes, unless the item has explicitly
     * expired due to the related ContentURLContainer's expiresOn value or the feed is forcibly
     * refreshed by the user.
     */
    @Override
    protected WebResourceContainer extractItems(URL url, int maxItems) {
        String userPreferredTranslation = null
        boolean loadNextSeasons
        if (url.getQuery()) {
            Map<String, String> userParams = Utils.getQueryParams(url)
            url = new URL(url.protocol, url.host, url.path)  // cleanup user's query
            if (userParams.containsKey("t")) userPreferredTranslation = userParams.get("t")
            if (userParams.containsKey("next")) loadNextSeasons = userParams.get("next") == "true"
        }
        String currentSeasonHTML = this.connection.GET(url).text
        if (loadNextSeasons) {
            List<String> allSeasons = Parser.seasons(currentSeasonHTML)
            int currentSeasonNumber = allSeasons.findIndexOf {it.contains(url.path)}
            List<String> currentAndNextSeasons = allSeasons[currentSeasonNumber..allSeasons.size() - 1]
            List<Map<String, String>> currentAndNextSeasonsInfo = currentAndNextSeasons.eachWithIndex{ String entry, int i -> }  collect { seasonURL ->
                extractSeasonInfo(new URL(url.protocol, url.host, seasonURL), userPreferredTranslation) // TODO One unnecessary request
            }
            return new WebResourceContainer(
                title: "${currentAndNextSeasonsInfo[0]["title"]} \u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435",
                thumbnailUrl: "http://cdn.seasonvar.ru/oblojka/large/${currentAndNextSeasonsInfo[0]["id"]}.jpg",
                items: currentAndNextSeasonsInfo.collect { season ->
                    Object json = this.connection.GET(new URL(url.protocol, url.host, season["playlist"])).json
                    List<Map<String, String>> series = (List<Map<String, String>>) json["playlist"]
                    series.collect { item ->
                        new WebResourceItem(
                            title: /${season["seasonId"]} \u0441\u0435\u0437\u043e\u043d ${item["comment"].replace("<br>", " ")}/,
                            additionalInfo: ['link': item["file"]]
                        )
                    }
                }.flatten() as List<WebResourceItem> // TODO Fix me please!
            )
        } else {
            Map<String, String> seasonInfo = extractSeasonInfo(url, userPreferredTranslation, currentSeasonHTML)
            Object json = this.connection.GET(new URL(url.protocol, url.host, seasonInfo["playlist"])).json
            List<Map<String, String>> series = (List<Map<String, String>>) json["playlist"]
            return new WebResourceContainer(
                title: seasonInfo["title"],
                thumbnailUrl: "http://cdn.seasonvar.ru/oblojka/large/${seasonInfo["id"]}.jpg",
                items: series.collect { item ->
                    new WebResourceItem(
                        title: item["comment"].replace("<br>", " "),
                        additionalInfo: ['link': item["file"]]
                    )
                }
            )
        }
    }

    /**
     * This method is called once for each item included in the created WebResourceContainer.
     * Performs the actual extraction of content information using the provided information.
     * If the object cannot be constructed the method should return null or throw an exception.
     *
     * @param webResourceItem an instance of org.serviio.library.online.WebResourceItem, as created in
     * extractItems() method.
     * @param preferredQuality includes value (HIGH, MEDIUM, LOW) of enumeration
     * org.serviio.library.online.PreferredQuality. It should be taken into consideration if the
     * online service offers multiple quality-based renditions of the content.
     * @return an instance of org.serviio.library.online.ContentURLContainer.
     * These are the properties of the class:
     * • String contentUrl – URL of the feed item's content; mandatory
     * • String thumbnailUrl – URL of the feed item's thumbnail; optional
     * • org.serviio.library.metadata.MediaFileType fileType – file type of the feed item; default is VIDEO
     * • Date expiresOn – a date the feed item expires on. It can mean the item itself expires or the
     * item's contentUrl expires; the whole feed will be parsed again on the earliest expiry date of
     * all feed items; optional
     * • boolean expiresImmediately – if true Serviio will extract the URL again when the play
     * request is received to get URL that is valid for the whole playback duration. Note this is
     * related to the content URL only, the feed item itself should still be valid and available; optional
     * • String cacheKey – a unique identifier of the content (i.e. this item with this quality) used
     * as a key to technical metadata cache; required if either expiresOn and/or expiresImmediately is provided
     * • boolean live – identifies the content as a live stream; optional (default is false)
     * • String userAgent – specifies a particular User-Agent HTTP header to use when retrieving the content
     */
    @Override
    protected ContentURLContainer extractUrl(WebResourceItem webResourceItem, PreferredQuality preferredQuality) {
        return new ContentURLContainer(
            fileType: MediaFileType.VIDEO,
            thumbnailUrl: webResourceItem.additionalInfo.thumbnailUrl,
            contentUrl: webResourceItem.additionalInfo.link,
            userAgent: headers["User-Agent"]
        )
    }
}

class Utils {
    static Map<String, String> getQueryParams(URL url) {
        List<NameValuePair> params = URLEncodedUtils.parse(url.toURI(), "UTF-8")
        return params.collectEntries {
            String name = URLDecoder.decode(it.name, "UTF-8").toLowerCase()
            String value = URLDecoder.decode(it.value, "UTF-8").toLowerCase()
            if (name == "translation") name = "t" // Some legacy :)
            return [(name): value]}
    }
}

class Parser {
    static private Matcher match(String text, String pattern) {
        Matcher matcher = text =~ pattern
        matcher.find()
        return matcher
    }

    static String secure(String text) {
        Matcher matcher = match(text, /(?s)var data4play = .*?'secureMark': '([a-f0-9]+)'/)
        return matcher.group(1)
    }

    static String time(String text) {
        Matcher matcher = match(text, /(?s)var data4play = .*?'secureMark': '[a-f0-9]+',.*?'time': ([0-9]+).*?/)
        return matcher.group(1)
    }

    static String id(String text) {
        Matcher matcher = match(text, /data-id-season="(\d+)"/)
        return matcher.group(1)
    }

    static String serial(String text) {
        Matcher matcher = match(text, /data-id-serial="(\d+)"/)
        return matcher.group(1)
    }

    static Map<String, String> translations(String text) {
        Map<String, String> translationVariants = [:]
        try {
            String translationsList = match(text, /(?s)<ul class="pgs-trans"(.*?)<\/ul>/).group(0)
            Matcher matcher = match(translationsList, /(?s)<li data-click="translate[^>]*?>([^<]+)<\/li>[\s\n]*?<script>pl\[\d+] = "(.*?)";/)
            matcher.iterator().each {
                translationVariants[matcher.group(1).toLowerCase()] = matcher.group(2)
            }
        } catch (IllegalStateException) {}
        return translationVariants
    }

    static String playlist(String text) {
        Matcher matcher = match(text, /var pl = \{'0': "(.+)"};/)
        return matcher.group(1)
    }

    static String title(String text) {
        Matcher matcher = match(text, /(?s)<h1\s+class="pgs-sinfo-title".*?>[\u0410-\u044f]+\s+(.*?)\s+[\u0410-\u044f]+<\/h1>/)
        return matcher.group(1).replaceAll(/\s+/, " ")
    }

    static String currentSeasonNumber(String text) {
        try {
            Matcher matcher = match(text, /(?s)<h1\s+class="pgs-sinfo-title".*?([0-9]+)\s+[\u0410-\u044f]+\s+[\u0410-\u044f]+<\/h1>/)
            return matcher.group(1)
        } catch (IllegalStateException) {
            return "0"
        }
    }

    static List<String> seasons(String text) {
        Matcher matcher = match(text,/(?s)<h2>.*?href="(\/serial-\d+-[^-.]+(?:-\d+-(?:sezon|season))?\.html)".*?/)
        return matcher.iterator().collect { matcher.group(1) }.unique()
    }
}

class HTTP {
    private Proxy proxy
    private List<String> supportedEncodings = ["gzip", "deflate"]
    private Map<String, String> headers
    private CookieStore cookieJar = new CustomCookieStore()
    private CookieManager cookieManager = new CookieManager(this.cookieJar, CookiePolicy.ACCEPT_ALL)

    HTTP(Map<String,String> headers = [:], Map<String, Map<String, String>> cookies = [:], Proxy proxy = Proxy.NO_PROXY) {
        this.proxy = proxy
        this.headers = headers << ["Accept-Encoding" : supportedEncodings.join(", ")]
        CookieHandler.setDefault(this.cookieManager)
        cookies.each { domain, map ->
            map.each { key, value ->
                this.cookieJar.add(new URI(domain), new HttpCookie(key, value))
            }
        }
    }

    private HttpURLConnection makeRequest(URL url) {
        HttpURLConnection connection = url.openConnection(this.proxy) as HttpURLConnection
        this.headers.each { key, value -> connection.setRequestProperty(key, value) }
        return connection
    }

    Response GET(URL url) {
        HttpURLConnection connection = this.makeRequest(url)
        connection.setRequestMethod("GET")
        connection.connect()
        return new Response(connection)
    }

    Response GET(String url) {
        return GET(new URL(url))
    }

    Response POST(String  url, Map<String, String> params) {
        HttpURLConnection connection = this.makeRequest(new URL(url))
        connection.with {
            setRequestMethod("POST")
            setRequestProperty("X-Requested-With", "XMLHttpRequest")
            setRequestProperty("Content-type", "application/x-www-form-urlencoded")
            doOutput = true
        }
        String data = URLEncodedUtils.format(params.collect{key, value -> new BasicNameValuePair(key, value)}, "UTF-8")
/*        String data = params.collect
            {key, value -> /${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}/}
            .join("&")*/ // TODO https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/client/utils/URLEncodedUtils.html#format(java.util.List,%20java.lang.String)
        OutputStreamWriter writer = new OutputStreamWriter(connection.outputStream)
        writer.with {
            write(data)
            flush()
            close()
        }
        connection.connect()
        return new Response(connection)
    }

    class CustomCookieStore implements CookieStore {
        /* https://stackoverflow.com/questions/45677375/add-httpcookie-to-cookiestore */

        CookieStore store = new CookieManager().getCookieStore()

        void add(URI uri, HttpCookie cookie) {
            if (!cookie.domain) cookie.domain = uri.isAbsolute() ? ".$uri.host" : ".$uri.path"
            if (!cookie.path) cookie.path = uri.isAbsolute() ? uri.path : "/"
            if (cookie.version == 1) cookie.version = 0  // Compatibility with RFC 2109
            this.store.add(uri, cookie)
        }

        List<HttpCookie> get(URI uri) {
            return this.store.get(uri)
        }

        List<HttpCookie> getCookies() {
            return this.store.getCookies()
        }

        List<URI> getURIs() {
            return this.store.getURIs()
        }

        boolean remove(URI uri, HttpCookie cookie) {
            return this.store.remove(uri, cookie)
        }

        boolean removeAll() {
            return this.store.removeAll()
        }
    }

    class Response {
        final String text
        final int responseCode
        final URL url
        def json

        Response(HttpURLConnection connection){
            this.text = decodeInputStream(connection)
            this.responseCode = connection.getResponseCode()
            this.url = connection.getURL()
        }

        private def getJson() {
            if (!this.json) this.json = new JsonSlurper().parseText(this.text)
            return this.json
        }

        private static String decodeInputStream(HttpURLConnection connection) {
            String encoding = connection.getContentEncoding()
            switch (encoding) {
                case "gzip":
                    return IOUtils.toString(new GZIPInputStream(connection.inputStream), "UTF-8")
                case "deflate":
                    return IOUtils.toString(new InflaterInputStream(connection.inputStream), "UTF-8")
                case null:
                    return IOUtils.toString(connection.inputStream, "UTF-8")
                default:
                    throw new UnsupportedEncodingException("The server has returned an unsupported encoding: $encoding")
            }
        }
    }
}