package main

import (
	"bufio"
	"errors"
	"fmt"
	"math/rand"
	"os"
	"strings"
	"time"

	"github.com/tlinden/golsky/rle"
)

type Grid struct {
	Data                   [][]bool
	Width, Height, Density int
	Empty                  bool
}

// Create new empty grid and allocate Data according to provided dimensions
func NewGrid(width, height, density int, empty bool) *Grid {
	grid := &Grid{
		Height:  height,
		Width:   width,
		Density: density,
		Data:    make([][]bool, height),
		Empty:   empty,
	}

	for y := 0; y < height; y++ {
		grid.Data[y] = make([]bool, width)
	}

	return grid
}

// Create a new 1:1 instance
func (grid *Grid) Clone() *Grid {
	newgrid := &Grid{}

	newgrid.Width = grid.Width
	newgrid.Height = grid.Height
	newgrid.Data = grid.Data

	return newgrid
}

// copy data
func (grid *Grid) Copy(other *Grid) {
	for y := range grid.Data {
		for x := range grid.Data[y] {
			other.Data[y][x] = grid.Data[y][x]
		}
	}
}

// delete all contents
func (grid *Grid) Clear() {
	for y := range grid.Data {
		for x := range grid.Data[y] {
			grid.Data[y][x] = false
		}
	}
}

// initialize with random life cells using the given density
func (grid *Grid) FillRandom() {
	if !grid.Empty {
		for y := range grid.Data {
			for x := range grid.Data[y] {
				if rand.Intn(grid.Density) == 1 {
					grid.Data[y][x] = true
				}
			}
		}
	}
}

func (grid *Grid) Dump() {
	for y := 0; y < grid.Height; y++ {
		for x := 0; x < grid.Width; x++ {
			if grid.Data[y][x] {
				fmt.Print("XX")
			} else {
				fmt.Print("  ")
			}
		}
		fmt.Println()
	}
}

// initialize using a given RLE pattern
func (grid *Grid) LoadRLE(pattern *rle.RLE) {
	if pattern != nil {
		startX := (grid.Width / 2) - (pattern.Width / 2)
		startY := (grid.Height / 2) - (pattern.Height / 2)
		var y, x int

		for rowIndex, patternRow := range pattern.Pattern {
			for colIndex := range patternRow {
				if pattern.Pattern[rowIndex][colIndex] > 0 {
					x = colIndex + startX
					y = rowIndex + startY

					grid.Data[y][x] = true
				}
			}
		}

		//grid.Dump()
	}
}

// load a lif file parameters like R and P are not supported yet
func LoadLIF(filename string) (*rle.RLE, error) {
	fd, err := os.Open(filename)
	if err != nil {
		return nil, err
	}

	scanner := bufio.NewScanner(fd)

	scanner.Split(bufio.ScanLines)

	gothead := false

	grid := &rle.RLE{}

	for scanner.Scan() {
		line := scanner.Text()
		items := strings.Split(line, "")

		if len(items) < 0 {
			continue
		}

		if strings.Contains(line, "# r") {
			parts := strings.Split(line, " ")
			if len(parts) == 2 {
				grid.Rule = parts[1]
			}

			continue
		}

		if items[0] == "#" {
			if gothead {
				break
			}

			continue
		}

		gothead = true

		row := make([]int, len(items))

		for idx, item := range items {
			switch item {
			case ".":
				row[idx] = 0
			case "o":
				fallthrough
			case "*":
				row[idx] = 1
			default:
				return nil, errors.New("cells must be . or o")
			}
		}

		grid.Pattern = append(grid.Pattern, row)
	}

	// sanity check the grid
	explen := 0
	rows := 0
	first := true
	for _, row := range grid.Pattern {
		length := len(row)

		if first {
			explen = length
			first = false
		}

		if explen != length {
			return nil, fmt.Errorf(
				fmt.Sprintf("all rows must be in the same length, got: %d, expected: %d",
					length, explen))
		}

		rows++
	}

	grid.Width = explen
	grid.Height = rows

	return grid, nil
}

// save  the contents  of  the  whole grid  as  a  simple lif  alike
// file. One line per row, 0 for dead and 1 for life cell.
// file format: https://conwaylife.com/wiki/Life_1.05
func (grid *Grid) SaveState(filename, rule string) error {
	file, err := os.Create(filename)
	if err != nil {
		return fmt.Errorf("failed to open state file: %w", err)
	}
	defer file.Close()

	fmt.Fprintf(file, "#Life 1.05\n#R %s\n#D golsky state file\n#P -1 -1\n", rule)

	for y := range grid.Data {
		for _, cell := range grid.Data[y] {
			row := "."
			if cell {
				row = "o"
			}

			_, err := file.WriteString(row)
			if err != nil {
				return fmt.Errorf("failed to write to state file: %w", err)
			}
		}
		file.WriteString("\n")
	}

	return nil
}

// generate filenames for dumps
func GetFilename(generations int64) string {
	now := time.Now()
	return fmt.Sprintf("dump-%s-%d.lif", now.Format("20060102150405"), generations)
}

func GetFilenameRLE(generations int64) string {
	now := time.Now()
	return fmt.Sprintf("rect-%s-%d.rle", now.Format("20060102150405"), generations)
}
