package main

import (
	"bufio"
	"fmt"
	"math/rand"
	"os"
	"os/exec"
	"strings"
	"time"
)

const max_rows = 23
const max_cols = 80
const max_len = max_rows * max_cols + max_rows - 1

type player struct {
	row   int
	col   int
	dir   int
	p     string
}

type bg struct {
	tick    int
	tunnel  string
	l       int
	r       int
	c_count int
	c_to    int
	pl      player
}


var game bg

func SetCharMode(on bool) {
	cmd := exec.Command("stty", "-cbreak", "echo")
	if on {
		cmd = exec.Command("stty", "cbreak", "-echo")
	}

  cmd.Stdin = os.Stdin
  cmd.Stdout = os.Stdout
  err := cmd.Run()
  if err != nil {
		panic(err)
  }
	fmt.Print("\033[?25l")
} 

func makeBg(p player) bg {
	out := bg{0, "", 3, 77, 0, 5, p}
	var line strings.Builder
	line.WriteString("####")
	for i := 0; i < 72; i++ {
		line.WriteString(" ")
	}
	line.WriteString("####")
	out.tunnel = line.String()
	return out
}

func makePlayer() player {
	return player{4, max_cols / 2, 0, "!"}
}

func exit(code int) {
	SetCharMode(false)
	fmt.Print("\033[2J\033[0;0H\033[?25h")
	os.Exit(0)
}

func getInput(ch chan int) {
	var m int
	reader := bufio.NewReader(os.Stdin)
	for {
		r, _, err := reader.ReadRune()
		if err != nil {
			close(ch)
			panic("Rune read error")
		}

		switch r {
		case 'h', 'H':
			m = -1
		case 'l', 'L':
			m = 1
		case 'k', 'K':
			m = 0
		case 'r', 'R':
			m = 100
		case 'q', 'Q':
			exit(0)
		default:
			continue
		}
		ch <- m
	}
}

func (b *bg) newLeft() int {
	rand.Seed(time.Now().UnixNano())
	var change int
	if b.c_count > 0 && b.c_count < b.c_to {
		b.c_count++
		if b.l > 4 {
			return rand.Intn(2) - 1
		}
		return rand.Intn(2)
	} else {
		b.c_count = 0
	}

	change = rand.Intn(4) - 1

	if b.r - b.l < 5 {
		change = rand.Intn(2) - 1
		b.c_count++
		b.c_to = rand.Intn(8) + rand.Intn(4) + 1
	}
	if b.l < 4 || change > 1 {
		change = 1
	}

	if change < -1 {
		change = -1
	}

	return change
}

func (b *bg) newRight() int {
	rand.Seed(time.Now().UnixNano())
	var change int = 100
	if b.c_count > 1 {
		if b.r < max_cols - 5 {
			return 1
		}
		return rand.Intn(2) - 1
	} 

	if b.r - b.l < 4 {
		change = rand.Intn(2)
	}
	if b.r > max_cols - 4 {
		change = -1
	}

	if change > 10 {
		change = rand.Intn(2) - 1
	}

	if change > 1 {
		change = 1
	}
	if change < -1 {
		change = -1
	}

	return change
}

func (b *bg) buildNextLine() string {

	b.l = b.l + b.newLeft()
	b.r = b.r + b.newRight()
	if b.l < 4 {
		b.l = 4
	}
	if b.r > max_cols - 4 {
		b.r = max_cols - 4
	}

	var out strings.Builder
	out.WriteString("\n")
	out.WriteString(strings.Repeat("#", b.l))
	out.WriteString(strings.Repeat(" ", b.r - b.l))
	out.WriteString(strings.Repeat("#", max_cols - b.r))
	return out.String()
}

func (b *bg) collision() bool {
	if b.tunnel[b.pl.row * max_cols + b.pl.col + b.pl.row - 1] == '#' {
		return true
	}
	return false
}

func (b *bg) update() {
	ln := b.buildNextLine()
	var out strings.Builder
	if len(b.tunnel) <= max_len {
		out.WriteString(b.tunnel)
	} else {
		out.WriteString(b.tunnel[max_cols + 1:])
	}
	out.WriteString(ln)
	b.tunnel = out.String()
	b.pl.col += b.pl.dir
	if b.pl.col < 1 {
		b.pl.col = 1
	} else if b.pl.col > max_cols - 1 {
		b.pl.col = max_cols - 1 
	}
}

func (b *bg) draw() {
	fmt.Print("\033[0;0H")
	fmt.Print(b.tunnel)
	fmt.Printf("\nDepth: %d ft.", b.tick * 6)
	fmt.Printf("\033[%d;%dH%s",b.pl.row,b.pl.col,b.pl.p)
}

func main(){
	var ch = make(chan int)
	defer close(ch)
	SetCharMode(true)
	defer SetCharMode(false)
	game := makeBg(makePlayer())
restart:
	rand.Seed(time.Now().UnixNano())
	for i := 0; i < max_rows; i++ {
		game.update()
	}
	fmt.Print("\033[2J\033[0;0H")
	fmt.Print(`
                            _                      _
                       _ __| |_  _ _ __  _ __  ___| |_
                      | '_ \ | || | '  \| '  \/ -_)  _|
                      | .__/_|\_,_|_|_|_|_|_|_\___|\__|
                      |_|`)
	fmt.Printf("\033[8;%dHTHREE",max_cols/2-3)
	time.Sleep(1 * time.Second)
	fmt.Printf("\033[9;%dHTWO",max_cols/2-2)
	time.Sleep(1 * time.Second)
	fmt.Printf("\033[10;%dHONE",max_cols/2-2)
	time.Sleep(1 * time.Second)
	go getInput(ch)
	oldtime := time.Now().UnixNano() / 1000000
	for {
		select {
		case val, ok := <-ch:
			if ok && val < 5 {
				game.pl.dir = val
				game.pl.col += game.pl.dir
				game.pl.dir = 0
			}
		default:
			// do nothing
		}
		game.tick++
		game.update()
		game.draw()
		if game.collision() {
			fmt.Printf("\033[2J\033[10;%dHGame Over",max_cols/2-6)
			fmt.Printf("\033[11;%dHDepth: %d ft.",max_cols/2-8, game.tick * 6)
			fmt.Printf("\033[13;%dHR To Restart",max_cols/2-7)
			fmt.Printf("\033[14;%dHQ To Exit",max_cols/2-6)
			for {
				val := <-ch
				if val == 100 {
					game = makeBg(makePlayer())
					goto restart
				}
			}
		}
		newtime := time.Now().UnixNano() / 1000000
		if dt := newtime - oldtime; dt < 60 && dt >= 0 {
			diff := time.Duration(100 - dt)
			time.Sleep(diff * time.Millisecond)
		}
		oldtime = newtime
	}
}
