import util;
import output;

////////////////////////////////////////////////////////////////////////////////

class Pos
{
	public immutable int y, x;
	mixin DeriveCreate;
	mixin DeriveCompare;
	mixin DeriveShow;

@property:
	Pos wait()  { return this; }
	Pos up()    { return new Pos(y+1, x); }
	Pos down()  { return new Pos(y-1, x); }
	Pos left()  { return new Pos(y, x-1); }
	Pos right() { return new Pos(y, x+1); }
	alias wait  W,w;
	alias up    U,u;
	alias down  D,d;
	alias left  L,l;
	alias right R,r;
}

unittest
{
	assert( (new Pos(2,1)).U == new Pos(3,1) );
	assert( (new Pos(0,1)).D == new Pos(-1,1) );
	assert( (new Pos(2,1)).L == new Pos(2,0) );
	assert( (new Pos(2,1)).R == new Pos(2,2) );
	int[Pos] aa;
	aa[new Pos(1,2)] = 1;
	aa[new Pos(1,2)] = 2;
	aa[new Pos(2,1)] = 3;
	assert( aa.length==2 );
	assert( aa[new Pos(1,2)]==2 );
}

////////////////////////////////////////////////////////////////////////////////

class Water
{
	public immutable int base, pace;
	mixin DeriveCreate;
	mixin DeriveCompare;
	mixin DeriveShow;

	static load(string[string] params)
	{
		return new Water(
			params.get("Water",    "0").to!int(),
			params.get("Flooding", "0").to!int()
		);
	}

	int level(int number_of_update)
	{
		return pace ? base+(number_of_update/pace) : base;
	}

	int until_rise(int number_of_update)
	{
		return pace ? pace-number_of_update%pace : int.max;
	}
}

unittest
{
	Water w = new Water(1, 3);
	assert( 1 == w.level(0) );
	assert( 1 == w.level(1) );
	assert( 1 == w.level(2) );
	assert( 2 == w.level(3) );
	assert( 2 == w.level(4) );
	assert( 2 == w.level(5) );
	assert( 3 == w.level(6) );

	w = new Water(1, 0);
	assert( 1 == w.level(0) );
	assert( 1 == w.level(1) );
	assert( 1 == w.level(2) );
	assert( 1 == w.level(3) );
	assert( 1 == w.level(4) );
	assert( 1 == w.level(5) );
}

////////////////////////////////////////////////////////////////////////////////

class Map
{
	mixin DeriveShow;

	static Map load(string[] raw_data, string[string] params)
	{
		// TODO: choose optimal representation.
		return new Map(raw_data, params);
	}

	private {
		char[][] data;
		Pos robot;
		Pos lift;
		int waterproof;
	}

	this(string[] raw_data, string[string] params)
	{
		int width = 0;
		foreach(r; raw_data)
			width = max(width, r.length);
		foreach(r; raw_data) {
			this.data ~= r.dup;
			this.data[$-1].length = width;
			this.data[$-1][r.length..$] = ' ';
		}

		for(int y=1; y<=H; ++y)
		for(int x=1; x<=W; ++x) {
			if(this[y,x] == 'R')
				this.robot = new Pos(y,x);
			if(this[y,x] == 'L')
				this.lift = new Pos(y,x);
		}

		this.waterproof = params.get("Waterproof", "5").to!int();
	}

	const @property {
		int H() { return data.length; }
		int W() { return data[0].length; }
	}

	char opIndex(int y, int x)
	{
		// Adjust coordinate to the spec. bottom-left is (1,1).
		--y, --x;
		if(y<0||H<=y||x<0||W<=x)
			return '#';
		return data[H-1-y][x];
	}

	char opIndex(Pos p)
	{
		return this[p.y, p.x];
	}

	void opIndexAssign(char c, int y, int x)
	{
		// Adjust coordinate to the spec. bottom-left is (1,1).
		--y, --x;
		if(y<0||H<=y||x<0||W<=x)
			return;
		data[H-1-y][x] = c;
	}

	void opIndexAssign(char c, Pos p)
	{
		this[p.y, p.x] = c;
	}

	bool cleared()
	{
		for(int y=1; y<=H; ++y)
		for(int x=1; x<=W; ++x)
			if(this[y,x] == 'L' || this[y,x] == 'O')
				return false;
		return true;
	}
	
	Tuple!(int,bool) command(char c)
	{
		if(c=='R') return move( 0, +1);
		if(c=='L') return move( 0, -1);
		if(c=='U') return move(+1,  0);
		if(c=='D') return move(-1,  0);
		if(c=='W') return move( 0,  0);
		assert(false);
	}

	Tuple!(int, bool) move(int dy, int dx)
	{
		int y = robot.y;
		int x = robot.x;
		assert( this[robot] == 'R' );
		int lambda = 0;
		bool dead = false;
		if( '\\' == this[y+dy,x+dx] )
			lambda++;
		if( " \\.O".count(this[y+dy,x+dx])==1 ) {
			this[y,x]=' ';
			this[y+dy,x+dx]='R';
			robot = new Pos(y+dy,x+dx);
		} else if(dy==0 && '*'==this[y+dy,x+dx] && ' '==this[y+dy*2,x+dx*2]) {
			this[y,x]=' ';
			this[y+dy,x+dx]='R';
			this[y+dy*2,x+dx*2]='*';
			robot = new Pos(y+dy,x+dx);
		}
		if( update() )
			dead = true;
		return tuple(lambda,dead);
	}

	bool update()
	{
		bool dead = false;

		char[][] next;
		foreach(y,s; data)
			next ~= s.dup;

		ref char access(Pos p) { return next[H-p.y][p.x-1]; }

		bool lambda = false;
		for(int y=1; y<=H; ++y)
		for(int x=1; x<=W; ++x)
			lambda |= (this[y,x] == '\\');

		for(int y=1; y<=H; ++y)
		for(int x=1; x<=W; ++x) {
			Pos p = new Pos(y,x);
			if(this[p]=='*') {
				if(this[p.D]==' ') {
					access(p)  =' ';
					access(p.D)='*';
					if(robot == p.D.D)
						dead=true;
				}
				else if((this[p.D]=='*' || this[p.D]=='\\') && this[p.R]==' ' && this[p.R.D]==' ') {
					access(p)=' ';
					access(p.R.D)='*';
					if(robot == p.R.D.D)
						dead=true;
				}
				else if(this[p.D]=='*' && this[p.L]==' ' && this[p.L.D]==' ') {
					access(p)=' ';
					access(p.L.D)='*';
					if(robot == p.L.D.D)
						dead=true;
				}
			}
			else if(this[p]=='L') {
				if(!lambda)
					access(p) = 'O';
			}
		}
		data = next;
		return dead;
	}
}

////////////////////////////////////////////////////////////////////////////////

class Game
{
	mixin DeriveShow;

	static Game load(File input)
	{
		string[]       raw_data;
		string[string] params;

		// Raw map data; read until empty line.
		for(string line; !(line=input.readln().chomp()).empty; )
			raw_data ~= line;

		// Additional commands; read until EOF.
		for(string line; !(line=input.readln()).empty; ) {
			string[] ss = line.split();
			if( ss.length == 2 )
				params[ss[0]] = ss[1];
		}

		return load(raw_data, params);
	}

	static Game load(string[] raw_data, string[string] params)
	{
		return new Game(raw_data, params);
	}

	this(string[] raw_data, string[string] params)
	{
		this.map = Map.load(raw_data, params);
		this.water = Water.load(params);
		this.output = new NilOutput;
	}

	void set_output(Output o) { this.output = (o is null ? new NilOutput : o); }

	void command(char c)
	{
		if(dead || cleared)
			return;
		this.output.command(c);

		if(c == 'A')
		{
			exit_bonus = 1;
			return;
		}

		// TODO: clarify the event order
		Tuple!(int,bool) ld = map.command(c);
		if( map.cleared() ) {
			exit_bonus = 2;
		}
		else {
			lambda += ld[0];
			if( ld[1] ) {
				dead = true;
			}
		}
		if( map.robot.y <= water_level )
			++under_warter;
		else
			under_warter = 0;
		if( under_warter > map.waterproof )
			dead = true;
		turn += 1;
	}

	Map map;
	Water water;
	Output output;

	int  turn = 0;
	bool dead = false;
	int  lambda = 0;
	int  exit_bonus = 0;
	int  under_warter = 0;
	@property {
		long score() { return lambda*25L*(1+exit_bonus) - turn; }
		int water_level() { return water.level(turn); }
		int water_until_rise() { return water.until_rise(turn); }
		bool cleared() { return exit_bonus>0; }
		int hp() { return map.waterproof - under_warter; }
		long score_if_abort_now() { return lambda*25*(1+max(1,exit_bonus)) - turn; }
	}
}

unittest
{
	Game.load(["###","...","#RL"], ["xxx":"yyy"]);
}
