package main

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
)

// fileExists returns true if the given file exists, and false if it
// doesn't. It panics if an error is encountered.
func fileExists(filename string) bool {
	if _, err := os.Stat(filename); err == nil {
		return true
	} else if errors.Is(err, os.ErrNotExist) {
		return false
	} else {
		panic(err)
	}
}

// mustExist can be called to ensure that a file exists; it panics if
// the file doesn't exist.
func mustExist(filename string) {
	if fileExists(filename) != true {
		panic(os.ErrNotExist)
	}
}

// getConfig fetches the config file name for the given file extension.
// It returns two values: the first is true if the config file exists.
// If it does, the second value is the config filename.
// If it doesn't, the second value is blank and can be ignored.
func getConfig(extension string) (bool, string) {
	if extension == "" {
		return false, ""
	}
	// Assuming the file has an extension
	fileName := "config/" + extension[1:] + ".conf"
	if exists := fileExists(fileName); exists == false {
		return false, ""
	} else {
		return true, fileName
	}
}

// printFile is used when no config file can be found for the file extension
// It prints out the file as it reads it, with no modifications applied. Essentially
// works like 'cat'.
func printFile(fileName string) {
	mustExist(fileName)
	data, err := os.ReadFile(fileName)
	if err != nil {
		panic(err)
	}
	fmt.Print(string(data))
	return
}

func main() {

	// Check if user has provided a file name
	if len(os.Args) != 2 {
		panic("ERROR: Invalid number of arguments")
	}
	fileName := os.Args[1]
	mustExist(fileName)

	extension := filepath.Ext(fileName)
	configExists, configFilename := getConfig(extension)
	// If the given file has no corresponding config, print it out
	// and exit.
	if configExists == false {
		printFile(fileName)
		return
	}
	// If the given file has a config, load the config into a stack of regColors.
	regColorStack, err := loadConfig(configFilename)
	if err != nil {
		panic(err)
	}

	// Load the input file into a colorunit slice (units) and a byte slice (data)
	units, data := loadInputFile(fileName)

	// For each regular expression in the stack, apply it to the byte slice. Find
	// the first and last index of all matches of the regex. Then apply the corresponding color
	// to every character within these indices.
	//
	// The infinite for loop exists, because I couldn't figure out a way to pop an element from
	// the stack inside the 'for' statement. The loop exits when the 'pop' call returns 'false',
	// indicating that the stack is empty.
	for {
		regclr, ok := regColorStack.Pop()
		// regColorStack.Pop() returns false when there are no more elements to pop
		if ok != true {
			break
		}
		re := regclr.re
		clr := regclr.clr
		// Returns an int double-slice, where each slice contains the start and end indices
		// of the match. In this case, I am finding all the matches of 're' in 'data'.
		matches := re.FindAllSubmatchIndex(data, -1)
		if matches == nil {
			continue
		}
		// For each match, apply the corresponding color to all characters in the match.
		for _, match := range matches {
			units = applyColor(units, match[0], match[1], clr)
		}
	}

	// After all possible regexes have been matched, print out the contents of 'units'.
	for _, unit := range units {
		unit.print()
	}
}
