package devices.Oled.SSD1306;

import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.util.Arrays;

import anywheresoftware.b4a.BA;
import anywheresoftware.b4a.keywords.Bit;
import jGPIO.I2C.I2C_BUS;
import jGPIO.I2C.I2C_Device;

@BA.Events(values= {
		"log(msg as string)"
})
@BA.ShortName("SSD1306_OLED")

//@SuppressWarnings("unused")
public class OLEDDisplay {
	/**
     * Used to specify the orientation of a display.
     * This allows mounting the oled display upright or flipping it.
     * <p>
     * All rotation values are for clockwise rotation.
     * </p>
     */
    protected enum Rotation {
        DEG_0,
        DEG_90,
        DEG_180,
        DEG_270
    }


    private int DISPLAY_WIDTH = 128;
    private int DISPLAY_HEIGHT = 64;
    private final int MAX_INDEX = (DISPLAY_HEIGHT / 8) * DISPLAY_WIDTH;

    private final byte SSD1306_SETCONTRAST = (byte) 0x81;
    private final byte SSD1306_DISPLAYALLON_RESUME = (byte) 0xA4;
    
	@SuppressWarnings("unused")
	private final byte SSD1306_DISPLAYALLON = (byte) 0xA5;
    private final byte SSD1306_NORMALDISPLAY = (byte) 0xA6;
    private final byte SSD1306_INVERTDISPLAY = (byte) 0xA7;
    private final byte SSD1306_DISPLAYOFF = (byte) 0xAE;
    private final byte SSD1306_DISPLAYON = (byte) 0xAF;

    private final byte SSD1306_SETDISPLAYOFFSET = (byte) 0xD3;
    private final byte SSD1306_SETCOMPINS = (byte) 0xDA;

    private final byte SSD1306_SETVCOMDETECT = (byte) 0xDB;

    private final byte SSD1306_SETDISPLAYCLOCKDIV = (byte) 0xD5;
    private final byte SSD1306_SETPRECHARGE = (byte) 0xD9;

    private final byte SSD1306_SETMULTIPLEX = (byte) 0xA8;

    @SuppressWarnings("unused")
	private final byte SSD1306_SETLOWCOLUMN = (byte) 0x00;
    @SuppressWarnings("unused")
	private final byte SSD1306_SETHIGHCOLUMN = (byte) 0x10;

    private final byte SSD1306_SETSTARTLINE = (byte) 0x40;

    private final byte SSD1306_MEMORYMODE = (byte) 0x20;
    private final byte SSD1306_COLUMNADDR = (byte) 0x21;
    private final byte SSD1306_PAGEADDR = (byte) 0x22;

    @SuppressWarnings("unused")
	private final byte SSD1306_COMSCANINC = (byte) 0xC0;
    private final byte SSD1306_COMSCANDEC = (byte) 0xC8;

    private final byte SSD1306_SEGREMAP = (byte) 0xA0;

    private final byte SSD1306_CHARGEPUMP = (byte) 0x8D;

    @SuppressWarnings("unused")
	private final byte SSD1306_EXTERNALVCC = (byte) 0x1;
    @SuppressWarnings("unused")
	private final byte SSD1306_SWITCHCAPVCC = (byte) 0x2;

    private final byte SSD1306_ACTIVATE_SCROLL = (byte) 0x2F;
    private final byte SSD1306_DEACTIVATE_SCROLL = (byte) 0x2E;
    private final byte SSD1306_SET_VERTICAL_SCROLL_AREA = (byte) 0xA3;
    private final byte SSD1306_RIGHT_HORIZONTAL_SCROLL = (byte) 0x26;
    private final byte SSD1306_LEFT_HORIZONTAL_SCROLL = (byte) 0x27;
    private final byte SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL = (byte) 0x29;
    private final byte SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL = (byte) 0x2A;
    
    private I2C_BUS bus;
    private I2C_Device device;
    private Rotation rotation;

    private byte[] imageBuffer;

    private BA ba;
    private Object caller;
    private String eventname;
    private final Object Me = this;
    private boolean need_log_event = false;
    private boolean device_opened = false;
    
    
    /**
     * Initialize OLED Display
     * @param caller : caller object
     * @param eventname : event name
     */
    public void Initialize(BA ba, Object caller, String eventname) {
    	this.ba = ba;
    	this.caller = caller;
    	this.eventname = eventname.trim();
    	if (this.ba != null) {
    		if (this.caller != null) {
    			if (!this.eventname.isEmpty()) {
    				need_log_event = this.ba.subExists(this.eventname+"_log");
    			}
    		}
    	}
    	
    	
    	//add shutdown hook that clears the display
        //and closes the bus correctly when the software
        //if terminated.
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                shutdown();
            }
        });
        
        
    }
    
    
    
    /**
     * Open
     * @param busname : i2c bus name
     * @param default_address : if true, will use default address 0x3C. If false, will use alternate address 0x3D
     * @param Rotation_Mode : 0, 90, 180, or 270. Default at 0
     * @param Width : width in pixel, usually 128
     * @param Height : height in pixel, usually 64
     * @return true if success
     */
    public boolean Open(String busname, boolean default_address, int Rotation_Mode, int Width, int Height) {
    	if (busname.isEmpty()) {
    		raise_log("Open failed, busname is empty");
    		return false;
    	}
    	
    	if (Width < 1) {
    		raise_log("Open failed, Width must be positive number");
    		return false;
    	}
    	
    	if (Height < 1) {
    		raise_log("Open failed, Height must be positive number");
    		return false;
    	}
    	
    	this.DISPLAY_WIDTH = Width;
    	this.DISPLAY_HEIGHT = Height;
    	this.imageBuffer = new byte[(DISPLAY_WIDTH * DISPLAY_HEIGHT) / 8];
    	
    	switch(Rotation_Mode){
    	case 90 :
    		this.rotation = Rotation.DEG_90;
    		break;
    	case 180 : 
    		this.rotation = Rotation.DEG_180;
    		break;
    	case 270 :
    		this.rotation = Rotation.DEG_270;
    		break;
    	default:
    		this.rotation = Rotation.DEG_0;
    		break;
    	}
    	
    	
    	
    	bus = new I2C_BUS(busname);
    	if (bus.Open_Bus()) {
    		int address = 0x3C;
    		if (!default_address) address = 0x3D;
    		
    		device = new I2C_Device(bus.I2C_Handle(), address);
    		if (device.OpenDevice_SLAVE()) {
    			device_opened = true;
    			
    			clear();
    			init();
    			
    			raise_log("OLEDDisplay opened");
    			return true;
    		} else raise_log("OLEDDisplay failed to create I2C_Device at address "+Bit.ToHexString(address));
    	} else raise_log("OLEDDisplay failed to open I2C_BUS at "+busname);
    	return false;
    }
    
    /**
     * Check if OLEDDisplay is opened
     * @return true if opened
     */
    public boolean getIsOpened() {
    	return device_opened;
    }
    
    /**
     * Sets if the display should be inverted
     * @param invert Invert state
     */
    public void invertDisplay(boolean invert) {
    	if (device_opened) {
    		if (invert) {
                writeCommand(SSD1306_INVERTDISPLAY);
            } else {
            	writeCommand(SSD1306_NORMALDISPLAY);
            }
    	}
    }
    
    private void raise_log(String msg) {
    	if (need_log_event) ba.raiseEventFromDifferentThread(Me, null, 0, eventname+"_log", false, new Object[] {msg});
    }

    public synchronized void clear() {
    	if (imageBuffer!=null) Arrays.fill(imageBuffer, (byte) 0x00);
    }

    public int getWidth() {
        switch (rotation) {
            case DEG_90:
            case DEG_270:
                return DISPLAY_HEIGHT;
            case DEG_0:
            case DEG_180:
            default:
                return DISPLAY_WIDTH;
        }
    }

    public int getHeight() {
        switch (rotation) {
            case DEG_90:
            case DEG_270:
                return DISPLAY_WIDTH;
            case DEG_0:
            case DEG_180:
            default:
                return DISPLAY_HEIGHT;
        }
    }

    private boolean writeCommand(byte command) {
    	if (device!=null) {
    		if (device.WriteBytes(0, command)>0) {
    			return true;
    		}
    	}
    	return false;
    }
    
    private boolean writeCommand(int command) {
    	return writeCommand((byte)command);
    }

    private void init() {
    	if (device_opened) {
    		    writeCommand(SSD1306_DISPLAYOFF);                    // 0xAE
    	        writeCommand(SSD1306_SETDISPLAYCLOCKDIV);            // 0xD5
    	        writeCommand((byte) 0x80);                           // the suggested ratio 0x80
    	        writeCommand(SSD1306_SETMULTIPLEX);                  // 0xA8
    	        writeCommand((byte) 0x3F);
    	        writeCommand(SSD1306_SETDISPLAYOFFSET);              // 0xD3
    	        writeCommand((byte) 0x0);                            // no offset
    	        writeCommand((byte) (SSD1306_SETSTARTLINE | 0x0));   // line #0
    	        writeCommand(SSD1306_CHARGEPUMP);                    // 0x8D
    	        writeCommand((byte) 0x14);
    	        writeCommand(SSD1306_MEMORYMODE);                    // 0x20
    	        writeCommand((byte) 0x00);                           // 0x0 act like ks0108
    	        writeCommand((byte) (SSD1306_SEGREMAP | 0x1));
    	        writeCommand(SSD1306_COMSCANDEC);
    	        writeCommand(SSD1306_SETCOMPINS);                    // 0xDA
    	        writeCommand((byte) 0x12);
    	        writeCommand(SSD1306_SETCONTRAST);                   // 0x81
    	        writeCommand((byte) 0xCF);
    	        writeCommand(SSD1306_SETPRECHARGE);                  // 0xd9
    	        writeCommand((byte) 0xF1);
    	        writeCommand(SSD1306_SETVCOMDETECT);                 // 0xDB
    	        writeCommand((byte) 0x40);
    	        writeCommand(SSD1306_DISPLAYALLON_RESUME);           // 0xA4
    	        writeCommand(SSD1306_NORMALDISPLAY);

    	        writeCommand(SSD1306_DEACTIVATE_SCROLL);
    	        
    	        writeCommand(SSD1306_DISPLAYON);//--turn on oled panel
    	} else raise_log("Init failed, device not opened");
       
    }

    public synchronized void setPixel(int x, int y, boolean on) {
        switch (rotation) {
            default:
            case DEG_0:
                updateImageBuffer(x, y, on);
                break;
            case DEG_90:
                updateImageBuffer(y, getWidth() - x - 1, on);
                break;
            case DEG_180:
                updateImageBuffer(getWidth() - x - 1, getHeight() - y - 1, on);
                break;
            case DEG_270:
                updateImageBuffer(getHeight() - y - 1, x, on);
                break;
        }
    }

    private synchronized void updateImageBuffer(int x, int y, boolean on) {
        if (imageBuffer==null) return;
        
    	final int pos = x + (y / 8) * DISPLAY_WIDTH;
        if (pos >= 0 && pos < MAX_INDEX) {
        	
            if (on) {
                this.imageBuffer[pos] |= (1 << (y & 0x07));
            } else {
                this.imageBuffer[pos] &= ~(1 << (y & 0x07));
            }
        }
    }

    
    protected synchronized void drawChar(char c, FontPack font, int x, int y, boolean on) {
        font.drawChar(this, c, x, y, on);
    }
 
    
    /**
     * Draw a String at specific location
     * x and y define as top-left of the String
     * @param font : Font used for String
     * @param string : string value
     * @param x : coordinate X, start at 1
     * @param y : coordinate Y, start at 1
     */
    public synchronized void drawString(FontPack font, String string, int x, int y) {
    	if (font==null) return;
    	
    	if (x<1) x = 1;
    	if (y<1) y = 1;
    	
        int posX = x;
        int posY = y;
        
        
        
        for (char c : string.toCharArray()) {
            if (c == '\n') {
                posY += font.getOuterHeight();
                posX = x;
            } else {
                if (posX >= 0 && posX + font.getWidth() < this.getWidth()
                        && posY >= 0 && posY + font.getHeight() < this.getHeight()) {
                    drawChar(c, font, posX, posY, true);
                }
                posX += font.getOuterWidth();
            }
        }
    }

//    /**

    
    /**
    * Draw string at centered location of Y
    * Y is defined as top of the string
    * @param font : Font used for drawStringCentered
    * @param string : string value
    * @param y : coordinate Y, start at 1
    */
   public synchronized void drawStringCentered(FontPack font, String string, int y) {
   	   if (font==null) return;
	   if (y<1) y = 1;
   	
   	   final int strSizeX = string.length() * font.getOuterWidth();
       final int x = (this.getWidth() - strSizeX) / 2;
       drawString(font,string,  x, y);
   }
   
   /**
    * Draw a Character
    * @param font FontPack used
    * @param c character to draw
    * @param x top left coordinate
    * @param y top left coordiante
    * @param on if on, then pixel show
    */
   public synchronized void drawCharacter(FontPack font, char c, int x, int y, boolean on) {
	   if (font==null) return;
	   if (x<1) return;
	   if (y<1) return;
	   if (x>this.getWidth()) return;
	   if (y>this.getHeight()) return;
	   drawChar(c, font, x, y, on);
   }

    public synchronized void clearRect(int x, int y, int width, int height, boolean on) {
        for (int posX = x; posX < x + width; ++posX) {
            for (int posY = y; posY < y + height; ++posY) {
                setPixel(posX, posY, on);
            }
        }
    }
    
    // Gak guna
    @BA.Hide
    public void scrollHorizontally(boolean left, int start, int end) {
    	if (device_opened) {
    		    writeCommand(left ? SSD1306_LEFT_HORIZONTAL_SCROLL : SSD1306_RIGHT_HORIZONTAL_SCROLL);
    	        writeCommand(0);
    	        writeCommand(start);
    	        writeCommand(0);
    	        writeCommand(end);
    	        writeCommand(1);
    	        writeCommand(0xFF);
    	        writeCommand(SSD1306_ACTIVATE_SCROLL);
    	}
       
    }

    // gak guna
    @BA.Hide
    public void scrollDiagonally(boolean left, int start, int end) {
    	if (device_opened) {
    		writeCommand(SSD1306_SET_VERTICAL_SCROLL_AREA);
        	writeCommand(0);
        	writeCommand(this.getHeight());
        	writeCommand(left ? SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL :
                    SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL);
        	writeCommand(0);
        	writeCommand(start);
        	writeCommand(0);
        	writeCommand(end);
        	writeCommand(1);
        	writeCommand(SSD1306_ACTIVATE_SCROLL);
    	}
    	
    }


    // gak guna
    @BA.Hide
    public void stopScroll() {
    	if (device_opened) {
    		writeCommand(SSD1306_DEACTIVATE_SCROLL);
    	}
    	
    }

    /**
     * draws the given image over the current image buffer. The image
     * is automatically converted to a binary image (if it not already
     * is).
     * <p>
     * Note that the current buffer is not cleared before, so if you
     * want the image to completely overwrite the current display
     * content you need to call clear() before.
     * </p>
     *
     * @param image
     * @param x
     * @param y
     */
    public synchronized void drawImage(BufferedImage image, int x, int y) {
        BufferedImage tmpImage = new BufferedImage(this.getWidth(), this.getHeight(), BufferedImage.TYPE_BYTE_BINARY);
        tmpImage.getGraphics().drawImage(image, x, y, null);

        int index = 0;
        int pixelval;
        final byte[] pixels = ((DataBufferByte) tmpImage.getRaster().getDataBuffer()).getData();
        for (int posY = 0; posY < DISPLAY_HEIGHT; posY++) {
            for (int posX = 0; posX < DISPLAY_WIDTH / 8; posX++) {
                for (int bit = 0; bit < 8; bit++) {
                    pixelval = (byte) ((pixels[index/8] >>  (7 - bit)) & 0x01);
                    setPixel(posX * 8 + bit, posY, pixelval > 0);
                    index++;
                }
            }
        }
    }

    /**
     * sends the current buffer to the display
     */
    public synchronized void update() {
    	if (device_opened) {
    		writeCommand(SSD1306_COLUMNADDR);
            writeCommand((byte) 0);   // Column start address (0 = reset)
            writeCommand((byte) (DISPLAY_WIDTH - 1)); // Column end address (127 = reset)

            writeCommand(SSD1306_PAGEADDR);
            writeCommand((byte) 0); // Page start address (0 = reset)
            writeCommand((byte) 7); // Page end address

            for (int i = 0; i < ((DISPLAY_WIDTH * DISPLAY_HEIGHT / 8) / 16); i++) {
                // send a bunch of data in one xmission
            	
                device.WriteBytes((byte) 0x40, imageBuffer, i * 16, 16);
            }
    	} else raise_log("Unable to Update, device noot opened");
        
    }

    private synchronized void shutdown() {
       
        
        clear();
        update();
        

        if (device!=null) {
        	device.CloseDevice();
        	device = null;
        }
        //now we close the bus
        if (bus!=null) {
        	bus.Close_Bus();	
        	bus = null;
        }
        device_opened = false;
    }
}
