class PlugPyCommunicator{
    constructor( vid=0xF056 , iface=2 , inEp=0x83 , outEp=0x03 ){
		String.prototype.endsWith = function(sf){
			return this.indexOf(sf, this.length - sf.length) !== -1;	
		}
		self.disableUserRxCbSwitch = false;
		self.disableForwardCommandAnswerToUserCB = false;
		self.isBusy = false;
		self.isConnected = false;
		self.port = null;		
		self.cbStateFunction = null;
		setInterval(() => self.checkDeviceConnection(), 3000);	
		self.Port = function(device) {
			this.device_ = device;
			this.interfaceNumber_ = iface;  
			this.endpointIn_ = inEp;
			this.endpointOut_ = outEp;
			this.sendStringCounter = 1;
			this.sendBytesDelayed = function( bIn,txInterval=PlugPyCommunicator.HAL_TX_INTERVAL){				
				return new Promise(function(resolve){					
					setTimeout(function(){
						// console.log("this.directTransferOut : ",bIn);
						self.port.device_.transferOut(self.port.endpointOut_,bIn).then(function(res){						
							self.port.sendStringCounter--;	
							resolve();							
						}).catch(function(res){
							self.isConnected = false;
							self.isBusy = false;
							self.port = null;
							if ( self.cbStateFunction != null )
								self.cbStateFunction(PlugPyCommunicator.CONN_STATE_DISCONNECTED);
						});							
					},1+(self.port.sendStringCounter*txInterval));					
					self.port.sendStringCounter++;
				});				
			};
			this.directTransferOut = function(bIn){
				return self.port.device_.transferOut(self.port.endpointOut_,bIn);
			}
		}
		self.checkDeviceConnection = function() {
            if (self.isConnected && self.port) {
                navigator.usb.getDevices().then(devices => {
                    const isStillConnected = devices.some(device => device === self.port.device_);

                    // Si le périphérique n'est plus trouvé, mettre à jour l'état
                    if (!isStillConnected) {
                        console.warn("Le périphérique a été déconnecté.");
                        self.isConnected = false;
                        self.port = null;

                        // Appeler le callback d'état pour signaler la déconnexion
                        if (self.cbStateFunction) {
                            self.cbStateFunction(PlugPyCommunicator.CONN_STATE_DISCONNECTED);
                        }
                    }
                }).catch(err => console.error("Erreur lors de la vérification de connexion :", err));
            }
        };
		self.communicator = {};		
		self.txCMDCb = null;
		self.rxRawCb = null;		
		self.requestPort = function() {
			return navigator.usb.requestDevice({ filters:[{vendorId: 0xF056}] }).then(
			//return navigator.usb.requestDevice({filters:[]}).then(
					device => new self.Port(device)
			);
		}
		self.communicator.getPorts = function() {
			return navigator.usb.getDevices().then(devices => {
				return devices.map(device => new Port(device));
			});
		};
		self.Port.prototype.connect = function() {
			
			
			let readLoop = () => {
			  this.device_.transferIn(this.endpointIn_,64).then(result => {
				this.onReceive(result.data);
				readLoop();
			  }, error => {
				  
			  });
			};
			
			
			return this.device_.open()
				.then(() => {
				  if (this.device_.configuration === null) {					
					return this.device_.selectConfiguration(1);
				  }
				})
				.then(() => {
				  var configurationInterfaces = this.device_.configuration.interfaces;
				  configurationInterfaces.forEach(element => {
					element.alternates.forEach(elementalt => {
					  if (elementalt.interfaceClass==0xFF) {
						this.interfaceNumber_ = element.interfaceNumber;
						elementalt.endpoints.forEach(elementendpoint => {
						  if (elementendpoint.direction == "out") {
							this.endpointOut_ = elementendpoint.endpointNumber;
						  }
						  if (elementendpoint.direction=="in") {
							this.endpointIn_ =elementendpoint.endpointNumber;
						  }
						})
					  }
					})
				  })
				})
				.then(() => this.device_.claimInterface(this.interfaceNumber_))
				.then(() => this.device_.selectAlternateInterface(this.interfaceNumber_, 0))
				.then(() => {
				  readLoop();
				});
		};
		self.Port.prototype.disconnect = function() {
			return this.device_.controlTransferOut({
				'requestType': 'class',
				'recipient': 'interface',
				'request': 0x00,
				'value': 0x00,
				'index': this.interfaceNumber_
			})
			.then(() => {try{this.device_.close()}catch(err){console.warn("Failed to close the connection.");} })
			.then(() => {
				if ( self.cbStateFunction != null )
						self.cbStateFunction(PlugPyCommunicator.CONN_STATE_DISCONNECTED);
				self.isConnected = false;	
				self.port = null;
			});		
		};
		self.strToArrayBuffer = function(str,arrayLen=64) {
			// console.log(typeof str);
			var buf = new ArrayBuffer(64,0x00);
			var bufView = new Uint8Array(buf);
			for (var i=0 ; i < str.length ; i++) {
				bufView[i] = str.charCodeAt(i);
			}
			return buf;
		}		
		// self.strToArrayBuffer = function(str,arrayLen=PlugPyCommunicator.HAL_PACKET_LEN) {
		// 	var buf = new ArrayBuffer(arrayLen,0x00);
		// 	var bufView = new Uint8Array(buf);
		// 	for (var i=0 ; i < str.length ; i++) {
		// 		bufView[i] = str.charCodeAt(i);
		// 	}
		// 	return buf;
		// }		
		self.Port.prototype.sendString = function(strIn,packetLen=PlugPyCommunicator.HAL_PACKET_LEN,txInterval=PlugPyCommunicator.HAL_TX_INTERVAL,txCB=null) {						
			if (strIn.length > packetLen){	
				while (strIn.length > packetLen){
						var str64 = strIn.substr(0,packetLen);
						this.sendBytesDelayed(self.strToArrayBuffer(str64,packetLen),txInterval);										
						strIn = strIn.substr(packetLen,strIn.length-packetLen);
				}
				if (strIn.length){
					this.sendBytesDelayed(self.strToArrayBuffer(strIn,packetLen),txInterval).then(function(){
						if (txCB != null)
							txCB("OK");
					});
				}
			}else{
				this.sendBytesDelayed(self.strToArrayBuffer(strIn,packetLen),txInterval).then(function(){
					if (txCB != null)
						txCB("OK");
				});				
			}					
		};
		self.Port.prototype.sendCtrlKeyCode = function(keyCode,intervalTimeoutMs=100){		
			if (typeof(keyCode) == "number"){
				var buf = new ArrayBuffer(64,0);
				var bufView = new Uint8Array(buf);
				bufView[0] = keyCode;
				this.device_.transferOut(this.endpointOut_,buf).catch(function(res){
					self.isConnected = false;
					self.isBusy = false;
					self.port = null;
					if ( self.cbStateFunction != null )
						self.cbStateFunction(PlugPyCommunicator.CONN_STATE_DISCONNECTED);
				});	
			}else if(typeof(keyCode) == "object"){
				self.device_ = this.device_;
				self.endpointOut_ = this.endpointOut_;
				return new Promise(function(resolve){
					var counter = 0;
					var readInterval = setInterval(function(){				
						var buf = new ArrayBuffer(64,0);
						var bufView = new Uint8Array(buf);
						bufView[0] = keyCode[counter];
						self.device_.transferOut(self.endpointOut_,buf).catch(function(res){
							self.isConnected = false;
							self.isBusy = false;
							self.port = null;
							if ( self.cbStateFunction != null )
								self.cbStateFunction(PlugPyCommunicator.CONN_STATE_DISCONNECTED);
						});	
						counter += 1;
						if ( counter == keyCode.length ){
							clearInterval(readInterval);
							resolve();
						}		
					},intervalTimeoutMs);
				});
			}
		}
		self.receptionStringBuffer = "";
		self.Port.prototype.decoder = new TextDecoder("utf-8");
		self.communicator.connectPort = function(port){			
			port.connect().then(() => {
					self.isConnected = true;
					port.onReceive = data => {
						if (self.port != null){
							var newData = self.port.decoder.decode(data);
							self.receptionStringBuffer += newData;
							if (self.rxRawCb != null && !self.disableUserRxCbSwitch)
								self.rxRawCb(newData);
						}
					}
					port.onReceiveError = error => {
						if ( self.cbStateFunction != null )
							self.cbStateFunction(PlugPyCommunicator.CONN_STATE_RECEIVE_ERROR);
						console.warn('Receive error: ' + error);
					};
					self.port = port;
					if ( self.cbStateFunction != null )
						self.cbStateFunction(PlugPyCommunicator.CONN_STATE_CONNECTED);
				}, error => {					
					port.disconnect();
					self.isConnected = false;
				}
			);			
		}
		this.getClearRxBuff = function(){			
			var buffCpy = self.receptionStringBuffer;
			self.receptionStringBuffer = "";
			return buffCpy;			
		}
		this.tryAutoConnect = function(deviceNoFoundCb=null){			
			self.communicator.getPorts().then(ports => {
				if (ports.length != 0) {
					for (let port of ports){
						if ( port.device_.vendorId == vid /* && port.device_.productId == pid */ ){
							if ( self.cbStateFunction != null )
								self.cbStateFunction(PlugPyCommunicator.CONN_STATE_CONNECTING);
							self.communicator.connectPort(port);
							break;				
						}else{
							if (deviceNoFoundCb != null)
								deviceNoFoundCb();
						}				
					}	
				}else{
					if (deviceNoFoundCb != null)
						deviceNoFoundCb();
				}				
			});
		}
		this.connectDisconnect = function(){
			if(self.port != null && self.isConnected){
				self.port.disconnect();
			}else{				
				self.requestPort().then(selectedPort => {
					self.communicator.connectPort(selectedPort);
				}).catch(error => {
					if ( self.cbStateFunction != null )
							self.cbStateFunction(PlugPyCommunicator.CONN_STATE_DISCONNECTED);
						console.warn('Connection error: ' + error);
				});
			}
		}
		this.isConnected = function(){
			return (self.port != null && self.isConnected);
		}
		self.Port.prototype.readUntil = function(timeoutMs,strComp,emptyBuffAtStart=true,intervalTimeoutMs=5){
			if (emptyBuffAtStart)
				self.receptionStringBuffer = "";		
			var promise = new Promise(function(resolve){
			// var promise = new Promise(function(resolve,reject){
				var timeoutTim = setTimeout(function(){
					clearInterval(readInterval);
					// reject(self.receptionStringBuffer);				
				},timeoutMs);			
				var readInterval = setInterval(function(){					
					if ( self.receptionStringBuffer.indexOf(strComp) != -1 ){
						clearTimeout(timeoutTim);
						clearInterval(readInterval);
						var res = self.receptionStringBuffer.substr(0,self.receptionStringBuffer.indexOf(strComp)+strComp.length);
						self.receptionStringBuffer = self.receptionStringBuffer.substr(self.receptionStringBuffer.indexOf(strComp)+strComp.length);					
						resolve(res);						
					}					
				},intervalTimeoutMs);
			});			
			return promise;
		}
        // // self.rawReplProcess = function(cbDone,cbFailed){
        self.rawReplProcess = function(cbDone){
            self.port.sendCtrlKeyCode([0x03,0x03,0x01]).then(function(){						
				self.port.readUntil(1000,"raw REPL; CTRL-B to exit\r\n>").then(
					function(rawReplRes){
						if (rawReplRes.endsWith("raw REPL; CTRL-B to exit\r\n>")){							
							self.port.sendCtrlKeyCode([0x04]).then(function(){
								self.port.readUntil(1000,"soft reboot\r\n").then(
									function (softRebootRes){
										if (softRebootRes.endsWith("soft reboot\r\n")){
											self.port.readUntil(1000,"raw REPL; CTRL-B to exit\r\n").then(
												function(rawReplRes1){
													if (rawReplRes1.endsWith("raw REPL; CTRL-B to exit\r\n")){																
														setTimeout(function(){		
															self.inRAWREPLMode = true;
															if ( self.cbStateFunction != null )
																self.cbStateFunction(PlugPyCommunicator.CONN_ENTERED_RAW_REPL);
															if ( self.cbStateFunction != null )
																self.cbStateFunction(PlugPyCommunicator.STATE_IDLE);
															self.isBusy = false;
															self.enterRawReplRetry=5;
															cbDone(true);
														},100);	
													}
												}
											)};
										
									});
								
							});
						}
					});
			});
		
		}										
			// else{
			// 											if ( self.cbStateFunction != null )
			// 												self.cbStateFunction(PlugPyCommunicator.CONN_REQUEST_ERROR);																
			// 											this.cbFailed("Could not enter raw repl(1).");
			// 										}												
			// 									}
			// 								).catch(function(res){
			// 									if ( self.cbStateFunction != null )
			// 										self.cbStateFunction(PlugPyCommunicator.CONN_REQUEST_ERROR);	
			// 									this.cbFailed("Could not enter raw repl(2).");
			// 								});	
			// 							}else{
			// 								if ( self.cbStateFunction != null )
			// 									self.cbStateFunction(PlugPyCommunicator.CONN_REQUEST_ERROR);
			// 								this.cbFailed("Could not enter raw repl(3).");
			// 							}	
                                       		
			// 						}
			// 					).catch(function(res){
			// 						if ( self.cbStateFunction != null )
			// 							self.cbStateFunction(PlugPyCommunicator.CONN_REQUEST_ERROR);
			// 						this.cbFailed("Could not soft reboot.");
			// 					});
			// 				});					
			// 			}else{
			// 				if ( self.cbStateFunction != null )
			// 					self.cbStateFunction(PlugPyCommunicator.CONN_REQUEST_ERROR);	
			// 				this.cbFailed("Could not enter raw repl(4).");
			// 			}
			// 		}
			// 	).catch(function(res){
			// 		if ( self.cbStateFunction != null )
			// 			self.cbStateFunction(PlugPyCommunicator.CONN_REQUEST_ERROR);	
			// 		this.cbFailed("Could not enter raw repl(5).");
			// 	});
			// }).catch(function(res){
			// 	if ( self.cbStateFunction != null )
			// 			self.cbStateFunction(PlugPyCommunicator.CONN_REQUEST_ERROR);	
			// 		this.cbFailed("Could not enter raw repl(6).");
			// });
			
			
		
        
		// this.cbFailed = function(msg){
		// 	return msg;
		// }
        this.enterRawRepl = function(){			
			if(self.port != null && self.isConnected){
				self.isBusy = true;
				if ( self.cbStateFunction != null )
					self.cbStateFunction(PlugPyCommunicator.STATE_BUSY);
				// return new Promise(function(resolve,reject){					
				return new Promise(function(resolve){					
					
					
					self.rawReplProcess(					
						function(doneState){							
							resolve(doneState);							
						});
                })
            }else{
				console.warn("Device not ready.");
				// return new Promise(function(resolve,reject){reject("Device not ready");});
			}
        }
        self.inRAWREPLMode = false;
		this.isInRAWReplMode = function(){
			return self.inRAWREPLMode;
		}
		this.exitRawRepl = function(exitCB=null){
			if(self.port != null && self.isConnected){
				self.port.sendCtrlKeyCode(0x02);
			}
			setTimeout(function(){
				if ( self.cbStateFunction != null )
					self.cbStateFunction(PlugPyCommunicator.CONN_EXITED_RAW_REPL);
			    self.inRAWREPLMode = false;	
            	self.receptionStringBuffer = "";
				if (exitCB != null)
					exitCB();
			},200);			
		}
        self.sendRawString = function(str,len=PlugPyCommunicator.HAL_PACKET_LEN,txInterval=PlugPyCommunicator.HAL_TX_INTERVAL,txCB=null){
			if(self.port != null && self.isConnected){
				if (typeof(str) == "string"){
					self.port.sendString(str,len,txInterval,txCB);
				}else
					console.warn("sendRawString : Ivalid type");
			}
		}
		self.strInCumul = "";
		this.sendString = function(str,len=PlugPyCommunicator.HAL_PACKET_LEN){
			if(self.port != null && self.isConnected){
				self.sendRawString(str,len);
			}
		}
		this.sendCtrlKey = function(key,callback=null){
			if(self.port != null && self.isConnected){
				self.port.sendCtrlKeyCode([key]).then(function(){
					if (callback!=null)
						callback(1);
				}).catch(function(){
					if (callback!=null)
						callback(null);
				});
			}
        }
        self.Port.prototype.exec_raw_no_follow = function(command,blindNoReturn=false,txInterval=PlugPyCommunicator.HAL_TX_INTERVAL,packetLen=PlugPyCommunicator.HAL_PACKET_LEN){
			return new Promise(function(resolve){				
			// return new Promise(function(resolve,reject){				
				self.port.readUntil(1000,">",false).then(
					function(res){
						self.sendRawString(command,packetLen,txInterval,function(){
							self.port.sendCtrlKeyCode([0x04]).then(function(){
								self.port.readUntil(5000,"OK",false).then(function(res){
									if (blindNoReturn){										
										resolve("OK");										
									}else{										
										self.port.readUntil(5000,">",false).then(function(res){
										self.receptionStringBuffer = ">";
										resolve(res.substr(0,res.length-3));								
										}).catch(function(res){								
											// reject("EXEC_RAW : Cannot get result");
										});										
									}									
								}).catch(function(res){							
									// reject("EXEC_RAW : NO ACK");							
								});
							}).catch(function(res){
								// reject("0x04 Failed");
							});	
						});												
					}
				).catch(function(res){
					if ( self.cbStateFunction != null )
						self.cbStateFunction(PlugPyCommunicator.CONN_REQUEST_ERROR);
					// reject("Could not get RAW REPL prompt.");
				});
			});
		}
        self.lastExecTime = Date.now();
		self.executionCounter = 0;
        this.exec = function(command,leaveRiple=false,blindNoReturn=false){
			if(self.port != null && self.isConnected){				
				if (self.disableForwardCommandAnswerToUserCB)
					self.disableUserRxCbSwitch = true;				
				if ( self.txCMDCb != null )
					self.txCMDCb(command);
				if(self.port != null && self.isConnected){
					self.isBusy = true;
					if ( self.cbStateFunction != null ){
							self.cbStateFunction(PlugPyCommunicator.STATE_BUSY);
					}
					// return new Promise(function(resolve,reject){					
					return new Promise(function(resolve){					
						var execDelay = 0;
						if ( Date.now() - self.lastExecTime < 50 )
							execDelay = 200;					
						setTimeout(function(){
							self.port.exec_raw_no_follow(command,blindNoReturn).then(function(res){							
								self.lastExecTime = Date.now();	
								self.executionCounter--;								
								if ( self.cbStateFunction != null )
									self.cbStateFunction(PlugPyCommunicator.STATE_IDLE);
								if (leaveRiple){
									if ( self.cbStateFunction != null )
										self.cbStateFunction(PlugPyCommunicator.CONN_EXITED_RAW_REPL);
									self.inRAWREPLMode = false;
								}
								self.isBusy = false;
								self.disableUserRxCbSwitch = false;
								resolve(res);							
							}).catch(function(errorMsg){							
								self.lastExecTime = Date.now();
								self.executionCounter--;
								if ( self.cbStateFunction != null )
									self.cbStateFunction(PlugPyCommunicator.STATE_IDLE);
								self.isBusy = false;
								self.disableUserRxCbSwitch = false;
								// reject(errorMsg);							
							});	
						},execDelay*(1+self.executionCounter));
						self.executionCounter++;
											
					});
				}
			}
			// return new Promise(function(resolve,reject){reject("Device not ready");});
		}
        this.registerConnectionStateCallback = function(cbFunction){			
			self.cbStateFunction = cbFunction;			
		}	
		this.registerRXRAWCb = function(cbFunc){
			self.rxRawCb = cbFunc;
		}
		this.registerTxCommandCb = function(cbFunc){
			self.txCMDCb = cbFunc;
		}
		this.disableCmdAnswerForwardDataToCb = function(dsState){
			self.disableForwardCommandAnswerToUserCB = dsState;
		}
    }
}

PlugPyCommunicator.HAL_TX_INTERVAL		=		15;
PlugPyCommunicator.HAL_PACKET_LEN		=		62;
PlugPyCommunicator.CONN_STATE_CONNECTING = 		0x00;
PlugPyCommunicator.CONN_STATE_CONNECTED = 		0x01;
PlugPyCommunicator.CONN_STATE_RECEIVE_ERROR = 	0x02;
PlugPyCommunicator.CONN_STATE_DISCONNECTED = 	0x03;
PlugPyCommunicator.CONN_REQUEST_ERROR = 		0x04;
PlugPyCommunicator.CONN_ENTERED_RAW_REPL = 		0x05;
PlugPyCommunicator.CONN_EXITED_RAW_REPL = 		0x06;
PlugPyCommunicator.STATE_BUSY			= 		0x07;
PlugPyCommunicator.STATE_IDLE			= 		0x08;