module main

import os

const delete_skipped = true

const git_log_cmd = 'git log -n 500 --pretty=format:"%s" --simplify-merges'

enum Category {
	checker
	breaking
	improvements
	parser
	stdlib
	web
	orm
	db
	native
	cgen
	js_backend
	comptime
	tools
	compiler_internals
	examples
	vfmt
	os_support
	interpreter
}

const category_titles = '#### Improvements in the language

#### V interpreter

#### Breaking changes

#### Checker improvements/fixes

#### Parser improvements

#### Compiler internals

#### Standard library

#### Web

#### ORM

#### Database drivers

#### Native backend

#### C backend

#### JavaScript backend

#### vfmt

#### Tools

#### Operating System support

#### Examples
'

struct Line {
	category Category
	text     string
}

const log_txt = 'log.txt'

struct App {
	version     string // e.g. "0.4.5"
	total_lines int
mut:
	result  string // resulting CHANGELOG.md
	counter int
}

const is_interactive = false

// Instantly updates CHANGELOG.md without confirming each line
fn no_interactive(version string) {
}

fn main() {
	mut version := ''

	if os.args.len == 2 && os.args[1].starts_with('0.') {
		version = os.args[1]
		// no_interactive(version)
		// return
	} else {
		println('Usage: v run tools/changelog_helper.v 0.4.5')
		return
	}
	if !os.exists(log_txt) {
		os.execute(git_log_cmd + ' > ' + log_txt)
		println('log.txt generated')
		// println('log.txt generated, remove unnecessary commits from it and run the tool again')
		// return
	}
	mut lines := os.read_lines(log_txt)!
	// Trim everything before current version, commit "(tag: 0.4.4) V 0.4.4"
	mut prev_version := get_prev_version(version)
	println('prev version=${prev_version}')
	for i, line in lines {
		if line == ('V ${prev_version}') {
			lines = lines[..i].clone()
			break
		}
	}
	os.write_file(log_txt, lines.join('\n'))!
	if true {
		// return
	}
	mut app := &App{
		total_lines: lines.len
	}
	// Write categories at the top first
	app.result = os.read_file('CHANGELOG.md')!.replace_once('V ${version} TODO', 'V ${version}\n' +
		category_titles)
	os.write_file('CHANGELOG.md', app.result)!
	changelog_txt := os.read_file('CHANGELOG.md')!.to_lower()
	if true {
		// println(changelog_txt)
		// return
	}
	// mut counter := 0 // to display how many commits are left
	for line in lines {
		s := line.trim_space()
		if s == '' {
			app.counter++
		}
	}
	// println('${counter} / ${lines.len}')
	for line in lines {
		s := line.to_lower()
		if line != '' && (changelog_txt.contains(s) || changelog_txt.contains(s.after(':'))) {
			println('Duplicate: "${line}"')
			// skip duplicate
			delete_processed_line_from_log(line)!
			continue
		}

		app.process_line(line.trim_space())!
	}
	println('writing changelog.md')
	if !is_interactive {
		os.write_file('CHANGELOG.md', app.result)!
	}
	println('done.')
}

fn (mut app App) process_line(text string) ! {
	if text == '' {
		return
	}
	semicolon_pos := text.index(': ') or {
		println('no `:` in commit, skipping: "${text}"')
		return
	}
	prefix := text[..semicolon_pos]
	// Get category based on keywords in the commit message/prefix
	mut category := Category.examples
	if text.contains('checker:') {
		category = .checker
	} else if is_interpreter(text) {
		category = .interpreter
	} else if is_examples(text) {
		category = .examples
		// println("Skipping line (example) $text")
		// return
	} else if is_skip(text) {
		// Always skip cleanups, typos etc
		println('Skipping line (cleanup/typo)\n${text}\n')
		if delete_skipped {
			delete_processed_line_from_log(text)!
		}
		return
	} else if is_os_support(text) {
		category = .os_support
	} else if is_cgen(text) {
		category = .cgen
	} else if is_js_backend(text) {
		category = .js_backend
	} else if is_comptime(text) {
		category = .comptime
	} else if is_db(text) {
		category = .db
	} else if is_stdlib(text) {
		category = .stdlib
	} else if is_orm(text) {
		category = .orm
	} else if is_web(text) {
		category = .web
	} else if is_tools(text) {
		category = .tools
	} else if is_parser(text) {
		category = .parser
	} else if is_internal(text) {
		category = .compiler_internals
	} else if is_improvements(text) {
		category = .improvements
	} else if is_native(text) {
		category = .native
	} else if is_vfmt(text) {
		category = .vfmt
	} else if text.contains('docs:') || text.contains('doc:') {
		// Always skip docs
		delete_processed_line_from_log(text)!
		return
	} else {
		println('Skipping line (unknown category)\n${text}\n')
		// if delete_skipped {
		// delete_processed_line_from_log(text)!
		//}
		return
	}
	println('process_line: cat=${category} "${text}"')

	// Trim everything to the left of `:` for some commits (e.g. `checker: `)
	mut s := text
	// println("PREFIX='${prefix}'")
	// if true {
	// exit(0)
	//}
	if (semicolon_pos < 15
		&& prefix in ['checker', 'cgen', 'orm', 'parser', 'v.parser', 'native', 'ast', 'jsgen', 'v.gen.js', 'fmt', 'vfmt', 'tools', 'examples', 'eval'])
		|| (semicolon_pos < 30 && prefix.contains(', ')) {
		s = '- ' + text[semicolon_pos + 2..].capitalize()
	}

	if is_interactive {
		// Get input from the user
		print('\033[H\033[J')
		println('${app.counter} / ${app.total_lines}')
		// println('\n')
		println(text)
		input := os.input('${category}? ')
		println("INPUT='${input}'")
		match input {
			'' {
				println('GOT ENTER')
				line := Line{category, s}
				save_line_interactive(line)!
			}
			'n', '0', 'no' {
				// Ignore commit
				println('ignored.')
			}
			's', 'skip' {
				// Skip
				println('skipped.')
				return
			}
			'c', 'change' {
				// Change category
				for {
					print_category_hint()
					custom_category := os.input('${category} ?').int()
					if custom_category == 0 {
						println('wrong category')
					} else {
						unsafe {
							line := Line{Category(custom_category - 1), s}
							save_line_interactive(line)!
						}
						break
					}
				}
			}
			else {}
		}
		app.counter++
	} else {
		line := Line{category, s}
		app.save_line(line)!
	}
	// Don't forget to remove the line we just processed from log.txt
	delete_processed_line_from_log(text)!
}

fn (mut app App) save_line(line Line) ! {
	// println('save line ${line}')
	app.result = line.write_at_category(app.result) or { return error('') }
}

fn save_line_interactive(line Line) ! {
	println('save line interactive ${line}')
	mut txt := os.read_file('CHANGELOG.md')!
	txt = line.write_at_category(txt) or { return error('') }
	os.write_file('CHANGELOG.md', txt)!
}

const category_map = {
	Category.checker:    '#### Checker improvements'
	.breaking:           '#### Breaking changes'
	.improvements:       '#### Improvements in the'
	.interpreter:        '#### V interpreter'
	.parser:             '#### Parser improvements'
	.stdlib:             '#### Standard library'
	.web:                '#### Web'
	.orm:                '#### ORM'
	.db:                 '#### Database drivers'
	.native:             '#### Native backend'
	.cgen:               '#### C backend'
	.js_backend:         '#### JavaScript backend'
	.comptime:           '#### Comptime'
	.tools:              '#### Tools'
	.compiler_internals: '#### Compiler internals'
	.examples:           '#### Examples'
	.vfmt:               '#### vfmt'
	.os_support:         '#### Operating System'
}

fn (l Line) write_at_category(txt string) ?string {
	title := category_map[l.category]
	title_pos := txt.index(title)?
	// Find the position of the ### category title
	pos := txt.index_after('\n', title_pos + 1) or { return none }
	first_half := txt[..pos]
	second_half := txt[pos..]
	if txt.contains(l.text) {
		// Avoid duplicates (just in case)
		println("Got a duplicate: '${txt}'")
		return txt
	}
	// Now insert the line in the middle, under the ### category title
	mut line_text := l.text

	// Trim "prefix:" for some categories
	// mut capitalized := false
	mut has_prefix := true
	if l.category in [.cgen, .checker, .improvements, .native, .orm, .interpreter] {
		has_prefix = false
		if semicolon_pos := line_text.index(': ') {
			prefix := line_text[..semicolon_pos]
			println("PREFIX='${prefix}'")
			if semicolon_pos < 15 {
				line_text = line_text[semicolon_pos + 2..].capitalize()
				// capitalized = true
			}
		}
	}
	if !has_prefix {
		line_text = line_text.capitalize()
	}
	if !line_text.starts_with('- ') {
		line_text = '- ' + line_text
	}
	return first_half + '\n' + line_text + second_half
}

fn delete_processed_line_from_log(line string) ! {
	text := os.read_file(log_txt)!
	new_text := text.replace_once(line, '')
	os.write_file(log_txt, new_text)!
}

const db_strings = [
	'db:',
	'db.sqlite',
	'db.mysql',
	'db.pg',
	'pg:',
]

const parser_strings = [
	'parser:',
	'ast:',
]

const stdlib_strings = [
	'gg:',
	'json:',
	'json2:',
	'time:',
	'sync:',
	'datatypes:',
	'math:',
	'math.',
	'math.big',
	'crypto',
	'sokol',
	'os:',
	'rand:',
	'math:',
	'toml:',
	'vlib:',
	'arrays:',
	'os.',
	'term:',
	'sync.',
	'builtin:',
	'builtin,',
	'strconv',
	'readline',
	'cli:',
	'eventbus:',
	'encoding.',
	'bitfield:',
	'io:',
	'io.',
	'log:',
	'flag:',
	'regex:',
	'tmpl:',
	'hash:',
	'stbi:',
]

fn is_stdlib(text string) bool {
	return is_xxx(text, stdlib_strings)
}

fn is_db(text string) bool {
	return is_xxx(text, db_strings)
}

const orm_strings = [
	'orm:',
]

fn is_orm(text string) bool {
	return is_xxx(text, orm_strings)
}

const cgen_strings = [
	'cgen:',
	'cgen,',
	'v.gen.c:',
]

fn is_cgen(text string) bool {
	return is_xxx(text, cgen_strings)
}

const js_backend_strings = [
	'js:',
	'v.gen.js:',
	'jsgen:',
]

fn is_js_backend(text string) bool {
	return is_xxx(text, js_backend_strings)
}

const internal_strings = [
	'scanner:',
	'transformer:',
	'markused:',
	'builder:',
	'pref:',
	'v.util',
]

fn is_internal(text string) bool {
	return is_xxx(text, internal_strings)
}

const improvements_strings = [
	'all:',
	'v:',
	'coroutines:',
	'autofree',
]

fn is_improvements(text string) bool {
	return is_xxx(text, improvements_strings)
}

const examples_strings = [
	'example',
]
const skip_strings = [
	'tests',
	'readme:',
	'.md:',
	'typos',
	' typo',
	'cleanup',
	'clean up',
]

fn is_examples(text string) bool {
	return is_xxx(text, examples_strings)
}

fn is_skip(text string) bool {
	return is_xxx(text, skip_strings)
}

const tools_strings = [
	'tools:',
	'vpm:',
	'ci:',
	'github:',
	'gitignore',
	'benchmark',
	'v.help:',
	'vtest',
	'repl',
	'REPL',
	'vet',
	'tools.',
]

fn is_tools(text string) bool {
	return is_xxx(text, tools_strings)
}

fn is_parser(text string) bool {
	return is_xxx(text, parser_strings)
}

const web_strings = [
	'vweb',
	'veb',
	'x.vweb',
	'websocket:',
	'pico',
	'x.sessions',
	'picoev:',
	'mbedtls',
	'net:',
	'net.',
	'wasm:',
	'http:',
]

fn is_web(text string) bool {
	return is_xxx(text, web_strings)
}

const native_strings = [
	'native:',
]

fn is_native(text string) bool {
	return is_xxx(text, native_strings)
}

const vfmt_strings = [
	'vfmt:',
	'fmt:',
]

fn is_vfmt(text string) bool {
	return is_xxx(text, vfmt_strings)
}

const os_support_strings = [
	'FreeBSD',
	'freebsd',
	'OpenBSD',
	'openbsd',
	'macOS',
	'macos',
	'Windows',
	'windows',
	'Linux',
	'linux',
]

fn is_os_support(text string) bool {
	return is_xxx(text, os_support_strings)
}

fn is_comptime(text string) bool {
	return text.contains('comptime:')
}

fn is_interpreter(text string) bool {
	return text.starts_with('eval:')
}

fn is_xxx(text string, words []string) bool {
	for s in words {
		if text.contains(s) {
			return true
		}
	}
	return false
}

fn print_category_hint() {
	$for val in Category.values {
		println('${int(val.value) + 1} - ${val.name}; ')
	}
}



// For 0.4.12 returns 0.4.11 etc
fn get_prev_version(version string) string {
	parts := version.split('.')
	if parts.len != 3 {
		return ''
	}
	major := parts[0]
	minor := parts[1]
	patch := parts[2].int()
	return '${major}.${minor}.${patch-1}'
}
