import fetch from"../../SoopyV2/utils/networkUtils";

const Instant=Java.type("java.time.Instant");
import logger from"../logger";
const File=Java.type("java.io.File");
import metadata from"../metadata.js";
import soopyV2Server from"../socketConnection";
import NonPooledThread from"../utils/nonPooledThread";
import{setRendering}from"../utils/renderJavaUtils";
import{registerForge as registerForgeBase,unregisterForge as unregisterForgeBase}from"./forgeEvents.js";

const JSLoader=Java.type("com.chattriggers.ctjs.engine.langs.js.JSLoader");
const UrlModuleSourceProvider=Java.type("org.mozilla.javascript.commonjs.module.provider.UrlModuleSourceProvider");
const UrlModuleSourceProviderInstance=new UrlModuleSourceProvider(null,null);
const StrongCachingModuleScriptProviderClass=Java.type("org.mozilla.javascript.commonjs.module.provider.StrongCachingModuleScriptProvider");
let StrongCachingModuleScriptProvider=new StrongCachingModuleScriptProviderClass(UrlModuleSourceProviderInstance);
let CTRequire=new JSLoader.CTRequire(StrongCachingModuleScriptProvider);

const System=Java.type("java.lang.System");

let loadedModules=new Set;
let shouldRequireForceNoCache=false;

function RequireNoCache(place){
if(!shouldRequireForceNoCache){
if(!logger.isDev)return require(place);
if(!loadedModules.has(place)){
loadedModules.add(place);
return require(place);
}
}

StrongCachingModuleScriptProvider=new StrongCachingModuleScriptProviderClass(UrlModuleSourceProviderInstance);
CTRequire=new JSLoader.CTRequire(StrongCachingModuleScriptProvider);
return CTRequire(place);
}

class FeatureManager{
constructor(){

this.isDev=logger.isDev;

this.messagePrefix="&6[SOOPY V2]&7 ";
this.enabled=true;

this.features={};
this.events={};
this.eventObjects={};
this.soopyEventHandlers={};

this.parent=undefined;

this.finishedLoading=false;

this.commandFuncs={};

this.lastEventId=0;

this.customEvents={};
this.lastChatEventId=0;

this.forgeEvents={};
this.lastForgeEventId=0;

this.lastSoopyEventId=0;

this.featureSettingsDataLastUpdated=false;


this.recordingPerformanceUsage=false;
this.performanceUsage={};
this.flameGraphData=[];

this.longEventTime=20;

this.perfTrackingFeatures=false;
this.stack=[];


this.featureMetas={};

this.featureSettingsData={};

let fetchD=fetch("https://soopy.dev/api/soopyv2/ping",{timeout:10000});
fetchD.load().then(()=>{
if(fetchD.responseCode()>=400||fetchD.responseCode()===-1){
ChatLib.chat(this.messagePrefix+"&cError: Could not connect to Soopy's server. This may cause issues with some features but will (hopefully) be back soon.");
}

new NonPooledThread(()=>{
this.loadSoopy();
}).start();
}).catch((e)=>{
ChatLib.chat(this.messagePrefix+"&cError: Could not connect to Soopy's server. This may cause issues with some features but will (hopefully) be back soon.");

new NonPooledThread(()=>{
this.loadSoopy();
}).start();
});

this.registerStep(false,30,()=>{
if(this.featureSettingsDataLastUpdated){
new Thread(()=>{
this.saveFeatureSettings();
}).start();
}
},this);

this.registerEvent("worldUnload",this.saveFeatureSettings,this);

this.registerEvent("gameUnload",()=>{
this.saveFeatureSettings();
this.unloadAllFeatures();

this.enabled=false;
},this);

if(this.isDev){
this.registerStep(true,2,()=>{
if(this.reloadModuleTime!==0&&Date.now()-this.reloadModuleTime>0){
new Thread(()=>{
this.reloadModuleTime=0;
this.reloadingModules.forEach((m)=>{
this.unloadFeature(m);
});
this.reloadingModules.forEach((m)=>{
this.loadFeature(m);
});
this.reloadingModules.clear();
}).start();
}
},this);

this.watches={};
this.addedWatches=new Set;
this.watchService=Java.type("java.nio.file.FileSystems").getDefault().newWatchService();
this.reloadingModules=new Set;
this.reloadModuleTime=0;
new NonPooledThread(()=>{
while(this.enabled){
key=this.watchService.take();
let moduleToReload=this.watches[key];
if(this.features[moduleToReload]&&!this.reloadingModules.has(moduleToReload)){
this.reloadingModules.add(moduleToReload);
this.reloadModuleTime=Date.now()+100;
}
key.pollEvents();


key.reset();
}
}).start();
}

this.registerCommand("soopyunloadfeature",(args)=>{
new Thread(()=>{
this.unloadFeature(args);
}).start();
},this);
this.registerCommand("soopyloadfeature",(args)=>{
new Thread(()=>{
this.loadFeature(args);
}).start();
},this);
this.registerCommand("soopyunload",()=>{
new Thread(()=>{
this.unloadSoopy();
}).start();
},this);
this.registerCommand("soopyload",()=>{
new NonPooledThread(()=>{
this.loadSoopy();
}).start();
},this);
this.registerCommand("soopyreload",()=>{
new NonPooledThread(()=>{
this.unloadSoopy();
this.loadSoopy();
}).start();
},this);
this.registerCommand("soopyreloadfeature",(args)=>{
new Thread(()=>{
this.unloadFeature(args);

this.loadFeature(args);
}).start();
},this);
this.registerCommand("soopysetlongeventtime",(args)=>{
this.longEventTime=parseInt(args);
},this);
this.registerCommand("soopylaginformation",(full)=>{
if(full==="full"){
this.loadPerformanceData();
}else{
this.loadPerformanceDataFast();
}
},this);
}

getId(){
return"FeatureManager";
}
loadPerformanceDataFast(){
new Thread(()=>{
this.loadEventLag(true);
}).start();
}

loadPerformanceData(){
new NonPooledThread(()=>{
ChatLib.chat(this.messagePrefix+"Recording performance impact, this will take around 60 seconds to complete!");
shouldRequireForceNoCache=true;
let eventLagData=this.loadEventLag();
ChatLib.chat(this.messagePrefix+"ETA: 40s");
this.perfTrackingFeatures=true;
this.unloadSoopy();
this.loadSoopy();
Thread.sleep(1000);
let eventLagDataFull=this.loadEventLag();
this.perfTrackingFeatures=false;
this.unloadSoopy();
this.loadSoopy();
Thread.sleep(1000);
ChatLib.chat(this.messagePrefix+"ETA: 25s");
let forgeLagData=this.loadForgeRenderLag();
ChatLib.chat(this.messagePrefix+"ETA: 15s");
let soopyLagData=this.loadSoopyLag();

let lagData={
eventLagDataFull,
eventLagData,
forgeLagData,
soopyLagData};


shouldRequireForceNoCache=false;
let url=this.reportLagData(lagData);
ChatLib.chat(this.messagePrefix+"Done!");
new TextComponent(this.messagePrefix+"See the report at "+url).setClick("open_url",url).setHover("show_text","Click to open the report.").chat();


this.performanceUsage={};
this.flameGraphData=[];
}).start();
}

reportLagData(data){
return fetch("https://soopy.dev/soopy/submitlag",{postData:data}).textSync();
}

loadSoopyLag(){

let framesWith=0;
let framesWithOut=0;
let event=this.registerEvent("renderWorld",()=>{
framesWith++;
},this);

Thread.sleep(5000);
this.unregisterEvent(event);

this.unloadSoopy();
Thread.sleep(1000);
event=this.registerEvent("renderWorld",()=>{
framesWithOut++;
},this);

Thread.sleep(5000);
this.unregisterEvent(event);





this.loadSoopy();

return{
fpsWith:framesWith/5,
fpsWithout:framesWithOut/5};

}

loadForgeRenderLag(){

let framesWith=0;
let framesWithOut=0;
let renderingForge=true;
let event=this.registerEvent("renderWorld",()=>{
if(renderingForge){
framesWith++;
}else{
framesWithOut++;
}
},this);

for(let i=0;i<10;i++){
Thread.sleep(1000);
renderingForge=!renderingForge;
setRendering(renderingForge);
}
this.unregisterEvent(event);




return{
fpsWith:framesWith/5,
fpsWithout:framesWithOut/5};

}

loadEventLag(sendMessage=false){
this.recordingPerformanceUsage=true;
this.performanceUsage={};
this.flameGraphData=[];
if(sendMessage)ChatLib.chat(this.messagePrefix+"Recording Event Lag...");

Thread.sleep(10000);

let totalMsGlobal=0;
this.recordingPerformanceUsage=false;
if(sendMessage){
ChatLib.chat(this.messagePrefix+"Event Lag:");
Object.keys(this.performanceUsage).sort((a,b)=>{
let totalMsA=0;
Object.keys(this.performanceUsage[a]).forEach((event)=>{
totalMsA+=this.performanceUsage[a][event].time;
});
let totalMsB=0;
Object.keys(this.performanceUsage[b]).forEach((event)=>{
totalMsB+=this.performanceUsage[b][event].time;
});

return totalMsA-totalMsB;
}).forEach((moduleName)=>{
let totalMs=0;
let totalCalls=0;
Object.keys(this.performanceUsage[moduleName]).forEach((event)=>{
totalMs+=this.performanceUsage[moduleName][event].time;
totalCalls+=this.performanceUsage[moduleName][event].count;
});

totalMsGlobal+=totalMs;

ChatLib.chat("&eModule: &7"+moduleName);
ChatLib.chat("&eTotal: &7"+totalMs.toFixed(2)+"ms ("+totalCalls+" calls)");
Object.keys(this.performanceUsage[moduleName]).sort((a,b)=>{return this.performanceUsage[moduleName][a].time-this.performanceUsage[moduleName][b].time}).forEach((event)=>{
ChatLib.chat("  &eEvent:&7 "+event+" - "+this.performanceUsage[moduleName][event].time.toFixed(2)+"ms ("+this.performanceUsage[moduleName][event].count+" calls) ["+(this.performanceUsage[moduleName][event].time/this.performanceUsage[moduleName][event].count).toFixed(2)+"ms avg]");
});
});

ChatLib.chat("&eTotal: &7"+totalMsGlobal.toFixed(2)+"ms");
}
return{performanceUsage:this.performanceUsage,flameGraphData:this.flameGraphData};
}

loadFeatureSettings(){
logger.logMessage("Loading settings",4);

let data=FileLib.read("soopyAddonsData","soopyaddonsbetafeaturesdata.json");

if(!data){
this.loadDefaultFeatureSettings();
return;
}

try{
data=JSON.parse(data);
}catch(e){
ChatLib.chat(this.messagePrefix+"&cYour settings file corrupted and could not be read! Resetting to defaults.");
data={};
}

this.featureSettingsData=data;

this.ensureNewSettingsExist();
}

saveFeatureSettings(){
if(!this.featureSettingsDataLastUpdated)return;

FileLib.write("soopyAddonsData","soopyaddonsbetafeaturesdata.json",JSON.stringify(this.featureSettingsData));

this.featureSettingsDataLastUpdated=false;

logger.logMessage("Saved settings",4);
}

loadDefaultFeatureSettings(){
Object.keys(this.featureMetas).forEach((feature)=>{
this.featureSettingsData[feature]={
enabled:this.featureMetas[feature].defaultEnabled,
subSettings:{}};

});

this.featureSettingsDataLastUpdated=true;

logger.logMessage("Loaded default settings",4);
}

ensureNewSettingsExist(){
Object.keys(this.featureMetas).forEach((feature)=>{
if(!this.featureSettingsData[feature]){
this.featureSettingsData[feature]={
enabled:this.featureMetas[feature].defaultEnabled,
subSettings:{}};

this.featureSettingsDataLastUpdated=true;
logger.logMessage("Loaded default settings for "+feature,4);
}
});
}

startCatchingEvent(event){
if(this.eventObjects[event])return;















this.eventObjects[event]=register(event,(...args)=>{

this.triggerEvent(event,args);

});


logger.logMessage("Registered "+event+" event",4);
}

triggerEvent(event,args){
if(this.events[event]){
try{
for(let Event of Object.values(this.events[event])){
if(Event.context.enabled){
if(this.recordingPerformanceUsage)this.startRecordingPerformance(Event.context.getId(),event);
let start=Date.now();
Event.func?.call(Event.context,...args);
let time=Date.now()-start;
if(time>this.longEventTime){
logger.logMessage("Long event triggered ["+time+"ms] ("+Event.context.getId()+"/"+event+")",3);
}
if(this.recordingPerformanceUsage)this.stopRecordingPerformance(Event.context.getId(),event);
}
}
}catch(e){
logger.logMessage("Error in "+event+" event: "+JSON.stringify(e,undefined,2),1);
logger.logMessage(e.stack,2);

soopyV2Server.reportError(e,"Error in "+event+" event.");
}
}
}
triggerSoopy(event,args){
if(this.soopyEventHandlers[event]){
try{
for(let Event of Object.values(this.soopyEventHandlers[event])){
if(Event.context.enabled){
if(this.recordingPerformanceUsage)this.startRecordingPerformance(Event.context.getId(),event);
let start=Date.now();
Event.func?.call(Event.context,...args);
let time=Date.now()-start;
if(time>this.longEventTime){
logger.logMessage("Long event triggered ["+time+"ms] ("+Event.context.getId()+"/"+event+")",3);
}
if(this.recordingPerformanceUsage)this.stopRecordingPerformance(Event.context.getId(),event);
}
}
}catch(e){
logger.logMessage("Error in soopy "+event+" event: "+JSON.stringify(e,undefined,2),1);
logger.logMessage(e.stack,2);
soopyV2Server.reportError(e,"Error in soopy "+event+" event.");
}
}
}

stopCatchingEvent(event){
if(!this.eventObjects[event])return;

this.eventObjects[event].unregister();
delete this.eventObjects[event];
delete this.events[event];
logger.logMessage("Unregistered "+event+" event",4);
}

registerEvent(event,func,context){
if(!this.events[event]){
this.events[event]=[];
this.startCatchingEvent(event);
}

let theEvent={
func:func,
context:context,
id:this.lastEventId++,
event:event};

this.events[event].push(theEvent);

return theEvent;
}
registerSoopy(event,func,context){
if(!this.soopyEventHandlers[event]){
this.soopyEventHandlers[event]=[];
}

let theEvent={
func:func,
context:context,
id:this.lastSoopyEventId++,
event:event};

this.soopyEventHandlers[event].push(theEvent);

return theEvent;
}

registerChat(criteria,func,context){
let event=this.registerCustom("chat",func,context);

event.trigger.setChatCriteria(criteria);

return event;
}

registerSoundPlay(criteria,func,context){
let event=this.registerCustom("soundPlay",func,context);

event.trigger.setCriteria(criteria);

return event;
}

registerActionBar(criteria,func,context){

let event=this.registerCustom("actionBar",func,context);

event.trigger.setChatCriteria(criteria);

return event;
}
registerCommand(commandName,func,context,completions){

let event=this.registerCustom("command",func,context);

if(completions)event.trigger.setTabCompletions(completions);

event.trigger.setName(commandName,true);

return event;
}
registerStep(isFps,interval,func,context){
let event=this.registerCustom("step",func,context);

event.trigger[isFps?"setFps":"setDelay"](interval);

return event;
}

registerCustom(type,func,context){
let id=this.lastChatEventId++;

if(!func)throw new Error("Function must not be null");

this.customEvents[id]={
func,
context,
trigger:register(type,(...args)=>{
try{
if(this.customEvents[id]?.eventT&&!this.customEvents[id].eventT.enabled)return;

if(context.enabled){
if(this.recordingPerformanceUsage)this.startRecordingPerformance(context.getId(),type);
let start=Date.now();
this.customEvents[id].func.call(context,...(args||[]));
let time=Date.now()-start;
if(time>this.longEventTime){
logger.logMessage("Long event triggered ["+time+"ms] ("+context.getId()+"/"+type+")",3);
}
if(this.recordingPerformanceUsage)this.stopRecordingPerformance(context.getId(),type);
}
}catch(e){
logger.logMessage("Error in "+type+" event: "+JSON.stringify(e,undefined,2),2);
logger.logMessage(e.stack,1);

soopyV2Server.reportError(e,"Error in "+type+" event.");
}
}),
id,
type};


return this.customEvents[id];
}

registerForge(event,func,priority,context){
let id=this.lastForgeEventId++;

this.forgeEvents[id]={
func:func,
context:context,
trigger:registerForgeBase(event,priority,(...args)=>{
try{
if(context.enabled){
if(this.recordingPerformanceUsage)this.startRecordingPerformance(context.getId(),event.class.name);
let start=Date.now();
func.call(context,...(args||[]));
let time=Date.now()-start;
if(time>this.longEventTime){
logger.logMessage("Long (forge) event triggered ("+context.getId()+"/"+event.class.toString()+")",3);
}
if(this.recordingPerformanceUsage)this.stopRecordingPerformance(context.getId(),event.class.name);
}
}catch(e){
logger.logMessage("Error in "+event.class.toString()+" (forge) event: "+JSON.stringify(e,undefined,2),2);
logger.logMessage(e.stack,1);

soopyV2Server.reportError(e,"Error in "+event.class.toString()+" (forge) event.");
}
}),
id:id};


return this.forgeEvents[id];
}

unregisterForge(event){
if(!this.forgeEvents[event.id])return;
unregisterForgeBase(this.forgeEvents[event.id].trigger);
delete this.forgeEvents[event.id];
}

unregisterCustom(event){
event.trigger.unregister();

delete this.customEvents[event.id];
}

unregisterEvent(event){
if(!this.events[event.event])return;

this.events[event.event]=this.events[event.event].filter((e)=>{
return e.id!==event.id;
});

if(this.events[event.event].length===0){
this.stopCatchingEvent(event.event);
delete this.events[event.event];
}
}

unregisterSoopy(event){
if(!this.soopyEventHandlers[event.event])return;

this.soopyEventHandlers[event.event]=this.soopyEventHandlers[event.event].filter((e)=>{
return e.id!==event.id;
});

if(this.soopyEventHandlers[event.event].length===0){
delete this.events[event.event];
}
}

loadFeatureMetas(){
let featuresDir=new File("./config/ChatTriggers/modules/"+metadata.name+"/features");

featuresDir.list().forEach((pathName)=>{
if(pathName.includes("."))return;

try{
let data=JSON.parse(FileLib.read(metadata.name+"/features/"+pathName,"metadata.json"));
if(data===null){
return;
}
data.id=pathName;
this.featureMetas[pathName]=data;
}catch(e){
logger.logMessage("Error loading feature metadata for "+pathName,1);
logger.logMessage(JSON.stringify(e,undefined,2),1);
}
});
}

addPerformanceTracking(feature){
let featureId=feature.getId();
if(!this.perfTrackingFeatures)return;

Object.getOwnPropertyNames(Object.getPrototypeOf(feature)).forEach((key)=>{
if(typeof feature[key]==="function"){
let fun=feature[key].bind(feature);
feature[key]=(...args)=>{
if(!this.recordingPerformanceUsage||Thread.currentThread().getId()!==1||!this.perfTrackingFeatures){
let err=undefined;
try{
args?fun(...args):fun();
}catch(e){
err=e;
}
if(err)throw err;
return;
}

let pushedId=false;
let start=this.getExactTime();

if(this.stack.length===0){
this.stack.push([featureId,0]);

pushedId=true;
}
this.stack.push([featureId+"."+key,0]);


let err=undefined;
try{
args?fun(...args):fun();
}catch(e){
err=e;
}
let nowTime=this.getExactTime();
let time=nowTime-start-this.stack[this.stack.length-1][1];

this.stack[this.stack.length-2][1]+=nowTime-start;

if(!this.performanceUsage[featureId])this.performanceUsage[featureId]={};
if(!this.performanceUsage[featureId].functions)this.performanceUsage[featureId].functions={};
if(!this.performanceUsage[featureId].functions[key])this.performanceUsage[featureId].functions[key]={time:0,count:0};
this.performanceUsage[featureId].functions[key].count++;
this.performanceUsage[featureId].functions[key].time+=nowTime-start;

this.flameGraphData.push(this.stack.map((a)=>a[0]).join(";")+" "+time);
this.stack.pop()[1];
if(pushedId){
let time=nowTime-start-this.stack[this.stack.length-1][1];
this.flameGraphData.push(this.stack.map((a)=>a[0]).join(";")+" "+time);
this.stack.pop();
}

if(err)throw err;
};
}
});
}

unloadSoopy(){
this.saveFeatureSettings();
this.unloadAllFeatures();
}

loadSoopy(){
ChatLib.chat(this.messagePrefix+"Loading SoopyV2...");
let startLoading=Date.now();
this.loadFeatureMetas();

this.loadFeatureSettings();

let loadedFeatures=new Map;

this.loadFeature("dataLoader");

Object.keys(this.featureMetas).forEach((feature)=>{
if(feature==="dataLoader")return;

if(this.featureSettingsData[feature]&&this.featureSettingsData[feature].enabled){
loadedFeatures.set(feature,false);
new Thread(()=>{
this.loadFeature(feature);
loadedFeatures.set(feature,true);
}).start();
}
});

while([...loadedFeatures.values()].some((a)=>!a)){
Thread.sleep(100);
}

this.finishedLoading=true;
ChatLib.chat(this.messagePrefix+"SoopyV2 Loaded!");
logger.logMessage("SoopyV2 took "+((Date.now()-startLoading)/1000).toFixed(2)+"s to load",3);
}

loadFeature(feature){
if(this.features[feature])return;

try{

let LoadedFeature=RequireNoCache("../features/"+feature+"/index.js");


this.features[feature]=LoadedFeature;

LoadedFeature.class.setId(feature);

this.addPerformanceTracking(LoadedFeature.class);

LoadedFeature.class._onEnable(this);

logger.logMessage("Loaded feature "+feature,3);

if(this.isDev&&!this.addedWatches.has(feature)){
this.addedWatches.add(feature);
let path=Java.type("java.nio.file.Paths").get("./config/ChatTriggers/modules/SoopyV2/features/"+feature+"/");
this.watches[path.register(this.watchService,Java.type("java.nio.file.StandardWatchEventKinds").ENTRY_MODIFY)]=feature;
}
}catch(e){
logger.logMessage("Error loading feature "+feature,1);
logger.logMessage(JSON.stringify(e,undefined,2),1);
logger.logMessage(e.stack,1);
ChatLib.chat(this.messagePrefix+"Error loading feature "+feature);

soopyV2Server.reportError(e,"Error loading feature "+feature);
}

return this;
}

unloadFeature(feature){
if(!this.features[feature])return;

this.features[feature].class._onDisable();

delete this.features[feature];

logger.logMessage("Unloaded feature "+feature,3);

return this;
}

unloadAllFeatures(){
Object.keys(this.features).forEach((feature)=>{
this.unloadFeature(feature);
});
}

isFeatureLoaded(feature){
return!!this.features[feature];
}

getLoadedFeatures(){
return Object.keys(this.features);
}

createCustomEvent(eventId){
logger.logMessage("Registered custom "+eventId+" event",4);

return{
trigger:(...args)=>{
this.triggerSoopy(eventId,args);
}};

}

getExactTime(){
return System.nanoTime()/1000000;
}
startRecordingPerformance(feature,event){
if(!this.recordingPerformanceUsage)return;

if(!this.performanceUsage[feature])this.performanceUsage[feature]={};
if(!this.performanceUsage[feature][event])this.performanceUsage[feature][event]={time:0,count:0};

let time=this.getExactTime();

this.performanceUsage[feature][event].startTime=time;
}
stopRecordingPerformance(feature,event){
if(!this.recordingPerformanceUsage)return;

let time=this.getExactTime();

this.performanceUsage[feature][event].time+=time-this.performanceUsage[feature][event].startTime;
this.performanceUsage[feature][event].count++;
}}


if(!global.soopyv2featuremanagerthing){
global.soopyv2featuremanagerthing=new FeatureManager;
register("gameUnload",()=>{
global.soopyv2featuremanagerthing=undefined;
});
}
export default global.soopyv2featuremanagerthing;