package repl

import (
	_ "bufio"
	"encoding/binary"
	"fmt"
	"io"
	"os"
	"sort"
	"strings"

	"gitlab.com/olanguage/olang/color"
	"gitlab.com/olanguage/olang/compiler"
	"gitlab.com/olanguage/olang/evaluator"
	"gitlab.com/olanguage/olang/lexer"
	"gitlab.com/olanguage/olang/object"
	"gitlab.com/olanguage/olang/parser"
	"gitlab.com/olanguage/olang/systemlib"
	"gitlab.com/olanguage/olang/token"
	"gitlab.com/olanguage/olang/vm"
	"gitlab.com/olanguage/olang/watcher"

	"gitlab.com/olanguage/olang/readline"
	"gitlab.com/olanguage/olang/version"
)

var PROMPT = "olang-" + version.Version() + "> "

// getCompilerBuiltinNames compiler'daki PreBuiltFunctions'taki isimleri sıralı döndürür
func getCompilerBuiltinNames() []string {
	var names []string
	for name := range compiler.PreBuiltFunctions {
		names = append(names, name)
	}
	sort.Strings(names) // Sıralı döndür
	return names
}

func GetBuiltins() string {
	// Key'leri bir slice'a ekleyelim
	keys := make([]string, 0, len(evaluator.PreBuiltFunctions))
	for key := range evaluator.PreBuiltFunctions {
		keys = append(keys, key+"()")
	}

	// Slice'ı joinleyerek tek bir string haline getirelim
	joinedKeys := strings.Join(keys, ", ")

	return fmt.Sprintf("Functions: \n\t%s", joinedKeys)
}

func GetTokens() string {
	// Key'leri bir slice'a ekleyelim
	keys := make([]string, 0, len(token.PreBuiltTokens))
	for key := range token.PreBuiltTokens {
		keys = append(keys, key)
	}

	// Slice'ı joinleyerek tek bir string haline getirelim
	joinedKeys := strings.Join(keys, ", ")

	return fmt.Sprintf("Tokens: \n\t%s", joinedKeys)
}

func printHelp() {
	fmt.Println("O Programming Language (olang)")
	fmt.Println("")
	fmt.Println(version.FullInfo())
	fmt.Println("")
	fmt.Println("System Oriented Functional Programming Language (SOFPL) is a specific structure that interprets each line separately and outputs the result. Shorten the functions performed in normal programming languages.")
	fmt.Println("")
	fmt.Println(GetTokens())
	fmt.Println("")
	fmt.Println(GetBuiltins())
	fmt.Println("")
	fmt.Println("Documentation:\n\tonix-project.com/O_Language")
	fmt.Println("Gitlab:\n\tgitlab.com/olanguage/olang")
	fmt.Println("")
}

func Start(in io.Reader, out io.Writer, debug bool) {
	promptVar := color.Green(PROMPT)
	getPromptEnv := os.Getenv("OLANG_PROMPT")
	if getPromptEnv != "" {
		promptVar = getPromptEnv
	}
	//scanner := bufio.NewScanner(in)
	env := object.NewEnvironment()

	for {
		rl, err := readline.New(promptVar)
		if err != nil {
			fmt.Println(err)
			return
		}
		defer rl.Close()
		line, err := rl.Readline()
		if err != nil {
			fmt.Println(err)
			return
		}

		switch line {
		case "help":
			printHelp()
		case "version":
			fmt.Println(version.FullInfo())
		case "exit":
			os.Exit(1)
		default:
			l := lexer.New(line)
			p := parser.New(l)

			program := p.ParseProgram()
			if len(p.Errors()) != 0 {
				printParserErrors(out, p.Errors())
				continue
			}

			evaluated := evaluator.Eval(program, env)

			if evaluated != nil {
				if evaluated.Result() != "" {
					io.WriteString(out, evaluated.Result())
					io.WriteString(out, "\n")
				}
			}
		}

	}
}

func FileCompile(filename string, output string, out io.Writer, debug bool) {
	content, err := os.ReadFile(filename)
	constants := []object.Object{}
	symbolTable := compiler.NewSymbolTable()

	builtinNames := getCompilerBuiltinNames()
	for i, name := range builtinNames {
		symbolTable.DefineBuiltin(i, name)
	}

	if err != nil {
		printParserError(out, err)
	}

	contstr := string(content)

	if len(contstr) > 0 {

		l := lexer.New(contstr)
		p := parser.New(l)

		program := p.ParseProgram()
		if len(p.Errors()) != 0 {
			printParserErrors(out, p.Errors())
		}

		comp := compiler.NewWithState(symbolTable, constants)
		err := comp.Compile(program)
		if err != nil {
			fmt.Fprintf(out, "Compilation failed:\n %s\n", err)
		}

		code := comp.Bytecode()

		if output != "" {
			err := saveBytecode(output, code)
			if err != nil {
				fmt.Fprintf(out, "Failed to save bytecode:\n %s\n", err)
				return
			}
			os.Chmod(output, 0755)
			fmt.Fprintf(out, "Compiled successfully to: %s\n", output)
		} else {
			print(code.Instructions.String())
			constants = code.Constants

			machine := vm.New(code)

			builtinNames := getCompilerBuiltinNames()
			for i, name := range builtinNames {
				if builtin, exists := compiler.PreBuiltFunctions[name]; exists {
					machine.RegisterBuiltin(uint16(i), builtin)
				}
			}

			err = machine.Run()
			if err != nil {
				fmt.Fprintf(out, "Executing bytecode failed:\n %s\n", err)
			}

			stackTop := machine.LastPoppedStackElement()
			if stackTop != nil {
				io.WriteString(out, stackTop.Inspect())
				io.WriteString(out, "\n")
			}
		}

	}
}

func saveBytecode(filename string, bc *vm.Bytecode) error {
	scriptFile, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer scriptFile.Close()

	if err := binary.Write(scriptFile, binary.BigEndian, uint32(len(bc.Instructions))); err != nil {
		return err
	}
	if _, err := scriptFile.Write(bc.Instructions); err != nil {
		return err
	}

	if err := binary.Write(scriptFile, binary.BigEndian, uint32(len(bc.Constants))); err != nil {
		return err
	}

	for _, obj := range bc.Constants {
		if err := writeObject(scriptFile, obj); err != nil {
			return err
		}
	}

	return nil
}

func writeObject(file *os.File, obj object.Object) error {
	var typeByte byte
	switch obj.Type() {
	case object.INTEGER_OBJ:
		typeByte = 1
	case object.STRING_OBJ:
		typeByte = 2
	case object.BOOLEAN_OBJ:
		typeByte = 3
	case object.NULL_OBJ:
		typeByte = 4
	case object.ARRAY_OBJ:
		typeByte = 5
	case object.HASH_OBJ:
		typeByte = 6
	case object.COMPILED_FUNCTION_OBJ:
		typeByte = 7
	default:
		typeByte = 0
	}

	if err := binary.Write(file, binary.BigEndian, typeByte); err != nil {
		return err
	}

	switch o := obj.(type) {
	case *object.Integer:
		if err := binary.Write(file, binary.BigEndian, o.Value); err != nil {
			return err
		}
	case *object.String:
		if err := binary.Write(file, binary.BigEndian, uint32(len(o.Value))); err != nil {
			return err
		}
		if _, err := file.WriteString(o.Value); err != nil {
			return err
		}
	case *object.Boolean:
		if o.Value {
			if err := binary.Write(file, binary.BigEndian, byte(1)); err != nil {
				return err
			}
		} else {
			if err := binary.Write(file, binary.BigEndian, byte(0)); err != nil {
				return err
			}
		}
	case *object.Array:
		if err := binary.Write(file, binary.BigEndian, uint32(len(o.Elements))); err != nil {
			return err
		}
		for _, elem := range o.Elements {
			if err := writeObject(file, elem); err != nil {
				return err
			}
		}
	case *object.Hash:
		if err := binary.Write(file, binary.BigEndian, uint32(len(o.Pairs))); err != nil {
			return err
		}
		for _, pair := range o.Pairs {
			if err := writeObject(file, pair.Key); err != nil {
				return err
			}
			if err := writeObject(file, pair.Value); err != nil {
				return err
			}
		}
	case *object.CompiledFunction:
		if err := binary.Write(file, binary.BigEndian, uint32(len(o.Instructions))); err != nil {
			return err
		}
		if _, err := file.Write(o.Instructions); err != nil {
			return err
		}
		if err := binary.Write(file, binary.BigEndian, o.NumLocals); err != nil {
			return err
		}
		if err := binary.Write(file, binary.BigEndian, o.NumParameters); err != nil {
			return err
		}
	}

	return nil
}

func RunBytecodeFile(filename string, out io.Writer, debug bool) error {
	file, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	var instrLen uint32
	if err := binary.Read(file, binary.BigEndian, &instrLen); err != nil {
		return err
	}

	instructions := make([]byte, instrLen)
	if _, err := file.Read(instructions); err != nil {
		return err
	}

	var constLen uint32
	if err := binary.Read(file, binary.BigEndian, &constLen); err != nil {
		return err
	}

	constants := make([]object.Object, constLen)
	for i := uint32(0); i < constLen; i++ {
		obj, err := readObject(file)
		if err != nil {
			return err
		}
		constants[i] = obj
	}

	bytecode := &vm.Bytecode{
		Instructions: instructions,
		Constants:    constants,
	}

	machine := vm.New(bytecode)

	builtinNames := getCompilerBuiltinNames()
	for i, name := range builtinNames {
		if builtin, exists := compiler.PreBuiltFunctions[name]; exists {
			machine.RegisterBuiltin(uint16(i), builtin)
		}
	}

	err = machine.Run()
	if err != nil {
		return err
	}

	stackTop := machine.LastPoppedStackElement()
	if stackTop != nil {
		io.WriteString(out, stackTop.Inspect())
		io.WriteString(out, "\n")
	}

	return nil
}

func readObject(file *os.File) (object.Object, error) {
	var typeByte byte
	if err := binary.Read(file, binary.BigEndian, &typeByte); err != nil {
		return nil, err
	}

	switch typeByte {
	case 1:
		var val int64
		if err := binary.Read(file, binary.BigEndian, &val); err != nil {
			return nil, err
		}
		return &object.Integer{Value: val}, nil
	case 2:
		var strLen uint32
		if err := binary.Read(file, binary.BigEndian, &strLen); err != nil {
			return nil, err
		}
		buf := make([]byte, strLen)
		if _, err := file.Read(buf); err != nil {
			return nil, err
		}
		return &object.String{Value: string(buf)}, nil
	case 3:
		var val byte
		if err := binary.Read(file, binary.BigEndian, &val); err != nil {
			return nil, err
		}
		return &object.Boolean{Value: val == 1}, nil
	case 4:
		return &object.Null{}, nil
	case 5:
		var elemLen uint32
		if err := binary.Read(file, binary.BigEndian, &elemLen); err != nil {
			return nil, err
		}
		elements := make([]object.Object, elemLen)
		for i := uint32(0); i < elemLen; i++ {
			elem, err := readObject(file)
			if err != nil {
				return nil, err
			}
			elements[i] = elem
		}
		return &object.Array{Elements: elements}, nil
	case 6:
		var pairLen uint32
		if err := binary.Read(file, binary.BigEndian, &pairLen); err != nil {
			return nil, err
		}
		pairs := make(map[object.HashKey]object.HashPair)
		for i := uint32(0); i < pairLen; i++ {
			key, err := readObject(file)
			if err != nil {
				return nil, err
			}
			value, err := readObject(file)
			if err != nil {
				return nil, err
			}
			var hashed object.HashKey
			switch k := key.(type) {
			case *object.Integer:
				hashed = k.HashKey()
			case *object.String:
				hashed = k.HashKey()
			case *object.Boolean:
				hashed = k.HashKey()
			default:
				hashed = object.HashKey{Value: uint64(i)}
			}
			pairs[hashed] = object.HashPair{Key: key, Value: value}
		}
		return &object.Hash{Pairs: pairs}, nil
	case 7:
		var instrLen uint32
		if err := binary.Read(file, binary.BigEndian, &instrLen); err != nil {
			return nil, err
		}
		instr := make([]byte, instrLen)
		if _, err := file.Read(instr); err != nil {
			return nil, err
		}
		var numLocals int
		if err := binary.Read(file, binary.BigEndian, &numLocals); err != nil {
			return nil, err
		}
		var numParams int
		if err := binary.Read(file, binary.BigEndian, &numParams); err != nil {
			return nil, err
		}
		return &object.CompiledFunction{Instructions: instr, NumLocals: numLocals, NumParameters: numParams}, nil
	}

	return &object.Null{}, nil
}

func StringParse(input string, out io.Writer, debug bool) {
	env := object.NewEnvironment()

	contstr := string(input)

	if len(contstr) > 0 {

		l := lexer.New(contstr)
		p := parser.New(l)

		program := p.ParseProgram()

		if len(p.Errors()) != 0 {
			printParserErrors(out, p.Errors())
			if debug {
				fmt.Printf("Stoped Line:\n")
				fmt.Printf("%#v\n", p.LexerReturn())
			}
			return
		}

		evaluated := evaluator.Eval(program, env)

		if evaluated != nil {
			if evaluated.Result() != "" {
				io.WriteString(out, evaluated.Result())
				if debug {
					fmt.Printf("\n%#v\n", evaluated)
				}
			}
		}
	}
}

func FileParse(filename string, out io.Writer, debug bool, live bool, libraries systemlib.Libraries) {
	env := object.NewEnvironment()

	contstr := ""
	for _, lib := range libraries {
		content, err := os.ReadFile(lib.File)
		w := watcher.New()
		if err != nil {
			printParserError(out, err)
		}

		if live {
			w.Add(filename)
		}
		contstr += string(content)
	}

	content, err := os.ReadFile(filename)
	w := watcher.New()
	if err != nil {
		printParserError(out, err)
	}

	if live {
		w.Add(filename)
	}

	contstr += string(content)

	if len(contstr) > 0 {

		l := lexer.New(contstr)
		p := parser.New(l)

		program := p.ParseProgram()

		if len(p.Errors()) != 0 {
			printParserErrors(out, p.Errors())
			if debug {
				fmt.Printf("Stoped Line:\n")
				fmt.Printf("%#v\n", p.LexerReturn())
				fmt.Printf("%#v\n", p.ErrorTokens())
			}
			return
		}

		evaluated := evaluator.Eval(program, env)

		if evaluated != nil {
			if evaluated.Result() != "" {
				io.WriteString(out, evaluated.Result())
				if debug {
					fmt.Printf("\n%#v\n", evaluated)
				}
			}
		}

		if live {
			w.Run(func() {
				FileParse(filename, out, debug, live, libraries)
			})
		}
	}
}

func printParserError(out io.Writer, err error) {
	io.WriteString(out, color.Bold(color.Red("Your code not valid!\n")))
	io.WriteString(out, color.Bold(color.Blue("  syntax errors:\n")))

	io.WriteString(out, "\t"+fmt.Sprintf("%s", err)+"\n")
}

func printParserErrors(out io.Writer, errors []string) {
	io.WriteString(out, color.Bold(color.Red("Your code not valid!\n")))
	io.WriteString(out, color.Bold(color.Blue("  syntax errors:\n")))

	for _, msg := range errors {
		io.WriteString(out, "\t"+msg+"\n")
	}
}
