package com.diozero.internal.board.tinkerboard;

/*
 * #%L
 * Organisation: mattjlewis
 * Project:      Device I/O Zero - Core
 * Filename:     TinkerBoardMmapGpio.java  
 * 
 * This file is part of the diozero project. More information about this project
 * can be found at http://www.diozero.com/
 * %%
 * Copyright (C) 2016 - 2017 mattjlewis
 * %%
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * #L%
 */


import java.nio.ByteOrder;
import java.nio.IntBuffer;

import org.pmw.tinylog.Logger;

import com.diozero.api.DeviceMode;
import com.diozero.api.GpioPullUpDown;
import com.diozero.internal.board.chip.ChipMmapGpio;
import com.diozero.internal.provider.MmapGpioInterface;
import com.diozero.util.LibraryLoader;
import com.diozero.util.MmapBufferNative;
import com.diozero.util.MmapByteBuffer;
import com.diozero.util.SleepUtil;

/*
 * https://github.com/torvalds/linux/blob/master/arch/arm/boot/dts/rk3288.dtsi
 * https://chromium.googlesource.com/chromiumos/third_party/coreboot/+/chromeos-2013.04/src/soc/rockchip/rk3288/gpio.c
 */
public class TinkerBoardMmapGpio implements MmapGpioInterface {
	private static final String MEM_DEVICE = "/dev/mem";
	private static final int PMU_BASE		= 0xff730000;
	private static final int GPIO_BASE		= 0xff750000;
	private static final int GRF_BASE		= 0xff770000;
	private static final int GPIO_LENGTH	= 0x00010000;
	private static final int GPIO_CHANNEL	= 0x00020000;
	
	private static final int PAGE_SIZE = 4 * 0x400;
	
	private static final int UNKNOWN = -1;
	
	private static final int PMU_GPIO0C_IOMUX_INT_OFFSET = 0x008c / 4;
	private static final int GRF_GPIO5B_IOMUX_INT_OFFSET = 0x0050 / 4;
	private static final int GRF_GPIO5C_IOMUX_INT_OFFSET = 0x0054 / 4;
	private static final int GRF_GPIO6A_IOMUX_INT_OFFSET = 0x005c / 4;
	//private static final int GRF_GPIO6B_IOMUX_INT_OFFSET = 0x0060 / 4;
	//private static final int GRF_GPIO6C_IOMUX_INT_OFFSET = 0x0064 / 4;
	private static final int GRF_GPIO7A_IOMUX_INT_OFFSET = 0x006c / 4;
	private static final int GRF_GPIO7B_IOMUX_INT_OFFSET = 0x0070 / 4;
	private static final int GRF_GPIO7CL_IOMUX_INT_OFFSET = 0x0074 / 4;
	private static final int GRF_GPIO7CH_IOMUX_INT_OFFSET = 0x0078 / 4;
	private static final int GRF_GPIO8A_IOMUX_INT_OFFSET = 0x0080 / 4;
	private static final int GRF_GPIO8B_IOMUX_INT_OFFSET = 0x0084 / 4;

	private static final int PMU_GPIO0C_P_INT_OFFSET = 0x006c / 4;
	private static final int GRF_GPIO5B_P_INT_OFFSET = 0x0184 / 4;
	private static final int GRF_GPIO5C_P_INT_OFFSET = 0x0188 / 4;
	private static final int GRF_GPIO6A_P_INT_OFFSET = 0x0190 / 4;
	//private static final int GRF_GPIO6B_P_INT_OFFSET = 0x0194 / 4;
	//private static final int GRF_GPIO6C_P_INT_OFFSET = 0x0198 / 4;
	private static final int GRF_GPIO7A_P_INT_OFFSET = 0x01a0 / 4;
	private static final int GRF_GPIO7B_P_INT_OFFSET = 0x01a4 / 4;
	private static final int GRF_GPIO7C_P_INT_OFFSET = 0x01a8 / 4;
	private static final int GRF_GPIO8A_P_INT_OFFSET = 0x01b0 / 4;
	private static final int GRF_GPIO8B_P_INT_OFFSET = 0x01b4 / 4;
	
	private static final int GPIO_SWPORTA_DR_INT_OFFSET = 0x0000 / 4;
	private static final int GPIO_SWPORTA_DDR_INT_OFFSET = 0x0004 / 4;
	private static final int GPIO_EXT_PORTA_INT_OFFSET = 0x0050 / 4;
	private static final int MUX_FUNC_GPIO = 0;
	private static final int MUX_FUNC_PWM = 3;
	
	private GpioBank[] gpioBanks;
	private MmapByteBuffer pmuMmap;
	private IntBuffer pmuIntBuffer;
	private MmapByteBuffer grfMmap;
	private IntBuffer grfIntBuffer;
	
	@Override
	public synchronized void initialise() {
		if (gpioBanks == null) {
			gpioBanks = new GpioBank[9];
			for (int i=0; i<gpioBanks.length; i++) {
				gpioBanks[i] = new GpioBank(GPIO_BASE + i*GPIO_LENGTH + (i>0 ? GPIO_CHANNEL : 0));
			}
			pmuMmap = MmapBufferNative.createMmapBuffer(MEM_DEVICE, PMU_BASE, PAGE_SIZE);
			pmuIntBuffer = pmuMmap.getBuffer().order(ByteOrder.LITTLE_ENDIAN).asIntBuffer();
			grfMmap = MmapBufferNative.createMmapBuffer(MEM_DEVICE, GRF_BASE, PAGE_SIZE);
			grfIntBuffer = grfMmap.getBuffer().order(ByteOrder.LITTLE_ENDIAN).asIntBuffer();
		}
	}

	@Override
	public synchronized void close() {
		if (gpioBanks != null) {
			for (int i=0; i<gpioBanks.length; i++) {
				MmapBufferNative.closeMmapBuffer(gpioBanks[i].mmap.getFd(), gpioBanks[i].mmap.getAddress(),
						gpioBanks[i].mmap.getLength());
			}
			gpioBanks = null;
		}
		if (pmuMmap != null) {
			MmapBufferNative.closeMmapBuffer(pmuMmap.getFd(), pmuMmap.getAddress(),
					pmuMmap.getLength());
			pmuMmap = null;
			pmuIntBuffer = null;
		}
		if (grfMmap != null) {
			MmapBufferNative.closeMmapBuffer(grfMmap.getFd(), grfMmap.getAddress(),
					grfMmap.getLength());
			grfMmap = null;
			grfIntBuffer = null;
		}
	}

	@Override
	public DeviceMode getMode(int gpio) {
		IntBuffer mux_int_buffer = (gpio < 24) ? pmuIntBuffer : grfIntBuffer;
		int iomux_offset = getIoMuxOffsetForGpio(gpio);
		if (iomux_offset == UNKNOWN) {
			Logger.warn("Unknown IOMUX offset for GPIO #{}", Integer.valueOf(gpio));
			return DeviceMode.UNKNOWN;
		}
		int config_val = mux_int_buffer.get(iomux_offset);
		int mux_func;
		int mux_mask = getMuxMask(gpio);
		if (gpio == 238 || gpio == 239) {
			mux_func = (config_val >> (((gpio-4)%8)*4)) & mux_mask;
		} else {
			mux_func = (config_val >> ((gpio%8)*2)) & mux_mask;
		}

		DeviceMode mode;
		if (mux_func == MUX_FUNC_GPIO) {
			int bank;
			int shift;
			if (gpio < 24) {
				bank = gpio / 32;
				shift = gpio % 32;
			} else {
				bank = (gpio + 8) / 32;
				shift = (gpio + 8) % 32;
			}
			if ((gpioBanks[bank].gpioIntBuffer.get(GPIO_SWPORTA_DDR_INT_OFFSET) & 1<<shift) != 0) {
				mode = DeviceMode.DIGITAL_OUTPUT;
			} else {
				mode = DeviceMode.DIGITAL_INPUT;
			}
		} else if (mux_func == MUX_FUNC_PWM && (gpio == 238 || gpio == 239)) {
			// PWM can be selected on 238 / 239
			mode = DeviceMode.PWM_OUTPUT;
		} else {
			mode = DeviceMode.UNKNOWN;
		}
		
		return mode;
	}

	@Override
	public void setMode(int gpio, DeviceMode mode) {
		IntBuffer mux_int_buffer = (gpio < 24) ? pmuIntBuffer : grfIntBuffer;
		int iomux_offset = getIoMuxOffsetForGpio(gpio);
		if (iomux_offset == UNKNOWN) {
			Logger.warn("Unknown IOMUX offset for GPIO #{}", Integer.valueOf(gpio));
			return;
		}
		// Configure mode
		int config_val = mux_int_buffer.get(iomux_offset);
		switch (mode) {
		case DIGITAL_INPUT:
		case DIGITAL_OUTPUT:
			if (gpio == 238 || gpio == 239) {
				config_val = (config_val | (0x0f<<(16+(gpio%8-4)*4))) & (~(0x0f<<((gpio%8-4)*4)));
			} else {
				config_val = (config_val | (0x03<<((gpio%8)*2+16))) & (~(0x03<<((gpio%8)*2)));
			}
			mux_int_buffer.put(iomux_offset, config_val);
			
			// Set digital direction
			int bank;
			int shift;
			if (gpio < 24) {
				bank = gpio / 32;
				shift = gpio % 32;
			} else {
				bank = (gpio + 8) / 32;
				shift = (gpio + 8) % 32;
			}
			int gpio_val = gpioBanks[bank].gpioIntBuffer.get(GPIO_SWPORTA_DDR_INT_OFFSET);
			if (mode == DeviceMode.DIGITAL_INPUT) {
				gpio_val &= ~(1<<shift);
			} else {
				gpio_val |= (1<<shift);
			}
			gpioBanks[bank].gpioIntBuffer.put(GPIO_SWPORTA_DDR_INT_OFFSET, gpio_val);
			break;
		case PWM_OUTPUT:
			Logger.warn("Mode {} not yet implemented for GPIO #{}", mode, Integer.valueOf(gpio));
			return;
		default:
			Logger.warn("Invalid mode ({}) for GPIO #{}", mode, Integer.valueOf(gpio));
			return;
		}
	}

	@Override
	public void setPullUpDown(int gpio, GpioPullUpDown pud) {
		Logger.debug("setPullUpDown({}, {})", Integer.valueOf(gpio), pud);
		int bit0, bit1;
		switch (pud) {
		case PULL_UP:
			bit0 = 1;
			bit1 = 0;
			break;
		case PULL_DOWN:
			bit0 = 0;
			bit1 = 1;
			break;
		case NONE:
		default:
			bit0 = 0;
			bit1 = 0;
			break;
		}
		IntBuffer int_buffer = (gpio < 24) ? pmuIntBuffer : grfIntBuffer;
		int pud_offset = getPudOffsetForGpio(gpio);
		int_buffer.put(pud_offset,
				(int_buffer.get(pud_offset) | (0x03 << ((gpio % 8) * 2 + 16))) & (~(0x03 << ((gpio % 8) * 2)))
						| (bit1 << ((gpio % 8) * 2 + 1)) | (bit0 << ((gpio % 8) * 2)));
		//*(grf+pud) = (*(grf+pud) | (0x03<<((pin%8)*2+16))) & (~(0x03<<((pin%8)*2))) | (bit1<<((pin%8)*2+1)) | (bit0<<((pin%8)*2));
	}

	@Override
	public boolean gpioRead(int gpio) {
		int bank;
		int shift;
		if (gpio < 24) {
			bank = gpio / 32;
			shift = gpio % 32;
		} else {
			bank = (gpio + 8) / 32;
			shift = (gpio + 8) % 32;
		}
		return (gpioBanks[bank].gpioIntBuffer.get(GPIO_EXT_PORTA_INT_OFFSET) & (1 << shift)) != 0;
	}

	@Override
	public void gpioWrite(int gpio, boolean value) {
		int bank;
		int shift;
		if (gpio < 24) {
			bank = gpio / 32;
			shift = gpio % 32;
		} else {
			bank = (gpio + 8) / 32;
			shift = (gpio + 8) % 32;
		}
		int reg_val = gpioBanks[bank].gpioIntBuffer.get(GPIO_SWPORTA_DR_INT_OFFSET);
		if (value) {
			reg_val |= (1<<shift);
		} else {
			reg_val &= ~(1<<shift);
		}
		gpioBanks[bank].gpioIntBuffer.put(GPIO_SWPORTA_DR_INT_OFFSET, reg_val);
	}
	
	private static int getIoMuxOffsetForGpio(int gpio) {
		switch (gpio) {
		// GPIO0_C1
		case 17:
			return PMU_GPIO0C_IOMUX_INT_OFFSET;
		// GPIO5B
		case 160:
		case 161:
		case 162:
		case 163:
		case 164:
		case 165:
		case 166:
		case 167:
			return GRF_GPIO5B_IOMUX_INT_OFFSET;
		// GPIO5C
		case 168:
		case 169:
		case 170:
		case 171:
			return GRF_GPIO5C_IOMUX_INT_OFFSET;
		// GPIO6A
		case 184:
		case 185:
		case 187:
		case 188:
			return GRF_GPIO6A_IOMUX_INT_OFFSET;
		// GPIO7A
		case 223:
			return GRF_GPIO7A_IOMUX_INT_OFFSET;
		// GPIO7B
		case 224:
		case 225:
		case 226:
			return GRF_GPIO7B_IOMUX_INT_OFFSET;
		case 233:
		case 234:
			return GRF_GPIO7CL_IOMUX_INT_OFFSET;
		case 238:
		case 239:
			return GRF_GPIO7CH_IOMUX_INT_OFFSET;
		// GPIO8A
		case 251:
		case 252:
		case 253:
		case 254:
		case 255:
			return GRF_GPIO8A_IOMUX_INT_OFFSET;
		// GPIO8B
		case 256:
		case 257:
			return GRF_GPIO8B_IOMUX_INT_OFFSET;
		default:
			return UNKNOWN;
		}
	}
	
	private static int getMuxMask(int gpio) {
		switch (gpio) {
		// GPIO0_C1
		case 17:
			return 0x00000003;
		// GPIO5B
		case 160:
		case 161:
		case 162:
		case 163:
		case 164:
		case 165:
		case 166:
		case 167:
			return 0x00000003;
		// GPIO5C
		case 168:
			return 0x00000003;
		case 169:
		case 170:
		case 171:
			return 0x00000001;
		// GPIO6A
		case 184:
		case 185:
		case 187:
		case 188:
			return 0x00000001;
		// GPIO7A
		case 223:
			return 0x00000003;
		// GPIO7B
		case 224:
		case 225:
		case 226:
			return 0x00000003;
		case 233:
		case 234:
			return 0x00000001;
		case 238:
			return 0x00000003;
		case 239:
			return 0x00000007;
		// GPIO8A
		case 251:
		case 252:
		case 253:
		case 254:
		case 255:
			return 0x00000003;
		// GPIO8B
		case 256:
		case 257:
			return 0x00000003;
		default:
			return UNKNOWN;
		}
	}
	
	private static int getPudOffsetForGpio(int gpio) {
		switch (gpio) {
		// GPIO0
		case 17:
			return PMU_GPIO0C_P_INT_OFFSET;
		// GPIO5B
		case 160:
		case 161:
		case 162:
		case 163:
		case 164:
		case 165:
		case 166:
		case 167:
			return GRF_GPIO5B_P_INT_OFFSET;
		// GPIO5C
		case 168:
		case 169:
		case 170:
		case 171:
			return GRF_GPIO5C_P_INT_OFFSET;
		// GPIO6A
		case 184:
		case 185:
		case 187:
		case 188:
			return GRF_GPIO6A_P_INT_OFFSET;
		// GPIO7A
		case 223:
			return GRF_GPIO7A_P_INT_OFFSET;
		// GPIO7B
		case 224:
		case 225:
		case 226:
			return GRF_GPIO7B_P_INT_OFFSET;
		case 233:
		case 234:
		case 238:
		case 239:
			return GRF_GPIO7C_P_INT_OFFSET;
		// GPIO8A
		case 251:
		case 252:
		case 253:
		case 254:
		case 255:
			return GRF_GPIO8A_P_INT_OFFSET;
		// GPIO8B
		case 256:
		case 257:
			return GRF_GPIO8B_P_INT_OFFSET;
		default:
			return UNKNOWN;
		}
	}
	
	private static class GpioBank {
		MmapByteBuffer mmap;
		IntBuffer gpioIntBuffer;
		
		public GpioBank(int offset) {
			mmap = MmapBufferNative.createMmapBuffer(MEM_DEVICE, offset, PAGE_SIZE);
			gpioIntBuffer = mmap.getBuffer().order(ByteOrder.LITTLE_ENDIAN).asIntBuffer();
		}
	}
	
	public static void main(String[] args) {
		LibraryLoader.loadLibrary(ChipMmapGpio.class, "diozero-system-utils");
		try (TinkerBoardMmapGpio mmap_gpio = new TinkerBoardMmapGpio()) {
			mmap_gpio.initialise();
			
			int gpio = 184;
			
			for (int i=0; i<10; i++) {
				DeviceMode mode = mmap_gpio.getMode(gpio);
				Logger.debug("Mode for GPIO #{}: {}, value: {}", Integer.valueOf(gpio), mode,
						Boolean.valueOf(mmap_gpio.gpioRead(gpio)));
				SleepUtil.sleepSeconds(1);

				// Toggle input/output mode
				mmap_gpio.setMode(gpio,
						mode == DeviceMode.DIGITAL_INPUT ? DeviceMode.DIGITAL_OUTPUT : DeviceMode.DIGITAL_INPUT);
			}
			
			mmap_gpio.setMode(gpio, DeviceMode.DIGITAL_OUTPUT);
			
			for (int i=0; i<10; i++) {
				Logger.debug("GPIO #{} On", Integer.valueOf(gpio));
				mmap_gpio.gpioWrite(gpio, true);
				Logger.debug("Mode for GPIO #{}: {}, value: {}", Integer.valueOf(gpio), mmap_gpio.getMode(gpio),
						Boolean.valueOf(mmap_gpio.gpioRead(gpio)));
				SleepUtil.sleepSeconds(1);
				
				Logger.debug("GPIO #{} Off", Integer.valueOf(gpio));
				mmap_gpio.gpioWrite(gpio, false);
				Logger.debug("Mode for GPIO #{}: {}, value: {}", Integer.valueOf(gpio), mmap_gpio.getMode(gpio),
						Boolean.valueOf(mmap_gpio.gpioRead(gpio)));
				SleepUtil.sleepSeconds(1);
			}
		}
	}
}