package devices.LCDDotMatrix;

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import anywheresoftware.b4a.BA;
import devices.PCF8574;
import jGPIO.jGPIO;

@BA.ShortName("LCD")
@BA.Events(values= {
		"log(msg as string)",
		"lcdupdated(row as int, msg as string)"
})

public class LCD implements LCDStringEvent {

	private BA ba;
	private Object myobject;
	private String event;
	private boolean need_log_event = false;
	private boolean need_lcdupdated_event = false;
	
	private int rowcount = 2;		// default value
	private int columncount = 16;	// default value
	private PCF8574 pcf;
	private boolean opened = false;
	private LCDString[] messages;
	private int menuindex = 0;
	
	private final int BL_Pin = 8; 
	private final int RS_pin = 1; 
	private final int RW_pin= 2; 
	private final int EN_pin  = 4; 
	private final int D7_pin  = 128;
	private final int D6_pin  = 64;
	private final int D5_pin  = 32;
	private final int D4_pin = 16;
	private int bufdata = 0;
	
	private int backlightcounter = 0;
	private Timer backlighttimer ;
	private int backlightdefault = 60;
	
	
	ExecutorService executor = Executors.newSingleThreadExecutor();
	
	public LCD() {
		if (jGPIO.osname=="") jGPIO.detectOS();
	}
	/**
	 * Initialize LCD object in B4J
	 * @param callerobject : caller object
	 * @param Eventname : eventname
	 */
	public void Initialize(BA bax, Object callerobject, String Eventname) {
		ba = bax;
		myobject = callerobject;
		event = Eventname.trim();
		
		if (ba instanceof BA) {
			if (myobject!=null) {
				if (!event.isEmpty()) {
					need_log_event = ba.subExists(event+"_log");	
					need_lcdupdated_event = ba.subExists(event+"_lcdupdated");
				}
			}
		}
		
		// auto close kalau java program closed
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                Close();
            }
        });
	}
	
	/**
	 * Open LCD
	 * @param bus_name : i2c bus name
	 * @param slave_address : i2c address
	 * @param row : row count. default value = 2
	 * @param column : column count. default value = 16
	 * @return true if opened
	 */
	public boolean Open(String bus_name, int slave_address, int row, int column) {
		if (!jGPIO.IsLinux) return false;
		opened = false;
		if (row>0) rowcount = row;
		if (column>0) columncount = column;
		pcf = new PCF8574();
		//pcf.Initialize(ba, caller, eventname); 	No need to call this, only for B4J
		opened = pcf.Open(bus_name, slave_address);
		if (opened) {
			messages = new LCDString[row];
			for(int ii=0;ii<messages.length;ii++) {
				messages[ii] = new LCDString(ii,columncount);
				messages[ii].SetJavaEvent(this);
				
			}
			backlighttimer = new Timer();
			backlighttimer.scheduleAtFixedRate(new TimerTask() {

				@Override
				public void run() {
					if (backlightcounter>0) {
						backlightcounter -= 1;
						if (backlightcounter==0) {
							BackLight(false); // turn off backlight	
						}
					}
					
				}
				
			}, 1000, 1000);
			
		}
		
		return opened;
	}
	
	/**
	 * Close LCD
	 */
	public void Close() {
		
		//if (executor instanceof ExecutorService) {
		//	executor.shutdownNow();
		//}
		
		if (messages!=null) {
			for(int ii=0;ii<rowcount;ii++) {
				LCDString xx = messages[ii];
				if (xx instanceof LCDString) {
					xx.Close();
					xx = null;
				}
			}
			messages=null;
		}
		
		if (pcf instanceof PCF8574) {
			pcf.Close();
			pcf = null;
		}
		
		if (backlighttimer instanceof Timer) {
			backlighttimer.cancel();
			backlighttimer.purge();
			backlighttimer = null;
		}
		
		opened = false;
		
	}
	
	/**
	 * Check if LCD is opened
	 * @return true if opened
	 */
	public boolean getIsOpened() {
		return opened;
	}
	
	/**
	 * Get LCD rows
	 * @return row count, default is 2.
	 */
	public int getRows() {
		return rowcount;
	}
	
	/**
	 * Get LCD columns
	 * @return column count, default is 16
	 */
	public int getColumns() {
		return columncount;
	}
	
	@BA.Hide
	/**
	 * Set LCD Content
	 * @param row : row index, start from 0
	 * @param msg : String message
	 * @param menutag : Menuindex which this message belong to.
	 * @return true if content stored (not yet displayed)
	 */
	public boolean SetContent(int row, String msg, int menutag) {
		
		if (messages==null) return false;
		if (row > messages.length-1) return false;
		menuindex = menutag;
		messages[row].UpdateContent(msg, menutag);
		return true;
	}
	
	/**
	 * Set LCD Content, and crop the message if length more than colum size
	 * @param row : row index, start from 0
	 * @param msg : Message to display
	 * @param menutag : menuindex which this message belong to
	 * @return true if content stored
	 */
	public boolean SetContentCrop(int row, String msg, int menutag) {
		if (messages != null) {
			if (row < messages.length) {
				menuindex = menutag;
				if (msg.length()>columncount) {
					msg = msg.substring(0,columncount);
				}
				messages[row].UpdateContent(msg, menutag);
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Get current LCD row content
	 * @param row : row index, start from 0
	 * @return empty string if not found
	 */
	public String GetContent(int row) {
		if (messages!=null) {
			if (row < messages.length) {
				return messages[row].CurrentContent();
			}
		}
		return "";
	}
	
	/**
	 * Compare content with row content, and return true if same value
	 * @param row : row index, start from 0
	 * @param content : content to compare with internal row content
	 * @return true if the same (case sensitive)
	 */
	public boolean CompareContent(int row, String content) {
		if (messages!=null) {
			if (row<messages.length) {
				if (content instanceof String) {
					return messages[row].CurrentContent().compareTo(content) == 0 ? true : false; 
				}
				
			}
		}
		return false;
	}
	
	
	
	/**
	 * Get current LCD menuindex
	 */
	public int getMenuIndex() {
		return menuindex;
	}
	
	/**
	 * Initialize Hardware LCD
	 * Must be called after Open
	 */
	public void LCD_Initialize() {
		//if (executor instanceof ExecutorService) executor.execute(new LCD_INIT());
		
		try {
			RS(false);
			RW(false);
			EN(false);
			Nibble(false,false,true,true);
			EN(true);
			EN(false);
			Thread.sleep(10);
			
			EN(true);
			EN(false);
			Thread.sleep(10);
			
			EN(true);
			EN(false);
			Thread.sleep(10);
			
			Nibble(false,false,true,false);
			EN(true);
			EN(false);
			Thread.sleep(10);
			
			LCD_write(0x28);
			Thread.sleep(5);
			
			LCD_write(0x0C);
			Thread.sleep(5);
			
			LCD_write(0x06);
			Thread.sleep(5);
			
			LCD_write(0x01);
			Thread.sleep(5);
		} catch (InterruptedException e) {
			
		}
		
	}
	
	/**
	 * Clear LCD Display
	 * @throws InterruptedException
	 */
	public void LCD_clear() {
		//if (executor instanceof ExecutorService) executor.execute(new LCD_CLEAR());
		
		try {
			RS(false);
			LCD_write(1);
			Thread.sleep(5);
		} catch (InterruptedException e1) {
			
		}
	}
	
	/**
	 * Set LCD Backlight ON or OFF
	 * @throws InterruptedException 
	 */
	public void BackLight(boolean isON) {
		
		if (isON) {
			BitSet(BL_Pin);
			backlightcounter = backlightdefault;
		} else {
			BitReset(BL_Pin);
			backlightcounter = 0;
		}
		PCFWrite((byte) bufdata);
		
		
	}
	
	/**
	 * Check if Backlight is ON or OFF
	 * @return true if ON
	 */
	public boolean getBackLightIsOn() {
		if (backlightcounter>0) return true; else return false;
	}
	
	/**
	 * Get / Set Default Backlight timeout , in seconds
	 */
	public int getBackLightInterval() {
		return backlightdefault;
	}
	
	public void setBackLightInterval(int value) {
		backlightdefault = value;
	}
	
	private void BitSet(int pin) {
		bufdata |= pin;
	}
	
	private void BitReset(int pin) {
		bufdata &= ~pin;
	}
	
	private void LCD_gotoxy(int x, int y)  {
		RS(false);
		switch(y) {
		case 0 :
			LCD_write(0x80+x);
					break;
		case 1 :
			LCD_write(0xC0 + x );
					break;
		case 2 :
			LCD_write(0x94 + x );
					break;
		default : 
			LCD_write(0xD4 + x );
					break;
		}
		
		
		
	}
	
	private void LCD_write(int bx)  {
		EN(false);
		Nibble((bx & 0x80)>0, (bx & 0x40)>0, (bx & 0x20)>0, (bx & 0x10)>0);
		EN(true);
		EN(false);
		Nibble((bx & 0x08)>0, (bx & 0x04)>0, (bx & 0x02)>0, (bx & 0x01)>0);
		EN(true);
		EN(false);
		
	}
	
	private void Nibble(boolean D7 , boolean D6 , boolean D5 , boolean D4 )  {
		if (D7) BitSet(D7_pin); else BitReset(D7_pin);
		if (D6) BitSet(D6_pin); else BitReset(D6_pin);
		if (D5) BitSet(D5_pin); else BitReset(D5_pin);
		if (D4) BitSet(D4_pin); else BitReset(D4_pin);
		PCFWrite((byte) bufdata);
		
	}
	
	private void RW(boolean isON)  {
		if (isON) {
			BitSet(RW_pin);
		} else {
			BitReset(RW_pin);
		}
		PCFWrite((byte) bufdata);
		
	}
	
	
	
	private void RS(boolean isON)  {
		if (isON) {
			BitSet(RS_pin);
		} else {
			BitReset(RS_pin);
		}
		PCFWrite((byte) bufdata);
		
	}
	
	private void EN(boolean isON)  {
		if (isON) {
			BitSet(EN_pin);
		} else {
			BitReset(EN_pin);
		}
		PCFWrite((byte) bufdata);
		
	}
	
	private void PCFWrite(byte bb) {
		if (pcf instanceof PCF8574) {
			pcf.Write(bb);	// gak perlu cek isopen, dah dilakuin di dalam
		}
	}
	
	
	
	private void raise_log(String msg) {
		if (need_log_event) {
			ba.raiseEventFromDifferentThread(myobject, null, 0, event+"_log", false, new Object[] {msg});
		}
	}
	
	private void raise_lcdupdated(int row, String msg) {
		if (need_lcdupdated_event) {
			ba.raiseEventFromDifferentThread(myobject, null, 0, event+"_lcdupdated", false, new Object[] {row,msg});
		}
	}
	
	
	@SuppressWarnings("unused")
	private class LCD_UPDATE implements Runnable{
		private int row;
		private String msg;
		public LCD_UPDATE(int row, String msg) {
			this.row = row;
			this.msg = msg;
		}
		@Override
		public void run() {
			LCD_gotoxy(0,row);
			RS(true);
			
			byte[] bbm = msg.getBytes();
			for(int ii=0;ii<bbm.length;ii++) {
				LCD_write(bbm[ii]);					
			}
			
			raise_lcdupdated(row,msg);
		}
		
	}

	@SuppressWarnings("unused")
	private class LCD_INIT implements Runnable {

		@Override
		public void run() {
			
			try {
				RS(false);
				RW(false);
				EN(false);
				Nibble(false,false,true,true);
				EN(true);
				EN(false);
				Thread.sleep(10);
				
				EN(true);
				EN(false);
				Thread.sleep(10);
				
				EN(true);
				EN(false);
				Thread.sleep(10);
				
				Nibble(false,false,true,false);
				EN(true);
				EN(false);
				Thread.sleep(10);
				
				LCD_write(0x28);
				Thread.sleep(5);
				
				LCD_write(0x0C);
				Thread.sleep(5);
				
				LCD_write(0x06);
				Thread.sleep(5);
				
				LCD_write(0x01);
				Thread.sleep(5);
			} catch (InterruptedException e1) {
				
			}
			
		}
	};
	
	@SuppressWarnings("unused")
	private class LCD_CLEAR implements Runnable{

		@Override
		public void run() {
			try {
				RS(false);
				LCD_write(1);
				Thread.sleep(5);
			} catch (InterruptedException e1) {
				
			}
			
		}
		
	}
	

	@Override
	@BA.Hide
	public synchronized void UpdateLCD(int row, String msg, int menutag) {
		if (menuindex != menutag) {
				raise_log("[DISCARDED] Row="+row+", Menu="+menutag+", Content="+msg);
				return;
		}
		
		//if (executor instanceof ExecutorService) executor.execute(new LCD_UPDATE(row, msg));
		
		LCD_gotoxy(0,row);
		RS(true);
		
		byte[] bbm = msg.getBytes();
		for(int ii=0;ii<bbm.length;ii++) {
			LCD_write(bbm[ii]);					
		}
		
		raise_lcdupdated(row,msg);
		
	}
}


