/*
 * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
 *
 * This file is part of PortaPack.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street,
 * Boston, MA 02110-1301, USA.
 */

#include "lcd_ili9341.hpp"

#include "portapack_io.hpp"
using namespace portapack;

#include "utility.hpp"

#include "ch.h"

namespace lcd {

namespace {

void lcd_reset() {
	io.lcd_reset_state(false);
	chThdSleepMilliseconds(1);
	io.lcd_reset_state(true);
	chThdSleepMilliseconds(10);
	io.lcd_reset_state(false);
	chThdSleepMilliseconds(120);
}

void lcd_init() {
	// LCDs are configured for IM[2:0] = 001
	// 8080-I system, 16-bit parallel bus

	//
	// 0x3a: DBI[2:0] = 101
	// MDT[1:0] = XX (if not in 18-bit mode, right?)

	// Power control B
	// 0
	// PCEQ=1, DRV_ena=0, Power control=3
	io.lcd_data_write_command_and_data(0xCF, { 0x00, 0xD9, 0x30 });

	// Power on sequence control
	io.lcd_data_write_command_and_data(0xED, { 0x64, 0x03, 0x12, 0x81 });

	// Driver timing control A
	io.lcd_data_write_command_and_data(0xE8, { 0x85, 0x10, 0x78 });

	// Power control A
	io.lcd_data_write_command_and_data(0xCB, { 0x39, 0x2C, 0x00, 0x34, 0x02 });

	// Pump ratio control
	io.lcd_data_write_command_and_data(0xF7, { 0x20 });

	// Driver timing control B
	io.lcd_data_write_command_and_data(0xEA, { 0x00, 0x00 });

	io.lcd_data_write_command_and_data(0xB1, { 0x00, 0x1B });

	// Blanking Porch Control
	// VFP = 0b0000010 = 2 (number of HSYNC of vertical front porch)
	// VBP = 0b0000010 = 2 (number of HSYNC of vertical back porch)
	// HFP = 0b0001010 = 10 (number of DOTCLOCK of horizontal front porch)
	// HBP = 0b0010100 = 20 (number of DOTCLOCK of horizontal back porch)
	io.lcd_data_write_command_and_data(0xB5, { 0x02, 0x02, 0x0a, 0x14 });

	// Display Function Control
	// PT[1:0] = 0b10
	// PTG[1:0] = 0b10
	// ISC[3:0] = 0b0010 (scan cycle interval of gate driver: 5 frames)
	// SM = 0 (gate driver pin arrangement in combination with GS)
	// SS = 1 (source output scan direction S720 -> S1)
	// GS = 0 (gate output scan direction G1 -> G320)
	// REV = 1 (normally white)
	// NL = 0b100111 (default)
	// PCDIV = 0b000000 (default?)
	io.lcd_data_write_command_and_data(0xB6, { 0x0A, 0xA2, 0x27, 0x00 });

	// Power Control 1
	//VRH[5:0]
	io.lcd_data_write_command_and_data(0xC0, { 0x1B });

	// Power Control 2
	//SAP[2:0];BT[3:0]
	io.lcd_data_write_command_and_data(0xC1, { 0x12 });

	// VCOM Control 1
	io.lcd_data_write_command_and_data(0xC5, { 0x32, 0x3C });

	// VCOM Control 2
	io.lcd_data_write_command_and_data(0xC7, { 0x9B });

	// Memory Access Control
	// Invert X and Y memory access order, so upper-left of
	// screen is (0,0) when writing to display.
	io.lcd_data_write_command_and_data(0x36, {
		(1 << 7) |	// MY=1
		(1 << 6) |	// MX=1
		(0 << 5) |	// MV=0
		(1 << 4) |	// ML=1: reverse vertical refresh to simplify scrolling logic
		(1 << 3)	// BGR=1: For Kingtech LCD, BGR filter.
	});

	// COLMOD: Pixel Format Set
	// DPI=101 (16 bits/pixel), DBI=101 (16 bits/pixel)
	io.lcd_data_write_command_and_data(0x3A, { 0x55 });

	//io.lcd_data_write_command_and_data(0xF6, { 0x01, 0x30 });
	// WEMODE=1 (reset column and page number on overflow)
	// MDT[1:0]
	// EPF[1:0]=00 (use channel MSB for LSB)
	// RIM=0 (If COLMOD[6:4]=101 (65k color), 16-bit RGB interface (1 transfer/pixel))
	// RM=0 (system interface/VSYNC interface)
	// DM[1:0]=00 (internal clock operation)
	// ENDIAN=0 (doesn't matter with 16-bit interface)
	io.lcd_data_write_command_and_data(0xF6, { 0x01, 0x30, 0x00 });

	// 3Gamma Function Disable
	io.lcd_data_write_command_and_data(0xF2, { 0x00 });

	// Gamma curve selected
	io.lcd_data_write_command_and_data(0x26, { 0x01 });

	// Set Gamma
	io.lcd_data_write_command_and_data(0xE0, {
		0x0F, 0x1D, 0x19, 0x0E, 0x10, 0x07, 0x4C, 0x63,
		0x3F, 0x03, 0x0D, 0x00, 0x26, 0x24, 0x04
	});

	// Set Gamma
	io.lcd_data_write_command_and_data(0xE1, {
		0x00, 0x1C, 0x1F, 0x02, 0x0F, 0x03, 0x35, 0x25,
		0x47, 0x04, 0x0C, 0x0B, 0x29, 0x2F, 0x05
	});

	// Exit Sleep
	io.lcd_data_write_command_and_data(0x11, {});
	chThdSleepMilliseconds(120);

	// Display on
	io.lcd_data_write_command_and_data(0x29, {});

	// Turn on Tearing Effect Line (TE) output signal.
	io.lcd_data_write_command_and_data(0x35, { 0b00000000 });
}

void lcd_set(const uint_fast8_t command, const uint_fast16_t start, const uint_fast16_t end) {
	io.lcd_data_write_command_and_data(command, {
		static_cast<uint8_t>(start >> 8), static_cast<uint8_t>(start & 0xff),
		static_cast<uint8_t>(end   >> 8), static_cast<uint8_t>(end   & 0xff)
	});
}

void lcd_ramwr_start() {
	io.lcd_data_write_command_and_data(0x2c, {});
}

void lcd_caset(const uint_fast16_t start_column, uint_fast16_t end_column) {
	lcd_set(0x2a, start_column, end_column);
}

void lcd_paset(const uint_fast16_t start_page, const uint_fast16_t end_page) {
	lcd_set(0x2b, start_page, end_page);
}

void lcd_start_ram_write(
	const ui::Point p,
	const ui::Size s
) {
	lcd_caset(p.x, p.x + s.w - 1);
	lcd_paset(p.y, p.y + s.h - 1);
	lcd_ramwr_start();
}

void lcd_start_ram_write(
	const ui::Rect& r
) {
	lcd_start_ram_write(r.pos, r.size);
}

void lcd_vertical_scrolling_definition(
	const uint_fast16_t top_fixed_area,
	const uint_fast16_t vertical_scrolling_area,
	const uint_fast16_t bottom_fixed_area
) {
	io.lcd_data_write_command_and_data(0x33, {
		static_cast<uint8_t>(top_fixed_area            >> 8),
		static_cast<uint8_t>(top_fixed_area          & 0xff),
		static_cast<uint8_t>(vertical_scrolling_area   >> 8),
		static_cast<uint8_t>(vertical_scrolling_area & 0xff),
		static_cast<uint8_t>(bottom_fixed_area         >> 8),
		static_cast<uint8_t>(bottom_fixed_area       & 0xff)
	});
}

void lcd_vertical_scrolling_start_address(
	const uint_fast16_t vertical_scrolling_pointer
) {
	io.lcd_data_write_command_and_data(0x37, {
		static_cast<uint8_t>(vertical_scrolling_pointer >> 8),
		static_cast<uint8_t>(vertical_scrolling_pointer & 0xff)
	});
}

}

void ILI9341::init() {
	lcd_reset();
	lcd_init();

	io.lcd_backlight(1);
}

void ILI9341::shutdown() {
	io.lcd_backlight(0);
	lcd_reset();
}

void ILI9341::fill_rectangle(ui::Rect r, const ui::Color c) {
	const auto r_clipped = r.intersect(screen_rect());
	if( !r_clipped.is_empty() ) {
		lcd_start_ram_write(r_clipped);
		size_t count = r_clipped.size.w * r_clipped.size.h;
		io.lcd_write_pixels(c, count);
	}
}

void ILI9341::fill_circle(
	const ui::Point center,
	const ui::Dim radius,
	const ui::Color foreground,
	const ui::Color background
) {
	const uint32_t radius2 = radius * radius;
	for(int32_t y=-radius; y<radius; y++) {
		const int32_t y2 = y * y;
		for(int32_t x=-radius; x<radius; x++) {
			const int32_t x2 = x * x;
			const uint32_t d2 = x2 + y2;
			const bool inside = d2 < radius2;
			const auto color = inside ? foreground : background;
			draw_pixel({
				static_cast<ui::Coord>(x + center.x),
				static_cast<ui::Coord>(y + center.y)
			}, color);
		}
	}
}

void ILI9341::draw_pixel(
	const ui::Point p,
	const ui::Color color
) {
	if( screen_rect().contains(p) ) {
		lcd_start_ram_write(p, { 1, 1 });
		io.lcd_write_pixel(color);
	}
}

void ILI9341::draw_pixels(
	const ui::Rect r,
	const ui::Color* const colors,
	const size_t count
) {
	/* TODO: Assert that rectangle width x height < count */
	lcd_start_ram_write(r.pos, r.size);
	io.lcd_write_pixels(colors, count);
}

void ILI9341::draw_glyph(
	const ui::Point p,
	const ui::Glyph& glyph,
	const ui::Color foreground,
	const ui::Color background
) {
	lcd_start_ram_write(p, glyph.size());

	const size_t count = glyph.w() * glyph.h();
	const auto pixels = glyph.pixels();
	for(size_t i=0; i<count; i++) {
		const auto pixel = pixels[i >> 3] & (1U << (i & 0x7));
		io.lcd_write_pixel(pixel ? foreground : background);
	}
}

void ILI9341::scroll_set_area(
	const ui::Coord top_y,
	const ui::Coord bottom_y
) {
	scroll_state.top_area = top_y;
	scroll_state.bottom_area = height() - bottom_y;
	scroll_state.height = bottom_y - top_y;
	lcd_vertical_scrolling_definition(scroll_state.top_area, scroll_state.height, scroll_state.bottom_area);
}

ui::Coord ILI9341::scroll_set_position(
	const ui::Coord position
) {
	scroll_state.current_position = position % scroll_state.height;
	const uint_fast16_t address = scroll_state.top_area + scroll_state.current_position;
	lcd_vertical_scrolling_start_address(address);
	return address;
}

ui::Coord ILI9341::scroll(const int32_t delta) {
	return scroll_set_position(scroll_state.current_position + scroll_state.height - delta);
}

ui::Coord ILI9341::scroll_area_y(const ui::Coord y) const {
	const auto wrapped_y = (scroll_state.current_position + y) % scroll_state.height;
	return wrapped_y + scroll_state.top_area;
}

void ILI9341::scroll_disable() {
	lcd_vertical_scrolling_definition(0, height(), 0);
	lcd_vertical_scrolling_start_address(0);
}

} /* namespace lcd */
