//* Constants
const API_URL = 'https://api-v2.soundcloud.com/'
const APP_LOCALE = 'en'
const PLATFORM = 'Soundcloud'
const PLATFORM_CLAIMTYPE = 16;
const SOUNDCLOUD_APP_VERSION = '1735826482'
const USER_AGENT_DESKTOP = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
const USER_AGENT_MOBILE = 'Mozilla/5.0 (Linux; Android 10; Pixel 6a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'

const URL_BASE = "https://soundcloud.com";

let CLIENT_ID = 'iZIs9mchVcX5lhVRyQGGAYlNPVldzAoX' // correct as of June 2023, enable changes this to get the latest
const URL_ADDITIVE = `&app_version=${SOUNDCLOUD_APP_VERSION}&app_locale=${APP_LOCALE}`

const REGEX_CHANNEL_PLAYLISTS = /^https?:\/\/(www\.|m\.)?soundcloud\.com\/([a-zA-Z0-9_-]+)\/sets\/[a-zA-Z0-9_-]+(\?[^#]*)?$/;
const REGEX_SYSTEM_PLAYLISTS = /^https?:\/\/(www\.|m\.)?soundcloud\.com\/[a-zA-Z0-9_-]+\/(likes|popular-tracks|toptracks|tracks|reposts)$/
const REGEX_CHANNEL = /^https?:\/\/(www\.|m\.)?soundcloud\.com\/([a-zA-Z0-9_-]+)\/?$/;
const REGEX_TRACK = /(?:https?:\/\/)?(?:www\.|m\.)?soundcloud\.com\/[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+(?:\?[^\s#]*)?/;

const systemPlaylistsMaps = {
    likes: {
        path: 'likes',
        apiPath: 'likes',
        playlistTitle: 'Likes'
    },
    tracks: {
        path: 'tracks',
        apiPath: 'tracks',
        playlistTitle: 'Tracks'
    },
    "popular-tracks": {
        path: "popular-tracks",
        apiPath: 'toptracks',
        playlistTitle: 'Popular Tracks'
    },
    "reposts": {
        path: "reposts",
        apiPath: 'reposts',
        apiBasePath: 'https://api-v2.soundcloud.com/stream/users',
        playlistTitle: 'Reposts'
    },
}

let config = {}

let state = {
    channel: {}
}

//* Source
source.enable = function (conf, settings, saveStateStr) {
    config = conf ?? {}
    CLIENT_ID = getClientId()

    try {
        if (saveStateStr) {
            state = JSON.parse(saveStateStr);
        }
    } catch (ex) {
        log('Failed to parse saveState:' + ex);
    }

    return CLIENT_ID
}
source.getHome = function () {
    return new QueryPager({ page: 1, page_size: 20 })
}
source.searchSuggestions = function (query) {
    const url = `${API_URL}search/queries?q=${query}&client_id=${CLIENT_ID}&limit=10&offset=0&linked_partitioning=1${URL_ADDITIVE}`

    const resp = callUrl(url)

    /** @type {import("./types").SearchAutofillResponse} */
    const json = JSON.parse(resp.body)

    if (!json['collection']) {
        throw new ScriptException('Could not find collection')
    }

    /** @type {{output: string; query: string}[]} */
    const collection = json['collection']

    return collection.map((item) => item['query'])
}
source.getSearchCapabilities = () => {
    return {
        types: [Type.Feed.Mixed], // can also do albums, playlists, channels those do not have types yet
        sorts: [],
        filters: [], // filters depend on type
    }
}
source.search = function (query, type, order, filters) {
    return new SearchPagerVideos({ q: query, page: 1, page_size: 20, get_all: false })
}
source.getSearchChannelContentsCapabilities = function () {
    return {
        types: [Type.Feed.Mixed],
        sorts: [Type.Order.Chronological],
        filters: [],
    }
}
source.searchChannelContent = function (channelUrl, query, type, order, filters) {
    return []
}
source.searchChannels = function (query) {
    return new SearchPagerChannels({ q: query, page: 1, page_size: 20 })
}
source.isChannelUrl = function (url) {
    // see if it matches https://soundcloud.com/nfrealmusic
    return !source.isPlaylistUrl(url) && REGEX_CHANNEL.test(url)
}
source.getChannel = function (url) {

    if(state.channel[url]) {
        return state.channel[url];
    }

    const resp = callUrl(url)

    const html = resp.body

    const matched = html.match(/window\.__sc_hydration = (.+);/)
    if (!matched) {
        throw new ScriptException('Could not find channel info')
    }

    /** @type {import("./types").SCHydration[]} */
    const json = JSON.parse(matched[1])

    for (let object of json) {
        if (object.hydratable === 'user') {
            state.channel[url] = soundcloudUserToPlatformChannel(object.data);
            return state.channel[url];
        }
    }

    throw new ScriptException('Could not find channel info')
}
source.getChannelContents = function (url) {
    return new ChannelVideoPager({ url: url, page_size: 20, offset_date: 0 })
}

source.getChannelPlaylists = (url) => {

    const channelSlug = extractSoundCloudId(url);
    
    const channel = source.getChannel(`${URL_BASE}/${channelSlug}`);

    const author =  new PlatformAuthorLink(
        new PlatformID(PLATFORM,channel.id.value.toString(),config.id,PLATFORM_CLAIMTYPE),
        channel.name,
        channel.url,
        channel.thumbnail,
    );

    class ChannelPlaylistsPager extends ContentPager {
        constructor({
            results = [],
            hasMore = true,
            context = {withNext: []},
          }) {
            
            super(results, hasMore, context);
          }

          nextPage() {
              
            let withNext = this.context.withNext ?? [];
            let firstPage = this.context.firstPage ?? false;
            
            let all = firstPage ? (this.results ?? []) : [];

            let batch = http.batch();
            
            withNext.forEach(url => {
                batch.GET(url, {});
            });

            const responses = batch.execute();
        
            withNext = [];
        
            for(var ct = 0; ct < responses.length; ct++) {
                const res = responses[ct];
                
                if(res.isOk) {
                    const body = JSON.parse(res.body);
                    
                    if(body.next_href) {
                        withNext.push(`${body.next_href}$?client_id=${CLIENT_ID}`);
                    }
        
                    const currentCollection = body.collection.map(v => {
                        
                        return new PlatformPlaylist({
                            id: new PlatformID(PLATFORM, v.id.toString(), config.id, PLATFORM_CLAIMTYPE),
                            author: author,
                            name: v.title,
                            thumbnail: v.artwork_url,
                            videoCount: v?.track_count ?? -1,
                            datetime: dateToUnixSeconds(v.display_date),
                            url: v.permalink_url,
                          })
                    });            
        
                    all = [...all, ...currentCollection]
                    
                }
                
            }
        
            const hasMore = !!withNext.length;
            
            return new ChannelPlaylistsPager({results: all, hasMore, context: { withNext }});
          }
    }

    let withNext = [
        `albums`,
        `playlists_without_albums`
    ].map((path) => `https://api-v2.soundcloud.com/users/${channel.id.value}/${path}?client_id=${CLIENT_ID}&limit=10&offset=0&linked_partitioning=1&app_version=${SOUNDCLOUD_APP_VERSION}&app_locale=en`);

// system playlists
    let results = [
        'likes',
        'popular-tracks',
        'tracks',
        'reposts'
    ].map(path => {

        const info = systemPlaylistsMaps[path];

        const name = info?.playlistTitle ?? path;
        const playlistPath = info?.path ?? path;

        return new PlatformPlaylist({
            id: new PlatformID(PLATFORM, '', config.id, PLATFORM_CLAIMTYPE),
            author: author,
            name: name,
            thumbnail: channel.banner || channel.thumbnail || '',
            videoCount: -1,
            // datetime: dateToUnixSeconds(v.display_date),
            url: `https://soundcloud.com/${channelSlug}/${playlistPath}`,
          })
    })

    return new ChannelPlaylistsPager({ results, context: { withNext, firstPage: true } }).nextPage();

}


source.getChannelTemplateByClaimMap = () => {
    return {
        //SoundCloud
        17: {
            0: URL_BASE + "/{{CLAIMVALUE}}"
            //Unused! 1: https://api.soundcloud.com/users/{{CLAIMVALUE}}
        }
    };
};


source.isContentDetailsUrl = function (url) {
    // https://soundcloud.com/toosii2x/toosii-favorite-song
    return !source.isPlaylistUrl(url) && REGEX_TRACK.test(url)
}
source.getContentDetails = function (url) {
    const resp = callUrl(url)

    const html = resp.body

    const matched = html.match(/window\.__sc_hydration = (.+);/)
    if (!matched) {
        if(IS_TESTING)
            console.log(html);
        throw new ScriptException('Could not find video info')
    }

    /** @type {SCHydration[]} */
    const json = JSON.parse(matched[1])

    /** @type {import("./types").SoundcloudTrack} */
    let data
    /** @type {import("./types").SoundcloudTrack} */
    let sct

    for (let object of json) {
        if (object.hydratable === 'sound') {
            data = object.data
            sct = soundcloudTrackToPlatformVideo(data)
            break
        }
    }

    // for performance reasons, select just the mpeg transcoding if it exists; otherwise, select the first transcoding
    if (data.media.transcodings?.length === 0) throw new ScriptException('Could not find transcodings')
    const transcoding = data.media.transcodings.find((transcoding) => (transcoding.format.mime_type = 'audio/mpeg')) ?? data.media.transcodings[0]

    const authorization = data.track_authorization
    const generated_url = transcoding.url + `?client_id=${CLIENT_ID}&track_authorization=${authorization}`

    const hls_resp = callUrl(generated_url)
    const hls_url = JSON.parse(hls_resp.body).url

    const sources = [
        new HLSSource({
            name: `${transcoding.format.mime_type}`,
            duration: transcoding.duration,
            url: hls_url,
            language: "Unknown"
        }),
    ]

    sct.video = new UnMuxVideoSourceDescriptor([], sources)
    sct.description = data.description
    const likesCount = Number.isFinite(data?.likes_count) ? data.likes_count : 0;
    sct.rating = new RatingLikes(likesCount);

    return new PlatformVideoDetails(sct)
}
source.getComments = function (url) {
    return new ExtendableCommentPager({ url: url, page: 1, page_size: 20 })
}
// not in Soundcloud
source.getSubComments = function (comment) {
    return new CommentPager([], false, {})
}
source.getUserSubscriptions = function () {
    const following_resp = callUrl('https://soundcloud.com/you/following', true)
    const html = following_resp.body
    if(IS_TESTING)
        console.log(html)
    const matched = html.match(/window\.__sc_hydration = (.+);/)
    if (!matched) throw new ScriptException('Could not find user info')
    /** @type {SCHydration[]} */
    const following_json = JSON.parse(matched[1])
    let id
    for (let object of following_json) {
        if (object.hydratable === 'meUser') {
            id = object.data.id
            break
        }
    }
    if (!id) throw new ScriptException('Could not find user info')

    const resp = callUrl(`${API_URL}users/${id}/followings?client_id=${CLIENT_ID}&limit=12&offset=0&linked_partitioning=1${URL_ADDITIVE}}`, true)

    const json = JSON.parse(resp.body)

    /** @type {import("./types.d.ts").SoundcloudUser[]} */
    const users = json.collection

    return users.map((user) => user.permalink_url)
}
source.getUserPlaylists = function () {
    const url = `${API_URL}me/library/all?client_id=${CLIENT_ID}&limit=10&offset=0&linked_partioning=1${URL_ADDITIVE}`

    const resp = callUrl(url, true)

    if(IS_TESTING) {
        console.log(url)
        console.log(resp.body)
    }
    const json = JSON.parse(resp.body)

    if(IS_TESTING)
        console.log(json)

    /** @type {import("./types.d.ts").PlaylistWrapper[]} */
    const playlists = json.collection

    return playlists.map((playlist) => {
        if ('playlist' in playlist) {
            return playlist.playlist.permalink_url
        } else if ('system_playlist' in playlist) {
            return playlist.system_playlist.permalink_url
        }
    })
}
source.isPlaylistUrl = function (url) {
    return isSoundCloudChannelPlaylistUrl(url)
}
source.getPlaylist = function (url) {  

    if(isSoundCloudSystemPlaylist(url)){
        return standardPlaylistPager(url);
    }
    
    const resp = callUrl(url, true)

    const html = resp.body

    const matched = html.match(/window\.__sc_hydration = (.+);/)

    if (!matched) {
        throw new ScriptException('Could not find playlist info')
    }

    /** @type {SCHydration[]} */
    const json = JSON.parse(matched[1])
    
    /** @type {number[]} */
    let ids = []
    let playlistTitle = '';
    let playlistId = '';

    for (let object of json) {
        if (object.hydratable === 'systemPlaylist') {
            ids = object.data.tracks.map((track) => track.id) 
            break
        } else if (object.hydratable === 'playlist') {
            ids = object.data.tracks.map((track) => track.id)
            playlistTitle = object.data.title
            playlistId = object.data.id.toString()     
            break
        }
    }

    let user = json.find(object => object.hydratable === 'user');
    
    let author;

    if(user) {
        author =  new PlatformAuthorLink(
            new PlatformID(
              PLATFORM,
              user.data.id.toString(),
              config.id,
              PLATFORM_CLAIMTYPE,
            ),
            user.data.username,
            user.data.permalink_url,
            user.data.avatar_url,
          );
    } else {
        author =  new PlatformAuthorLink(
            new PlatformID(
                PLATFORM,
                '',
                config.id,
                PLATFORM_CLAIMTYPE,
            ),
            '',
            '',
            '',
          );
    }
    
    /** @type {import("./types.d.ts").SoundcloudTrack[]} */
    let tracks = []

    // split ids into chunks of 50
    for (let i = 0; i < ids.length; i += 50) {
        const chunk = ids.slice(i, i + 50)

        const generated_url = `${API_URL}tracks?ids=${chunk.join(',')}&client_id=${CLIENT_ID}${URL_ADDITIVE}`
        const chunk_resp = callUrl(generated_url, true)
        const found_tracks = JSON.parse(chunk_resp.body)

        tracks = tracks.concat(found_tracks)
    }
    
    const content = tracks.map(soundcloudTrackToPlatformVideo);
    
    return new PlatformPlaylistDetails({
        url: url,
        id: new PlatformID(PLATFORM, playlistId, config.id),
        author: author,
        name: playlistTitle,
        videoCount: content?.length ?? 0,
        contents: new VideoPager(content)
    });

}

source.saveState = () => {
    return JSON.stringify(state);
};

//* Internals
/**
 * Gets the URL with correct headers
 * @param {string} url
 * @param {boolean} is_authenticated
 * @param {boolean} use_mobile
 * @returns {HTTPResponse}
 */
function callUrl(url, is_authenticated = false, use_mobile = false) {
    
    if(!use_mobile) {
        url = removeMobilePrefix(url);
    }
    
    let headers = {
        'User-Agent': use_mobile ? USER_AGENT_MOBILE : USER_AGENT_DESKTOP,
        DNT: '1',
        Connection: 'keep-alive',
        Origin: 'https://soundcloud.com',
        Referer: 'https://soundcloud.com/',
    }

    let accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'

    if (url.includes('api-v2.soundcloud.com')) {
        accept = 'application/json, text/javascript, */*; q=0.01'
        headers['Host'] = 'api-v2.soundcloud.com'
        headers['SEC-FETCH-DEST'] = 'empty'
        headers['SEC-FETCH-MODE'] = 'cors'
        headers['SEC-FETCH-SITE'] = 'same-site'
    } else {
        headers['SEC-FETCH-DEST'] = 'document'
        headers['SEC-FETCH-MODE'] = 'navigate'
        headers['SEC-FETCH-SITE'] = 'none'
    }

    headers['Accept'] = accept

    return http.GET(url, headers, is_authenticated)
}

/**
 * Gets the client_id from the Soundcloud home page
 * @returns {string} returns the client_id
 */
function getClientId() {
    // request soundcloud.com to find the url of the js file that contains 50-_____.js
    const resp = callUrl('https://soundcloud.com/discover', false, true)
    const html = resp.body

    // find "clientId":"iZIs9mchVcX5lhVRyQGGAYlNPVldzAoX"
    const matched = html.match(/"clientId":"([a-zA-Z0-9-_]+)"/)

    if (!matched) {
        throw new ScriptException('Could not find client_id')
    }

    const clientId = matched[1]

    return clientId
}

/**
 * Gets the Soundcloud homepage content
 * @param {import("./types").HomeContext} context the search context
 * @returns {PlatformVideo[]} returns the homepage content
 */
function getHomepageContent(context) {
    const limit = context.page_size
    const offset = (context.page - 1) * limit
    const url = `${API_URL}featured_tracks/top/all-music?client_id=${CLIENT_ID}&limit=${limit}&offset=${offset}&linked_partitioning=1${URL_ADDITIVE}`

    const resp = callUrl(url)

    /** @type {import("./types").HomepageResponse} */
    const json = JSON.parse(resp.body)

    /** @type {import("./types").SoundcloudTrack[]} */
    const tracks = json['collection']

    const results = ensureUniqueByProperty(tracks, 'id')
    .map((track) => {
        return soundcloudTrackToPlatformVideo(track)
    });

    const hasMore = json?.['next_href'] !== null

    return { results, hasMore }
}

//* Pagers
class QueryPager extends VideoPager {
    /**
     * @param {import("./types.d.ts").HomeContext} context the query params
     */
    constructor(context) {
        const data = getHomepageContent(context);
        super(data.results, data.hasMore, context)
    }

    nextPage() {
        this.context.page = this.context.page + 1
        const data = getHomepageContent(this.context)
        this.results = data.results;
        this.hasMore = data.hasMore;
        return this
    }
}
class SearchPagerVideos extends VideoPager {
    /**
     * @param {import("./types").SearchContext} context the query params
     */
    constructor(context) {
        if (context.get_all) {
            // https://api-v2.soundcloud.com/search?q=search%20and%20destroy%20drake&client_id=VDJ3iu7ZYtUMibDTM2XcUbRijDa3L6ug&limit=20&offset=0&linked_partitioning=1&app_version=1683798046&app_locale=en
            const limit = context.page_size
            const offset = (context.page - 1) * limit

            const url = `${API_URL}search?q=${encodeURIComponent(
                context.q
            )}&client_id=${CLIENT_ID}&limit=${limit}&offset=${offset}&linked_partitioning=1${URL_ADDITIVE}`

            const resp = callUrl(url)

            /** @type {import("./types").AnySearchResponse} */
            const json = JSON.parse(resp.body)

            if (json['collection'] === undefined) {
                if(IS_TESTING)
                    console.log('Soundcloud search response: ' + resp.body)
                throw new ScriptException('Could not find collection')
            }

            /** @type {(PlatformVideo | PlatformChannel | PlatformPlaylist)[]} */
            const results = []

            for (const result of json['collection']) {
                if (result['kind'] === 'track') {
                    results.push(soundcloudTrackToPlatformVideo(result))
                } else if (result['kind'] === 'user') {
                    continue
                    results.push(soundcloudUserToPlatformChannel(result))
                } else if (result['kind'] === 'playlist' || result['kind'] === 'album') {
                    // results.push(soundcloudPlaylistToPlatformPlaylist(result))
                } else {
                    if(IS_TESTING)
                        console.log('Soundcloud search result: ' + JSON.stringify(result))
                    throw new ScriptException('Unknown kind: ' + result['kind'])
                }
            }

            super(results, results.length >= context.page_size, context)
        } else {
            const limit = context.page_size
            const offset = (context.page - 1) * limit
            const url = `${API_URL}search/tracks?limit=${limit}&offset=${offset}&q=${context.q}&client_id=${CLIENT_ID}${URL_ADDITIVE}`

            const resp = callUrl(url)

            /** @type {SearchResponse} */
            const json = JSON.parse(resp.body)

            /** @type {SoundcloudTrack[]} */
            const tracks = json['collection']

            const results = tracks.map((track) => soundcloudTrackToPlatformVideo(track))

            super(results, results.length >= context.page_size, context)
        }
    }

    nextPage() {
        this.context.page = this.context.page + 1
        return new SearchPagerVideos(this.context)
    }
}
class ChannelVideoPager extends VideoPager {
    /**
     * @param {import("./types.d.ts").ChannelVideoPagerContext} context
     */
    constructor(context) {
        if (!context.id) {
            const resp = callUrl(context.url)
            const matched = resp.body.match(/window\.__sc_hydration = (.+);/)
            if (!matched) {
                throw new ScriptException('Could not find channel info')
            }

            /** @type {import("./types").SCHydration[]} */
            const json = JSON.parse(matched[1])

            for (let object of json) {
                if (object.hydratable === 'user') {
                    /** @type {import("./types").SoundcloudUser} */
                    const data = object.data

                    context.id = data.id
                    break
                }
            }
        }

        const url = `${API_URL}users/${context.id}/tracks?representation=&client_id=${CLIENT_ID}&limit=${context.page_size}&offset=${context.offset_date}&linked_partitioning=1${URL_ADDITIVE}`

        if(IS_TESTING)
            console.log('Soundcloud channel url: ' + url)

        const resp = callUrl(url)
        const parsed = JSON.parse(resp.body)

        /** @type {import("./types").SoundcloudTrack[]} */
        const tracks = parsed['collection']

        const videos = tracks.map((track) => soundcloudTrackToPlatformVideo(track))

        context['offset_date'] = tracks[tracks.length - 1]?.created_at

        super(videos, tracks.length > 0, context)
    }

    nextPage() {
        this.context.page = this.context.page + 1
        return new ChannelVideoPager(this.context)
    }
}
class SearchPagerChannels extends ChannelPager {
    /**
     * @param {import("./types").SearchContext} context the query params
     */
    constructor(context) {
        const limit = context.page_size
        const offset = (context.page - 1) * limit
        const url = `${API_URL}search/users?q=${context.q}&client_id=${CLIENT_ID}&limit=${limit}&offset=${offset}&linked_partitioning=1${URL_ADDITIVE}`

        const resp = callUrl(url)

        /** @type {SearchResponse} */
        const json = JSON.parse(resp.body)

        /** @type {SoundcloudUser[]} */
        const users = json['collection']

        const results = users.map((user) => soundcloudUserToPlatformChannel(user))

        super(results, results.length >= context.page_size, context)
    }

    nextPage() {
        this.context.page = this.context.page + 1
        return new SearchPagerChannels(this.context)
    }
}
class ExtendableCommentPager extends CommentPager {
    /**
     * @param {import("./types.d.ts").HomeContext & {url: string; id: number|null}} context
     */
    constructor(context) {
        if (!context.id) {
            const resp = callUrl(context.url)
            const html = resp.body
            const matched = html.match(/window\.__sc_hydration = (.+);/)
            if (!matched) {
                throw new ScriptException('Could not find comment info')
            }

            /** @type {import("./types").SCHydration[]} */
            const json = JSON.parse(matched[1])

            for (let object of json) {
                if (object.hydratable === 'sound') {
                    /** @type {import("./types").SoundcloudTrack} */
                    const data = object.data

                    context.id = data.id
                    break
                }
            }
        }

        // https://api-v2.soundcloud.com/tracks/1506477625/comments?sort=newest&threaded=1&client_id=TihN0nuDfhghD9GVPbTtrSEa558lYo4V&limit=20&offset=0&linked_partitioning=1&app_version=1684153290&app_locale=en
        const limit = context.page_size
        const offset = (context.page - 1) * limit

        const url = `${API_URL}tracks/${context.id}/comments?sort=newest&threaded=1&client_id=${CLIENT_ID}&limit=${limit}&offset=${offset}&linked_partitioning=1${URL_ADDITIVE}`

        const resp = callUrl(url)

        /** @type {import("./types").CommentResponse} */
        const json = JSON.parse(resp.body)

        const comments = json['collection'].map((comment) => {
            return new Comment({
                contextUrl: context.url,
                author: new PlatformAuthorLink(
                    new PlatformID(PLATFORM, comment.user.id.toString(), config.id, PLATFORM_CLAIMTYPE),
                    comment.user.username,
                    comment.user.permalink_url,
                    comment.user.avatar_url
                ),
                message: comment.body,
                rating: new RatingLikes(0),
                date: parseInt(new Date(comment.created_at).getTime() / 1000),
                replyCount: 0,
                context: null,
            })
        })

        super(comments, json['next_href'] !== null, context)
    }

    nextPage() {
        this.context.page = this.context.page + 1
        return new ExtendableCommentPager(this.context)
    }
}

//* CONVERTERS
/**
 * Convert a Soundcloud person to a PlatformChannel
 * @param { import("./types").SoundcloudUser } scu
 * @returns { PlatformChannel }
 */
function soundcloudUserToPlatformChannel(scu) {
    if (!scu || typeof scu !== 'object') {
        throw new ScriptException('Invalid SoundCloud user object');
    }

    const visuals = scu.visuals?.visuals || [];
    const banner = visuals?.[0]?.visual_url || '';
    const links = visuals.map(v => v.link).filter(Boolean);

    return new PlatformChannel({
        id: new PlatformID(PLATFORM, scu.id.toString(), config.id, PLATFORM_CLAIMTYPE),
        name: scu.username,
        thumbnail: scu.avatar_url,
        banner,
        subscribers: scu.followers_count || 0,
        description: scu.description,
        url: scu.permalink_url,
        links,
    });
}

/**
 * Convert a Soundcloud Track to a PlatformVideo
 * @param { import("./types").SoundcloudTrack } sct
 * @returns { PlatformVideo }
 */
function soundcloudTrackToPlatformVideo(sct) {
    return new PlatformVideo({
        id: new PlatformID(PLATFORM, sct.id.toString(), config.id),
        name: sct.title,
        thumbnails: new Thumbnails([new Thumbnail(sct.artwork_url !== null ? sct.artwork_url.replace('large', 't500x500') : sct.artwork_url, 0)]),
        author: new PlatformAuthorLink(
            new PlatformID(PLATFORM, sct.user_id.toString(), config.id, PLATFORM_CLAIMTYPE),
            sct.user.username,
            sct.user.permalink_url,
            sct.user.avatar_url
        ),
        uploadDate: parseInt(new Date(sct.created_at).getTime() / 1000),
        duration: parseInt(sct.duration / 1000),
        viewCount: sct.playback_count,
        url: sct.permalink_url,
        isLive: false,
    })
}

/**
 * Replace the "m." prefix in a SoundCloud URL with an empty string.
 * 
 * @param {string} url - The SoundCloud URL to modify.
 * @returns {string} - The modified URL without the "m." prefix.
 */
function removeMobilePrefix(url) {
    return url.trim().replace("https://m.", "https://");
}

/**
 * Currently the trending pages has some duplicates, this function ensures that the array is unique based on the specified property.
 * Ensures that each item in the array is unique based on the specified property.
 * @param {Array} array - The array of objects to process.
 * @param {string} property - The property to use for uniqueness.
 * @returns {Array} - A new array with unique items based on the specified property.
 */
function ensureUniqueByProperty(array, property) {
    const seen = new Set();
    return array.filter(item => {
        if (item[property] && !seen.has(item[property])) {
            seen.add(item[property]);
            return true;
        }
        return false;
    });
}


function extractSoundCloudId(url) {
    if (!url) return null;

    const match = url.match(REGEX_CHANNEL);
    if (match) {
        return match[2]; // The second capturing group contains the SoundCloud ID
    }

    return null; // Return null if no match
}

function isSoundCloudChannelPlaylistUrl(url) {
    return REGEX_CHANNEL_PLAYLISTS.test(url) || REGEX_SYSTEM_PLAYLISTS.test(url);
}

function isSoundCloudSystemPlaylist(url) {
    return REGEX_SYSTEM_PLAYLISTS.test(url);
}

function standardPlaylistPager(url){
    
    const urlDetails = extractSoundCloudDetails(url);
    
    const channelSlug = urlDetails.userId;
    const playlist = urlDetails.trackId;
    
    const channel = source.getChannel(`${URL_BASE}/${channelSlug}`);
    
    const info = systemPlaylistsMaps[playlist];

    const apiPath = info?.apiPath ?? playlist;
    const playlistTitle =  info?.playlistTitle ?? playlist;
    const apiBasePath = info?.apiBasePath ?? 'https://api-v2.soundcloud.com/users';

    let withNext = [
        `${apiBasePath}/${channel.id.value}/${apiPath}?client_id=${CLIENT_ID}&limit=20&offset=0&linked_partitioning=1&app_version=${SOUNDCLOUD_APP_VERSION}&app_locale=en`
    ]
    
    class ChannelPlaylistsPager extends VideoPager {
        constructor({
            results,
            hasMore,
            context,
          }) {
            
            super(results, hasMore, context);
          }

          nextPage() {
              
            let withNext = this.context.withNext ?? [];
            let seen = this.context.seen ?? [];
            
            let all = this.results ?? [];

            let batch = http.batch();
            
            withNext.forEach(url => {
                batch.GET(url, {});
            });

            const responses = batch.execute();
        
            withNext = [];
            
            for(var ct = 0; ct < responses.length; ct++) {
                const res = responses[ct];
                
                if(res.isOk) {
                    
                    const body = JSON.parse(res.body);
                     
                    if(body.next_href) {
                        withNext.push(`${body.next_href}&client_id=${CLIENT_ID}`);
                    }
        
                    const currentCollection = body.collection.filter(c => c.track || c.kind === 'track').map(c => soundcloudTrackToPlatformVideo(c.track ?? c));
                       
                    all = [...all, ...currentCollection].filter(a => seen.indexOf(a.id.value) === -1);

                    seen = [...seen, ...all.map(a => a.id.value)];
                    
                }
                
            }
        
            const hasMore = !!withNext.length;
            
            return new ChannelPlaylistsPager({results: all, hasMore, context: { withNext, seen }});
          }
    }

    const author =  new PlatformAuthorLink(
        new PlatformID(PLATFORM,channel.id.value.toString(),config.id,PLATFORM_CLAIMTYPE),
        channel.name,
        channel.url,
        channel.thumbnail,
    );
    
    let contentPager = new ChannelPlaylistsPager({ context: { withNext } }).nextPage();
        
    return new PlatformPlaylistDetails({
        url: url,
        id: new PlatformID(PLATFORM, '', config.id),
        author: author,
        name: playlistTitle,
        // thumbnail: "",
        videoCount: -1,
        contents: contentPager
    });
}

function extractSoundCloudDetails(url) {
    if (!url) return null;

    const match = url.match(/^https?:\/\/(www\.|m\.)?soundcloud\.com\/([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+)\/?$/);
    if (match) {
        return {
            userId: match[2], // Extracted user/artist name
            trackId: match[3] // Extracted track identifier
        };
    }

    return null; // Return null if the URL doesn't match the expected pattern
}

function dateToUnixSeconds(date) {
    if (!date) {
      return 0;
    }
    
    return Math.round(Date.parse(date) / 1000);
}

console.log('LOADED')