import os
import os.asset
import gg

const csize = 32

struct Pos {
	x int
	y int
}

enum Direction {
	up
	down
	left
	right
}

@[heap]
struct Game {
mut:
	warehouse [][]rune // the warehouse: `#`=wall, ` `=floor, `@`=storage, `b`=box, `p`=player
	ww        int
	wh        int
	player    Pos
	boxes     []Pos
	win       bool
	level     int
	titles    []string
	levels    []string
	moves     int
	pushes    int
	ctx       &gg.Context = unsafe { nil }
	//
	id_box            int
	id_box_on_storage int
	id_wall           int
	id_floor          int
	id_other          int
	id_player         int
	id_storage        int
}

fn (mut g Game) parse_level(lnumber int) ! {
	level := g.levels[lnumber] or { return error('invalid level number ${lnumber}') }
	if level == '' {
		return error('empty level ${lnumber}')
	}
	lines := level.split('\n').map(it.trim_space_right()).filter(it != '' && !it.starts_with('//'))
	mut warehouse := [][]rune{}
	mut warehouse_w := 0
	mut player := Pos{-1, -1}
	mut boxes := []Pos{}
	for y, line in lines#[1..] {
		mut row := []rune{}
		for x, c in line {
			match c {
				`#`, ` ` {
					row << c
				}
				`b`, `$` { // a normal box
					row << ` `
					boxes << Pos{x, y}
				}
				`p`, `@` { // a normal player
					row << ` `
					player = Pos{x, y}
				}
				`.` { // storage
					row << `@`
				}
				`*` { // box on storage
					row << `@`
					boxes << Pos{x, y}
				}
				`+` { // player on storage
					row << `@`
					player = Pos{x, y}
				}
				else {
					return error('unknown rune `${rune(c)}` at position ${y}x${x}, in level: ${c}')
				}
			}
		}
		warehouse << row
		warehouse_w = int_max(row.len, warehouse_w)
	}
	g.warehouse = warehouse
	g.ww = warehouse_w * csize
	g.wh = warehouse.len * csize
	g.titles = lines[0].split('@').filter(it != '')
	g.player = player
	g.boxes = boxes
	g.moves = 0
	g.pushes = 0
	g.win = false
}

// is_valid_pos returns whether the given `pos` is inside the game field, and not inside an inner wall
fn (g &Game) is_valid_pos(pos Pos) bool {
	return pos.y >= 0 && pos.y < g.warehouse.len && pos.x >= 0 && pos.x < g.warehouse[pos.y].len
		&& g.warehouse[pos.y][pos.x] != `#`
}

fn (mut g Game) move_player(dir Direction) {
	dx, dy := match dir {
		.up { 0, -1 }
		.down { 0, 1 }
		.left { -1, 0 }
		.right { 1, 0 }
	}
	target := Pos{g.player.x + dx, g.player.y + dy}
	if !g.is_valid_pos(target) {
		return
	}
	// Check for a box at the target position:
	mut target_box_index := -1
	for i, box in g.boxes {
		if box.x == target.x && box.y == target.y {
			target_box_index = i
			break
		}
	}
	if target_box_index >= 0 {
		// try pushing the box
		target_after := Pos{g.player.x + 2 * dx, g.player.y + 2 * dy}
		if !g.is_valid_pos(target_after) {
			return
		}
		// if there is another box at that place, prevent the push:
		for box in g.boxes {
			if box.x == target_after.x && box.y == target_after.y {
				return
			}
		}
		g.boxes[target_box_index] = target_after
		g.pushes++
	}
	g.player = target
	g.moves++
}

fn (g &Game) get_cell_iid(pos Pos) int {
	c := g.warehouse[pos.y][pos.x]
	if pos.x == g.player.x && pos.y == g.player.y {
		return g.id_player
	}
	for box in g.boxes {
		if box.x == pos.x && box.y == pos.y {
			if c == `@` {
				return g.id_box_on_storage
			}
			return g.id_box
		}
	}
	match c {
		` ` { return g.id_floor }
		`#` { return g.id_wall }
		`@` { return g.id_storage }
		else { return g.id_other }
	}
}

fn (mut g Game) next_level() {
	nlevel := (g.level + 1) % g.levels.len
	g.parse_level(nlevel) or { eprintln('level error: ${err}') }
	g.level = nlevel
}

fn (mut g Game) key_down(key gg.KeyCode, _ gg.Modifier, _ voidptr) {
	// controls:
	match key {
		.escape {
			g.ctx.quit()
		}
		.space {
			if g.win {
				g.next_level()
				return
			}
		}
		.r {
			g.parse_level(g.level) or {}
			return
		}
		.n {
			g.next_level()
		}
		else {}
	}
	if g.win {
		return
	}
	// player movement:
	dir := match key {
		.w, .up { Direction.up }
		.s, .down { Direction.down }
		.a, .left { Direction.left }
		.d, .right { Direction.right }
		else { return }
	}
	g.move_player(dir)
	g.win = g.boxes.all(g.warehouse[it.y][it.x] == `@`)
}

fn (g &Game) ctext(ws gg.Size, oy int, message string, size int, color gg.Color) {
	g.ctx.draw_text(ws.width / 2, ws.height + oy, message,
		color:          color
		size:           size
		align:          .center
		vertical_align: .middle
	)
}

fn (g &Game) draw_frame(_ voidptr) {
	g.ctx.begin()
	ws := gg.window_size()
	ox := (ws.width - g.ww) / 2
	oy := (ws.height - 40 - g.wh) / 2
	for y in 0 .. g.warehouse.len {
		for x in 0 .. g.warehouse[y].len {
			pos := Pos{x, y}
			iid := g.get_cell_iid(pos)
			if iid == g.id_player {
				// the player is transparent
				g.ctx.draw_image_by_id(ox + x * csize, oy + y * csize, 32, 32, g.id_floor)
			}
			g.ctx.draw_image_by_id(ox + x * csize, oy + y * csize, 32, 32, iid)
		}
	}
	g.ctx.draw_rect_filled(0, ws.height - 70, ws.width, 70, gg.black)
	if g.win {
		g.ctext(ws, -50, 'You win!!!', 60, gg.yellow)
		g.ctext(ws, -15, 'Press `space` to continue.', 20, gg.gray)
	} else {
		for idx, title in g.titles {
			g.ctext(ws, -65 + (idx * 20), title, 22, gg.white)
		}
		g.ctext(ws, -65 + (g.titles.len * 20), 'Boxes: ${g.boxes.len:04}', 16, gg.gray)
	}
	g.ctx.draw_rect_filled(0, 0, ws.width, 40, gg.black)
	g.ctx.draw_text(30, 0, 'Level: ${g.level + 1:02}', color: gg.green, size: 40)
	g.ctx.draw_text(ws.width - 225, 0, 'Moves: ${g.moves:04}', color: gg.green, size: 40)
	g.ctx.draw_text(ws.width / 2 - 110, 0, 'Pushes: ${g.pushes:04}', color: gg.green, size: 40)
	g.ctx.end()
}

fn (mut g Game) iid(name string) !int {
	return g.ctx.create_image(asset.get_path('/', name))!.id
}

fn main() {
	mut g := &Game{}
	if os.args.len > 1 {
		for fpath in os.args[1..] {
			content := os.read_file(fpath)!
			if content.starts_with(';') {
				// many levels in the same file:
				parts := content.trim_space().split('\n\n')
				for part in parts {
					g.levels << '${fpath}${part}'
				}
			} else {
				// a single level:
				g.levels << content
			}
		}
	} else {
		all_level_names := asset.read_text('/', '_all_levels.txt')!.split_into_lines()
		g.levels = all_level_names.map(asset.read_text('/', it)!)
	}
	g.parse_level(0)!
	g.ctx = gg.new_context(
		width:        800
		height:       640
		window_title: 'V Sokoban'
		user_data:    g
		frame_fn:     g.draw_frame
		keydown_fn:   g.key_down
	)
	g.id_box = g.iid('box.png')!
	g.id_box_on_storage = g.iid('box_on_storage.png')!
	g.id_wall = g.iid('wall.png')!
	g.id_floor = g.iid('floor.png')!
	g.id_other = g.iid('other.png')!
	g.id_player = g.iid('player.png')!
	g.id_storage = g.iid('storage.png')!
	g.ctx.run()
}
