/^ The CrossLang Package Manager Client ^/
class Tesses.CrossLang.PackageManager
{
    private configRoot;
    private packageCache;
    /^ Work offline (defaults to false) ^/
    public Offline = false;

    /^ Construct the Package Manager ^/
    public PackageManager()
    {
        this.configRoot = Env.CrossLangConfig;
        this.packageCache = this.configRoot / "PackageCache";
        
        FS.Local.CreateDirectory(this.packageCache);
    }
    /^ Parse package filename ^/
    public ParseFileName(name)
    {
        
        var index = name.LastIndexOf('.');
        if(index == -1) throw "We expect an extension";
        if(name.Substring(index) != ".crvm") throw "We expect the extension .crvm";
        name = name.Substring(0,index);
        //Name-VERSION-[dev|alpha|beta|prod]

        var lastIndex = name.LastIndexOf('-');
        var verStage = name.Substring(lastIndex+1);
        var versionStr = verStage;
        if(verStage == "dev" || verStage == "alpha" || verStage == "beta" || verStage == "prod")
        {
            var lastIndex2 = name.LastIndexOf('-', lastIndex-1);
            return {
                Name = name.Substring(0,lastIndex2),
                Version = name.Substring(lastIndex2+1)
            };
        }
        else
        {
            return {
                Name = name.Substring(0,lastIndex),
                Version = name.Substring(lastIndex+1)
            };
        }
    }
    /^ Get package servers list ^/
    public GetPackageServers()
    {
        var packageConfigFile = configRoot / "package_servers.json";
            
        if(FS.Local.RegularFileExists(packageConfigFile))
        {
            return Json.Decode(FS.ReadAllText(FS.Local, packageConfigFile));
        }
        return ["https://cpkg.tesseslanguage.com/"];
    } 
    /^ Get a package (returns bytearray of package data or null if not found), use GetLatest to get the latest version ^/
    public GetPackage(name, version)
    {
        var v = Version.Parse(version);
            var useCache = v.Stage != "dev";
            var pkgFile = packageCache / name / v.ToString();
            
            if(useCache && FS.Local.RegularFileExists(pkgFile))   
            {
                return FS.ReadAllBytes(FS.Local,pkgFile);
            }
            if(this.Offline) return null;

            each(var item : this.GetPackageServers())
            {
                //https://cpkg.tesseslanguage.com/api/v1/download?name=MyPackage&version=1.0.0.0-prod
                var uri = $"{item.TrimEnd('/')}/api/v1/download?name={Net.Http.UrlEncode(name)}&version={Net.Http.UrlEncode(version)}";
                Console.WriteLine($"Downloading: {name} {version} from {item}");
                var req = Net.Http.MakeRequest(uri);
                if(req.StatusCode == 200)
                {
                    var strm = new MemoryStream(true);
                    req.CopyToStream(strm);
                    
                    if(useCache) {
                        FS.Local.CreateDirectory(packageCache / name);
                        var f = FS.Local.OpenFile(pkgFile,"wb");
                        strm.Seek(0,0);
                        strm.CopyTo(f);
                        f.Close();
                    }
                    var data = strm.GetBytes();
                    strm.Close();
                    req.Close();
                    return data;
                }
            }
            return null;
    }
    /^ Get the latest version of a package ^/
    public GetLatest(name)
    {
        var pkgServers = this.GetPackageServers();
            if(this.Offline || pkgServers.Count == 0)
            {
                //user has declared they are offline or don't have packageServers look through packages locally
                var version = new Version(0,0,0,0,"dev");
                var configRoot = Env.CrossLangConfig;
                var dir = configRoot / "PackageCache" / name;
                if(FS.Local.DirectoryExists(dir))
                each(var f : FS.Local.EnumeratePaths(dir))
                {
                    var v = Version.Parse(f.GetFileName());
                    if(v >= version)
                    {
                        version = v;
                    }
                }

                if(version.VersionInt == 0) return null;
                return version.ToString();
            }
            else
            {
                each(var item : pkgServers)
                {
                    var uri = $"{item.TrimEnd('/')}/api/v1/latest?name={Net.Http.UrlEncode(name)}";
                    var req = Net.Http.MakeRequest(uri);
                    if(req.StatusCode == 200)
                    {
                        var res0 = req.ReadAsString();
                      
                        var res = Json.Decode(res0);
                        req.Close();
                        return res.version;
                    }
                }

            }
            return null;
    }
    /^
        Search for packages based on query
        Options is a {"server": "SERVER_URL", "type": "lib", "pluginHost": "yourPluginHost","offset":0,"limit": 20}
    ^/
    public Search(query, $options)
    {
        options = options ?? {};

        var urlBase = $"/api/v1/search?q={Net.Http.UrlEncode(query)}";
        if(TypeOf(options.type) == "String") urlBase += $"&type={Net.Http.UrlEncode(options.type)}";
        if(TypeOf(options.pluginHost) == "String") urlBase += $"&pluginHost={Net.Http.UrlEncode(options.pluginHost)}";
        if(TypeOf(options.offset) == "Long") urlBase += $"&offset={options.offset}";
        if(TypeOf(options.limit) == "Long") urlBase += $"&limit={options.limit}";
        

        func handle(server)
        {
            var req = Net.Http.MakeRequest($"{server.TrimEnd('/')}{urlBase}");
            if(req.StatusCode==200)
            {
                try {
                    var res2=Json.Decode(req.ReadAsString());
                    return res2.packages;
                }catch(ex) {
                }
            }

            return null;
        }

        if(TypeOf(options.server) == "String")
            return handle(options.server) ?? [];
        else {
            each(var item : this.GetPackageServers())
            {
                var r = handle(item);
                if(TypeOf(r) == "List") return r;

            }
            return [];
        }
        
    }
    /^
        Download a plugin to directory
        returns true if succeeds or false if it fails
    ^/
    public DownloadPlugin(dirFS, name, version)
    {
        var pkg = this.GetPackage(name,version.ToString());
        if(pkg == null) return false;
        var ms = new MemoryStream(true);
        ms.WriteBlock(pkg,0,pkg.Count);
        ms.Seek(0,0);
        var vm = VM.LoadExecutable(ms);
        ms.Close();
        var info = {};
        try{info=Json.Decode(vm.Info);}catch(ex){}
        var short_name = TypeOf(info.short_name) == "String" ? info.short_name : name;
        dirFS.CreateDirectory(/short_name);
        each(var file : dirFS.EnumeratePaths(/short_name))
        {
            if(file.GetExtension() == ".crvm" && dirFS.FileExists(file))
            {
                dirFS.DeleteFile(file);
            }
        }
        FS.WriteAllBytes(dirFS,/short_name/$"{short_name}.crvm", pkg);

        func dlDeps(_vm)
        {
            each(var dep : _vm.Dependencies)
            {
                var path = /short_name/$"{dep.Name}-{dep.Version.ToString()}";
                if(!dirFs.RegularFileExists(path))
                {
                    var pkg2 = this.GetPackage(dep.Name,dep.Version.ToString());
                    if(pkg2 == null) return false;
                    FS.WriteAllBytes(dirFS,path,pkg2);
                    var ms2 = new MemoryStream(true);
                    ms2.WriteBlock(pkg2,0,pkg2.Count);
                    ms2.Seek(0,0);
                    var vm2 = VM.LoadExecutable(ms2);
                    ms2.Close();
                    dlDeps(vm2);
                }
            }

            return true;
        }
        
        return dlDeps(vm);
    }
}