package main

import (
	"bytes"
	"errors"
	"flag"
	"fmt"
	"math"
	"os"
	"path/filepath"
)

// fileExists returns true if the given file exists, and false if it
// doesn't. If it encounters an error, it prints the error and exits.
func fileExists(filename string) bool {
	if _, err := os.Stat(filename); err == nil {
		return true
	} else if errors.Is(err, os.ErrNotExist) {
		return false
	} else {
		printErrAndExit(err.Error())
		return false // NEVER REACHED
	}
}

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

// 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(configPath, extension string) (bool, string) {
	if extension == "" {
		return false, ""
	}
	// Assuming the file has an extension
	fileName := filepath.Join(configPath, 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 {
		printErrAndExit(err.Error())
	}
	fmt.Print(string(data))
	return
}

// computeLineNumDigits computes the number of digits in the number of lines
// in the given byte array.
func computeLineNumDigits(data []byte) int {
	numLines := bytes.Count(data, []byte{'\n'}) + 1
	return int(math.Round(math.Log10(float64(numLines))))
}

func main() {
	disableColorFlag := flag.Bool("d", false, "Disable color")
	lineNumberFlag := flag.Bool("n", false, "Print line numbers")
	// Used only if lineNumberFlag is true
	var lineNumDigits int
	var lineNum int
	flag.Parse()

	// Check if config exists. If it doesn't, generate the config files.
	userHomeDir, err := os.UserHomeDir() // Get current user's home directory, to construct config path
	if err != nil {
		panic(err)
	}
	configPath := filepath.Join(userHomeDir + "/.config/ccat/")
	if _, err := os.Stat(configPath); os.IsNotExist(err) {
		generateDefaultConfigs(configPath)
	}

	// Check if user has provided a file name
	if len(flag.Args()) < 1 {
		printErrAndExit("No File specified")
	}
	fileName := flag.Args()[0]

	// Check if file exists.
	mustExist(fileName)

	extension := filepath.Ext(fileName)
	configExists, configFilename := getConfig(configPath, extension)
	// If the given file has no corresponding config, print the file out and exit.
	if configExists == false {
		printFile(fileName)
		return
	}

	// To save computing time, determine here if the file is empty. If it is, exit
	// the program.
	finfo, err := os.Stat(fileName)
	if err != nil {
		panic(err)
	}
	if finfo.Size() == 0 {
		os.Exit(0)
	}

	// Assuming the file is not empty...
	// If a ccat.colors file exists in the config directory, load all the colors in it
	if fileExists(filepath.Join(configPath, "ccat.colors")) {
		err := loadColorsFromFile(filepath.Join(configPath, "ccat.colors"))
		if err != nil {
			printErrAndExit(err.Error())
		}
	}
	// If the given file has a config, load the config into a stack of regColors.
	regColorStack, err := loadConfig(configFilename)
	if err != nil {
		printErrAndExit(err.Error())
	}

	// Load the input file into a colorunit slice (units) and a byte slice (data)
	units, data, err := loadInputFile(fileName)
	if err != nil {
		printErrAndExit(err.Error())
	}

	// If the '-n' flag is set, compute the number of digits in the number of lines
	// in the file, to determine the padding for the line numbers.
	if *lineNumberFlag {
		lineNumDigits = computeLineNumDigits(data)
	}
	// 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.
	//
	// The loop is also only run if the 'disable color' flag is not set.
	for *disableColorFlag == false {
		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.FindAllSubmatch(string(data))
		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].StartIdx, match[0].EndIdx, clr)
		}
	}

	// After all possible regexes have been matched, print out the contents of 'units'.

	// If the line number flag is set, initialize the lineNum variable and print the first line number
	// with the appropriate padding.
	if *lineNumberFlag {
		lineNum = 1
		fmt.Printf("   %*d  ", lineNumDigits, lineNum)
		lineNum++
	}
	for idx, unit := range units {
		unit.print()
		// If the flag is set and we encounter a newline (and the newline isn't a trailing newline),
		// then print the next line number.
		if *lineNumberFlag && unit.ch == '\n' && idx != len(units)-1 {
			fmt.Printf("   %*d  ", lineNumDigits, lineNum)
			lineNum++
		}
	}
}