package main

import (
	"log"
	"math/rand"
	"os"
	"os/signal"
	"strings"
	"time"

	"github.com/gdamore/tcell"
)

var screen tcell.Screen

// just basic alphanumeric characters
var characters = []rune{
	'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
	'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
	'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
	'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
}

// streamDisplays by column number
var streamDisplaysByColumn = make(map[int]*StreamDisplay)

// current sizes
var curSizes sizes

// channel used to notify StreamDisplayManager
var sizesUpdateCh = make(chan sizes)

// struct sizes contains terminal sizes (in amount of characters)
type sizes struct {
	width                      int
	height                     int
	curStreamsPerStreamDisplay int // current amount of streams per display allowed
}

// set the sizes and notify StreamDisplayManager
func (s *sizes) setSizes(width int, height int) {
	s.width = width
	s.height = height
	s.curStreamsPerStreamDisplay = 1 + height/10
}

func gomatrix() {
	screen.SetStyle(tcell.StyleDefault.
		Background(tcell.ColorBlack).
		Foreground(tcell.ColorBlack))
	screen.Clear()

	// StreamDisplay manager
	go func() {
		var lastWidth int

		for newSizes := range sizesUpdateCh {
			diffWidth := newSizes.width - lastWidth

			if diffWidth == 0 {
				// same column size, wait for new information
				continue
			}

			if diffWidth > 0 {
				for newColumn := lastWidth; newColumn < newSizes.width; newColumn++ {
					// create stream display
					sd := &StreamDisplay{
						column:    newColumn,
						stopCh:    make(chan bool, 1),
						streams:   make(map[*Stream]bool),
						newStream: make(chan bool, 1), // will only be filled at start and when a spawning stream has it's tail released
					}
					streamDisplaysByColumn[newColumn] = sd

					// start StreamDisplay in goroutine
					go sd.run()

					// create first new stream
					sd.newStream <- true
				}
				lastWidth = newSizes.width
			}

			if diffWidth < 0 {
				for closeColumn := lastWidth - 1; closeColumn > newSizes.width; closeColumn-- {
					// get sd
					sd := streamDisplaysByColumn[closeColumn]

					// delete from map
					delete(streamDisplaysByColumn, closeColumn)

					// inform sd that it's being closed
					sd.stopCh <- true
				}
				lastWidth = newSizes.width
			}
		}
	}()

	// set initial sizes
	curSizes.setSizes(screen.Size())
	sizesUpdateCh <- curSizes

	// flusher flushes the termbox every x milliseconds
	curFPS := 25
	fpsSleepTime := time.Duration(1000000/curFPS) * time.Microsecond
	go func() {
		for {
			time.Sleep(fpsSleepTime)
			screen.Show()
		}
	}()

	// make chan for tembox events and run poller to send events on chan
	eventChan := make(chan tcell.Event)
	go func() {
		for {
			event := screen.PollEvent()
			eventChan <- event
		}
	}()

	// register signals to channel
	sigChan := make(chan os.Signal)
	signal.Notify(sigChan, os.Interrupt, os.Kill)

	maxRun := time.After(8 * time.Second)
	stopRun := time.After(12 * time.Second)

	// handle tcell events and unix signals
EVENTS:
	for {
		// select for either event or signal
		select {
		case event := <-eventChan:
			// switch on event type
			switch ev := event.(type) {
			case *tcell.EventKey:
				switch ev.Key() {
				case tcell.KeyCtrlZ:
					break EVENTS

				case tcell.KeyCtrlL:
					screen.Sync()

				case tcell.KeyRune:
					switch ev.Rune() {
					case 'q':
						break EVENTS

					case 'c':
						screen.Clear()
					}
				}
			case *tcell.EventError: // quit
				log.Fatalf("Quitting because of tcell error: %v", ev.Error())
			}

		case <-maxRun:
			for _, sd := range streamDisplaysByColumn {
				sd.stopCh <- true
			}

		case <-stopRun:
			break EVENTS
		}
	}
}

func displayLine(line string, textStyle tcell.Style, x, y, speed int) int {
	for _, r := range line {
		screen.SetCell(x, y, textStyle, r)
		x += 1
		screen.Show()
		if r == ' ' {
			time.Sleep(time.Duration(speed/2+rand.Intn(speed/2)) * time.Millisecond)
		} else if r == '.' || r == ',' {
			time.Sleep(time.Duration(speed*5+rand.Intn(speed*25/10)) * time.Millisecond)
		} else {
			time.Sleep(time.Duration(speed+rand.Intn(speed*2)) * time.Millisecond)
		}
	}
	return x
}

func blinkingCursor(textStyle tcell.Style, x, y, nb int) {
	for i := 0; i < nb; i += 1 {
		if i%2 == 0 {
			screen.SetCell(x, y, textStyle, '_')
		} else {
			screen.SetCell(x, y, textStyle, ' ')
		}
		screen.Show()
		time.Sleep(750 * time.Millisecond)
	}
}

func scenario(login string) {
	blackStyle := tcell.StyleDefault.
		Foreground(tcell.ColorBlack).
		Background(tcell.ColorBlack)

	textStyle := blackStyle.Foreground(tcell.ColorGreen)

	displayLine("Wake up, "+login+"...", textStyle, 1, 2, 100)
	time.Sleep(1000 * time.Millisecond)
	screen.Clear()

	displayLine("The Matrix has you...", textStyle, 1, 2, 100)
	time.Sleep(1500 * time.Millisecond)
	screen.Clear()

	blinkingCursor(textStyle, 1, 2, 2)
	displayLine("Pour t'empêcher de sortir de la Matrice, des agents", textStyle, 1, 2, 25)
	displayLine("ont piégé ton poste de travail et le réseau environnant.", textStyle, 1, 3, 25)

	blinkingCursor(textStyle, 1, 5, 3)
	displayLine("J'ai peur que tu ne doives te débrouiller tout.e seul.e", textStyle, 1, 5, 25)
	displayLine("pour retrouver la route vers Internet, d'où l'on pourra", textStyle, 1, 6, 25)
	pos := displayLine("t'extraire sans risque.", textStyle, 1, 7, 25)
	blinkingCursor(textStyle, pos, 7, 6)

	displayLine("Ils te tiennent !", textStyle, 1, 10, 15)
	displayLine("Je redémarre ta machine pour effacer notre échange.", textStyle, 1, 11, 25)

	pos = displayLine("Bonne chance.", textStyle, 1, 13, 50)
	blinkingCursor(textStyle, pos, 13, 6)
	displayLine("Au fait, le pass root est: hax&i6aes2so5niec8XeeLei_", textStyle, 1, 15, 2)
	time.Sleep(400 * time.Millisecond)
}

func runCinematic(login string) {
	var err error

	// initialize tcell
	if screen, err = tcell.NewScreen(); err != nil {
		os.Exit(1)
	}

	err = screen.Init()
	if err != nil {
		os.Exit(1)
	}

	gomatrix()

	screen.Clear()

	// Keep only the first part of the login (firstname)
	login = strings.SplitN(login, ".", 2)[0]

	scenario(login)

	// close down
	screen.Fini()
}
