package odin_printer

import "core:odin/ast"
import "core:odin/tokenizer"
import "core:strings"
import "core:fmt"
import "core:mem"

Type_Enum :: enum {Line_Comment, Value_Decl, Switch_Stmt, Struct, Assign, Call, Enum, If, For, Proc_Lit}

Line_Type :: bit_set[Type_Enum]

/*
	Represents an unwrapped line
*/
Line :: struct {
	format_tokens: [dynamic]Format_Token,
	finalized:     bool,
	used:          bool,
	depth:         int,
	types:         Line_Type, //for performance, so you don't have to verify what types are in it by going through the tokens - might give problems when adding linebreaking
}

/*
	Represents a singular token in a unwrapped line
*/
Format_Token :: struct {
	kind:            tokenizer.Token_Kind,
	text:            string,
	type:            Type_Enum,
	spaces_before:   int,
	parameter_count: int,
}

Printer :: struct {
	string_builder:       strings.Builder,
	config:               Config,
	depth:                int, //the identation depth
	comments:             [dynamic]^ast.Comment_Group,
	latest_comment_index: int,
	allocator:            mem.Allocator,
	file:                 ^ast.File,
	source_position:      tokenizer.Pos,
	last_source_position: tokenizer.Pos,
	lines:                [dynamic]Line, //need to look into a better data structure, one that can handle inserting lines rather than appending
	skip_semicolon:       bool,
	current_line:         ^Line,
	current_line_index:   int,
	last_line_index:      int,
	last_token:           ^Format_Token,
	merge_next_token:     bool,
	space_next_token:     bool,
	debug:                bool,
}

Config :: struct {
	spaces:               int,  //Spaces per indentation
	newline_limit:        int,  //The limit of newlines between statements and declarations.
	tabs:                 bool, //Enable or disable tabs
	convert_do:           bool, //Convert all do statements to brace blocks
	semicolons:           bool, //Enable semicolons
	split_multiple_stmts: bool,
	align_switch:         bool,
	brace_style:          Brace_Style,
	align_assignments:    bool,
	align_structs:        bool,
	align_style:          Alignment_Style,
	align_enums:          bool,
	align_length_break:   int,
	indent_cases:         bool,
	newline_style:        Newline_Style,
}

Brace_Style :: enum {
	_1TBS,
	Allman,
	Stroustrup,
	K_And_R,
}

Block_Type :: enum {
	None,
	If_Stmt,
	Proc,
	Generic,
	Comp_Lit,
	Switch_Stmt,
}

Alignment_Style :: enum {
	Align_On_Type_And_Equals,
	Align_On_Colon_And_Equals,
}

Newline_Style :: enum {
	CRLF,
	LF,
}

default_style := Config {
	spaces               = 4,
	newline_limit        = 2,
	convert_do           = false,
	semicolons           = false,
	tabs                 = true,
	brace_style          = ._1TBS,
	split_multiple_stmts = true,
	align_assignments    = true,
	align_style          = .Align_On_Type_And_Equals,
	indent_cases         = false,
	align_switch         = true,
	align_structs        = true,
	align_enums          = true,
	newline_style        = .CRLF,
	align_length_break   = 9,
}

make_printer :: proc(config: Config, allocator := context.allocator) -> Printer {
	return {
		config = config,
		allocator = allocator,
		debug = false,
	}
}

print :: proc(p: ^Printer, file: ^ast.File) -> string {
	p.comments = file.comments

	if len(file.decls) > 0 {
		p.lines = make([dynamic]Line, 0, (file.decls[len(file.decls) - 1].end.line - file.decls[0].pos.line) * 2, context.temp_allocator)
	}

	set_source_position(p, file.pkg_token.pos)

	p.last_source_position.line = 1

	set_line(p, 0)

	push_generic_token(p, .Package, 0)
	push_ident_token(p, file.pkg_name, 1)

	for decl in file.decls {
		visit_decl(p, cast(^ast.Decl)decl)
	}

	if len(p.comments) > 0 {
		infinite := p.comments[len(p.comments) - 1].end
		infinite.offset = 9999999
		push_comments(p, infinite)
	}

	fix_lines(p)

	builder := strings.make_builder(0, mem.megabytes(5), p.allocator)

	last_line := 0

	newline: string

	if p.config.newline_style == .LF {
		newline = "\n"
	} else {
		newline = "\r\n"
	}

	for line, line_index in p.lines {
		diff_line := line_index - last_line

		for i := 0; i < diff_line; i += 1 {
			strings.write_string(&builder, newline)
		}

		if p.config.tabs {
			for i := 0; i < line.depth; i += 1 {
				strings.write_byte(&builder, '\t')
			}
		} else {
			for i := 0; i < line.depth * p.config.spaces; i += 1 {
				strings.write_byte(&builder, ' ')
			}
		}

		if p.debug {
			strings.write_string(&builder, fmt.tprintf("line %v: ", line_index))
		}

		for format_token in line.format_tokens {

			for i := 0; i < format_token.spaces_before; i += 1 {
				strings.write_byte(&builder, ' ')
			}

			strings.write_string(&builder, format_token.text)
		}

		last_line = line_index
	}

	strings.write_string(&builder, newline)

	return strings.to_string(builder)
}

fix_lines :: proc(p: ^Printer) {
	align_var_decls(p)
	format_generic(p)
	align_comments(p) //align them last since they rely on the other alignments
}

format_value_decl :: proc(p: ^Printer, index: int) {

	eq_found := false
	eq_token: Format_Token
	eq_line:  int
	largest := 0

	found_eq: for line, line_index in p.lines[index:] {
		for format_token in line.format_tokens {

			largest += len(format_token.text) + format_token.spaces_before

			if format_token.kind == .Eq {
				eq_token = format_token
				eq_line = line_index + index
				eq_found = true
				break found_eq
			}
		}
	}

	if !eq_found {
		return
	}

	align_next := false

	//check to see if there is a binary operator in the last token(this is guaranteed by the ast visit), otherwise it's not multilined
	for line in p.lines[eq_line:] {

		if len(line.format_tokens) == 0 {
			break
		}

		if align_next {
			line.format_tokens[0].spaces_before = largest + 1
			align_next = false
		}

		kind := find_last_token(line.format_tokens).kind

		if tokenizer.Token_Kind.B_Operator_Begin < kind && kind <= tokenizer.Token_Kind.Cmp_Or {
			align_next = true
		}

		if !align_next {
			break
		}
	}
}

find_last_token :: proc(format_tokens: [dynamic]Format_Token) -> Format_Token {

	for i := len(format_tokens) - 1; i >= 0; i -= 1 {

		if format_tokens[i].kind != .Comment {
			return format_tokens[i]
		}
	}

	panic("not possible")
}

format_assignment :: proc(p: ^Printer, index: int) {
}

format_call :: proc(p: ^Printer, line_index: int, format_index: int) {

	paren_found := false
	paren_token:       Format_Token
	paren_line:        int
	paren_token_index: int
	largest := 0

	found_paren: for line, i in p.lines[line_index:] {
		for format_token, j in line.format_tokens {

			largest += len(format_token.text) + format_token.spaces_before

			if i == 0 && j < format_index {
				continue
			}

			if format_token.kind == .Open_Paren && format_token.type == .Call {
				paren_token = format_token
				paren_line = line_index + i
				paren_found = true
				paren_token_index = j
				break found_paren
			}
		}
	}

	if !paren_found {
		panic("Should not be possible")
	}

	paren_count := 1
	done        := false

	for line in p.lines[paren_line:] {

		if len(line.format_tokens) == 0 {
			continue
		}

		for format_token, i in line.format_tokens {

			if format_token.kind == .Comment {
				continue
			}

			if line_index == 0 && i <= paren_token_index {
				continue
			}

			if format_token.kind == .Open_Paren {
				paren_count += 1
			} else if format_token.kind == .Close_Paren {
				paren_count -= 1
			}

			if paren_count == 0 {
				done = true
			}
		}

		if line_index != 0 {
			line.format_tokens[0].spaces_before = largest
		}

		if done {
			return
		}
	}
}

format_keyword_to_brace :: proc(p: ^Printer, line_index: int, format_index: int, keyword: tokenizer.Token_Kind) {

	keyword_found := false
	keyword_token: Format_Token
	keyword_line:  int

	largest := 0
	brace_count := 0
	done        := false

	found_keyword: for line, i in p.lines[line_index:] {
		for format_token in line.format_tokens {

			largest += len(format_token.text) + format_token.spaces_before

			if format_token.kind == keyword {
				keyword_token = format_token
				keyword_line = line_index + i
				keyword_found = true
				break found_keyword
			}
		}
	}

	if !keyword_found {
		panic("Should not be possible")
	}

	for line, line_idx in p.lines[keyword_line:] {

		if len(line.format_tokens) == 0 {
			continue
		}

		for format_token, i in line.format_tokens {

			if format_token.kind == .Comment {
				break
			} else if format_token.kind == .Undef {
				return
			}

			if line_idx == 0 && i <= format_index {
				continue
			}

			if format_token.kind == .Open_Brace {
				brace_count += 1
			} else if format_token.kind == .Close_Brace {
				brace_count -= 1
			}

			if brace_count == 1 {
				done = true
			}
		}

		if line_idx != 0 {
			line.format_tokens[0].spaces_before = largest + 1
		}

		if done {
			return
		}
	}
}

format_generic :: proc(p: ^Printer) {
	next_struct_line := 0

	for line, line_index in p.lines {

		if len(line.format_tokens) <= 0 {
			continue
		}

		for format_token, token_index in line.format_tokens {
			#partial switch format_token.kind {
			case .For, .If, .When, .Switch:
				format_keyword_to_brace(p, line_index, token_index, format_token.kind)
			case .Proc:
				if format_token.type == .Proc_Lit {
					format_keyword_to_brace(p, line_index, token_index, format_token.kind)
				}
			case:
				if format_token.type == .Call {
					format_call(p, line_index, token_index)
				}
			}
		}

		if .Switch_Stmt in line.types && p.config.align_switch {
			align_switch_stmt(p, line_index)
		}

		if .Enum in line.types && p.config.align_enums {
			align_enum(p, line_index)
		}

		if .Struct in line.types && p.config.align_structs && next_struct_line <= 0 {
			next_struct_line = align_struct(p, line_index)
		}

		if .Value_Decl in line.types {
			format_value_decl(p, line_index)
		}

		if .Assign in line.types {
			format_assignment(p, line_index)
		}

		next_struct_line -= 1
	}
}

align_var_decls :: proc(p: ^Printer) {

	current_line:        int
	current_typed:       bool
	current_not_mutable: bool

	largest_lhs := 0
	largest_rhs := 0

	TokenAndLength :: struct {
		format_token: ^Format_Token,
		length:       int,
	}

	colon_tokens := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator)
	type_tokens  := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator)
	equal_tokens := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator)

	for line, line_index in p.lines {

		//It is only possible to align value decls that are one one line, otherwise just ignore them
		if .Value_Decl not_in line.types {
			continue
		}

		typed         := true
		not_mutable   := false
		continue_flag := false

		for i := 0; i < len(line.format_tokens); i += 1 {
			if line.format_tokens[i].kind == .Colon && line.format_tokens[min(i + 1, len(line.format_tokens) - 1)].kind == .Eq {
				typed = false
			}

			if line.format_tokens[i].kind == .Colon && line.format_tokens[min(i + 1, len(line.format_tokens) - 1)].kind == .Colon {
				not_mutable = true
			}

			if line.format_tokens[i].kind == .Union ||
			   line.format_tokens[i].kind == .Enum ||
			   line.format_tokens[i].kind == .Struct ||
			   line.format_tokens[i].kind == .For ||
			   line.format_tokens[i].kind == .If ||
			   line.format_tokens[i].kind == .Comment {
				continue_flag = true
			}

			//enforced undef is always on the last line, if it exists
			if line.format_tokens[i].kind == .Proc && line.format_tokens[len(line.format_tokens)-1].kind != .Undef {
				continue_flag = true
			}

		}

		if continue_flag {
			continue
		}

		if line_index != current_line + 1 || typed != current_typed || not_mutable != current_not_mutable {

			if p.config.align_style == .Align_On_Colon_And_Equals || !current_typed || current_not_mutable {
				for colon_token in colon_tokens {
					colon_token.format_token.spaces_before = largest_lhs - colon_token.length + 1
				}
			} else if p.config.align_style == .Align_On_Type_And_Equals {
				for type_token in type_tokens {
					type_token.format_token.spaces_before = largest_lhs - type_token.length + 1
				}
			}

			if current_typed {
				for equal_token in equal_tokens {
					equal_token.format_token.spaces_before = largest_rhs - equal_token.length + 1
				}
			} else {
				for equal_token in equal_tokens {
					equal_token.format_token.spaces_before = 0
				}
			}

			clear(&colon_tokens)
			clear(&type_tokens)
			clear(&equal_tokens)

			largest_rhs = 0
			largest_lhs = 0
			current_typed = typed
			current_not_mutable = not_mutable
		}

		current_line = line_index

		current_token_index := 0
		lhs_length          := 0
		rhs_length          := 0

		//calcuate the length of lhs of a value decl i.e. `a, b:`
		for; current_token_index < len(line.format_tokens); current_token_index += 1 {

			lhs_length += len(line.format_tokens[current_token_index].text) + line.format_tokens[current_token_index].spaces_before

			if line.format_tokens[current_token_index].kind == .Colon {
				append(&colon_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index], length = lhs_length})

				if len(line.format_tokens) > current_token_index && line.format_tokens[current_token_index + 1].kind != .Eq {
					append(&type_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index + 1], length = lhs_length})
				}

				current_token_index += 1
				largest_lhs = max(largest_lhs, lhs_length)
				break
			}
		}

		//calcuate the length of the rhs i.e. `[dynamic]int = 123123`
		for; current_token_index < len(line.format_tokens); current_token_index += 1 {

			rhs_length += len(line.format_tokens[current_token_index].text) + line.format_tokens[current_token_index].spaces_before

			if line.format_tokens[current_token_index].kind == .Eq {
				append(&equal_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index], length = rhs_length})
				largest_rhs = max(largest_rhs, rhs_length)
				break
			}
		}

	}

	//repeating myself, move to sub procedure
	if p.config.align_style == .Align_On_Colon_And_Equals || !current_typed || current_not_mutable {
		for colon_token in colon_tokens {
			colon_token.format_token.spaces_before = largest_lhs - colon_token.length + 1
		}
	} else if p.config.align_style == .Align_On_Type_And_Equals {
		for type_token in type_tokens {
			type_token.format_token.spaces_before = largest_lhs - type_token.length + 1
		}
	}

	if current_typed {
		for equal_token in equal_tokens {
			equal_token.format_token.spaces_before = largest_rhs - equal_token.length + 1
		}
	} else {
		for equal_token in equal_tokens {
			equal_token.format_token.spaces_before = 0
		}
	}
}

align_switch_stmt :: proc(p: ^Printer, index: int) {
	switch_found := false
	brace_token: Format_Token
	brace_line:  int

	found_switch_brace: for line, line_index in p.lines[index:] {
		for format_token in line.format_tokens {
			if format_token.kind == .Open_Brace && switch_found {
				brace_token = format_token
				brace_line = line_index + index
				break found_switch_brace
			} else if format_token.kind == .Open_Brace {
				break
			} else if format_token.kind == .Switch {
				switch_found = true
			}
		}
	}

	if !switch_found {
		return
	}

	largest    := 0
	case_count := 0

	TokenAndLength :: struct {
		format_token: ^Format_Token,
		length:       int,
	}

	format_tokens := make([dynamic]TokenAndLength, 0, brace_token.parameter_count, context.temp_allocator)

	//find all the switch cases that are one lined
	for line, line_index in p.lines[brace_line + 1:] {

		case_found  := false
		colon_found := false
		length      := 0

		for format_token, i in line.format_tokens {

			if format_token.kind == .Comment {
				break
			}

			//this will only happen if the case is one lined
			if case_found && colon_found {
				append(&format_tokens, TokenAndLength {format_token = &line.format_tokens[i], length = length})
				largest = max(length, largest)
				break
			}

			if format_token.kind == .Case {
				case_found = true
				case_count += 1
			} else if format_token.kind == .Colon {
				colon_found = true
			}

			length += len(format_token.text) + format_token.spaces_before
		}

		if case_count >= brace_token.parameter_count {
			break
		}
	}

	for token in format_tokens {
		token.format_token.spaces_before = largest - token.length + 1
	}

}

align_enum :: proc(p: ^Printer, index: int) {
	enum_found := false
	brace_token: Format_Token
	brace_line:  int

	found_enum_brace: for line, line_index in p.lines[index:] {
		for format_token in line.format_tokens {
			if format_token.kind == .Open_Brace && enum_found {
				brace_token = format_token
				brace_line = line_index + index
				break found_enum_brace
			} else if format_token.kind == .Open_Brace {
				break
			} else if format_token.kind == .Enum {
				enum_found = true
			}
		}
	}

	if !enum_found {
		return
	}

	largest     := 0
	comma_count := 0

	TokenAndLength :: struct {
		format_token: ^Format_Token,
		length:       int,
	}

	format_tokens := make([dynamic]TokenAndLength, 0, brace_token.parameter_count, context.temp_allocator)

	for line, line_index in p.lines[brace_line + 1:] {
		length := 0

		for format_token, i in line.format_tokens {
			if format_token.kind == .Comment {
				break
			}

			if format_token.kind == .Eq {
				append(&format_tokens, TokenAndLength {format_token = &line.format_tokens[i], length = length})
				largest = max(length, largest)
				break
			} else if format_token.kind == .Comma {
				comma_count += 1
			}

			length += len(format_token.text) + format_token.spaces_before
		}

		if comma_count >= brace_token.parameter_count {
			break
		}
	}

	for token in format_tokens {
		token.format_token.spaces_before = largest - token.length + 1
	}

}

align_struct :: proc(p: ^Printer, index: int) -> int {
	struct_found := false
	brace_token: Format_Token
	brace_line:  int

	found_struct_brace: for line, line_index in p.lines[index:] {
		for format_token in line.format_tokens {
			if format_token.kind == .Open_Brace && struct_found {
				brace_token = format_token
				brace_line = line_index + index
				break found_struct_brace
			} else if format_token.kind == .Open_Brace {
				break
			} else if format_token.kind == .Struct {
				struct_found = true
			}
		}
	}

	if !struct_found {
		return 0
	}

	largest     := 0
	colon_count := 0
	nested      := false
	seen_brace  := false

	TokenAndLength :: struct {
		format_token: ^Format_Token,
		length:       int,
	}

	format_tokens := make([]TokenAndLength, brace_token.parameter_count, context.temp_allocator)

	if brace_token.parameter_count == 0 {
		return 0
	}

	end_line_index := 0

	for line, line_index in p.lines[brace_line + 1:] {
		length := 0

		for format_token, i in line.format_tokens {

			//give up on nested structs
			if format_token.kind == .Comment {
				break
			} else if format_token.kind == .Open_Paren {
				break
			} else if format_token.kind == .Open_Brace {
				seen_brace = true
			} else if format_token.kind == .Close_Brace {
				seen_brace = false
			} else if seen_brace {
				continue
			}

			if format_token.kind == .Colon {
				format_tokens[colon_count] = {format_token = &line.format_tokens[i + 1], length = length}

				if format_tokens[colon_count].format_token.kind == .Struct {
					nested = true
				}

				colon_count += 1
				largest = max(length, largest)
			}

			length += len(format_token.text) + format_token.spaces_before
		}

		if nested {
			end_line_index = line_index + brace_line + 1
		}

		if colon_count >= brace_token.parameter_count {
			break
		}
	}

	//give up aligning nested, it never looks good
	if nested {
		for line, line_index in p.lines[end_line_index:] {
			for format_token in line.format_tokens {
				if format_token.kind == .Close_Brace {
					return end_line_index + line_index - index
				}
			}
		}
	}

	for token in format_tokens {
		token.format_token.spaces_before = largest - token.length + 1
	}

	return 0
}

align_comments :: proc(p: ^Printer) {

	Comment_Align_Info :: struct {
		length: int,
		begin:  int,
		end:    int,
		depth:  int,
	}

	comment_infos := make([dynamic]Comment_Align_Info, 0, context.temp_allocator)

	current_info: Comment_Align_Info

	for line, line_index in p.lines {
		if len(line.format_tokens) <= 0 {
			continue
		}

		if .Line_Comment in line.types {
			if current_info.end + 1 != line_index || current_info.depth != line.depth ||
			   (current_info.begin == current_info.end && current_info.length == 0) {

				if (current_info.begin != 0 && current_info.end != 0) || current_info.length > 0 {
					append(&comment_infos, current_info)
				}

				current_info.begin = line_index
				current_info.end = line_index
				current_info.depth = line.depth
				current_info.length = 0
			}

			length := 0

			for format_token, i in line.format_tokens {
				if format_token.kind == .Comment {
					current_info.length = max(current_info.length, length)
					current_info.end = line_index
				}

				length += format_token.spaces_before + len(format_token.text)
			}
		}
	}

	if (current_info.begin != 0 && current_info.end != 0) || current_info.length > 0 {
		append(&comment_infos, current_info)
	}

	for info in comment_infos {

		if info.begin == info.end || info.length == 0 {
			continue
		}

		for i := info.begin; i <= info.end; i += 1 {
			l := p.lines[i]

			length := 0

			for format_token in l.format_tokens {
				if format_token.kind == .Comment {
					if len(l.format_tokens) == 1 {
						l.format_tokens[i].spaces_before = info.length + 1
					} else {
						l.format_tokens[i].spaces_before = info.length - length + 1
					}
				}

				length += format_token.spaces_before + len(format_token.text)
			}
		}
	}
}
