// ACME - a crossassembler for producing 6502/65c02/65816/65ce02 code.
// Copyright (C) 1998-2020 Marco Baye
// Have a look at "acme.c" for further info
//
// Output stuff
// 24 Nov 2007	Added possibility to suppress segment overlap warnings
// 25 Sep 2011	Fixed bug in !to (colons in filename could be interpreted as EOS)
//  5 Mar 2014	Fixed bug where setting *>0xffff resulted in hangups.
// 19 Nov 2014	Merged Johann Klasek's report listing generator patch
// 22 Sep 2015	Added big-endian output functions
// 20 Apr 2019	Prepared for "make segment overlap warnings into errors" later on
#include "output.h"
#include <stdlib.h>
#include <string.h>	// for memset()
#include "acme.h"
#include "alu.h"
#include "config.h"
#include "cpu.h"
#include "dynabuf.h"
#include "global.h"
#include "input.h"
#include "platform.h"
#include "tree.h"


// constants
#define NO_SEGMENT_START	(-1)	// invalid value to signal "not in a segment"


// structure for linked list of segment data
struct segment {
	struct segment	*next,
			*prev;
	intval_t	start,
			length;
};

// structure for all output stuff:
struct output {
	// output buffer stuff
	intval_t	bufsize;	// either 64 KiB or 16 MiB
	char		*buffer;	// holds assembled code
	intval_t	write_idx;	// index of next write
	intval_t	lowest_written;		// smallest address used
	intval_t	highest_written;	// largest address used
	boolean		initvalue_set;
	struct {
		intval_t	start;	// start of current segment (or NO_SEGMENT_START)
		intval_t	max;	// highest address segment may use
		bits		flags;	// segment flags ("overlay" and "invisible", see header file)
		struct segment	list_head;	// head element of doubly-linked ring list
	} segment;
	char		xor;		// output modifier
};

// for offset assembly:
static struct pseudopc	*pseudopc_current_context;	// current struct (NULL when not in pseudopc block)


// variables
static struct output	default_output;
static struct output	*out	= &default_output;
// FIXME - make static
struct vcpu		CPU_state;	// current CPU state

// FIXME - move output _file_ stuff to some other .c file!
// possible file formats
enum output_format {
	OUTPUT_FORMAT_UNSPECIFIED,	// default (uses "plain" actually)
	OUTPUT_FORMAT_APPLE,		// load address, length, code
	OUTPUT_FORMAT_CBM,		// load address, code (default for "!to" pseudo opcode)
	OUTPUT_FORMAT_PLAIN		// code only
};
// predefined stuff
// tree to hold output formats (FIXME - a tree for three items, really?)
static struct ronode	file_format_tree[]	= {
	PREDEF_START,
#define KNOWN_FORMATS	"'plain', 'cbm', 'apple'"	// shown in CLI error message for unknown formats
	PREDEFNODE("apple",	OUTPUT_FORMAT_APPLE),
	PREDEFNODE("cbm",	OUTPUT_FORMAT_CBM),
//	PREDEFNODE("o65",	OUTPUT_FORMAT_O65),
	PREDEF_END("plain",	OUTPUT_FORMAT_PLAIN),
	//    ^^^^ this marks the last element
};
// chosen file format
static enum output_format	output_format	= OUTPUT_FORMAT_UNSPECIFIED;
const char			outputfile_formats[]	= KNOWN_FORMATS;	// string to show if outputfile_set_format() returns nonzero


// report binary output
static void report_binary(char value)
{
	if (report->bin_used == 0)
		report->bin_address = out->write_idx;	// remember address at start of line
	if (report->bin_used < REPORT_BINBUFSIZE)
		report->bin_buf[report->bin_used++] = value;
}


// set up new out->segment.max value according to the given address.
// just find the next segment start and subtract 1.
static void find_segment_max(intval_t new_pc)
{
	struct segment	*test_segment	= out->segment.list_head.next;

	// search for smallest segment start address that
	// is larger than given address
	// use list head as sentinel
// FIXME - if +1 overflows intval_t, we have an infinite loop!
	out->segment.list_head.start = new_pc + 1;
	while (test_segment->start <= new_pc)
		test_segment = test_segment->next;
	if (test_segment == &out->segment.list_head)
		out->segment.max = out->bufsize - 1;
	else
		out->segment.max = test_segment->start - 1;	// last free address available
}


//
static void border_crossed(int current_offset)
{
	if (current_offset >= out->bufsize)
		Throw_serious_error("Produced too much code.");
	// TODO - get rid of FIRST_PASS condition, because user can suppress these warnings if they want
	if (FIRST_PASS) {
		// TODO: make warn/err an arg for a general "Throw" function
		if (config.segment_warning_is_error)
			Throw_error("Segment reached another one, overwriting it.");
		else
			Throw_warning("Segment reached another one, overwriting it.");
		find_segment_max(current_offset + 1);	// find new (next) limit
	}
}


// function ptr to write byte into output buffer (might point to real fn or error trigger)
void (*Output_byte)(intval_t byte);


// send low byte to output buffer, automatically increasing program counter
static void real_output(intval_t byte)
{
	// CAUTION - there are two copies of these checks!
	// TODO - add additional check for current segment's "limit" value
	// did we reach next segment?
	if (out->write_idx > out->segment.max)
		border_crossed(out->write_idx);
	// new minimum address?
	if (out->write_idx < out->lowest_written)
		out->lowest_written = out->write_idx;
	// new maximum address?
	if (out->write_idx > out->highest_written)
		out->highest_written = out->write_idx;
	// write byte and advance ptrs
	if (report->fd)
		report_binary(byte & 0xff);	// file for reporting, taking also CPU_2add
	out->buffer[out->write_idx++] = (byte & 0xff) ^ out->xor;
	++CPU_state.add_to_pc;
}


// throw error (pc undefined) and use fake pc from now on
static void no_output(intval_t byte)
{
	Throw_error(exception_pc_undefined);
	// now change fn ptr to not complain again.
	Output_byte = real_output;
	Output_byte(byte);	// try again
}


// skip over some bytes in output buffer without starting a new segment
// (used by "!skip", and also called by "!binary" if really calling
// Output_byte would be a waste of time)
void output_skip(int size)
{
	if (size < 1) {
		// FIXME - ok for zero, but why is there no error message
		// output for negative values?
		return;
	}

	// check whether ptr undefined
	if (Output_byte == no_output) {
		Output_byte(0);	// trigger error with a dummy byte
		--size;	// fix amount to cater for dummy byte
	}
	// CAUTION - there are two copies of these checks!
	// TODO - add additional check for current segment's "limit" value
	// did we reach next segment?
	if (out->write_idx + size - 1 > out->segment.max)
		border_crossed(out->write_idx + size - 1);
	// new minimum address?
	if (out->write_idx < out->lowest_written)
		out->lowest_written = out->write_idx;
	// new maximum address?
	if (out->write_idx + size - 1 > out->highest_written)
		out->highest_written = out->write_idx + size - 1;
	// advance ptrs
	out->write_idx += size;
	CPU_state.add_to_pc += size;
}


// fill output buffer with given byte value
static void fill_completely(char value)
{
	memset(out->buffer, value, out->bufsize);
}


// define default value for empty memory ("!initmem" pseudo opcode)
// returns zero if ok, nonzero if already set
int output_initmem(char content)
{
	// if MemInit flag is already set, complain
	if (out->initvalue_set) {
		Throw_warning("Memory already initialised.");
		return 1;	// failed
	}
	// set MemInit flag
	out->initvalue_set = TRUE;
	// init memory
	fill_completely(content);
	// enforce another pass
	if (pass.undefined_count == 0)
		pass.undefined_count = 1;
	//if (pass.needvalue_count == 0)	FIXME - use? instead or additionally?
	//	pass.needvalue_count = 1;
// FIXME - enforcing another pass is not needed if there hasn't been any
// output yet. But that's tricky to detect without too much overhead.
// The old solution was to add &&(out->lowest_written < out->highest_written+1) to "if" above
	return 0;	// ok
}


// try to set output format held in DynaBuf. Returns zero on success.
int outputfile_set_format(void)
{
	void	*node_body;

	// perform lookup
	if (!Tree_easy_scan(file_format_tree, &node_body, GlobalDynaBuf))
		return 1;

	output_format = (enum output_format) node_body;
	return 0;
}

// if file format was already chosen, returns zero.
// if file format isn't set, chooses CBM and returns 1.
int outputfile_prefer_cbm_format(void)
{
	if (output_format != OUTPUT_FORMAT_UNSPECIFIED)
		return 0;

	output_format = OUTPUT_FORMAT_CBM;
	return 1;
}

// select output file ("!to" pseudo opcode)
// returns zero on success, nonzero if already set
int outputfile_set_filename(void)
{
	// if output file already chosen, complain and exit
	if (output_filename) {
		Throw_warning("Output file already chosen.");
		return 1;	// failed
	}

	// get malloc'd copy of filename
	output_filename = DynaBuf_get_copy(GlobalDynaBuf);
	return 0;	// ok
}


// init output struct (done later)
void Output_init(signed long fill_value, boolean use_large_buf)
{
	out->bufsize = use_large_buf ? 0x1000000 : 0x10000;
	out->buffer = safe_malloc(out->bufsize);
	if (fill_value == MEMINIT_USE_DEFAULT) {
		fill_value = FILLVALUE_INITIAL;
		out->initvalue_set = FALSE;
	} else {
		out->initvalue_set = TRUE;
	}
	// init output buffer (fill memory with initial value)
	fill_completely(fill_value & 0xff);
	// init ring list of segments
	out->segment.list_head.next = &out->segment.list_head;
	out->segment.list_head.prev = &out->segment.list_head;
}


// dump used portion of output buffer into output file
void Output_save_file(FILE *fd)
{
	intval_t	start,
			amount;

	if (out->highest_written < out->lowest_written) {
		// nothing written
		start = 0;	// I could try to use some segment start, but what for?
		amount = 0;
	} else {
		start = out->lowest_written;
		amount = out->highest_written - start + 1;
	}
	if (config.process_verbosity)
		printf("Saving %ld (0x%lx) bytes (0x%lx - 0x%lx exclusive).\n",
			amount, amount, start, start + amount);
	// output file header according to file format
	switch (output_format) {
	case OUTPUT_FORMAT_APPLE:
		PLATFORM_SETFILETYPE_APPLE(output_filename);
		// output 16-bit load address in little-endian byte order
		putc(start & 255, fd);
		putc(start >> 8, fd);
		// output 16-bit length in little-endian byte order
		putc(amount & 255, fd);
		putc(amount >> 8, fd);
		break;
	case OUTPUT_FORMAT_UNSPECIFIED:
	case OUTPUT_FORMAT_PLAIN:
		PLATFORM_SETFILETYPE_PLAIN(output_filename);
		break;
	case OUTPUT_FORMAT_CBM:
		PLATFORM_SETFILETYPE_CBM(output_filename);
		// output 16-bit load address in little-endian byte order
		putc(start & 255, fd);
		putc(start >> 8, fd);
	}
	// dump output buffer to file
	fwrite(out->buffer + start, amount, 1, fd);
}


// link segment data into segment ring
static void link_segment(intval_t start, intval_t length)
{
	struct segment	*new_segment,
			*test_segment	= out->segment.list_head.next;

	// init new segment
	new_segment = safe_malloc(sizeof(*new_segment));
	new_segment->start = start;
	new_segment->length = length;
	// use ring head as sentinel
	out->segment.list_head.start = start;
	out->segment.list_head.length = length + 1;	// +1 to make sure sentinel exits loop
	// walk ring to find correct spot
	while ((test_segment->start < new_segment->start)
	|| ((test_segment->start == new_segment->start) && (test_segment->length < new_segment->length)))
		test_segment = test_segment->next;
	// link into ring
	new_segment->next = test_segment;
	new_segment->prev = test_segment->prev;
	new_segment->next->prev = new_segment;
	new_segment->prev->next = new_segment;
}


// check whether given PC is inside segment.
// only call in first pass, otherwise too many warnings might be thrown	(TODO - still?)
static void check_segment(intval_t new_pc)
{
	struct segment	*test_segment	= out->segment.list_head.next;

	// use list head as sentinel
	out->segment.list_head.start = new_pc + 1;	// +1 to make sure sentinel exits loop
	out->segment.list_head.length = 1;
	// search ring for matching entry
	while (test_segment->start <= new_pc) {
		if ((test_segment->start + test_segment->length) > new_pc) {
			// TODO - include overlap size in error message!
			if (config.segment_warning_is_error)
				Throw_error("Segment starts inside another one, overwriting it.");
			else
				Throw_warning("Segment starts inside another one, overwriting it.");
			return;
		}

		test_segment = test_segment->next;
	}
}


// clear segment list and disable output
void Output_passinit(void)
{
//	struct segment	*temp;

//FIXME - why clear ring list in every pass?
// Because later pass shouldn't complain about overwriting the same segment from earlier pass!
// Currently this does not happen because segment warnings are only generated in first pass. FIXME!
	// delete segment list (and free blocks)
//	while ((temp = segment_list)) {
//		segment_list = segment_list->next;
//		free(temp);
//	}

	// invalidate start and end (first byte actually written will fix them)
	out->lowest_written = out->bufsize - 1;
	out->highest_written = 0;
	// deactivate output - any byte written will trigger error:
	Output_byte = no_output;
	out->write_idx = 0;	// same as pc on pass init!
	out->segment.start = NO_SEGMENT_START;	// TODO - "no active segment" could be made a segment flag!
	out->segment.max = out->bufsize - 1;	// TODO - use end of bank?
	out->segment.flags = 0;
	out->xor = 0;

	//vcpu stuff:
	CPU_state.pc.ntype = NUMTYPE_UNDEFINED;	// not defined yet
	CPU_state.pc.flags = 0;
	// FIXME - number type is "undefined", but still the intval 0 below will
	// be used to calculate diff when pc is first set.
	CPU_state.pc.val.intval = 0;	// same as output's write_idx on pass init
	CPU_state.add_to_pc = 0;	// increase PC by this at end of statement

	// pseudopc stuff:
	pseudopc_current_context = NULL;
}


// show start and end of current segment
// called whenever a new segment begins, and at end of pass.
void Output_end_segment(void)
{
	intval_t	amount;

	// in later passes, ignore completely
	if (!FIRST_PASS)
		return;

	// if there is no segment, there is nothing to do
	if (out->segment.start == NO_SEGMENT_START)
		return;

	// ignore "invisible" segments
	if (out->segment.flags & SEGMENT_FLAG_INVISIBLE)
		return;

	// ignore empty segments
	amount = out->write_idx - out->segment.start;
	if (amount == 0)
		return;

	// link to segment list
	link_segment(out->segment.start, amount);
	// announce
	if (config.process_verbosity > 1)
		// TODO - change output to start, limit, size, name:
		// TODO - output hex numbers as %04x? What about limit 0x10000?
		printf("Segment size is %ld (0x%lx) bytes (0x%lx - 0x%lx exclusive).\n",
			amount, amount, out->segment.start, out->write_idx);
}


// change output pointer and enable output
// TODO - this only gets called from vcpu_set_pc so could be made static!
void Output_start_segment(intval_t address_change, bits segment_flags)
{
	// properly finalize previous segment (link to list, announce)
	Output_end_segment();

	// calculate start of new segment
	out->write_idx = (out->write_idx + address_change) & (out->bufsize - 1);
	out->segment.start = out->write_idx;
	out->segment.flags = segment_flags;
	// allow writing to output buffer
	Output_byte = real_output;
	// in first pass, check for other segments and maybe issue warning
	// TODO - remove FIRST_PASS condition
	if (FIRST_PASS) {
		if (!(segment_flags & SEGMENT_FLAG_OVERLAY))
			check_segment(out->segment.start);
		find_segment_max(out->segment.start);
	}
}


char output_get_xor(void)
{
	return out->xor;
}
void output_set_xor(char xor)
{
	out->xor = xor;
}


// set program counter to defined value (FIXME - allow for undefined!)
// if start address was given on command line, main loop will call this before each pass.
// in addition to that, it will be called on each "*= VALUE".
void vcpu_set_pc(intval_t new_pc, bits segment_flags)
{
	intval_t	pc_change;

	// support stupidly bad, old, ancient, deprecated, obsolete behaviour:
	if (pseudopc_current_context != NULL) {
		if (config.wanted_version < VER_SHORTER_SETPC_WARNING) {
			Throw_warning("Offset assembly still active at end of segment. Switched it off.");
			pseudopc_end_all();
		} else if (config.wanted_version < VER_DISABLED_OBSOLETE_STUFF) {
			Throw_warning("Offset assembly still active at end of segment.");
			pseudopc_end_all();	// warning no longer said it
			// would switch off, but still did. nevertheless, there
			// is something different to older versions: when the
			// closing '}' or !realpc is encountered, _really_ weird
			// stuff happens! i see no reason to try to mimic that.
		}
	}
	pc_change = new_pc - CPU_state.pc.val.intval;
	CPU_state.pc.val.intval = new_pc;	// FIXME - oversized values are accepted without error and will be wrapped at end of statement!
	CPU_state.pc.ntype = NUMTYPE_INT;	// FIXME - remove when allowing undefined!
	CPU_state.pc.addr_refs = 1;	// yes, PC counts as address
	// now tell output buffer to start a new segment
	Output_start_segment(pc_change, segment_flags);
}
/*
TODO - overhaul program counter and memory pointer stuff:
general stuff: PC and mem ptr might be marked as "undefined" via flags field.
However, their "value" fields are still updated, so we can calculate differences.

on pass init:
	if value given on command line, set PC and out ptr to that value
	otherwise, set both to zero and mark as "undefined"
when ALU asks for "*":
	return current PC (value and flags)
when encountering "!pseudopc VALUE { BLOCK }":
	parse new value (NEW: might be undefined!)
	remember difference between current and new value
	set PC to new value
	after BLOCK, use remembered difference to change PC back
when encountering "*= VALUE":
	parse new value (NEW: might be undefined!)
	calculate difference between current PC and new value
	set PC to new value
	tell outbuf to add difference to mem ptr (starting a new segment) - if new value is undefined, tell outbuf to disable output

Problem: always check for "undefined"; there are some problematic combinations.
I need a way to return the size of a generated code block even if PC undefined.
Maybe like this:
	*= new_address [, invisible] [, overlay] [, &size_symbol_ref {]
		...code...
	[} ; at end of block, size is written to size symbol given above!]
*/


// get program counter
void vcpu_read_pc(struct number *target)
{
	*target = CPU_state.pc;
}


// get size of current statement (until now) - needed for "!bin" verbose output
int vcpu_get_statement_size(void)
{
	return CPU_state.add_to_pc;
}


// adjust program counter (called at end of each statement)
void vcpu_end_statement(void)
{
	CPU_state.pc.val.intval = (CPU_state.pc.val.intval + CPU_state.add_to_pc) & (out->bufsize - 1);
	CPU_state.add_to_pc = 0;
}


// struct to describe a pseudopc context
struct pseudopc {
	struct pseudopc	*outer;	// next layer (to be able to "unpseudopc" labels by more than one level)
	intval_t	offset;	// inner minus outer pc
	enum numtype	ntype;	// type of outer pc (INT/UNDEFINED)
};
// start offset assembly
void pseudopc_start(struct number *new_pc)
{
	struct pseudopc	*new_context;

	new_context = safe_malloc(sizeof(*new_context));	// create new struct (this must never be freed, as it gets linked to labels!)
	new_context->outer = pseudopc_current_context;	// let it point to previous one
	pseudopc_current_context = new_context;	// make it the current one

	new_context->ntype = CPU_state.pc.ntype;
	new_context->offset = new_pc->val.intval - CPU_state.pc.val.intval;
	CPU_state.pc.val.intval = new_pc->val.intval;
	CPU_state.pc.ntype = NUMTYPE_INT;	// FIXME - remove when allowing undefined!
	//new: CPU_state.pc.flags = new_pc->flags & (NUMBER_IS_DEFINED | NUMBER_EVER_UNDEFINED);
}
// end offset assembly
void pseudopc_end(void)
{
	if (pseudopc_current_context == NULL) {
		// trying to end offset assembly though it isn't active:
		// in current versions this cannot happen and so must be a bug.
		// but in versions older than 0.94.8 this was possible using
		// !realpc, and offset assembly got automatically disabled when
		// encountering "*=".
		// so if wanted version is new enough, choke on bug!
		if (config.wanted_version >= VER_DISABLED_OBSOLETE_STUFF)
			Bug_found("ClosingUnopenedPseudopcBlock", 0);
	} else {
		CPU_state.pc.val.intval = (CPU_state.pc.val.intval - pseudopc_current_context->offset) & (out->bufsize - 1);	// pc might have wrapped around
		CPU_state.pc.ntype = pseudopc_current_context->ntype;
		pseudopc_current_context = pseudopc_current_context->outer;	// go back to outer block
	}
}
// this is only for old, deprecated, obsolete, stupid "realpc":
void pseudopc_end_all(void)
{
	while (pseudopc_current_context)
		pseudopc_end();
}
// un-pseudopc a label value by given number of levels
// returns nonzero on error (if level too high)
int pseudopc_unpseudo(struct number *target, struct pseudopc *context, unsigned int levels)
{
	while (levels--) {
		//if (target->ntype == NUMTYPE_UNDEFINED)
		//	return 0;	// ok (no sense in trying to unpseudo this, and it might be an unresolved forward ref anyway)

		if (context == NULL) {
			Throw_error("Un-pseudopc operator '&' has no !pseudopc context.");
			return 1;	// error
		}
		// FIXME - in future, check both target and context for NUMTYPE_UNDEFINED!
		target->val.intval = (target->val.intval - context->offset) & (out->bufsize - 1);	// FIXME - is masking really needed?	TODO
		context = context->outer;
	}
	return 0;	// ok
}
// return pointer to current "pseudopc" struct (may be NULL!)
// this gets called when parsing label definitions
struct pseudopc *pseudopc_get_context(void)
{
	return pseudopc_current_context;
}
