const bypassCorsHeaders: string = "https://cors-tiktok.herokuapp.com/?u="; //Bypass CORS (Github : https://github.com/abdelyouni/cors-tiktok)
const linkPattern: string = '<link data-react-helmet="true" rel="canonical" href="';
const greaterSymbol: string = '>';
const closingTag: string = '/>';
const invertedTag: string = '</';
const idPattern: string = 'id="__NEXT_DATA__"';
const downloadParameter: string = '&d=1';
const tiktokRegex: RegExp = /https?:\/\/www\.tiktok\.com\/@.+\/video\/\d*/gm;
const tiktokMobileRegex: RegExp = /https?:\/\/m\.tiktok\.com\/v\/\d*/gm;
const tiktokMobileAltRegex: RegExp = /https?:\/\/vm\.tiktok\.com\/\d*/gm;

export namespace JSTikTok {
    export interface DownloadResponse {
        blob: Blob;
        fileName: string;
        type: string;
    }
    
    export interface VideoInfo {
        id: any,
        height: any,
        duration: any,
        sizeFormat: any,
        cover: any,
        animatedCover: any,
        likes: any,
        shares: any,
        comments: any,
        views: any,
        title: any,
        keywords: any,
        description: any,
        original_url: any,
        download_url: any
    }
    
    export interface MusicInfo {
        id: any,
        title: any,
        cover_large: any,
        cover_medium: any,
        cover_small: any,
        artist: any,
        album: any,
        duration: any,
        url: any
    }
    
    export interface AuthorInfo {
        id: any,
        uniqueId: any,
        username: any,
        avatar_large: any,
        avatar_medium: any,
        avatar_small: any,
        signature: any,
        createDate: any,
        isVerified: any,
        followers: any,
        followings: any,
        hearts: any,
        totalVideos: any,
        diggCount: any,
    }
    
    export interface TikTokInfo {
        video: VideoInfo,
        music: MusicInfo,
        author: AuthorInfo
    }
    
    export function validateUrl(url: string): boolean {
        return url && (tiktokRegex.test(url) || tiktokMobileRegex.test(url) || tiktokMobileAltRegex.test(url));
    }
    
    export function urlEncode(str: string): string {
        if (str) {
            str = encodeURIComponent(str)
                .replace('!', '%21')
                .replace('\'', '%27')
                .replace('(', '%28')
                .replace(')', '%29')
                .replace('*', '%2A')
                .replace('%20', '+');
        }       
        return str;
    }
    
    export function getBlob(url: string): Promise<Blob> {
        return new Promise<Blob>((response, reject) => {
            const xhr: XMLHttpRequest = new XMLHttpRequest();
            xhr.open('get', url, true);
            xhr.responseType = 'blob';
            xhr.onload = () => response(xhr.response);
            xhr.onerror = (e) => reject(e);
            xhr.send();	
        });	 
    }
    
    export async function getInfo(url: string): Promise<TikTokInfo> {
        let tiktokUrl: string = bypassCorsHeaders + urlEncode(url);
        let resp: string =  await fetch(tiktokUrl).then(response => response.text()).then((data) => { return data; });
        
        if (resp.includes(linkPattern)) {
            tiktokUrl = urlEncode(resp.split(linkPattern)[1].split(greaterSymbol)[0]);
            resp =  await fetch(bypassCorsHeaders + tiktokUrl).then(response => response.text()).then((data) => { return data; });
        }
        
        const json: any = JSON.parse(resp.split(idPattern)[1].split(invertedTag)[0].split(closingTag)[1]);
    
        const video : VideoInfo = {
            id: json.props.pageProps.itemInfo.itemStruct.video.id,
            height: json.props.pageProps.itemInfo.itemStruct.video.height,
            duration: json.props.pageProps.itemInfo.itemStruct.video.duration,
            sizeFormat: json.props.pageProps.itemInfo.itemStruct.video.ratio,
            cover: json.props.pageProps.itemInfo.itemStruct.video.cover,
            animatedCover: json.props.pageProps.itemInfo.itemStruct.video.dynamicCover,
            likes: json.props.pageProps.itemInfo.itemStruct.stats.diggCount,
            shares: json.props.pageProps.itemInfo.itemStruct.stats.shareCount,
            comments: json.props.pageProps.itemInfo.itemStruct.stats.commentCount,
            views: json.props.pageProps.itemInfo.itemStruct.stats.playCount,
            title: json.props.pageProps.metaParams.title,
            keywords: json.props.pageProps.metaParams.keywords,
            description: json.props.pageProps.metaParams.description,
            original_url: json.props.pageProps.metaParams.canonicalHref,
            download_url: json.props.pageProps.itemInfo.itemStruct.video.downloadAddr,
        };
    
        const music: MusicInfo = {
            id:json.props.pageProps.itemInfo.itemStruct.music.id,
            title:json.props.pageProps.itemInfo.itemStruct.music.title,
            cover_large:json.props.pageProps.itemInfo.itemStruct.music.coverLarge,
            cover_medium:json.props.pageProps.itemInfo.itemStruct.music.coverMedium,
            cover_small:json.props.pageProps.itemInfo.itemStruct.music.coverThumb,
            artist:json.props.pageProps.itemInfo.itemStruct.music.authorName,
            album:json.props.pageProps.itemInfo.itemStruct.music.album,
            duration:json.props.pageProps.itemInfo.itemStruct.music.duration,
            url:json.props.pageProps.itemInfo.itemStruct.music.playUrl,
        };
    
        const author: AuthorInfo = {
            id:json.props.pageProps.itemInfo.itemStruct.author.id,
            uniqueId:json.props.pageProps.itemInfo.itemStruct.author.uniqueId,
            username:json.props.pageProps.itemInfo.itemStruct.author.nickname,
            avatar_large:json.props.pageProps.itemInfo.itemStruct.author.avatarLarger,
            avatar_medium:json.props.pageProps.itemInfo.itemStruct.author.avatarMedium,
            avatar_small:json.props.pageProps.itemInfo.itemStruct.author.avatarThumb,
            signature:json.props.pageProps.itemInfo.itemStruct.author.signature,
            createDate:json.props.pageProps.itemInfo.itemStruct.author.createTime,
            isVerified:json.props.pageProps.itemInfo.itemStruct.author.verified,
            followers:json.props.pageProps.itemInfo.itemStruct.authorStats.followerCount,
            followings:json.props.pageProps.itemInfo.itemStruct.authorStats.followingCount,
            hearts:json.props.pageProps.itemInfo.itemStruct.authorStats.heart,
            totalVideos:json.props.pageProps.itemInfo.itemStruct.authorStats.videoCount,
            diggCount:json.props.pageProps.itemInfo.itemStruct.authorStats.diggCount,
        };
    
        return {
            video: video,
            music: music,
            author: author
        };
    }
    
    export async function downloadMusic(url: string): Promise<DownloadResponse> {
        if(url == null) {
            throw 'No URL was provided.';
        }
        if (!validateUrl(url)) {
            throw 'The URL provided is not valid.'
        }
    
        const info: TikTokInfo = await getInfo(url);
        const downloadUrl: string = bypassCorsHeaders + urlEncode(info.music.url) + downloadParameter;
        const blob: Blob = await getBlob(downloadUrl);
        return {
            blob: blob,
            fileName: info.music.id,
            type: 'mp3'
        };
    }
    
    export async function downloadVideo(url: string): Promise<DownloadResponse> {
        if(url == null) {
            throw 'No URL was provided.';
        }
        if (!validateUrl(url)) {
            throw 'The URL provided is not valid.'
        }
        
        const info: TikTokInfo = await getInfo(url);
        const downloadUrl: string = bypassCorsHeaders + urlEncode(info.video.download_url) + downloadParameter;
        const blob: Blob = await getBlob(downloadUrl);
        return {
            blob: blob,
            fileName: info.video.id,
            type: 'mp4'
        };
    }
}