/*
 *  PS3 flash memory utility.
 *
 *  Copyright (C) 2006 Sony Computer Entertainment Inc.
 *  Copyright 2006 Sony Corp.
 *
 *  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; version 2 of the License.
 *
 *  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, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#if defined(HAVE_CONFIG_H)
#include "config.h"
#endif
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <inttypes.h>

#define PS3_UTILS_APP_NAME "ps3-flash-util"

#if defined(PACKAGE_VERSION) && defined(PACKAGE_NAME)
#define PS3_UTILS_VERSION PS3_UTILS_APP_NAME " (" PACKAGE_NAME ") " PACKAGE_VERSION "\n\n"
#else
#define PS3_UTILS_VERSION ""
#endif

#if defined(PACKAGE_BUGREPORT)
#define PS3_UTILS_BUGREPORT "Send bug reports to " PACKAGE_BUGREPORT ".\n\n"
#else
#define PS3_UTILS_BUGREPORT ""
#endif

#define MIN(a,b) ((a) < (b) ? (a) : (b))

static void print_usage_exit(int r)
{
	printf(PS3_UTILS_VERSION);
	puts("Usage:");
	puts("  " PS3_UTILS_APP_NAME " [-b|-B] [-g|-r] [boot_loader_img]");
	puts("  " PS3_UTILS_APP_NAME " -s");
	puts("  " PS3_UTILS_APP_NAME " -d");
	puts("");
	puts("Options:");
	puts("  -h   print this message");
	puts("  -s   show current settings");
	puts("  -d   print difference of clock time between game os and linux");
	puts("  -b   change boot flag : boot game os");
	puts("  -B   change boot flag : boot linux");
	puts("  -g   change boot loader format flag : compressed by gzip");
	puts("  -r   change boot loader format flag : not compressed");
	printf(PS3_UTILS_BUGREPORT);
	exit(r);
}

static uint_least32_t read_be32(uint8_t *buf)
{
	int i;
	uint_least32_t v = 0;
	for (i = 0; i < 4; i++) {
		v <<= 8;
		v += buf[i];
	}
	return v;
}

static uint_least64_t read_be64(uint8_t *buf)
{
	uint_least64_t v;
	v = read_be32(buf);
	v <<= 32;
	v += read_be32(buf+4);
	return v;
}

static void write_be32(uint8_t *buf, uint_least32_t v)
{
	int i;
	for (i = 3; i >= 0; i--) {
		buf[i] = v & 0xff;
		v >>= 8;
	}
}

#define NR_PARAMS_MAX	8
struct other_os_flash_header
{
	uint_least32_t version;
	uint_least32_t offset1;
	uint_least32_t offset2;

	uint_least32_t ldr_format;
	uint_least32_t ldr_size;

	uint_least32_t boot_flag;

	uint_least32_t num_params;
	uint8_t *param_data;
};

/**
 * read_header -
 *
 * return value:
 *    0 success
 *    1 no header
 *    2 invalid version number
 */

static int read_header(uint8_t *buf, struct other_os_flash_header *hdr)
{
	if (memcmp(buf, "cell_ext_os_area", 16) != 0) {
		return 1;
	}
	hdr->version = read_be32(buf + 0x10);
	hdr->offset1 = read_be32(buf + 0x14);
	hdr->offset2 = read_be32(buf + 0x18);

	if (hdr->version != 1) {
		return 2;
	}

	hdr->ldr_format	= read_be32(buf + 0x20);
	hdr->ldr_size	= read_be32(buf + 0x24);

	hdr->boot_flag	= read_be32(buf + 0x200);

	hdr->num_params	= read_be32(buf + 0x210);
	hdr->param_data = buf + 0x220;

	return 0;
}

static void print_ip_address(struct other_os_flash_header *hdr, int pos)
{
	printf("%"PRId8".%"PRId8".%"PRId8".%"PRId8"\n",
	       hdr->param_data[pos],
	       hdr->param_data[pos+1],
	       hdr->param_data[pos+2],
	       hdr->param_data[pos+3]);
}

static void show_header(struct other_os_flash_header *hdr)
{
	int i, j;

	printf("version: 0x%08"PRIxLEAST32"\n", hdr->version);
	printf("offset1: 0x%08"PRIxLEAST32"\n", hdr->offset1);
	printf("offset2: 0x%08"PRIxLEAST32"\n", hdr->offset2);

	printf("boot loader format: 0x%08"PRIxLEAST32" : ", hdr->ldr_format);
	switch(hdr->ldr_format){
	case 0:  printf("no compressed\n"); break;
	case 1:  printf("gziped\n"); break;
	default: printf("(unknown)\n"); break;
	}
	printf("boot loader size  : %"PRIdLEAST32, hdr->ldr_size);
	if (hdr->ldr_size == 0) {
		printf(" : whole of area\n");
	} else {
		printf("\n");
	}

	printf("boot flag: 0x%08"PRIxLEAST32" : ", hdr->boot_flag);
	switch(hdr->boot_flag){
	case 0:  printf("boot game os\n"); break;
	case 1:  printf("boot linux\n"); break;
	default: printf("(unknown)\n"); break;
	}

	printf("num_params: %"PRIxLEAST32"\n", hdr->num_params);
	for (i = 0; i < (int)MIN(NR_PARAMS_MAX, hdr->num_params); i++) {
		printf("param_data%d:", i);
		for (j = 0; j < 16; j++) {
			printf(" %02"PRIx8, hdr->param_data[i*16+j]);
		}
		printf("\n");
		switch (i) {
		case 0:
			printf(" rtc_diff   : %"PRId64"\n",
			       read_be64(hdr->param_data + 0x00));
			printf(" video_mode : 0x%02"PRIx8" : ",
			       hdr->param_data[0x08]);
			switch (hdr->param_data[0x08]) {
			case 0:  printf("NTSC\n"); break;
			case 1:  printf("PAL(RGB)\n"); break;
			case 2:  printf("PAL(YCbCr)\n"); break;
			case 3:  printf("SECAM\n"); break;
			default: printf("(unknown)\n"); break;
			}
			printf(" controller : 0x%02"PRIx8" : ",
			       hdr->param_data[0x09]);
			switch (hdr->param_data[0x09]) {
			case 0:  printf("O button means yes\n"); break;
			case 1:  printf("X button means yes\n"); break;
			default: printf("(unknown)\n"); break;
			}
			break;
		case 1:
			printf(" Static IP       : ");
			print_ip_address(hdr, 0x10);
			printf(" Network Mask    : ");
			print_ip_address(hdr, 0x14);
			printf(" Default Gateway : ");
			print_ip_address(hdr, 0x18);
			break;
		case 2:
			printf(" DNS Server 1    : ");
			print_ip_address(hdr, 0x20);
			printf(" DNS Server 2    : ");
			print_ip_address(hdr, 0x24);
			break;
		}
	}
}

static int show_clock_diff(struct other_os_flash_header *hdr, FILE *file)
{
	uint8_t buf[8];
	int_least64_t diff;
	long offset;

	offset = hdr->offset1 * 0x200;

	if ((fseek(file, offset, SEEK_SET) != 0) ||
	    (fread(buf, 8, 1, file) < 1)) {
		return -1;
	}

	diff = (int64_t)(uint64_t)read_be64(hdr->param_data + 0x00)
		- (int64_t)(uint64_t)read_be64(buf)
		- (365 * 23 + 366 * 7) * 24 * 60 * 60;

	printf("%"PRIdLEAST64"\n", diff);
	
	return 0;
}

int main(int argc, char *argv[])
{
	int i;
	char *p;

	int n_flag = -1;
	int n_file = 0;
	int b_show = 0;
	int n_format = -1;
	static const char flash_dev[] = "/dev/ps3flash";

	int ret = 0;
	FILE *file = NULL;
	unsigned long size;

	uint8_t hdr_buf[0x400];
	struct other_os_flash_header hdr;

	uint8_t *ldr_buf = NULL;
	unsigned long ldr_size = 0;

	/* parse options */

	if (argc < 1) {
		print_usage_exit(1);
	}

	if (argc == 1)
		print_usage_exit(0);

	for (i = 1; i < argc; i++) {
		p = argv[i];
		if (*p == '-') {
			p++;
			if (*p == '\0') {
				/* boot loader read from stdin */
				if (n_file != 0) {
					print_usage_exit(1);
				}
				n_file = -1;
				continue;
			}
			do {
				/* one letter options */
				if (*p == 'b') {
					if (n_flag >= 0) 
						print_usage_exit(1);
					n_flag = 0;
				} else if (*p == 'B') {
					if (n_flag >= 0)
						print_usage_exit(1);
					n_flag = 1;
				} else if (*p == 's') {
					if (b_show > 0)
						print_usage_exit(1);
					b_show = 1;
				} else if (*p == 'd') {
					if (b_show > 0)
						print_usage_exit(1);
					b_show = 2;
 				} else if (*p == 'g') {
					if (n_format >= 0)
						print_usage_exit(1);
					n_format = 1;
				} else if (*p == 'r') {
					if (n_format >= 0)
						print_usage_exit(1);
					n_format = 0;
				} else if (*p == 'h') {
						print_usage_exit(0);
					n_format = 0;
				} else {
					printf("unknown option: -%s\n", p);
					print_usage_exit(1);
				}
			} while (*++p != '\0');
		} else if (n_file == 0) {
			/* boot loader file name */
			n_file = i;
		} else {
			print_usage_exit(1);
		}
	}

	if ((b_show && (n_file || n_flag >= 0 || n_format >= 0))) {
		print_usage_exit(1);
	}

	/* read and check header*/
	file = fopen(flash_dev, "r+");
	if (file == NULL) {
		perror(flash_dev);
		ret = 2;
		goto end;
	}
	if (fread(hdr_buf, 0x200, 2, file) < 2) {
		perror(flash_dev);
		ret = 3;
		goto end;
	}
	ret = read_header(hdr_buf, &hdr);
	if (ret != 0) {
		printf("invalid format\n");
		ret += 3;
		goto end;
	}
	if (fseek(file, 0, SEEK_END) != 0) {
		perror(flash_dev);
		ret = 6;
		goto end;
	}
	size = ftell(file);
	if (hdr.offset1 > hdr.offset2 ||
	    hdr.offset2 * 0x200 > size) {
		printf("invalid offset value\n");
		ret = 7;
		goto end;
	}

	/* show info */
	if (b_show == 1) {
		show_header(&hdr);
	} else if (b_show == 2) {
		if (show_clock_diff(&hdr, file)) {
			perror(flash_dev);
			ret = 15;
			goto end;
		}
	}

	/* read file */
	if (n_file != 0) {
		FILE *ldrfile = stdin;

		ldr_buf = malloc(size);
		if (ldr_buf == NULL) {
			perror(argv[0]);
			ret = 8;
			goto end;
		}

		if (n_file >= 1) {
			ldrfile = fopen(argv[n_file], "r");
			if (ldrfile == NULL) {
				perror(argv[n_file]);
				ret = 9;
				goto end;
			}
		}

		ldr_size = fread(ldr_buf, 1, size, ldrfile);
		fclose(ldrfile);

		if (ldr_size > size - 0x200 * hdr.offset2) {
			printf("boot loader is too large.\n");
			ret = 10;
			goto end;
		}
	}

	/* change boot flag */
	if (n_flag >= 0) {
		write_be32(hdr_buf + 0x200, n_flag);
		if ((fseek(file, 0x200, SEEK_SET) != 0) ||
		    (fwrite(hdr_buf + 0x200, 4, 1, file) < 1)) {
			perror(flash_dev);
			ret = 11;
			goto end;
		}
		printf("boot flag changed.\n");
	}

	/* change boot loader format flag */
	if (n_format >= 0) {
		write_be32(hdr_buf + 0x20, n_format);
		if ((fseek(file, 0x20, SEEK_SET) != 0) ||
		    (fwrite(hdr_buf + 0x20, 4, 1, file) < 1)) {
			perror(flash_dev);
			ret = 12;
			goto end;
		}
		printf("boot loader format changed.\n");
	}

	/* write boot loader */
	if (n_file != 0) {
		if ((fseek(file, hdr.offset2 * 0x200, SEEK_SET) != 0) ||
		    (fwrite(ldr_buf, ldr_size, 1, file) < 1)) {
			perror(flash_dev);
			ret = 13;
			goto end;
		}
		write_be32(hdr_buf + 0x24, ldr_size);
		if ((fseek(file, 0x24, SEEK_SET) != 0) ||
		    (fwrite(hdr_buf + 0x24, 4, 1, file) < 1)) {
			perror(flash_dev);
			ret = 14;
			goto end;
		}
		printf("new boot loader wrote(%ld bytes).\n", ldr_size);
	}

 end:
	/* clean up */
	if (ldr_buf != NULL) free(ldr_buf);
	if (file != NULL) fclose(file);

	return ret;
}
