import { storage } from "../storage"
import CameraRoll from "@react-native-community/cameraroll"
import { Platform } from "react-native"
import { useStore } from "../state"
import { queueFileUpload } from "../upload"
import { getFilenameFromPath, Semaphore, randomIdUnsafe, getFileExt, promiseAllSettled, asyncJSON } from "../helpers"
import ReactNativeBlobUtil from "react-native-blob-util"
import { getDownloadPath } from "../download"
import { folderPresent, reportError } from "../api"
import BackgroundTimer from "react-native-background-timer"
import RNFS from "react-native-fs"
import { hasStoragePermissions, hasPhotoLibraryPermissions } from "../permissions"
import pathModule from "react-native-path"

const cameraUploadTimeout = 5000
const copySemaphore = new Semaphore(1)
const maxErrorCountIds = 8
const erroredIds = {}

const increaseErrorCountForId = (id) => {
    if(typeof erroredIds[id] == "undefined"){
        erroredIds[id] = 0
    }

    erroredIds[id] = erroredIds[id] + 1

    return true
}

const getErrorCountForId = (id) => {
    return typeof erroredIds[id] == "undefined" ? 0 : erroredIds[id]
}

export const isCameraUploadRunning = () => {
    try{
        return storage.getBoolean("cameraUploadRunning")
    }
    catch(e){
        console.log(e)

        return false
    }
}

export const setCameraUploadRunning = (val = true) => {
    try{
        storage.set("cameraUploadRunning", val)
    }
    catch(e){
        console.log(e)

        return false
    }

    return true
}

export const disableCameraUpload = (resetFolder = false) => {
    try{
        var userId = storage.getString("userId")

        storage.set("cameraUploadEnabled:" + userId, false)

        if(resetFolder){
            storage.delete("cameraUploadFolderUUID:" + userId)
            storage.delete("cameraUploadFolderName:" + userId)
        }
    }
    catch(e){
        console.log(e)

        return false
    }

    return true
}

export const runCameraUpload = async ({ maxQueue = 10, runOnce = false, callback = undefined }) => {
    const callCallback = (...args) => {
        if(typeof callback == "function"){
            return callback(...args)
        }

        return false
    }

    if(isCameraUploadRunning() && !runOnce){
        callCallback(false)

        return false
    }

    setCameraUploadRunning(true)

    const isDeviceReady = useStore.getState().isDeviceReady
    const netInfo = useStore.getState().netInfo

    if(!isDeviceReady){
        setCameraUploadRunning(false)
        callCallback(false)

        if(runOnce){
            return false
        }
        else{
            return BackgroundTimer.setTimeout(() => {
                runCameraUpload({ maxQueue, runOnce, callback })
            }, cameraUploadTimeout)
        }
    }

    try{
        var isLoggedIn = await storage.getBooleanAsync("isLoggedIn")
    }
    catch(e){
        console.log(e)

        reportError(e, "cameraUpload:getIsLoggedIn")

        setCameraUploadRunning(false)
        callCallback(false)

        if(runOnce){
            return false
        }
        else{
            return BackgroundTimer.setTimeout(() => {
                runCameraUpload({ maxQueue, runOnce, callback })
            }, cameraUploadTimeout)
        }
    }

    if(!isLoggedIn){
        setCameraUploadRunning(false)
        callCallback(false)

        if(runOnce){
            return false
        }
        else{
            return BackgroundTimer.setTimeout(() => {
                runCameraUpload({ maxQueue, runOnce, callback })
            }, cameraUploadTimeout)
        }
    }

    try{
        var userId = await storage.getNumberAsync("userId")
    }
    catch(e){
        console.log(e)

        reportError(e, "cameraUpload:getUserId")

        setCameraUploadRunning(false)
        callCallback(false)

        if(runOnce){
            return false
        }
        else{
            return BackgroundTimer.setTimeout(() => {
                runCameraUpload({ maxQueue, runOnce, callback })
            }, cameraUploadTimeout)
        }
    }

    if(typeof userId !== "number"){
        setCameraUploadRunning(false)
        callCallback(false)

        reportError("userId !== number", "cameraUpload:userId")

        if(runOnce){
            return false
        }
        else{
            return BackgroundTimer.setTimeout(() => {
                runCameraUpload({ maxQueue, runOnce, callback })
            }, cameraUploadTimeout)
        }
    }

    if(userId == 0){
        setCameraUploadRunning(false)
        callCallback(false)

        reportError("userId == 0", "cameraUpload:userId")

        if(runOnce){
            return false
        }
        else{
            return BackgroundTimer.setTimeout(() => {
                runCameraUpload({ maxQueue, runOnce, callback })
            }, cameraUploadTimeout)
        }
    }

    try{
        if(await storage.getBooleanAsync("onlyWifiUploads:" + userId) && netInfo.type !== "wifi"){
            setCameraUploadRunning(false)
            callCallback(false)

            if(runOnce){
                return false
            }
            else{
                return BackgroundTimer.setTimeout(() => {
                    runCameraUpload({ maxQueue, runOnce, callback })
                }, cameraUploadTimeout)
            }
        }
    }
    catch(e){
        console.log(e)

        reportError(e, "cameraUpload:getWifiOnlyUploads")
    }

    try{
        var cameraUploadEnabled = await storage.getBooleanAsync("cameraUploadEnabled:" + userId)
        var cameraUploadFolderUUID = await storage.getStringAsync("cameraUploadFolderUUID:" + userId)
        var cameraUploadFetchNewAssetsTimeout = await storage.getNumberAsync("cameraUploadFetchNewAssetsTimeout") || 0
        var cameraUploadUploadedIds = await asyncJSON.parse(await storage.getStringAsync("cameraUploadUploadedIds:" + userId) || "{}")
    }
    catch(e){
        console.log(e)

        reportError(e, "cameraUpload:getState")

        setCameraUploadRunning(false)
        callCallback(false)

        if(runOnce){
            return false
        }
        else{
            return BackgroundTimer.setTimeout(() => {
                runCameraUpload({ maxQueue, runOnce, callback })
            }, cameraUploadTimeout)
        }
    }

    useStore.setState({ cameraUploadUploaded: Object.keys(cameraUploadUploadedIds).length })

    if(!cameraUploadEnabled){
        setCameraUploadRunning(false)
        callCallback(false)

        return false
    }

    if(typeof cameraUploadFolderUUID !== "string"){
        setCameraUploadRunning(false)
        disableCameraUpload(true)
        callCallback(false)

        return false
    }

    if(cameraUploadFolderUUID.length < 32){
        setCameraUploadRunning(false)
        disableCameraUpload(true)
        callCallback(false)

        return false
    }

    if(!netInfo.isConnected || !netInfo.isInternetReachable){
        setCameraUploadRunning(false)
        callCallback(false)

        if(runOnce){
            return false
        }
        else{
            return BackgroundTimer.setTimeout(() => {
                runCameraUpload({ maxQueue, runOnce, callback })
            }, cameraUploadTimeout)
        }
    }

    let folderExists = false

    try{
        const isFolderPresent = await folderPresent({ uuid: cameraUploadFolderUUID })

        if(isFolderPresent.present){
            if(!isFolderPresent.trash){
                folderExists = true
            }
        }
    }
    catch(e){
        console.log(e)

        reportError(e, "cameraUpload:folderPresent")

        setCameraUploadRunning(false)
        callCallback(false)

        if(runOnce){
            return false
        }
        else{
            return BackgroundTimer.setTimeout(() => {
                runCameraUpload({ maxQueue, runOnce, callback })
            }, cameraUploadTimeout)
        }
    }

    if(!folderExists){
        setCameraUploadRunning(false)
        disableCameraUpload(true)

        return false
    }

    if(Math.floor(+new Date()) > cameraUploadFetchNewAssetsTimeout){
        try{
            var assets = await getCameraRollAssets()

            await storage.setAsync("cachedCameraUploadAssets:" + userId, await asyncJSON.stringify(assets))
            await storage.setAsync("cameraUploadFetchNewAssetsTimeout", (Math.floor(+new Date()) + 30000))
        }
        catch(e){
            console.log(e)

            reportError(e, "cameraUpload:getCameraRollAssets")

            setCameraUploadRunning(false)
            callCallback(false)

            if(runOnce){
                return false
            }
            else{
                return BackgroundTimer.setTimeout(() => {
                    runCameraUpload({ maxQueue, runOnce, callback })
                }, cameraUploadTimeout)
            }
        }
    }
    else{
        try{
            var assets = await asyncJSON.parse(await storage.getStringAsync("cachedCameraUploadAssets:" + userId) || "[]")
        }
        catch(e){
            console.log(e)

            reportError(e, "cameraUpload:getCachedCameraRollAssets")

            setCameraUploadRunning(false)
            callCallback(false)

            if(runOnce){
                return false
            }
            else{
                return BackgroundTimer.setTimeout(() => {
                    runCameraUpload({ maxQueue, runOnce, callback })
                }, cameraUploadTimeout)
            }
        }
    }

    useStore.setState({ cameraUploadTotal: assets.length })

    if(assets.length == 0){
        setCameraUploadRunning(false)
        callCallback(false)

        if(runOnce){
            return false
        }
        else{
            return BackgroundTimer.setTimeout(() => {
                runCameraUpload({ maxQueue, runOnce, callback })
            }, cameraUploadTimeout)
        }
    }

    /*if(Object.keys(cameraUploadUploadedIds).length == assets.length){
        setCameraUploadRunning(false)
        callCallback(false)

        if(runOnce){
            return false
        }
        else{
            return BackgroundTimer.setTimeout(() => {
                runCameraUpload({ maxQueue, runOnce, callback })
            }, cameraUploadTimeout)
        }
    }*/

    let currentQueue = 0
    const uploads = []

    const upload = (asset) => {
        return new Promise(async (resolve, reject) => {
            const id = getAssetId(asset)

            if(getErrorCountForId(id) >= maxErrorCountIds){
                return resolve()
            }

            await copySemaphore.acquire()

            const uploadName = getUploadName(asset)

            if(Platform.OS == "android"){
                try{
                    var stat = await RNFS.stat(asset.uri)
                    var copyPath = await getDownloadPath({ type: "temp" }) + randomIdUnsafe() + "." + getFileExt(uploadName)

                    if((await RNFS.exists(copyPath))){
                        await RNFS.unlink(copyPath)
                    }

                    await new Promise((resolve) => BackgroundTimer.setTimeout(resolve, 250)) // somehow needs to sleep a bit, otherwise the copy call fails on older/slower devices
                    
                    await RNFS.copyFile(asset.uri, copyPath)
                }
                catch(e){
                    console.log(e)

                    copySemaphore.release()

                    reportError(e, "cameraUpload:copyAndStat")
                    increaseErrorCountForId(id)

                    return reject(e)
                }

                if(typeof stat !== "object"){
                    copySemaphore.release()

                    reportError("camera upload: copy path stat !== object")
                    increaseErrorCountForId(id)

                    return reject("camera upload: copy path stat !== object")
                }

                var file = {
                    uri: copyPath.indexOf("file://") == -1 ? "file://" + copyPath : copyPath,
                    name: uploadName,
                    size: stat.size,
                    type: asset.type,
                    lastModified: asset.timestamp
                }
            }
            else{
                try{
                    var copyPath = await getDownloadPath({ type: "temp" }) + randomIdUnsafe() + "." + getFileExt(uploadName)

                    if((await RNFS.exists(copyPath))){
                        await RNFS.unlink(copyPath)
                    }

                    await new Promise((resolve) => BackgroundTimer.setTimeout(resolve, 250)) // somehow needs to sleep a bit, otherwise the copy call fails on older/slower devices

                    //todo: RN blob util supports copying raw heif/heic, make it an option
                    //await ReactNativeBlobUtil.fs.cp(asset.uri, copyPath)

                    if(asset.type.indexOf("image") !== -1){
                        await RNFS.copyAssetsFileIOS(asset.uri, copyPath, 0, 0)
                    }
                    else{
                        await RNFS.copyAssetsVideoIOS(asset.uri, copyPath)
                    }

                    await new Promise((resolve) => BackgroundTimer.setTimeout(resolve, 1000)) // somehow needs to sleep a bit, otherwise the stat call fails on older/slower devices

                    var stat = await RNFS.stat(copyPath)
                }
                catch(e){
                    console.log(e)

                    copySemaphore.release()

                    reportError(e, "cameraUpload:copyAndStat")
                    increaseErrorCountForId(id)

                    return reject(e)
                }

                if(typeof stat !== "object"){
                    copySemaphore.release()

                    reportError("camera upload: copy path stat !== object")
                    increaseErrorCountForId(id)

                    return reject("camera upload: copy path stat !== object")
                }

                var file = {
                    uri: copyPath.indexOf("file://") == -1 ? "file://" + copyPath : copyPath,
                    name: uploadName,
                    size: stat.size,
                    type: asset.type.indexOf("image") !== -1 ? "image/jpg" : "video/mp4",
                    lastModified: asset.timestamp
                }
            }

            copySemaphore.release()

            queueFileUpload({
                pickedFile: file,
                parent: cameraUploadFolderUUID,
                cameraUploadCallback: async (err) => {
                    if(!err){
                        try{
                            const uploadedIds = await asyncJSON.parse(await storage.getStringAsync("cameraUploadUploadedIds:" + userId) || "{}")

                            if(typeof uploadedIds[id] == "undefined"){
                                uploadedIds[id] = true

                                await storage.setAsync("cameraUploadUploadedIds:" + userId, await asyncJSON.stringify(uploadedIds))

                                useStore.setState({ cameraUploadUploaded: Object.keys(uploadedIds).length })
                            }
                        }
                        catch(e){
                            console.log(e)
                        }
                    }
                    else{
                        return reject(err)
                    }

                    return resolve()
                }
            })
        })
    }

    for(let i = 0; i < assets.length; i++){
        const id = getAssetId(assets[i])

        if(typeof cameraUploadUploadedIds[id] == "undefined" && maxQueue > currentQueue){
            currentQueue += 1

            uploads.push(upload(assets[i]))
        }
    }

    if(uploads.length > 0){
        try{
            await promiseAllSettled(uploads)
        }
        catch(e){
            console.log(e)
        }
    }

    setCameraUploadRunning(false)
    callCallback(true)

    if(runOnce){
        return true
    }
    else{
        return BackgroundTimer.setTimeout(() => {
            runCameraUpload({ maxQueue, runOnce, callback })
        }, cameraUploadTimeout)
    }
}

export const getUploadName = (asset) => {
    //new Date(asset.timestamp * 1000).toLocaleString().split(" ").join("_").split(",").join("_").split(":").join("_").split(".").join("_") + "_" + 

    if(Platform.OS == "ios"){
        return asset.rawId.split("/").join("_").split("-").join("_") + "." + (asset.type.indexOf("image") !== -1 ? "jpg" : "mp4")
    }
    else{
        return getFilenameFromPath(asset.uri)
    }
}

export const getAssetId = (asset) => {
    return asset.uri
}

export const convertPhAssetToAssetsLibrary = (localId, ext) => {
    const hash = localId.split("/")[0]

    return "assets-library://asset/asset." + ext + "?id=" + hash + "&ext=" + ext
}

export const getCameraRollAssets = () => {
    return new Promise((resolve, reject) => {
        hasStoragePermissions(false).then(() => {
            hasPhotoLibraryPermissions(false).then(async () => {
                try{
                    var userId = await storage.getStringAsync("userId")
                    var cameraUploadIncludeImages = await storage.getBooleanAsync("cameraUploadIncludeImages:" + userId)
                    var cameraUploadIncludeVideos = await storage.getBooleanAsync("cameraUploadIncludeVideos:" + userId)
                }
                catch(e){
                    return reject(e)
                }
        
                let assetType = "All"
        
                if(cameraUploadIncludeImages && !cameraUploadIncludeVideos){
                    assetType = "Photos"
                }
        
                if(!cameraUploadIncludeImages && cameraUploadIncludeVideos){
                    assetType = "Videos"
                }
        
                if(cameraUploadIncludeImages && cameraUploadIncludeVideos){
                    assetType = "All"
                }
        
                const max = 100
                let after = undefined
                const photos = []
                const existing = {}
        
                const get = (first, cursor) => {
                    CameraRoll.getPhotos({
                        first,
                        assetType,
                        include: [],
                        after: cursor
                    }).then((data) => {
                        try{
                            data.edges.forEach((edge) => {
                                const uri = pathModule.normalize(decodeURIComponent(edge.node.image.uri))
            
                                // Do not upload files that are locally saved (app isolated library), avoiding duplicates
                                if(
                                    uri.indexOf("/io.filen.app/") == -1
                                    && typeof existing[uri] == "undefined"
                                    && getErrorCountForId(uri) < maxErrorCountIds
                                ){
                                    existing[uri] = true
                                    
                                    photos.push({
                                        rawId: uri.replace("ph://", ""),
                                        uri: Platform.OS == "ios" ? convertPhAssetToAssetsLibrary(uri.replace("ph://", ""), edge.node.type === "image" ? "jpg" : "mov") : uri,
                                        rawURI: uri,
                                        type: edge.node.type,
                                        timestamp: Math.floor(edge.node.timestamp)
                                    })
                                }
                            })
            
                            if(data.page_info.has_next_page){
                                after = data.page_info.end_cursor
            
                                return get(first, after)
                            }
            
                            // There is sorting bug in the RN-CameraRoll lib so we have to manually sort the fetched assets by date taken DESC
                            const assets = photos.sort((a, b) => {
                                return b.timestamp > a.timestamp
                            })
            
                            return resolve(assets)
                        }
                        catch(e){
                            return reject(e)
                        }
                    }).catch(reject)
                }
        
                return get(max, after)
            }).catch(reject)
        }).catch(reject)
    })
}