/*
 * TilEm II
 *
 * Copyright (c) 2011 Benjamin Moody
 *
 * 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 3 of the
 * License, 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.  If not, see <http://www.gnu.org/licenses/>.
 */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <stdio.h>
#include <string.h>
#include <tilem.h>

#include "ti81prg.h"

#define tSpace  0x56
#define t0      0x10
#define t9      0x19
#define tDecPt	0x1A
#define tA      0x59
#define tZ      0x72
#define tTheta  0x73

#define ramStart      0xE000
#define cxCurApp      0xE347
#define cxOldApp      (cxCurApp + 1)
#define DimX_old      0xEEF9
#define DimX_new      0xF12D
#define prgm0Name     0xF1D3
#define prgm0Start    0xF2FB
#define prgm0End      (prgm0Start + 2)
#define prgmThetaEnd  (prgm0End + 36*2)
#define progMem       0xF347
#define progMemEnd    0xFCA6

#define cxPrgmEdit  2
#define cxPrgmExec  10
#define cxMenu      11

TI81Program * ti81_program_new(int size)
{
	TI81Program *prgm = tilem_new0(TI81Program, 1);

	prgm->info.slot = TI81_SLOT_AUTO;
	memset(prgm->info.name, tSpace, 8);

	if (size > 0) {
		prgm->info.size = size;
		prgm->data = tilem_new_atomic(byte, size);
	}

	return prgm;
}

void ti81_program_free(TI81Program *prgm)
{
	if (!prgm)
		return;

	if (prgm->data)
		tilem_free(prgm->data);
	tilem_free(prgm);
}

static byte *get_byte_ptr(const TilemCalc *calc, dword addr)
{
	if (addr < ramStart || addr > 0xffff)
		return NULL;

	return &calc->ram[addr - ramStart];
}

static int read_byte(const TilemCalc *calc, dword addr)
{
	const byte *p = get_byte_ptr(calc, addr);

	if (!p)
		return -1;
	else
		return *p;
}

static dword read_word(const TilemCalc *calc, dword addr)
{
	const byte *p = get_byte_ptr(calc, addr);

	if (!p)
		return 0;
	else
		return (p[0] | p[1] << 8);
}

static void write_word(TilemCalc *calc, dword addr, dword value)
{
	byte *p = get_byte_ptr(calc, addr);

	if (p) {
		p[0] = value & 0xff;
		p[1] = (value >> 8) & 0xff;
	}
}

static int check_busy(const TilemCalc *calc)
{
	int cur, old;

	cur = read_byte(calc, cxCurApp);
	old = read_byte(calc, cxCurApp);

	if (cur == cxPrgmEdit || cur == cxPrgmExec)
		return 1;
	else if (cur == cxMenu && (old == cxPrgmEdit || old == cxPrgmExec))
		return 1;
	else
		return 0;
}

static dword get_free_mem_end(const TilemCalc *calc)
{
	const byte *p;
	int n, i;

	p = get_byte_ptr(calc, DimX_new);

	/* DimX is always a small positive integer, so the first byte
	   must be between 80h and 82h, and the last five bytes must
	   always be zero.  On 1.1K, DimX_new is part of textShadow,
	   so none of these byte values make any sense. */

	if (p[0] < 0x80 || p[0] > 0x82 || p[7] != 0)
		p = get_byte_ptr(calc, DimX_old);

	if (p[0] < 0x80 || p[0] > 0x82)
		return 0;

	for (i = 3; i < 7; i++)
		if (p[i])
			return 0;

	n = ((p[2] & 0xf)
	     + ((p[2] >> 4) * 10)
	     + ((p[1] & 0xf) * 100)
	     + ((p[1] >> 4) * 1000));

	for (i = p[0]; i < 0x83; i++) {
		if (n % 10)
			return 0;
		n /= 10;
	}

	return (progMemEnd + 1 - 16 * n);
}

int ti81_get_program_info(const TilemCalc *calc, int slot, TI81ProgInfo *info)
{
	const byte *p;
	dword progstart, progend;

	if (slot < 0 || slot > TI81_SLOT_MAX)
		return TI81_ERR_INTERNAL;

	if (check_busy(calc))
		return TI81_ERR_BUSY;

	progstart = read_word(calc, prgm0Start + 2 * slot);
	progend = read_word(calc, prgm0Start + 2 * slot + 2);

	if (progstart < ramStart || progend < ramStart || progend < progstart)
		return TI81_ERR_BUSY;

	info->slot = slot;
	info->size = progend - progstart;
	info->addr = progstart;

	p = get_byte_ptr(calc, prgm0Name + 8 * slot);
	if (!p) return TI81_ERR_INTERNAL;
	memcpy(info->name, p, 8);

	return 0;
}

int ti81_get_program(const TilemCalc *calc, int slot, TI81Program **prgm)
{
	TI81ProgInfo info;
	const byte *p;
	int s;

	if ((s = ti81_get_program_info(calc, slot, &info))) {
		*prgm = NULL;
		return s;
	}

	*prgm = ti81_program_new(info.size);
	(*prgm)->info = info;
	if (info.size > 0 && (p = get_byte_ptr(calc, info.addr)))
		memcpy((*prgm)->data, p, info.size);

	return 0;
}

int ti81_load_program(TilemCalc *calc, const TI81Program *prgm)
{
	TI81ProgInfo info;
	int slot = prgm->info.slot;
	int s, i;
	dword progs_start, progs_end, mem_end, x;
	byte *p;

	if (slot == TI81_SLOT_AUTO) {
		for (slot = 0; slot <= TI81_SLOT_MAX; slot++) {
			if ((s = ti81_get_program_info(calc, slot, &info)))
				return s;
			if (info.size == 0 && info.name[0] == tSpace)
				break;
		}

		if (slot > TI81_SLOT_MAX)
			return TI81_ERR_SLOTS_FULL;
	}

	if ((s = ti81_get_program_info(calc, slot, &info)))
		return s;

	/* move later programs forward/backward in memory */

	progs_start = info.addr + info.size;
	progs_end = read_word(calc, prgmThetaEnd);
	if (progs_end < progs_start)
		return TI81_ERR_BUSY;

	mem_end = get_free_mem_end(calc);
	if (progs_end + prgm->info.size - info.size > mem_end)
		return TI81_ERR_MEMORY;

	if (prgm->info.size != info.size && progs_start != progs_end) {
		p = get_byte_ptr(calc, progs_start);
		if (!p) return TI81_ERR_INTERNAL;
		memmove(p + prgm->info.size - info.size, p,
		        progs_end - progs_start);
	}

	/* update program pointers */

	for (i = slot; i <= TI81_SLOT_MAX; i++) {
		x = read_word(calc, prgm0End + 2 * i);
		write_word(calc, prgm0End + 2 * i,
		           x + prgm->info.size - info.size);
	}

	/* copy program data */

	if (prgm->info.size != 0) {
		p = get_byte_ptr(calc, info.addr);
		if (!p) return TI81_ERR_INTERNAL;
		memcpy(p, prgm->data, prgm->info.size);
	}

	/* copy program name */

	p = get_byte_ptr(calc, prgm0Name + 8 * slot);
	if (!p) return TI81_ERR_INTERNAL;
	memcpy(p, prgm->info.name, 8);

	return 0;
}

int ti81_read_prg_file(FILE *f, TI81Program **prgm)
{
	byte buf[20];
	unsigned int size, i;
	unsigned int sum = 0;
	TI81Program *p;

	*prgm = NULL;

	if (fread(buf, 1, 20, f) != 20)
		return TI81_ERR_INVALID_FILE;

	if (strcmp((char *) buf, "**TI81**") || buf[9] != 0x6e)
		return TI81_ERR_INVALID_FILE;

	size = buf[10] | buf[11] << 8;

	p = ti81_program_new(size);

	memcpy(p->info.name, buf + 12, 8);

	for (i = 0; i < 8; i++)
		sum += buf[12 + i];

	if (fread(p->data, 1, size, f) != size) {
		ti81_program_free(p);
		return TI81_ERR_INVALID_FILE;
	}

	for (i = 0; i < size; i++)
		sum += p->data[i];

	if (fread(buf, 1, 2, f) != 2) {
		ti81_program_free(p);
		return TI81_ERR_INVALID_FILE;
	}

	sum -= (buf[0] | buf[1] << 8);
	if (sum & 0xffff)
		fprintf(stderr, "warning: checksum incorrect\n");

	*prgm = p;
	return 0;
}

int ti81_write_prg_file(FILE *f, const TI81Program *prgm)
{
	byte buf[20];
	unsigned int size, i;
	unsigned int sum = 0;

	memcpy(buf, "**TI81**\0n", 10);
	size = prgm->info.size;
	buf[10] = size & 0xff;
	buf[11] = (size >> 8) & 0xff;

	memcpy(buf + 12, prgm->info.name, 8);

	for (i = 0; i < 8; i++)
		sum += buf[12 + i];

	if (fwrite(buf, 1, 20, f) != 20)
		return TI81_ERR_FILE_IO;

	if (fwrite(prgm->data, 1, size, f) != size)
		return TI81_ERR_FILE_IO;

	for (i = 0; i < size; i++)
		sum += prgm->data[i];

	buf[0] = sum & 0xff;
	buf[1] = (sum >> 8) & 0xff;
	if (fwrite(buf, 1, 2, f) != 2)
		return TI81_ERR_FILE_IO;

	return 0;
}

char * ti81_program_slot_to_string(int slot)
{
	char buf[50];
	char *s;

	if (slot == TI81_SLOT_AUTO)
		strcpy(buf, "Automatic");
	else if (slot < 0 || slot > 36)
		strcpy(buf, "?");
	else if (slot < 10)
		sprintf(buf, "Prgm%c", slot + '0');
	else if (slot < 36)
		sprintf(buf, "Prgm%c", slot + 'A' - 10);
	else
		strcpy(buf, "Prgm\316\270");

	s = tilem_new_atomic(char, strlen(buf) + 1);
	strcpy(s, buf);
	return s;
}

char * ti81_program_name_to_string(const byte *prgname)
{
	char buf[50];
	char *s;
	int i, j;

	for (i = j = 0; i < 8; i++) {
		if (prgname[i] == tSpace)
			buf[j++] = '_';
		else if (prgname[i] == tDecPt)
			buf[j++] = '.';
		else if (prgname[i] == tTheta) {
			buf[j++] = '\316';
			buf[j++] = '\270';
		}
		else if (prgname[i] >= t0 && prgname[i] <= t9)
			buf[j++] = '0' + prgname[i] - t0;
		else if (prgname[i] >= tA && prgname[i] <= tZ)
			buf[j++] = 'A' + prgname[i] - tA;
		else
			buf[j++] = '?';
	}

	while (j > 0 && buf[j - 1] == '_')
		j--;
	buf[j] = 0;

	s = tilem_new_atomic(char, strlen(buf) + 1);
	strcpy(s, buf);
	return s;
}
