package f.f.freezer;

import android.util.Log;

import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.tag.FieldKey;
import org.jaudiotagger.tag.Tag;
import org.jaudiotagger.tag.TagOptionSingleton;
import org.jaudiotagger.tag.flac.FlacTag;
import org.jaudiotagger.tag.id3.ID3v23Tag;
import org.jaudiotagger.tag.id3.valuepair.ImageFormats;
import org.jaudiotagger.tag.images.Artwork;
import org.jaudiotagger.tag.images.ArtworkFactory;
import org.jaudiotagger.tag.reference.PictureTypes;
import org.json.JSONArray;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.RandomAccessFile;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Scanner;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.HttpsURLConnection;

public class Deezer {

    DownloadLog logger;
    String token;
    String arl;
    String sid;
    boolean authorized = false;
    boolean authorizing = false;

    Deezer() {}

    //Initialize for logging
    void init(DownloadLog logger, String arl) {
        this.logger = logger;
        this.arl = arl;

        //Load native
        System.loadLibrary("decryptor-jni");
    }

    //Authorize GWLight API
    public void authorize() {
        if (!authorized || sid == null || token == null) {
            authorizing = true;
            try {
                callGWAPI("deezer.getUserData", "{}");
                authorized = true;
            } catch (Exception e) {
                logger.warn("Error authorizing to Deezer API! " + e.toString());
            }
        }
        authorizing = false;
    }

    public native void decryptFile(String trackId, String inputFilename, String outputFilename);

    public JSONObject callGWAPI(String method, String params) throws Exception {
        //Get token
        if (token == null) {
            token = "null";
            callGWAPI("deezer.getUserData", "{}");
        }

        //Call
        URL url = new URL("https://www.deezer.com/ajax/gw-light.php?method=" + method + "&input=3&api_version=1.0&api_token=" + token);
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
        connection.setConnectTimeout(20000);
        connection.setDoOutput(true);
        connection.setRequestMethod("POST");
        connection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36");
        connection.setRequestProperty("Accept-Language", "*");
        connection.setRequestProperty("Content-Type", "application/json");
        connection.setRequestProperty("Accept", "*/*");
        connection.setRequestProperty("Content-Length", Integer.toString(params.getBytes(StandardCharsets.UTF_8).length));
        String cookies = "arl=" + arl + "; sid=" + sid;
        connection.setRequestProperty("Cookie", cookies);

        //Write body
        DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
        wr.writeBytes(params);
        wr.close();
        //Get response
        String data = "";
        Scanner scanner = new Scanner(connection.getInputStream());
        while (scanner.hasNext()) {
            data += scanner.nextLine();
        }

        //End
        try {
            connection.disconnect();
            scanner.close();
        } catch (Exception e) {}

        //Parse JSON
        JSONObject out = new JSONObject(data);

        //Save token
        if ((token == null || token.equals("null")) && method.equals("deezer.getUserData")) {
            token = out.getJSONObject("results").getString("checkForm");
            //SID
            try {
                String newSid = null;
                for (String cookie : connection.getHeaderFields().get("Set-Cookie")) {
                    if (cookie.startsWith("sid=")) {
                        newSid = cookie.split(";")[0].split("=")[1];
                    }
                }
                this.sid = newSid;
            } catch (Exception ignored) {}
        }

        return out;
    }


    //api.deezer.com/$method/$param
    public static JSONObject callPublicAPI(String method, String param) throws Exception {
        URL url = new URL("https://api.deezer.com/" + method + "/" + param);
        HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
        connection.setRequestMethod("GET");
        connection.setConnectTimeout(20000);
        connection.connect();

        //Get string data
        String data = "";
        Scanner scanner = new Scanner(url.openStream());
        while (scanner.hasNext()) {
            data += scanner.nextLine();
        }

        //Close
        try {
            connection.disconnect();
            scanner.close();
        } catch (Exception e) {}

        //Parse JSON
        JSONObject out = new JSONObject(data);
        return out;
    }

    //Generate track download URL
    public static String getTrackUrl(String trackId, String md5origin, String mediaVersion, int quality) {
        try {
            int magic = 164;

            ByteArrayOutputStream step1 = new ByteArrayOutputStream();
            step1.write(md5origin.getBytes());
            step1.write(magic);
            step1.write(Integer.toString(quality).getBytes());
            step1.write(magic);
            step1.write(trackId.getBytes());
            step1.write(magic);
            step1.write(mediaVersion.getBytes());
            //Get MD5
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            md5.update(step1.toByteArray());
            byte[] digest = md5.digest();
            String md5hex = bytesToHex(digest).toLowerCase();

            //Step 2
            ByteArrayOutputStream step2 = new ByteArrayOutputStream();
            step2.write(md5hex.getBytes());
            step2.write(magic);
            step2.write(step1.toByteArray());
            step2.write(magic);

            //Pad step2 with dots, to get correct length
            while(step2.size()%16 > 0) step2.write(46);

            //Prepare AES encryption
            Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
            SecretKeySpec key = new SecretKeySpec("jo6aey6haid2Teih".getBytes(), "AES");
            cipher.init(Cipher.ENCRYPT_MODE, key);
            //Encrypt
            StringBuilder step3 = new StringBuilder();
            for (int i=0; i<step2.size()/16; i++) {
                byte[] b = Arrays.copyOfRange(step2.toByteArray(), i*16, (i+1)*16);
                step3.append(bytesToHex(cipher.doFinal(b)).toLowerCase());
            }
            //Join to URL
            return "https://e-cdns-proxy-" + md5origin.charAt(0) + ".dzcdn.net/mobile/1/" + step3.toString();

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static String bytesToHex(byte[] bytes) {
        final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = HEX_ARRAY[v >>> 4];
            hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
        }
        return new String(hexChars);
    }

    public static String sanitize(String input) {
        return input.replaceAll("[\\\\/?*:%<>|\"]", "").replace("$", "\\$");
    }

    public static String generateFilename(String original, JSONObject publicTrack, JSONObject publicAlbum, int newQuality) throws Exception {
        original = original.replaceAll("%title%", sanitize(publicTrack.getString("title")));
        original = original.replaceAll("%album%", sanitize(publicTrack.getJSONObject("album").getString("title")));
        original = original.replaceAll("%artist%", sanitize(publicTrack.getJSONObject("artist").getString("name")));
        //Artists
        String artists = "";
        String feats = "";
        for (int i=0; i<publicTrack.getJSONArray("contributors").length(); i++) {
            String artist = publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
            if (!artists.contains(artist))
                artists += ", " + artist;
            if (i > 0 && !artists.contains(artist) && !feats.contains(artist))
                feats += ", " + artist;
        }
        original = original.replaceAll("%artists%", sanitize(artists).substring(2));
        if (feats.length() >= 2)
            original = original.replaceAll("%feats%", sanitize(feats).substring(2));
        //Track number
        int trackNumber = publicTrack.getInt("track_position");
        original = original.replaceAll("%trackNumber%", Integer.toString(trackNumber));
        original = original.replaceAll("%0trackNumber%", String.format("%02d", trackNumber));
        //Year
        original = original.replaceAll("%year%", publicTrack.getString("release_date").substring(0, 4));
        original = original.replaceAll("%date%", publicTrack.getString("release_date"));

        //Remove leading dots
        original = original.replaceAll("/\\.+", "/");

        if (newQuality == 9) return original + ".flac";
        return original + ".mp3";
    }

    public static String generateUserUploadedMP3Filename(String original, JSONObject privateJson) throws Exception {
        //Remove unavailable tags
        String[] ignored = {"%feats%", "%trackNumber%", "%0trackNumber%", "%year%", "%date%"};
        for (String i : ignored) {
            original = original.replaceAll(i, "");
        }
        //Basic tags
        original = original.replaceAll("%title%", privateJson.getString("SNG_TITLE"));
        original = original.replaceAll("%album%", privateJson.getString("ALB_TITLE"));
        original = original.replaceAll("%artist%", privateJson.getString("ART_NAME"));
        original = original.replaceAll("%artists%", privateJson.getString("ART_NAME"));
        return original;
    }

    //Tag track with data from API
    public void tagTrack(String path, JSONObject publicTrack, JSONObject publicAlbum, String cover, JSONObject lyricsData, JSONObject privateJson) throws Exception {
        TagOptionSingleton.getInstance().setAndroid(true);
        //Load file
        AudioFile f = AudioFileIO.read(new File(path));
        boolean isFlac = true;
        if (f.getAudioHeader().getFormat().contains("MPEG")) {
            f.setTag(new ID3v23Tag());
            isFlac = false;
        }
        Tag tag = f.getTag();

        tag.setField(FieldKey.TITLE, publicTrack.getString("title"));
        tag.setField(FieldKey.ALBUM, publicTrack.getJSONObject("album").getString("title"));
        //Artist
        String artists = "";
        for (int i=0; i<publicTrack.getJSONArray("contributors").length(); i++) {
            String artist = publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
            if (!artists.contains(artist))
                artists += ", " + artist;
        }
        tag.addField(FieldKey.ARTIST, artists.substring(2));
        tag.setField(FieldKey.TRACK, String.format("%02d", publicTrack.getInt("track_position")));
        tag.setField(FieldKey.DISC_NO, Integer.toString(publicTrack.getInt("disk_number")));
        tag.setField(FieldKey.ALBUM_ARTIST, publicAlbum.getJSONObject("artist").getString("name"));
        tag.setField(FieldKey.YEAR, publicTrack.getString("release_date").substring(0, 4));
        tag.setField(FieldKey.RECORD_LABEL, publicAlbum.getString("label"));
        tag.setField(FieldKey.ISRC, publicTrack.getString("isrc"));
        tag.setField(FieldKey.BARCODE, publicAlbum.getString("upc"));
        tag.setField(FieldKey.TRACK_TOTAL, Integer.toString(publicAlbum.getInt("nb_tracks")));

        //BPM
        if (publicTrack.has("bpm") && (int)publicTrack.getDouble("bpm") > 0)
            tag.setField(FieldKey.BPM, Integer.toString((int)publicTrack.getDouble("bpm")));

        //Unsynced lyrics
        if (lyricsData != null) {
            try {
                String lyrics = lyricsData.getString("LYRICS_TEXT");
                tag.setField(FieldKey.LYRICS, lyrics);
            } catch (Exception e) {
                Log.w("WARN", "Error adding unsynced lyrics!");
            }
        }

        //Genres
        String genres = "";
        for (int i=0; i<publicAlbum.getJSONObject("genres").getJSONArray("data").length(); i++) {
            String genre = publicAlbum.getJSONObject("genres").getJSONArray("data").getJSONObject(0).getString("name");
            if (!genres.contains(genre)) {
                genres += ", " + genre;
            }
        }
        if (genres.length() > 2)
            tag.setField(FieldKey.GENRE, genres.substring(2));

        //Additional tags from private api
        try {
            if (privateJson != null && privateJson.has("SNG_CONTRIBUTORS")) {
                JSONObject contrib = privateJson.getJSONObject("SNG_CONTRIBUTORS");
                //Composer
                if (contrib.has("composer")) {
                    JSONArray composers = contrib.getJSONArray("composer");
                    String composer = "";
                    for (int i = 0; i < composers.length(); i++)
                        composer += ", " + composers.getString(i);
                    if (composer.length() > 2)
                        tag.setField(FieldKey.COMPOSER, composer.substring(2));
                }
                //Engineer
                if (contrib.has("engineer")) {
                    JSONArray engineers = contrib.getJSONArray("engineer");
                    String engineer = "";
                    for (int i = 0; i < engineers.length(); i++)
                        engineer += ", " + engineers.getString(i);
                    if (engineer.length() > 2)
                        tag.setField(FieldKey.ENGINEER, engineer.substring(2));
                }
                //Mixer
                if (contrib.has("mixer")) {
                    JSONArray mixers = contrib.getJSONArray("mixer");
                    String mixer = "";
                    for (int i = 0; i < mixers.length(); i++)
                        mixer += ", " + mixers.getString(i);
                    if (mixer.length() > 2)
                        tag.setField(FieldKey.MIXER, mixer.substring(2));
                }
                //Producer
                if (contrib.has("producer")) {
                    JSONArray producers = contrib.getJSONArray("producer");
                    String producer = "";
                    for (int i = 0; i < producers.length(); i++)
                        producer += ", " + producers.getString(i);
                    if (producer.length() > 2)
                        tag.setField(FieldKey.MIXER, producer.substring(2));
                }

                //FLAC Only
                if (isFlac) {
                    //Author
                    if (contrib.has("author")) {
                        JSONArray authors = contrib.getJSONArray("author");
                        String author = "";
                        for (int i = 0; i < authors.length(); i++)
                            author += ", " + authors.getString(i);
                        if (author.length() > 2)
                            ((FlacTag) tag).setField("AUTHOR", author.substring(2));
                    }
                    //Writer
                    if (contrib.has("writer")) {
                        JSONArray writers = contrib.getJSONArray("writer");
                        String writer = "";
                        for (int i = 0; i < writers.length(); i++)
                            writer += ", " + writers.getString(i);
                        if (writer.length() > 2)
                            ((FlacTag) tag).setField("WRITER", writer.substring(2));
                    }
                }
            }
        } catch (Exception e) {
            logger.warn("Error writing contributors data: " + e.toString());
        }

        File coverFile = new File(cover);
        boolean addCover = (coverFile.exists() && coverFile.length() > 0);

        if (isFlac) {
            //FLAC Specific tags
            ((FlacTag)tag).setField("DATE", publicTrack.getString("release_date"));
            //Cover
            if (addCover) {
                RandomAccessFile cf = new RandomAccessFile(coverFile, "r");
                byte[] coverData = new byte[(int) cf.length()];
                cf.read(coverData);
                tag.setField(((FlacTag)tag).createArtworkField(
                    coverData,
                    PictureTypes.DEFAULT_ID,
                    ImageFormats.MIME_TYPE_JPEG,
                    "cover",
                    1400,
                    1400,
                    24,
                    0
                ));
            }
        } else {
            if (addCover) {
                Artwork art = ArtworkFactory.createArtworkFromFile(coverFile);
                //Artwork art = Artwork.createArtworkFromFile(coverFile);
                tag.addField(art);
            }
        }

        //Save
        AudioFileIO.write(f);
    }

    //Create JSON file, privateJsonData = `song.getLyrics`
    public static String generateLRC(JSONObject privateJsonData, JSONObject publicTrack) throws Exception {
        String output = "";

        //Create metadata
        String title = publicTrack.getString("title");
        String album = publicTrack.getJSONObject("album").getString("title");
        String artists = "";
        for (int i=0; i<publicTrack.getJSONArray("contributors").length(); i++) {
            artists += ", " + publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
        }
        //Write metadata
        output += "[ar:" + artists.substring(2) + "]\r\n[al:" + album + "]\r\n[ti:" + title + "]\r\n";

        //Get lyrics
        int counter = 0;
        JSONArray syncLyrics = privateJsonData.getJSONArray("LYRICS_SYNC_JSON");
        for (int i=0; i<syncLyrics.length(); i++) {
            JSONObject lyric = syncLyrics.getJSONObject(i);
            if (lyric.has("lrc_timestamp") && lyric.has("line")) {
                output += lyric.getString("lrc_timestamp") + lyric.getString("line") + "\r\n";
                counter += 1;
            }
        }

        if (counter == 0) throw new Exception("Empty Lyrics!");
        return output;
    }

    //Track decryption key
    static byte[] getKey(String id) {
        String secret = "g4el58wc0zvf9na1";
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            md5.update(id.getBytes());
            byte[] md5id = md5.digest();
            String idmd5 = bytesToHex(md5id).toLowerCase();
            String key = "";
            for(int i=0; i<16; i++) {
                int s0 = idmd5.charAt(i);
                int s1 = idmd5.charAt(i+16);
                int s2 = secret.charAt(i);
                key += (char)(s0^s1^s2);
            }
            return key.getBytes();
        } catch (Exception e) {
            Log.e("E", e.toString());
            return new byte[0];
        }
    }

    //Decrypt 2048b of data
    static byte[] decryptChunk(byte[] key, byte[] data) {
        try {
            byte[] IV = {00, 01, 02, 03, 04, 05, 06, 07};
            SecretKeySpec Skey = new SecretKeySpec(key, "Blowfish");
            Cipher cipher = Cipher.getInstance("Blowfish/CBC/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, Skey, new javax.crypto.spec.IvParameterSpec(IV));
            return cipher.doFinal(data);
        }catch (Exception e) {
            Log.e("D", e.toString());
            return new byte[0];
        }
    }

    static class QualityInfo {
        int quality;
        String md5origin;
        String mediaVersion;
        String trackId;
        int initialQuality;
        DownloadLog logger;

        QualityInfo(int quality, String trackId, String md5origin, String mediaVersion, DownloadLog logger) {
            this.quality = quality;
            this.initialQuality = quality;
            this.trackId = trackId;
            this.mediaVersion = mediaVersion;
            this.md5origin = md5origin;
            this.logger = logger;
        }

        boolean fallback(Deezer deezer) {
            //Quality fallback
            try {
                qualityFallback();
                //No quality
                if (quality == -1)
                    throw new Exception("No quality to fallback to!");

                //Success
                return true;
            } catch (Exception e) {
                logger.warn("Quality fallback failed! ID: " + trackId + " " + e.toString());
                quality = initialQuality;
            }

            //Track ID Fallback
            JSONObject privateJson = null;
            try {
                //Fetch meta
                JSONObject privateRaw = deezer.callGWAPI("deezer.pageTrack", "{\"sng_id\": \"" + trackId + "\"}");
                privateJson = privateRaw.getJSONObject("results").getJSONObject("DATA");
                if (privateJson.has("FALLBACK")) {
                    //Fetch new track
                    String fallbackId = privateJson.getJSONObject("FALLBACK").getString("SNG_ID");
                    if (!fallbackId.equals(trackId)) {
                        JSONObject newPrivate = deezer.callGWAPI("song.getListData", "{\"sng_ids\": [" + fallbackId + "]}");
                        JSONObject trackData = newPrivate.getJSONObject("results").getJSONArray("data").getJSONObject(0);
                        trackId = trackData.getString("SNG_ID");
                        md5origin = trackData.getString("MD5_ORIGIN");
                        mediaVersion = trackData.getString("MEDIA_VERSION");
                        return fallback(deezer);
                    }
                }
            } catch (Exception e) {
                logger.error("ID fallback failed! ID: " + trackId + " " + e.toString());
            }

            //ISRC Fallback
            try {
                JSONObject newTrackJson = Deezer.callPublicAPI("track", "isrc:" + privateJson.getString("ISRC"));
                //Same track check
                if (newTrackJson.getInt("id") == Integer.parseInt(trackId)) throw new Exception("No more to ISRC fallback!");
                //Get private data
                privateJson = deezer.callGWAPI("song.getListData", "{\"sng_ids\": [" + newTrackJson.getInt("id") + "]}");
                JSONObject trackData = privateJson.getJSONObject("results").getJSONArray("data").getJSONObject(0);
                trackId = trackData.getString("SNG_ID");
                md5origin = trackData.getString("MD5_ORIGIN");
                mediaVersion = trackData.getString("MEDIA_VERSION");
                return fallback(deezer);
            } catch (Exception e) {
                logger.error("ISRC Fallback failed, track unavailable! ID: " + trackId + " " + e.toString());
            }

            return false;
        }

        private void qualityFallback() throws Exception {
            //Create HEAD requests to check if exists
            URL url = new URL(getTrackUrl(trackId, md5origin, mediaVersion, quality));
            HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
            connection.setRequestMethod("HEAD");
            connection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36");
            connection.setRequestProperty("Accept-Language", "*");
            connection.setRequestProperty("Accept", "*/*");
            int rc = connection.getResponseCode();
            //Track not available
            if (rc > 400) {
                logger.warn("Quality fallback, response code: " + Integer.toString(rc) + ", current: " + Integer.toString(quality));
                //-1 if no quality available
                if (quality == 1) {
                    quality = -1;
                    return;
                }
                if (quality == 3) quality = 1;
                if (quality == 9) quality = 3;
                qualityFallback();
            }
        }

    }

}
