package org.schabi.newpipe.extractor.services.youtube;

import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.utils.JavaScript;
import org.schabi.newpipe.extractor.utils.Parser;
import org.schabi.newpipe.extractor.utils.StringUtils;

import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * <p>
 * YouTube's media is protected with a cipher,
 * which modifies the "n" query parameter of it's video playback urls.
 * This class handles extracting that "n" query parameter,
 * applying the cipher on it and returning the resulting url which is not throttled.
 * </p>
 *
 * <p>
 * https://r5---sn-4g5ednsz.googlevideo.com/videoplayback?n=VVF2xyZLVRZZxHXZ&other=other
 * </p>
 * becomes
 * <p>
 * https://r5---sn-4g5ednsz.googlevideo.com/videoplayback?n=iHywZkMipkszqA&other=other
 * </p>
 * <br>
 * <p>
 * Decoding the "n" parameter is time intensive. For this reason, the results are cached.
 * The cache can be cleared using {@link #clearCache()}
 * </p>
 *
 */
public class YoutubeThrottlingDecrypter {

    private static final Pattern N_PARAM_PATTERN = Pattern.compile("[&?]n=([^&]+)");
    private static final Pattern FUNCTION_NAME_PATTERN = Pattern.compile(
            "b=a\\.get\\(\"n\"\\)\\)&&\\(b=(\\w+)\\(b\\),a\\.set\\(\"n\",b\\)");

    private static final Map<String, String> nParams = new HashMap<>();

    private final String functionName;
    private final String function;

    /**
     * <p>
     * Use this if you care about the off chance that YouTube tracks with which videoId the cipher
     * is requested.
     * </p>
     * Otherwise use the no-arg constructor which uses a constant value.
     */
    public YoutubeThrottlingDecrypter(final String videoId) throws ParsingException {
        final String playerJsCode = YoutubeJavaScriptExtractor.extractJavaScriptCode(videoId);

        functionName = parseDecodeFunctionName(playerJsCode);
        function = parseDecodeFunction(playerJsCode, functionName);
    }

    public YoutubeThrottlingDecrypter() throws ParsingException {
        final String playerJsCode = YoutubeJavaScriptExtractor.extractJavaScriptCode();

        functionName = parseDecodeFunctionName(playerJsCode);
        function = parseDecodeFunction(playerJsCode, functionName);
    }

    private String parseDecodeFunctionName(final String playerJsCode)
            throws Parser.RegexException {
        return Parser.matchGroup1(FUNCTION_NAME_PATTERN, playerJsCode);
    }

    @Nonnull
    private String parseDecodeFunction(final String playerJsCode, final String functionName)
            throws Parser.RegexException {
        try {
            return parseWithParenthesisMatching(playerJsCode, functionName);
        } catch (Exception e) {
            return parseWithRegex(playerJsCode, functionName);
        }
    }

    private String parseWithParenthesisMatching(final String playerJsCode, final String functionName) {
        final String functionBase = functionName + "=function";
        return functionBase + StringUtils.matchToClosingParenthesis(playerJsCode, functionBase) + ";";
    }

    private String parseWithRegex(final String playerJsCode, final String functionName) throws Parser.RegexException {
        Pattern functionPattern = Pattern.compile(functionName + "=function(.*?}};)\n",
                Pattern.DOTALL);
        return "function " + functionName + Parser.matchGroup1(functionPattern, playerJsCode);
    }

    public String apply(final String url) throws Parser.RegexException {
        if (containsNParam(url)) {
            String oldNParam = parseNParam(url);
            String newNParam = decryptNParam(oldNParam);
            return replaceNParam(url, oldNParam, newNParam);
        } else {
            return url;
        }
    }

    private boolean containsNParam(final String url) {
        return Parser.isMatch(N_PARAM_PATTERN, url);
    }

    private String parseNParam(final String url) throws Parser.RegexException {
        return Parser.matchGroup1(N_PARAM_PATTERN, url);
    }

    private String decryptNParam(final String nParam) {
        if (nParams.containsKey(nParam)) {
            return nParams.get(nParam);
        }
        final String decryptedNParam = JavaScript.run(function, functionName, nParam);
        nParams.put(nParam, decryptedNParam);
        return decryptedNParam;
    }

    @Nonnull
    private String replaceNParam(@Nonnull final String url,
                                 final String oldValue,
                                 final String newValue) {
        return url.replace(oldValue, newValue);
    }

    /**
     * @return the number of the cached "n" query parameters.
     */
    public static int getCacheSize() {
        return nParams.size();
    }

    /**
     * Clears all stored "n" query parameters.
     */
    public static void clearCache() {
        nParams.clear();
    }
}
