/*
 * Copyright © 2014 Unity Technologies
 *
 * 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 (including the next
 * paragraph) 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.
 */

#include "ir_print_metal_visitor.h"
#include "ir_visitor.h"
#include "glsl_types.h"
#include "glsl_parser_extras.h"
#include "ir_unused_structs.h"
#include "loop_analysis.h"
#include "program/hash_table.h"
#include <math.h>


static void print_type(string_buffer& buffer, ir_instruction* ir, const glsl_type *t, bool arraySize);
static void print_type_post(string_buffer& buffer, const glsl_type *t, bool arraySize);


struct ga_entry_metal : public exec_node
{
	ga_entry_metal(ir_instruction* ir)
	{
		assert(ir);
		this->ir = ir;
	}
	ir_instruction* ir;
};
struct gconst_entry_metal : public exec_node
{
	gconst_entry_metal(ir_constant* ir, unsigned id)
	{
		assert(ir);
		this->ir = ir;
		this->id = id;
	}
	ir_constant* ir;
	unsigned id;
};


struct global_print_tracker_metal
{
	global_print_tracker_metal ()
	{
		mem_ctx = ralloc_context(0);
		var_counter = 0;
		var_hash = hash_table_ctor(0, hash_table_pointer_hash, hash_table_pointer_compare);
		const_counter = 0;
		const_hash = hash_table_ctor(0, hash_table_pointer_hash, hash_table_pointer_compare);
		main_function_done = false;
	}

	~global_print_tracker_metal()
	{
		hash_table_dtor (var_hash);
		hash_table_dtor (const_hash);
		ralloc_free(mem_ctx);
	}

	unsigned	var_counter;
	hash_table*	var_hash;
	exec_list	global_assignements;

	unsigned	const_counter;
	hash_table*	const_hash;
	exec_list	global_constants;

	void*		mem_ctx;
	bool		main_function_done;
};


struct metal_print_context
{
	metal_print_context(char* buffer)
	: str(buffer)
	, prefixStr(ralloc_strdup(buffer, ""))
	, inputStr(ralloc_strdup(buffer, ""))
	, outputStr(ralloc_strdup(buffer, ""))
	, inoutStr(ralloc_strdup(buffer, ""))
	, uniformStr(ralloc_strdup(buffer, ""))
	, paramsStr(ralloc_strdup(buffer, ""))
	, typedeclStr(ralloc_strdup(buffer, ""))
	, writingParams(false)
	, matrixCastsDone(false)
	, matrixConstructorsDone(false)
	, shadowSamplerDone(false)
	, textureCounter(0)
	, attributeCounter(0)
	, uniformLocationCounter(0)
	, colorCounter(0)
	{
	}

	string_buffer str;
	string_buffer prefixStr;
	string_buffer inputStr;
	string_buffer outputStr;
	string_buffer inoutStr;
	string_buffer uniformStr;
	string_buffer paramsStr;
	string_buffer typedeclStr;
	bool writingParams;
	bool matrixCastsDone;
	bool matrixConstructorsDone;
	bool shadowSamplerDone;
	int textureCounter;
	int attributeCounter;
	int uniformLocationCounter;
	int colorCounter;
};


class ir_print_metal_visitor : public ir_visitor {
public:
	ir_print_metal_visitor(metal_print_context& ctx_, string_buffer& buf, global_print_tracker_metal* globals_, PrintGlslMode mode_, const _mesa_glsl_parse_state* state_)
		: ctx(ctx_)
		, buffer(buf)
		, loopstate(NULL)
		, inside_loop_body(false)
		, inside_lhs(false)
		, skipped_this_ir(false)
		, previous_skipped(false)
		, mode_whole(mode_)
	{
		indentation = 0;
		expression_depth = 0;
		globals = globals_;
		mode = mode_;
		state = state_;
	}

	virtual ~ir_print_metal_visitor()
	{
	}


	void indent(void);
	void newline_indent();
	void end_statement_line();
	void newline_deindent();
	void print_var_name (ir_variable* v);

	virtual void visit(ir_variable *);
	virtual void visit(ir_function_signature *);
	virtual void visit(ir_function *);
	virtual void visit(ir_expression *);
	virtual void visit(ir_texture *);
	virtual void visit(ir_swizzle *);
	virtual void visit(ir_dereference_variable *);
	virtual void visit(ir_dereference_array *);
	virtual void visit(ir_dereference_record *);
	virtual void visit(ir_assignment *);
	virtual void visit(ir_constant *);
	virtual void visit(ir_call *);
	virtual void visit(ir_return *);
	virtual void visit(ir_discard *);
	virtual void visit(ir_if *);
	virtual void visit(ir_loop *);
	virtual void visit(ir_loop_jump *);
	virtual void visit(ir_precision_statement *);
	virtual void visit(ir_typedecl_statement *);
	virtual void visit(ir_emit_vertex *);
	virtual void visit(ir_end_primitive *);

	void emit_assignment_part (ir_dereference* lhs, ir_rvalue* rhs, unsigned write_mask, ir_rvalue* dstIndex);
	bool can_emit_canonical_for (loop_variable_state *ls);
	bool emit_canonical_for (ir_loop* ir);

	metal_print_context& ctx;
	int indentation;
	int expression_depth;
	string_buffer& buffer;
	global_print_tracker_metal* globals;
	const _mesa_glsl_parse_state* state;
	PrintGlslMode mode;
	const PrintGlslMode mode_whole;
	loop_state* loopstate;
	bool	inside_loop_body;
	bool	inside_lhs;
	bool	skipped_this_ir;
	bool	previous_skipped;
};


char*
_mesa_print_ir_metal(exec_list *instructions,
	    struct _mesa_glsl_parse_state *state,
		char* buffer, PrintGlslMode mode, int* outUniformsSize)
{
	metal_print_context ctx(buffer);

	// includes, prefix etc.
	ctx.prefixStr.asprintf_append ("#include <metal_stdlib>\n");
	ctx.prefixStr.asprintf_append ("#pragma clang diagnostic ignored \"-Wparentheses-equality\"\n");
	ctx.prefixStr.asprintf_append ("using namespace metal;\n");

	ctx.inputStr.asprintf_append("struct xlatMtlShaderInput {\n");
	ctx.outputStr.asprintf_append("struct xlatMtlShaderOutput {\n");
	ctx.uniformStr.asprintf_append("struct xlatMtlShaderUniform {\n");

	// remove unused struct declarations
	do_remove_unused_typedecls(instructions);

	global_print_tracker_metal gtracker;

	loop_state* ls = analyze_loop_variables(instructions);
	if (ls->loop_found)
		set_loop_controls(instructions, ls);

	foreach_in_list(ir_instruction, ir, instructions)
	{
		string_buffer* strOut = &ctx.str;
		ctx.writingParams = false;
		if (ir->ir_type == ir_type_variable)
		{
			ir_variable *var = static_cast<ir_variable*>(ir);

			// skip gl_ variables if they aren't used/assigned
			if (strstr(var->name, "gl_") == var->name)
			{
				if (NULL == strstr(var->name, "gl_FragData_") ) {
					if (!var->data.used && !var->data.assigned)
						continue;
				}
			}

			//
			if (var->data.mode == ir_var_uniform)
			{
				if (var->type->is_sampler())
				{
					strOut = &ctx.paramsStr;
					ctx.writingParams = true;
					strOut->asprintf_append ("\n  , ");
				}
				else
					strOut = &ctx.uniformStr;
			}
			if (var->data.mode == ir_var_system_value)
			{
				strOut = &ctx.paramsStr;
				ctx.writingParams = true;
				strOut->asprintf_append ("\n  , ");
			}
			if (var->data.mode == ir_var_shader_in)
				strOut = &ctx.inputStr;
			if (var->data.mode == ir_var_shader_out)
				strOut = &ctx.outputStr;
			if (var->data.mode == ir_var_shader_inout)
				strOut = &ctx.inoutStr;
		}
		
		if (ir->ir_type == ir_type_typedecl) {
			strOut = &ctx.typedeclStr;
		}

		ir_print_metal_visitor v (ctx, *strOut, &gtracker, mode, state);
		v.loopstate = ls;

		ir->accept(&v);
		if (ir->ir_type != ir_type_function && !v.skipped_this_ir)
		{
			if (!ctx.writingParams)
				strOut->asprintf_append (";\n");
		}
	}

	delete ls;

	// append inout variables to both input & output structs
	if (!ctx.inoutStr.empty())
	{
		ctx.inputStr.asprintf_append("%s", ctx.inoutStr.c_str());
		ctx.outputStr.asprintf_append("%s", ctx.inoutStr.c_str());
	}
	ctx.inputStr.asprintf_append("};\n");
	ctx.outputStr.asprintf_append("};\n");
	ctx.uniformStr.asprintf_append("};\n");

	// emit global array/struct constants
	
	ctx.prefixStr.asprintf_append("%s", ctx.typedeclStr.c_str());
	foreach_in_list_safe(gconst_entry_metal, node, &gtracker.global_constants)
	{
		ir_constant* c = node->ir;

		ir_print_metal_visitor v (ctx, ctx.prefixStr, &gtracker, mode, state);

		v.buffer.asprintf_append ("constant ");
		print_type(v.buffer, c, c->type, false);
		v.buffer.asprintf_append (" _xlat_mtl_const%i", (int)((gconst_entry_metal*)node)->id);
		print_type_post(v.buffer, c->type, false);
		v.buffer.asprintf_append (" = {");

		if (c->type->is_array())
		{
			for (unsigned i = 0; i < c->type->length; i++)
			{
				if (i != 0)
					v.buffer.asprintf_append (", ");
				c->get_array_element(i)->accept(&v);
			}
		}
		else
		{
			assert(c->type->is_record());
			bool first = true;
			foreach_in_list(ir_constant, inst, &c->components)
			{
				if (!first)
					v.buffer.asprintf_append (", ");
				first = false;
				inst->accept(&v);
			}
		}
		v.buffer.asprintf_append ("};\n");
	}


	ctx.prefixStr.asprintf_append("%s", ctx.inputStr.c_str());
	ctx.prefixStr.asprintf_append("%s", ctx.outputStr.c_str());
	ctx.prefixStr.asprintf_append("%s", ctx.uniformStr.c_str());
	ctx.prefixStr.asprintf_append("%s", ctx.str.c_str());

	*outUniformsSize = ctx.uniformLocationCounter;

	return ralloc_strdup(buffer, ctx.prefixStr.c_str());
}


void ir_print_metal_visitor::indent(void)
{
	if (previous_skipped)
		return;
	previous_skipped = false;
	for (int i = 0; i < indentation; i++)
		buffer.asprintf_append ("  ");
}

void ir_print_metal_visitor::end_statement_line()
{
	if (!skipped_this_ir)
		buffer.asprintf_append(";\n");
	previous_skipped = skipped_this_ir;
	skipped_this_ir = false;
}

void ir_print_metal_visitor::newline_indent()
{
	if (expression_depth % 4 == 0)
	{
		++indentation;
		buffer.asprintf_append ("\n");
		indent();
	}
}
void ir_print_metal_visitor::newline_deindent()
{
	if (expression_depth % 4 == 0)
	{
		--indentation;
		buffer.asprintf_append ("\n");
		indent();
	}
}


void ir_print_metal_visitor::print_var_name (ir_variable* v)
{
    uintptr_t id = (uintptr_t)hash_table_find (globals->var_hash, v);
	if (!id && v->data.mode == ir_var_temporary)
	{
        id = ++globals->var_counter;
        hash_table_insert (globals->var_hash, (void*)id, v);
	}
    if (id)
    {
        if (v->data.mode == ir_var_temporary)
            buffer.asprintf_append ("tmpvar_%d", (int)id);
        else
            buffer.asprintf_append ("%s_%d", v->name, (int)id);
    }
	else
	{
		buffer.asprintf_append ("%s", v->name);
	}
}



static void print_type_precision(string_buffer& buffer, const glsl_type *t, glsl_precision prec, bool arraySize)
{
	const bool halfPrec = (prec == glsl_precision_medium || prec == glsl_precision_low);

	const char* typeName = t->name;
	// scalars
	if (!strcmp(typeName, "float"))
		typeName = halfPrec ? "half" : "float";
	else if (!strcmp(typeName, "int"))
		typeName = halfPrec ? "short" : "int";
	// vectors
	else if (!strcmp(typeName, "vec2"))
		typeName = halfPrec ? "half2" : "float2";
	else if (!strcmp(typeName, "vec3"))
		typeName = halfPrec ? "half3" : "float3";
	else if (!strcmp(typeName, "vec4"))
		typeName = halfPrec ? "half4" : "float4";
	else if (!strcmp(typeName, "ivec2"))
		typeName = halfPrec ? "short2" : "int2";
	else if (!strcmp(typeName, "ivec3"))
		typeName = halfPrec ? "short3" : "int3";
	else if (!strcmp(typeName, "ivec4"))
		typeName = halfPrec ? "short4" : "int4";
	else if (!strcmp(typeName, "bvec2"))
		typeName = "bool2";
	else if (!strcmp(typeName, "bvec3"))
		typeName = "bool3";
	else if (!strcmp(typeName, "bvec4"))
		typeName = "bool4";
	// matrices
	else if (!strcmp(typeName, "mat2"))
		typeName = halfPrec ? "half2x2" : "float2x2";
	else if (!strcmp(typeName, "mat3"))
		typeName = halfPrec ? "half3x3" : "float3x3";
	else if (!strcmp(typeName, "mat4"))
		typeName = halfPrec ? "half4x4" : "float4x4";
	// non-square matrices
	else if (!strcmp(typeName, "mat2x2"))
		typeName = halfPrec ? "half2x2" : "float2x2";
	else if (!strcmp(typeName, "mat2x3"))
		typeName = halfPrec ? "half2x3" : "float2x3";
	else if (!strcmp(typeName, "mat2x4"))
		typeName = halfPrec ? "half2x4" : "float2x4";
	else if (!strcmp(typeName, "mat3x2"))
		typeName = halfPrec ? "half3x2" : "float3x2";
	else if (!strcmp(typeName, "mat3x3"))
		typeName = halfPrec ? "half3x3" : "float3x3";
	else if (!strcmp(typeName, "mat3x4"))
		typeName = halfPrec ? "half3x4" : "float3x4";
	else if (!strcmp(typeName, "mat4x2"))
		typeName = halfPrec ? "half4x2" : "float4x2";
	else if (!strcmp(typeName, "mat4x3"))
		typeName = halfPrec ? "half4x3" : "float4x3";
	else if (!strcmp(typeName, "mat4x4"))
		typeName = halfPrec ? "half4x4" : "float4x4";
	// samplers
	else if (!strcmp(typeName, "sampler2D"))
		typeName = halfPrec ? "texture2d<half>" : "texture2d<float>";
	else if (!strcmp(typeName, "samplerCube"))
		typeName = halfPrec ? "texturecube<half>" : "texturecube<float>";
	else if (!strcmp(typeName, "sampler3D"))
		typeName = halfPrec ? "texture3d<half>" : "texture3d<float>";
	else if (!strcmp(typeName, "sampler2DShadow"))
		typeName = "depth2d<float>"; // depth type must always be float
	else if (!strcmp(typeName, "samplerCubeShadow"))
		typeName = "depthcube<float>"; // depth type must always be float
	else if (!strcmp(typeName, "sampler2DArray"))
		typeName = halfPrec ? "texture2d_array<half>" : "texture2d_array<float>";

	if (t->base_type == GLSL_TYPE_ARRAY) {
		print_type_precision(buffer, t->fields.array, prec, true);
		if (arraySize)
			buffer.asprintf_append ("[%u]", t->length);
	} else if ((t->base_type == GLSL_TYPE_STRUCT)
			   && (strncmp("gl_", typeName, 3) != 0)) {
		buffer.asprintf_append ("%s", typeName);
	} else {
		buffer.asprintf_append ("%s", typeName);
	}
}


static void print_type(string_buffer& buffer, ir_instruction* ir, const glsl_type *t, bool arraySize)
{
	glsl_precision prec = precision_from_ir(ir);
	if (prec == glsl_precision_low)
		prec = glsl_precision_medium; // Metal does not have low precision; treat as medium
	print_type_precision(buffer, t, prec, arraySize);
}


static void print_type_post(string_buffer& buffer, const glsl_type *t, bool arraySize)
{
	if (t->base_type == GLSL_TYPE_ARRAY) {
		if (!arraySize)
			buffer.asprintf_append ("[%u]", t->length);
	}
}

static void get_metal_type_size(const glsl_type* type, glsl_precision prec, int& size, int& alignment)
{
	if (prec == glsl_precision_undefined)
		prec = glsl_precision_high;
	if (prec == glsl_precision_low)
		prec = glsl_precision_medium;
	const bool half = (prec == glsl_precision_medium);

	const int asize = type->is_array() ? type->length : 1;
	if (type->is_array())
		type = type->element_type();

	if (type->base_type == GLSL_TYPE_UINT || type->base_type == GLSL_TYPE_INT || type->base_type == GLSL_TYPE_FLOAT)
	{
		size = half ? 2 : 4;
	}
	else if (type->base_type == GLSL_TYPE_BOOL)
	{
		size = 1;
	}
	else
	{
		size = 0;
	}
	alignment = MAX2(size,1);

	int vsize = type->vector_elements;
	// float3 etc in Metal has both sizeof and alignment same as float4
	if (vsize == 3)
		vsize = 4;

	size *= vsize;
	alignment *= vsize;

	const int msize = type->matrix_columns;
	size *= msize;

	size *= asize;
}

void ir_print_metal_visitor::visit(ir_variable *ir)
{
	const char *const cent = (ir->data.centroid) ? "centroid " : "";
	const char *const inv = (ir->data.invariant) ? "invariant " : "";
	const char *const mode[ir_var_mode_count] = { "", "  ", "  ", "  ", "  ", "in ", "out ", "inout ", "", "", "" };

	const char *const interp[] = { "", "smooth ", "flat ", "noperspective " };

	// give an id to any variable defined in a function that is not an uniform
	if ((this->mode == kPrintGlslNone && ir->data.mode != ir_var_uniform))
	{
		uintptr_t id = (uintptr_t)hash_table_find (globals->var_hash, ir);
		if (id == 0)
		{
			id = ++globals->var_counter;
			hash_table_insert (globals->var_hash, (void*)id, ir);
		}
	}

	// auto/temp variables in global scope are postponed to main function
	if (this->mode != kPrintGlslNone && (ir->data.mode == ir_var_auto || ir->data.mode == ir_var_temporary))
	{
		assert (!this->globals->main_function_done);
		this->globals->global_assignements.push_tail (new(this->globals->mem_ctx) ga_entry_metal(ir));
		skipped_this_ir = true;
		return;
	}

	// if this is a loop induction variable, do not print it
	// (will be printed inside loop body)
	if (!inside_loop_body)
	{
		loop_variable_state* inductor_state = loopstate->get_for_inductor(ir);
		if (inductor_state && inductor_state->private_induction_variable_count == 1 &&
			can_emit_canonical_for(inductor_state))
		{
			skipped_this_ir = true;
			return;
		}
	}

	buffer.asprintf_append ("%s%s%s%s",
							cent, inv, interp[ir->data.interpolation], mode[ir->data.mode]);
	print_type(buffer, ir, ir->type, false);
	buffer.asprintf_append (" ");
	print_var_name (ir);
	print_type_post(buffer, ir->type, false);

	// special built-in variables
	if (!strcmp(ir->name, "gl_FragDepth"))
		buffer.asprintf_append (" [[depth(any)]]");
	else if (!strcmp(ir->name, "gl_FragCoord"))
		buffer.asprintf_append (" [[position]]");
	else if (!strcmp(ir->name, "gl_FrontFacing"))
		buffer.asprintf_append (" [[front_facing]]");
	else if (!strcmp(ir->name, "gl_PointCoord"))
		buffer.asprintf_append (" [[point_coord]]");
	else if (!strcmp(ir->name, "gl_PointSize"))
		buffer.asprintf_append (" [[point_size]]");
	else if (!strcmp(ir->name, "gl_Position"))
		buffer.asprintf_append (" [[position]]");
	else if (!strcmp(ir->name, "gl_VertexID"))
		buffer.asprintf_append (" [[vertex_id]]");
	else if (!strcmp(ir->name, "gl_InstanceID"))
		buffer.asprintf_append (" [[instance_id]]");

	// vertex shader input attribute?
	if (this->mode_whole == kPrintGlslVertex && ir->data.mode == ir_var_shader_in)
	{
		buffer.asprintf_append (" [[attribute(%i)]]", ctx.attributeCounter);
		ir->data.explicit_location = 1;
		ir->data.location = ctx.attributeCounter;
		++ctx.attributeCounter;
	}

	// fragment shader output?
	if (this->mode_whole == kPrintGlslFragment && (ir->data.mode == ir_var_shader_out || ir->data.mode == ir_var_shader_inout))
	{
		if (!ir->data.explicit_location)
		{
			ir->data.explicit_location = 1;
			ir->data.location = FRAG_RESULT_DATA0 + ctx.colorCounter;
			++ctx.colorCounter;
		}

		if (ir->data.explicit_location)
		{
			const int binding_base = (int)FRAG_RESULT_DATA0;
			const int location = ir->data.location - binding_base;
			if (location >= 0 && !ir->type->is_array())
				buffer.asprintf_append (" [[color(%d)]]", location);
		}
	}

	// uniform texture?
	if (ir->data.mode == ir_var_uniform && ctx.writingParams)
	{
		buffer.asprintf_append (" [[texture(%i)]]", ctx.textureCounter);
		buffer.asprintf_append (", sampler _mtlsmp_%s [[sampler(%i)]]", ir->name, ctx.textureCounter);
		ir->data.explicit_location = 1;
		ir->data.location = ctx.textureCounter;
		++ctx.textureCounter;
	}
	// regular uniform?
	if (ir->data.mode == ir_var_uniform && !ctx.writingParams)
	{
		int size, align;
		get_metal_type_size(ir->type, (glsl_precision)ir->data.precision, size, align);

		int loc = ctx.uniformLocationCounter;
		loc = (loc + align-1) & ~(align-1); // align it

		ir->data.explicit_location = 1;
		ir->data.location = loc;

		loc += size;
		ctx.uniformLocationCounter = loc;
	}

	if (ir->constant_value &&
		ir->data.mode != ir_var_shader_in &&
		ir->data.mode != ir_var_shader_out &&
		ir->data.mode != ir_var_shader_inout &&
		ir->data.mode != ir_var_function_in &&
		ir->data.mode != ir_var_function_out &&
		ir->data.mode != ir_var_function_inout)
	{
		buffer.asprintf_append (" = ");
		visit (ir->constant_value);
	}

	if ((ir->data.mode == ir_var_auto || ir->data.mode == ir_var_temporary) && (ir->type->matrix_columns == 1)) {
		switch (ir->type->base_type) {
			case GLSL_TYPE_INT:
			case GLSL_TYPE_FLOAT:
				buffer.asprintf_append (" = 0");
				break;
			case GLSL_TYPE_BOOL:
				buffer.asprintf_append (" = false");
				break;
			default:
				break;
		}
	}
}


void ir_print_metal_visitor::visit(ir_function_signature *ir)
{
	const bool isMain = (strcmp(ir->function()->name, "main") == 0);

	if (!isMain)
	{
		print_type(buffer, ir, ir->return_type, true);
		buffer.asprintf_append (" %s (", ir->function_name());

		if (!ir->parameters.is_empty())
		{
			buffer.asprintf_append ("\n");

			indentation++; previous_skipped = false;
			bool first = true;
			foreach_in_list(ir_variable, inst, &ir->parameters)
			{
				if (!first)
					buffer.asprintf_append (",\n");
				indent();
				inst->accept(this);
				first = false;
			}
			indentation--;

			buffer.asprintf_append ("\n");
			indent();
		}
	}
	else
	{
		if (this->mode_whole == kPrintGlslFragment)
			buffer.asprintf_append ("fragment ");
		if (this->mode_whole == kPrintGlslVertex)
			buffer.asprintf_append ("vertex ");
		buffer.asprintf_append ("xlatMtlShaderOutput xlatMtlMain (xlatMtlShaderInput _mtl_i [[stage_in]], constant xlatMtlShaderUniform& _mtl_u [[buffer(0)]]");
		if (!ctx.paramsStr.empty())
		{
			buffer.asprintf_append ("%s", ctx.paramsStr.c_str());
		}
	}

   if (ir->body.is_empty())
   {
	   buffer.asprintf_append (");\n");
	   return;
   }

   buffer.asprintf_append (")\n");

   indent();
   buffer.asprintf_append ("{\n");
   indentation++; previous_skipped = false;

	if (isMain)
	{
		// output struct
		indent(); buffer.asprintf_append ("xlatMtlShaderOutput _mtl_o;\n");

		// insert postponed global assigments and variable declarations
		assert (!globals->main_function_done);
		globals->main_function_done = true;
		foreach_in_list(ga_entry_metal, node, &globals->global_assignements)
		{
			ir_instruction* as = node->ir;
			as->accept(this);
			buffer.asprintf_append(";\n");
		}
	}

   foreach_in_list(ir_instruction, inst, &ir->body) {
      indent();
      inst->accept(this);
	   end_statement_line();
   }

	if (isMain)
	{
		// return stuff
		indent(); buffer.asprintf_append ("return _mtl_o;\n");
	}

   indentation--;
   indent();
   buffer.asprintf_append ("}\n");
}

void ir_print_metal_visitor::visit(ir_function *ir)
{
   bool found_non_builtin_proto = false;

   foreach_in_list(ir_function_signature, sig, &ir->signatures) {
	   if (!sig->is_builtin())
		   found_non_builtin_proto = true;
   }
   if (!found_non_builtin_proto)
      return;

   PrintGlslMode oldMode = this->mode;
   this->mode = kPrintGlslNone;

   foreach_in_list(ir_function_signature, sig, &ir->signatures) {
      indent();
      sig->accept(this);
      buffer.asprintf_append ("\n");
   }

   this->mode = oldMode;

   indent();
}


static const char *const operator_glsl_strs[] = {
	"~",
	"!",
	"-",
	"abs",
	"sign",
	"1.0/",
	"rsqrt",
	"sqrt",
	"normalize",
	"exp",
	"log",
	"exp2",
	"log2",
	"int",		// f2i
	"int",		// f2u
	"float",	// i2f
	"bool",		// f2b
	"float",	// b2f
	"bool",		// i2b
	"int",		// b2i
	"float",	// u2f
	"int",		// i2u
	"int",		// u2i
	"as_type_",	// bit i2f
	"as_type_",	// bit f2i
	"as_type_",	// bit u2f
	"as_type_",	// bit f2u
	"any",
	"trunc",
	"ceil",
	"floor",
	"fract",
	"rint",
	"sin",
	"cos",
	"fast::sin", // reduced
	"fast::cos", // reduced
	"dfdx",
	"dfdx", // coarse
	"dfdx", // fine
	"dfdy",
	"dfdy", // coarse
	"dfdy", // fine
	"packSnorm2x16",
	"packSnorm4x8",
	"packUnorm2x16",
	"packUnorm4x8",
	"packHalf2x16",
	"unpackSnorm2x16",
	"unpackSnorm4x8",
	"unpackUnorm2x16",
	"unpackUnorm4x8",
	"unpackHalf2x16",
	"unpackHalf2x16_splitX_TODO",
	"unpackHalf2x16_splitY_TODO",
	"bitfieldReverse",
	"bitCount",
	"findMSB",
	"findLSB",
	"saturate",
	"noise",
	"interpolateAtCentroid_TODO",
	"+",
	"-",
	"*",
	"*_imul_high_TODO",
	"/",
	"carry_TODO",
	"borrow_TODO",
	"fmod",
	"<",
	">",
	"<=",
	">=",
	"==",
	"!=",
	"==",
	"!=",
	"<<",
	">>",
	"&",
	"^",
	"|",
	"&&",
	"^^",
	"||",
	"dot",
	"min",
	"max",
	"pow",
	"packHalf2x16_split_TODO",
	"bfm_TODO",
	"uboloadTODO",
	"ldexp_TODO",
	"vectorExtract_TODO",
	"interpolateAtOffset_TODO",
	"interpolateAtSample_TODO",
	"fma",
	"clamp",
	"mix",
	"csel_TODO",
	"bfi_TODO",
	"bitfield_extract_TODO",
	"vector_insert_TODO",
	"bitfield_insert_TODO",
	"vectorTODO",
};


static bool is_binop_func_like(ir_expression_operation op, const glsl_type* type)
{
	if (op == ir_binop_mod ||
		(op >= ir_binop_dot && op <= ir_binop_pow))
		return true;
	return false;
}


static bool is_different_precision(glsl_precision a, glsl_precision b)
{
	// Tread "undefined" as high precision
	if (a == glsl_precision_undefined)
		a = glsl_precision_high;
	if (b == glsl_precision_undefined)
		b = glsl_precision_high;
	// Metal does not have "low" precision; treat as medium
	if (a == glsl_precision_low)
		a = glsl_precision_medium;
	if (b == glsl_precision_low)
		b = glsl_precision_medium;

	return a != b;
}


static void print_cast(string_buffer& buffer, glsl_precision prec, ir_rvalue* ir)
{
	buffer.asprintf_append ("(");
	print_type_precision(buffer, ir->type, prec, false);
	buffer.asprintf_append (")");
}


void ir_print_metal_visitor::visit(ir_expression *ir)
{
	++this->expression_depth;
	newline_indent();

	glsl_precision arg_prec = glsl_precision_undefined;
	if (ir->operands[0])
		arg_prec = higher_precision(arg_prec, ir->operands[0]->get_precision());
	if (ir->operands[1])
		arg_prec = higher_precision(arg_prec, ir->operands[1]->get_precision());
	if (ir->operands[2])
		arg_prec = higher_precision(arg_prec, ir->operands[2]->get_precision());
	glsl_precision res_prec = ir->get_precision();

	bool op0cast = ir->operands[0] && is_different_precision(arg_prec, ir->operands[0]->get_precision());
	bool op1cast = ir->operands[1] && is_different_precision(arg_prec, ir->operands[1]->get_precision());
	bool op2cast = ir->operands[2] && is_different_precision(arg_prec, ir->operands[2]->get_precision());
	const bool op0matrix = ir->operands[0] && ir->operands[0]->type->is_matrix();
	const bool op1matrix = ir->operands[1] && ir->operands[1]->type->is_matrix();
	bool op0castTo1 = false;
	bool op1castTo0 = false;

	// Metal does not support matrix precision casts, so when any of the arguments is a matrix,
	// take precision from it. This isn't fully robust now, but oh well.
	if (op0cast && op0matrix && !op1cast)
	{
		op0cast = false;
		arg_prec = ir->operands[0]->get_precision();
		op1cast = ir->operands[1] && is_different_precision(arg_prec, ir->operands[1]->get_precision());
	}
	if (op1cast && op1matrix && !op0cast)
	{
		op1cast = false;
		arg_prec = ir->operands[1]->get_precision();
		op0cast = ir->operands[0] && is_different_precision(arg_prec, ir->operands[0]->get_precision());
	}

	// Metal does not have matrix+scalar and matrix-scalar operations; we need to create matrices
	// out of the non-matrix argument.
	if (ir->operation == ir_binop_add || ir->operation == ir_binop_sub)
	{
		if (op0matrix && !op1matrix)
		{
			op1cast = true;
			op1castTo0 = true;
		}
		if (op1matrix && !op0matrix)
		{
			op0cast = true;
			op0castTo1 = true;
		}
		if (op1castTo0 || op0castTo1)
		{
			if (!ctx.matrixConstructorsDone)
			{
				ctx.prefixStr.asprintf_append(
											  "inline float4x4 _xlinit_float4x4(float v) { return float4x4(float4(v), float4(v), float4(v), float4(v)); }\n"
											  "inline float3x3 _xlinit_float3x3(float v) { return float3x3(float3(v), float3(v), float3(v)); }\n"
											  "inline float2x2 _xlinit_float2x2(float v) { return float2x2(float2(v), float2(v)); }\n"
											  "inline half4x4 _xlinit_half4x4(half v) { return half4x4(half4(v), half4(v), half4(v), half4(v)); }\n"
											  "inline half3x3 _xlinit_half3x3(half v) { return half3x3(half3(v), half3(v), half3(v)); }\n"
											  "inline half2x2 _xlinit_half2x2(half v) { return half2x2(half2(v), half2(v)); }\n"
											  );
				ctx.matrixConstructorsDone = true;
			}
		}
	}
	
	const bool rescast = is_different_precision(arg_prec, res_prec) && !ir->type->is_boolean();
	if (rescast)
	{
		buffer.asprintf_append ("(");
		print_cast (buffer, res_prec, ir);
	}

	if (ir->get_num_operands() == 1)
	{
		if (op0cast)
			print_cast (buffer, arg_prec, ir->operands[0]);
		if (ir->operation >= ir_unop_f2i && ir->operation <= ir_unop_u2i) {
			print_type(buffer, ir, ir->type, true);
			buffer.asprintf_append ("(");
		} else if (ir->operation >= ir_unop_bitcast_i2f && ir->operation <= ir_unop_bitcast_f2u) {
			buffer.asprintf_append("as_type<");
			print_type(buffer, ir, ir->type, true);
			buffer.asprintf_append(">(");
		} else if (ir->operation == ir_unop_rcp) {
			const bool halfCast = (arg_prec == glsl_precision_medium || arg_prec == glsl_precision_low);
			buffer.asprintf_append (halfCast ? "((half)1.0/(" : "(1.0/(");
		} else {
			switch(ir->operation) {
				case ir_unop_dFdy:
				case ir_unop_dFdy_coarse:
				case ir_unop_dFdy_fine:
					buffer.asprintf_append ("%s(-", operator_glsl_strs[ir->operation]);
					break;

				default:
					buffer.asprintf_append ("%s(", operator_glsl_strs[ir->operation]);
					break;
			}
		}
		if (ir->operands[0])
			ir->operands[0]->accept(this);
		buffer.asprintf_append (")");
		if (ir->operation == ir_unop_rcp) {
			buffer.asprintf_append (")");
		}
	}
	else if (ir->operation == ir_binop_vector_extract)
	{
		// a[b]

		if (ir->operands[0])
			ir->operands[0]->accept(this);
		buffer.asprintf_append ("[");
		if (ir->operands[1])
			ir->operands[1]->accept(this);
		buffer.asprintf_append ("]");
	}
	else if (is_binop_func_like(ir->operation, ir->type))
	{
		// binary operation that must be printed like a function, "foo(a,b)"
		if (ir->operation == ir_binop_mod)
		{
			buffer.asprintf_append ("(");
			print_type(buffer, ir, ir->type, true);
			buffer.asprintf_append ("(");
		}
		buffer.asprintf_append ("%s (", operator_glsl_strs[ir->operation]);

		if (ir->operands[0])
		{
			if (op0cast)
				print_cast (buffer, arg_prec, ir->operands[0]);
			ir->operands[0]->accept(this);
		}
		buffer.asprintf_append (", ");
		if (ir->operands[1])
		{
			if (op1cast)
				print_cast (buffer, arg_prec, ir->operands[1]);
			ir->operands[1]->accept(this);
		}
		buffer.asprintf_append (")");
		if (ir->operation == ir_binop_mod)
            buffer.asprintf_append ("))");
	}
	else if (ir->get_num_operands() == 2 && ir->operation == ir_binop_div && op0matrix && !op1matrix)
	{
		// "matrix/scalar" - Metal does not have it, so print multiply by inverse instead
		buffer.asprintf_append ("(");
		ir->operands[0]->accept(this);
		const bool halfCast = (arg_prec == glsl_precision_medium || arg_prec == glsl_precision_low);
		buffer.asprintf_append (halfCast ? " * (1.0h/half(" : " * (1.0/(");
		ir->operands[1]->accept(this);
		buffer.asprintf_append (")))");
	}
	else if (ir->get_num_operands() == 2)
	{
		// regular binary operator
		buffer.asprintf_append ("(");
		if (ir->operands[0])
		{
			if (op0castTo1)
			{
				buffer.asprintf_append ("_xlinit_");
				print_type_precision(buffer, ir->operands[1]->type, arg_prec, false);
				buffer.asprintf_append ("(");
			}
			else if (op0cast)
			{
				print_cast (buffer, arg_prec, ir->operands[0]);
				buffer.asprintf_append ("(");
			}
			ir->operands[0]->accept(this);
			if (op0castTo1 || op0cast)
			{
				buffer.asprintf_append (")");
			}
		}

		buffer.asprintf_append (" %s ", operator_glsl_strs[ir->operation]);

		if (ir->operands[1])
		{
			if (op1castTo0)
			{
				buffer.asprintf_append ("_xlinit_");
				print_type_precision(buffer, ir->operands[0]->type, arg_prec, false);
				buffer.asprintf_append ("(");
			}
			else if (op1cast)
			{
				print_cast (buffer, arg_prec, ir->operands[1]);
				buffer.asprintf_append ("(");
			}
			ir->operands[1]->accept(this);
			if (op1castTo0 || op1cast)
			{
				buffer.asprintf_append (")");
			}
		}
		buffer.asprintf_append (")");
	}
	else
	{
		// ternary op
		buffer.asprintf_append ("%s (", operator_glsl_strs[ir->operation]);
		if (ir->operands[0])
		{
			if (op0cast)
				print_cast (buffer, arg_prec, ir->operands[0]);
			ir->operands[0]->accept(this);
		}
		buffer.asprintf_append (", ");
		if (ir->operands[1])
		{
			if (op1cast)
				print_cast (buffer, arg_prec, ir->operands[1]);
			ir->operands[1]->accept(this);
		}
		buffer.asprintf_append (", ");
		if (ir->operands[2])
		{
			if (op2cast)
				print_cast (buffer, arg_prec, ir->operands[2]);
			ir->operands[2]->accept(this);
		}
		buffer.asprintf_append (")");
	}

	if (rescast)
	{
		buffer.asprintf_append (")");
	}


	newline_deindent();
	--this->expression_depth;
}

static int tex_sampler_dim_size[] = {
	1, 2, 3, 3, 2, 2, 2,
};


static void print_texture_uv (ir_print_metal_visitor* vis, ir_texture* ir, bool is_shadow, bool is_proj, bool is_array, int uv_dim, int sampler_uv_dim)
{
	if (!is_shadow)
	{
		if (!is_proj && !is_array)
		{
			// regular UV
			vis->buffer.asprintf_append (sampler_uv_dim == 3 ? "(float3)(" : "(float2)(");
			ir->coordinate->accept(vis);
			vis->buffer.asprintf_append (")");
		}
		else if (is_array)
		{
			// array sample
			vis->buffer.asprintf_append ("(float2)((");
			ir->coordinate->accept(vis);
			vis->buffer.asprintf_append (").xy), (uint)((");
			ir->coordinate->accept(vis);
			vis->buffer.asprintf_append (").z)");
		}
		else
		{
			// regular projected
			vis->buffer.asprintf_append (sampler_uv_dim == 3 ? "((float3)(" : "((float2)(");
			ir->coordinate->accept(vis);
			vis->buffer.asprintf_append (sampler_uv_dim == 3 ? ").xyz / (float)(" : ").xy / (float)(");
			ir->coordinate->accept(vis);
			vis->buffer.asprintf_append (uv_dim == 4 ? ").w)" : ").z)");
		}
	}
	else if (is_shadow)
	{
		// Note that on metal sample_compare works differently than shadow2DEXT on GLES:
		// it does not clamp neither the pixel value nor compare value to the [0.0, 1.0] range. To
		// preserve same behavior we're clamping the argument explicitly.
		if (!is_proj)
		{
			// regular shadow
			vis->buffer.asprintf_append (uv_dim == 4 ? "(float3)(" : "(float2)(");
			ir->coordinate->accept(vis);
			vis->buffer.asprintf_append (uv_dim == 4 ? ").xyz, (" : ").xy, saturate((float)(");
			ir->coordinate->accept(vis);
			vis->buffer.asprintf_append (uv_dim == 4 ? ").w" : ").z)");
		}
		else
		{
			// projected shadow
			vis->buffer.asprintf_append ("(float2)(");
			ir->coordinate->accept(vis);
			vis->buffer.asprintf_append (").xy / (float)(");
			ir->coordinate->accept(vis);
			vis->buffer.asprintf_append (").w, saturate((float)(");
			ir->coordinate->accept(vis);
			vis->buffer.asprintf_append (").z / (float)(");
			ir->coordinate->accept(vis);
			vis->buffer.asprintf_append (").w)");
		}
	}
}

void ir_print_metal_visitor::visit(ir_texture *ir)
{
	if (ir->op == ir_txs)
	{
		ir->sampler->accept(this);
		buffer.asprintf_append (".get_width(");
		ir->lod_info.lod->accept(this);
		buffer.asprintf_append ("), ");
		ir->sampler->accept(this);
		buffer.asprintf_append (".get_height(");
		ir->lod_info.lod->accept(this);
		buffer.asprintf_append (")");
		return;
	}
	glsl_sampler_dim sampler_dim = (glsl_sampler_dim)ir->sampler->type->sampler_dimensionality;
	const bool is_shadow = ir->sampler->type->sampler_shadow;
	const bool is_array = ir->sampler->type->sampler_array;
	const glsl_type* uv_type = ir->coordinate->type;
	const int uv_dim = uv_type->vector_elements;
	int sampler_uv_dim = tex_sampler_dim_size[sampler_dim];
	if (is_shadow)
		sampler_uv_dim += 1;
	const bool is_proj = (uv_dim > sampler_uv_dim) && !is_array;

    // Construct as the expected return type of shadow2D as sample_compare returns a scalar
    if (is_shadow)
    {
        buffer.asprintf_append("float4(");
    }

	// texture name & call to sample
	ir->sampler->accept(this);
	if (is_shadow)
	{
		// For shadow sampling, Metal right now needs a hardcoded sampler state :|
		if (!ctx.shadowSamplerDone)
		{
			ctx.prefixStr.asprintf_append("constexpr sampler _mtl_xl_shadow_sampler(address::clamp_to_edge, filter::linear, compare_func::less_equal);\n");
			ctx.shadowSamplerDone = true;
		}
		buffer.asprintf_append (".sample_compare(_mtl_xl_shadow_sampler");
	}
	else
	{
		buffer.asprintf_append (".sample(_mtlsmp_");
		ir->sampler->accept(this);
	}
	buffer.asprintf_append (", ");

	// texture coordinate
	print_texture_uv (this, ir, is_shadow, is_proj, is_array, uv_dim, sampler_uv_dim);

	// lod bias
	if (ir->op == ir_txb)
	{
		buffer.asprintf_append (", bias(");
		ir->lod_info.bias->accept(this);
		buffer.asprintf_append (")");
	}

	// lod
	if (ir->op == ir_txl)
	{
		buffer.asprintf_append (", level(");
		ir->lod_info.lod->accept(this);
		buffer.asprintf_append (")");
	}

	// grad
	if (ir->op == ir_txd)
	{
		if (sampler_dim == GLSL_SAMPLER_DIM_CUBE)
			buffer.asprintf_append (", gradientcube((float3)(");
		else
			buffer.asprintf_append (", gradient2d((float2)(");

		ir->lod_info.grad.dPdx->accept(this);

		if (sampler_dim == GLSL_SAMPLER_DIM_CUBE)
			buffer.asprintf_append ("), (float3)(");
		else
			buffer.asprintf_append ("), (float2)(");

		ir->lod_info.grad.dPdy->accept(this);
		buffer.asprintf_append ("))");
	}

	//@TODO: texelFetch
	//@TODO: projected
	//@TODO: shadowmaps
	//@TODO: pixel offsets

	buffer.asprintf_append (")");
	
    // Close float4 cast
    if (is_shadow)
    {
        buffer.asprintf_append(")");
    }
}


void ir_print_metal_visitor::visit(ir_swizzle *ir)
{
   const unsigned swiz[4] = {
      ir->mask.x,
      ir->mask.y,
      ir->mask.z,
      ir->mask.w,
   };

	if (ir->val->type == glsl_type::float_type || ir->val->type == glsl_type::int_type)
	{
		if (ir->mask.num_components != 1)
		{
			print_type(buffer, ir, ir->type, true);
			buffer.asprintf_append ("(");
		}
	}

	ir->val->accept(this);

	if (ir->val->type == glsl_type::float_type || ir->val->type == glsl_type::int_type)
	{
		if (ir->mask.num_components != 1)
		{
			buffer.asprintf_append (")");
		}
		return;
	}

   buffer.asprintf_append (".");
   for (unsigned i = 0; i < ir->mask.num_components; i++) {
		buffer.asprintf_append ("%c", "xyzw"[swiz[i]]);
   }
}

static void print_var_inout (string_buffer& buf, ir_variable* var, bool insideLHS)
{
	if (var->data.mode == ir_var_shader_in)
		buf.asprintf_append ("_mtl_i.");
	if (var->data.mode == ir_var_shader_out)
		buf.asprintf_append ("_mtl_o.");
	if (var->data.mode == ir_var_uniform && !var->type->is_sampler())
		buf.asprintf_append ("_mtl_u.");
	if (var->data.mode == ir_var_shader_inout)
		buf.asprintf_append (insideLHS ? "_mtl_o." : "_mtl_i.");
}

void ir_print_metal_visitor::visit(ir_dereference_variable *ir)
{
	ir_variable *var = ir->variable_referenced();
	print_var_inout(buffer, var, this->inside_lhs);
	print_var_name (var);
}


void ir_print_metal_visitor::visit(ir_dereference_array *ir)
{
   ir->array->accept(this);
   buffer.asprintf_append ("[");
   ir->array_index->accept(this);
   buffer.asprintf_append ("]");
}


void ir_print_metal_visitor::visit(ir_dereference_record *ir)
{
   ir->record->accept(this);
   buffer.asprintf_append (".%s", ir->field);
}




void ir_print_metal_visitor::emit_assignment_part (ir_dereference* lhs, ir_rvalue* rhs, unsigned write_mask, ir_rvalue* dstIndex)
{
	const bool prev_lhs_flag = this->inside_lhs;
	this->inside_lhs = true;

	lhs->accept(this);

	this->inside_lhs = prev_lhs_flag;

	const glsl_type* lhsType = lhs->type;
	if (dstIndex)
	{
		// if dst index is a constant, then emit a swizzle
		ir_constant* dstConst = dstIndex->as_constant();
		if (dstConst)
		{
			const char* comps = "xyzw";
			char comp = comps[dstConst->get_int_component(0)];
			buffer.asprintf_append (".%c", comp);
		}
		else
		{
			buffer.asprintf_append ("[");
			dstIndex->accept(this);
			buffer.asprintf_append ("]");
		}

		if (lhsType->matrix_columns <= 1 && lhsType->vector_elements > 1)
			lhsType = glsl_type::get_instance(lhsType->base_type, 1, 1);
	}

	char mask[5];
	unsigned j = 0;
	const glsl_type* rhsType = rhs->type;
	if (!dstIndex && lhsType->matrix_columns <= 1 && lhsType->vector_elements > 1 && write_mask != (1<<lhsType->vector_elements)-1)
	{
		for (unsigned i = 0; i < 4; i++) {
			if ((write_mask & (1 << i)) != 0) {
				mask[j] = "xyzw"[i];
				j++;
			}
		}
		lhsType = glsl_type::get_instance(lhsType->base_type, j, 1);
	}
	mask[j] = '\0';
	bool hasWriteMask = false;
	if (mask[0])
	{
		buffer.asprintf_append (".%s", mask);
		hasWriteMask = true;
	}

	buffer.asprintf_append (" = ");

	const bool typeMismatch = !dstIndex && (lhsType != rhsType);

	const bool precMismatch = is_different_precision (lhs->get_precision(), rhs->get_precision());
	const bool addSwizzle = hasWriteMask && typeMismatch;
	if (typeMismatch || precMismatch)
	{
		if (!addSwizzle)
		{
			if (lhsType->is_matrix())
			{
				// Metal does not have matrix precision casts right now, so emit workaround
				// functions that would do that.
				if (!ctx.matrixCastsDone)
				{
					ctx.prefixStr.asprintf_append(
												  "inline float4x4 _xlcast_float4x4(half4x4 v) { return float4x4(float4(v[0]), float4(v[1]), float4(v[2]), float4(v[3])); }\n"
												  "inline float3x3 _xlcast_float3x3(half3x3 v) { return float3x3(float3(v[0]), float3(v[1]), float3(v[2])); }\n"
												  "inline float2x2 _xlcast_float2x2(half2x2 v) { return float2x2(float2(v[0]), float2(v[1])); }\n"
												  "inline half4x4 _xlcast_half4x4(float4x4 v) { return half4x4(half4(v[0]), half4(v[1]), half4(v[2]), half4(v[3])); }\n"
												  "inline half3x3 _xlcast_half3x3(float3x3 v) { return half3x3(half3(v[0]), half3(v[1]), half3(v[2])); }\n"
												  "inline half2x2 _xlcast_half2x2(float2x2 v) { return half2x2(half2(v[0]), half2(v[1])); }\n"
												  );
					ctx.matrixCastsDone = true;
				}
				buffer.asprintf_append ("_xlcast_");
			}
			print_type(buffer, lhs, lhsType, true);
		}
		buffer.asprintf_append ("(");
	}

	rhs->accept(this);

	if (typeMismatch || precMismatch)
	{
		buffer.asprintf_append (")");
		if (addSwizzle)
			buffer.asprintf_append (".%s", mask);
	}
}


// Try to print (X = X + const) as (X += const), mostly to satisfy
// OpenGL ES 2.0 loop syntax restrictions.
static bool try_print_increment (ir_print_metal_visitor* vis, ir_assignment* ir)
{
	if (ir->condition)
		return false;

	// Needs to be + on rhs
	ir_expression* rhsOp = ir->rhs->as_expression();
	if (!rhsOp || rhsOp->operation != ir_binop_add)
		return false;

	// Needs to write to whole variable
	ir_variable* lhsVar = ir->whole_variable_written();
	if (lhsVar == NULL)
		return false;

	// Types must match
	if (ir->lhs->type != ir->rhs->type)
		return false;

	// Type must be scalar
	if (!ir->lhs->type->is_scalar())
		return false;

	// rhs0 must be variable deref, same one as lhs
	ir_dereference_variable* rhsDeref = rhsOp->operands[0]->as_dereference_variable();
	if (rhsDeref == NULL)
		return false;
	if (lhsVar != rhsDeref->var)
		return false;

	// rhs1 must be a constant
	ir_constant* rhsConst = rhsOp->operands[1]->as_constant();
	if (!rhsConst)
		return false;

	// print variable name
	const bool prev_lhs_flag = vis->inside_lhs;
	vis->inside_lhs = true;

	ir->lhs->accept (vis);

	vis->inside_lhs = prev_lhs_flag;


	// print ++ or +=const
	if (ir->lhs->type->base_type <= GLSL_TYPE_INT && rhsConst->is_one())
	{
		vis->buffer.asprintf_append ("++");
	}
	else
	{
		vis->buffer.asprintf_append(" += ");
		rhsConst->accept (vis);
	}

	return true;
}


void ir_print_metal_visitor::visit(ir_assignment *ir)
{
	// if this is a loop induction variable initial assignment, and we aren't inside loop body:
	// do not print it (will be printed when inside loop body)
	if (!inside_loop_body)
	{
		ir_variable* whole_var = ir->whole_variable_written();
		if (!ir->condition && whole_var)
		{
			loop_variable_state* inductor_state = loopstate->get_for_inductor(whole_var);
			if (inductor_state && inductor_state->private_induction_variable_count == 1 &&
				can_emit_canonical_for(inductor_state))
			{
				skipped_this_ir = true;
				return;
			}
		}
	}

	// assignments in global scope are postponed to main function
	if (this->mode != kPrintGlslNone)
	{
		assert (!this->globals->main_function_done);
		this->globals->global_assignements.push_tail (new(this->globals->mem_ctx) ga_entry_metal(ir));
		buffer.asprintf_append ("//"); // for the ; that will follow (ugly, I know)
		return;
	}

	// if RHS is ir_triop_vector_insert, then we have to do some special dance. If source expression is:
	//   dst = vector_insert (a, b, idx)
	// then emit it like:
	//   dst = a;
	//   dst.idx = b;
	ir_expression* rhsOp = ir->rhs->as_expression();
	if (rhsOp && rhsOp->operation == ir_triop_vector_insert)
	{
		// skip assignment if lhs and rhs would be the same
		bool skip_assign = false;
		ir_dereference_variable* lhsDeref = ir->lhs->as_dereference_variable();
		ir_dereference_variable* rhsDeref = rhsOp->operands[0]->as_dereference_variable();
		if (lhsDeref && rhsDeref)
		{
			if (lhsDeref->var == rhsDeref->var)
				skip_assign = true;
		}

		if (!skip_assign)
		{
			emit_assignment_part(ir->lhs, rhsOp->operands[0], ir->write_mask, NULL);
			buffer.asprintf_append ("; ");
		}
		emit_assignment_part(ir->lhs, rhsOp->operands[1], ir->write_mask, rhsOp->operands[2]);
		return;
	}

	if (try_print_increment (this, ir))
		return;

	if (ir->condition)
	{
	  ir->condition->accept(this);
	  buffer.asprintf_append (" ");
	}

	emit_assignment_part (ir->lhs, ir->rhs, ir->write_mask, NULL);
}

void ir_print_metal_visitor::visit(ir_constant *ir)
{
	const glsl_type* type = ir->type;

	// hoist array & struct constants into global scope
	if (type->is_array() || type->is_record())
	{
		size_t id = (size_t)hash_table_find(globals->const_hash, ir);
		if (id == 0)
		{
			id = ++globals->const_counter;
			hash_table_insert (globals->const_hash, (void*)id, ir);
			globals->global_constants.push_tail(new(globals->mem_ctx) gconst_entry_metal(ir,id));
		}
		buffer.asprintf_append("_xlat_mtl_const%i", (int)id);
		return;
	}

	if (type == glsl_type::float_type)
	{
		print_float (buffer, ir->value.f[0]);
		return;
	}
	else if (type == glsl_type::int_type)
	{
		buffer.asprintf_append ("%d", ir->value.i[0]);
		return;
	}
	else if (type == glsl_type::uint_type)
	{
		buffer.asprintf_append ("%u", ir->value.u[0]);
		return;
	}

   const glsl_type *const base_type = ir->type->get_base_type();

   print_type(buffer, ir, type, true);
   buffer.asprintf_append ("(");

	// should be dealt with above
	assert(!ir->type->is_array());
	assert(!ir->type->is_record());
	bool first = true;

	// Metal needs matrices to be constructed from vectors, not from a bunch of scalars.
	// So instead of printing mat2(1,2,3,4) like in glsl, we have to print float2x2(float2(1,2), float2(3,4))
	// here.
	const bool mtx = ir->type->is_matrix();
	const glsl_type* vec_type = NULL; // matrix column type
	if (mtx)
		vec_type = glsl_type::get_instance (ir->type->base_type, ir->type->vector_elements, 1);

	for (unsigned i = 0; i < ir->type->components(); i++)
	{
		if (!first)
		{
			if (mtx && (i % ir->type->matrix_columns == 0))
				buffer.asprintf_append (")");
			buffer.asprintf_append (", ");
		}
		first = false;

		if (mtx && (i % ir->type->matrix_columns == 0))
		{
			print_type(buffer, ir, vec_type, true);
			buffer.asprintf_append ("(");
		}

		switch (base_type->base_type) {
		case GLSL_TYPE_UINT:  buffer.asprintf_append ("%u", ir->value.u[i]); break;
		case GLSL_TYPE_INT:   buffer.asprintf_append ("%d", ir->value.i[i]); break;
		case GLSL_TYPE_FLOAT: print_float(buffer, ir->value.f[i]); break;
		case GLSL_TYPE_BOOL:  buffer.asprintf_append ("%d", ir->value.b[i]); break;
		default: assert(0);
		}
	}
	if (mtx)
		buffer.asprintf_append (")");
	buffer.asprintf_append (")");
}


void
ir_print_metal_visitor::visit(ir_call *ir)
{
	// calls in global scope are postponed to main function
	if (this->mode != kPrintGlslNone)
	{
		assert (!this->globals->main_function_done);
		this->globals->global_assignements.push_tail (new(this->globals->mem_ctx) ga_entry_metal(ir));
		buffer.asprintf_append ("//"); // for the ; that will follow (ugly, I know)
		return;
	}

	if (ir->return_deref)
	{
		visit(ir->return_deref);
		buffer.asprintf_append (" = ");
	}

   buffer.asprintf_append ("%s (", ir->callee_name());
   bool first = true;
   foreach_in_list(ir_instruction, inst, &ir->actual_parameters) {
	  if (!first)
		  buffer.asprintf_append (", ");
      inst->accept(this);
	  first = false;
   }
   buffer.asprintf_append (")");
}


void
ir_print_metal_visitor::visit(ir_return *ir)
{
   buffer.asprintf_append ("return");

   ir_rvalue *const value = ir->get_value();
   if (value) {
      buffer.asprintf_append (" ");
      value->accept(this);
   }
}


void
ir_print_metal_visitor::visit(ir_discard *ir)
{
   buffer.asprintf_append ("discard_fragment()");

   if (ir->condition != NULL) {
      buffer.asprintf_append (" TODO ");
      ir->condition->accept(this);
   }
}


void
ir_print_metal_visitor::visit(ir_if *ir)
{
   buffer.asprintf_append ("if (");
   ir->condition->accept(this);

   buffer.asprintf_append (") {\n");
	indentation++; previous_skipped = false;


   foreach_in_list(ir_instruction, inst, &ir->then_instructions) {
      indent();
      inst->accept(this);
	   end_statement_line();
   }

   indentation--;
   indent();
   buffer.asprintf_append ("}");

   if (!ir->else_instructions.is_empty())
   {
	   buffer.asprintf_append (" else {\n");
	   indentation++; previous_skipped = false;

	   foreach_in_list(ir_instruction, inst, &ir->else_instructions) {
		  indent();
		  inst->accept(this);
		   end_statement_line();
	   }
	   indentation--;
	   indent();
	   buffer.asprintf_append ("}");
   }
}


bool ir_print_metal_visitor::can_emit_canonical_for (loop_variable_state *ls)
{
	if (ls == NULL)
		return false;

	if (ls->induction_variables.is_empty())
		return false;

	if (ls->terminators.is_empty())
		return false;

	// only support for loops with one terminator condition
	int terminatorCount = ls->terminators.length();
	if (terminatorCount != 1)
		return false;

	return true;
}

bool ir_print_metal_visitor::emit_canonical_for (ir_loop* ir)
{
	loop_variable_state* const ls = this->loopstate->get(ir);

	if (!can_emit_canonical_for(ls))
		return false;

	hash_table* terminator_hash = hash_table_ctor(0, hash_table_pointer_hash, hash_table_pointer_compare);
	hash_table* induction_hash = hash_table_ctor(0, hash_table_pointer_hash, hash_table_pointer_compare);

	buffer.asprintf_append("for (");
	inside_loop_body = true;

	// emit loop induction variable declarations.
	// only for loops with single induction variable, to avoid cases of different types of them
	if (ls->private_induction_variable_count == 1)
	{
		foreach_in_list(loop_variable, indvar, &ls->induction_variables)
		{
			if (!this->loopstate->get_for_inductor(indvar->var))
				continue;

			ir_variable* var = indvar->var;
			print_type(buffer, var, var->type, false);
			buffer.asprintf_append (" ");
			print_var_inout(buffer, var, true);
			print_var_name (var);
			print_type_post(buffer, var->type, false);
			if (indvar->initial_value)
			{
				buffer.asprintf_append (" = ");
				indvar->initial_value->accept(this);
			}
		}
	}
	buffer.asprintf_append("; ");

	// emit loop terminating conditions
	foreach_in_list(loop_terminator, term, &ls->terminators)
	{
		hash_table_insert(terminator_hash, term, term->ir);

		// IR has conditions in the form of "if (x) break",
		// whereas for loop needs them negated, in the form
		// if "while (x) continue the loop".
		// See if we can print them using syntax that reads nice.
		bool handled = false;
		ir_expression* term_expr = term->ir->condition->as_expression();
		if (term_expr)
		{
			// Binary comparison conditions
			const char* termOp = NULL;
			switch (term_expr->operation)
			{
				case ir_binop_less: termOp = ">="; break;
				case ir_binop_greater: termOp = "<="; break;
				case ir_binop_lequal: termOp = ">"; break;
				case ir_binop_gequal: termOp = "<"; break;
				case ir_binop_equal: termOp = "!="; break;
				case ir_binop_nequal: termOp = "=="; break;
				default: break;
			}
			if (termOp != NULL)
			{
				term_expr->operands[0]->accept(this);
				buffer.asprintf_append(" %s ", termOp);
				term_expr->operands[1]->accept(this);
				handled = true;
			}

			// Unary logic not
			if (!handled && term_expr->operation == ir_unop_logic_not)
			{
				term_expr->operands[0]->accept(this);
				handled = true;
			}
		}

		// More complex condition, print as "!(x)"
		if (!handled)
		{
			buffer.asprintf_append("!(");
			term->ir->condition->accept(this);
			buffer.asprintf_append(")");
		}
	}
	buffer.asprintf_append("; ");

	// emit loop induction variable updates
	bool first = true;
	foreach_in_list(loop_variable, indvar, &ls->induction_variables)
	{
		hash_table_insert(induction_hash, indvar, indvar->first_assignment);
		if (!first)
			buffer.asprintf_append(", ");
		visit(indvar->first_assignment);
		first = false;
	}
	buffer.asprintf_append(") {\n");

	inside_loop_body = false;

	// emit loop body
	indentation++; previous_skipped = false;
	foreach_in_list(ir_instruction, inst, &ir->body_instructions) {
		// skip termination & induction statements,
		// they are part of "for" clause
		if (hash_table_find(terminator_hash, inst))
			continue;
		if (hash_table_find(induction_hash, inst))
			continue;

		indent();
		inst->accept(this);
		end_statement_line();
	}
	indentation--;

	indent();
	buffer.asprintf_append("}");

	hash_table_dtor (terminator_hash);
	hash_table_dtor (induction_hash);

	return true;
}


void
ir_print_metal_visitor::visit(ir_loop *ir)
{
	if (emit_canonical_for(ir))
		return;

	buffer.asprintf_append ("while (true) {\n");
	indentation++; previous_skipped = false;
	foreach_in_list(ir_instruction, inst, &ir->body_instructions) {
		indent();
		inst->accept(this);
		end_statement_line();
	}
	indentation--;
	indent();
	buffer.asprintf_append ("}");
}


void
ir_print_metal_visitor::visit(ir_loop_jump *ir)
{
   buffer.asprintf_append ("%s", ir->is_break() ? "break" : "continue");
}

void
ir_print_metal_visitor::visit(ir_precision_statement *ir)
{
}

void
ir_print_metal_visitor::visit(ir_typedecl_statement *ir)
{
	const glsl_type *const s = ir->type_decl;
	buffer.asprintf_append ("struct %s {\n", s->name);

	for (unsigned j = 0; j < s->length; j++) {
		buffer.asprintf_append ("  ");
		//if (state->es_shader)
		//	buffer.asprintf_append ("%s", get_precision_string(s->fields.structure[j].precision)); //@TODO
		print_type_precision(buffer, s->fields.structure[j].type, s->fields.structure[j].precision, false);
		buffer.asprintf_append (" %s", s->fields.structure[j].name);
		print_type_post(buffer, s->fields.structure[j].type, false);
		buffer.asprintf_append (";\n");
	}
	buffer.asprintf_append ("}");
}

void
ir_print_metal_visitor::visit(ir_emit_vertex *ir)
{
	buffer.asprintf_append ("emit-vertex-TODO");
}

void
ir_print_metal_visitor::visit(ir_end_primitive *ir)
{
	buffer.asprintf_append ("end-primitive-TODO");
}
