package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

const SENTINEL int = -2
const FREE_BLOCK int = -1

type Block struct {
	Prev, Next *Block
	Size       int
	FileId     int
}

func (before *Block) InsertAfter(after *Block) {
	before.Next.Prev = after
	after.Next, before.Next = before.Next, after
	after.Prev = before
}

func (block *Block) Detach() {
	block.Prev.Next, block.Next.Prev = block.Next, block.Prev
	block.Prev, block.Next = nil, nil
}

func (block *Block) TryMergeFreeWithNext() {
	if block.FileId == FREE_BLOCK && block.Next.FileId == FREE_BLOCK {
		block.Size += block.Next.Size
		block.Next.Detach()
	}
}

func (block *Block) TryMergeFree() *Block {
	block.TryMergeFreeWithNext()
	if block.Prev.FileId == FREE_BLOCK {
		block = block.Prev
		block.TryMergeFreeWithNext()
	}
	return block
}

func (block *Block) Split(Size int) {
	if Size >= block.Size {
		panic("please")
	}

	if Size == block.Size {
		return
	}

	newBlock := &Block{nil, nil, block.Size - Size, block.FileId}
	block.Size = Size
	block.InsertAfter(newBlock)
}

type Disk struct {
	Sentinel *Block
}

func readInt(b byte) int {
	return int(b - '0')
}

func MakeDisk(description string) (disk Disk) {
	disk.Sentinel = new(Block)
	disk.Sentinel.Size = 0
	disk.Sentinel.FileId = SENTINEL
	disk.Sentinel.Next = disk.Sentinel
	disk.Sentinel.Prev = disk.Sentinel

	nextFileId := 0
	nextIsFree := false

	head := disk.Sentinel

	for _, b := range []byte(description) {
		block := &Block{
			Prev: nil, Next: nil, Size: readInt(b),
		}

		if block.Size == 0 {
			nextIsFree = !nextIsFree
			continue
		}

		if nextIsFree {
			block.FileId = FREE_BLOCK
			nextIsFree = false
		} else {
			block.FileId = nextFileId
			nextFileId++
			nextIsFree = true
		}

		head.InsertAfter(block)
		head = block
	}

	return
}

func (disk *Disk) Print() {
	fmt.Println("===============")
	for b := disk.Sentinel.Next; b != disk.Sentinel; b = b.Next {
		fmt.Println(b)
	}
	fmt.Println("===============")
	bufio.NewReader(os.Stdin).ReadBytes('\n')
}

func (disk *Disk) Compactify() {
	head := disk.Sentinel.Next
	tail := disk.Sentinel.Prev

	for head != tail {
		if head.FileId >= 0 {
			head = head.Next
			continue
		}

		if tail.FileId == FREE_BLOCK {
			tail = tail.Prev
			continue
		}

		if tail.Size > head.Size {
			tail.Split(tail.Size - head.Size)
			tail = tail.Next
		}

		if head.Size > tail.Size {
			head.Split(tail.Size)
		}

		head.FileId, tail.FileId = tail.FileId, head.FileId
		tail = tail.TryMergeFree()
	}
}

func (disk *Disk) Defragment() {
	tail := disk.Sentinel.Prev

	for tail != disk.Sentinel {
		if tail.FileId == FREE_BLOCK {
			tail = tail.Prev
			continue
		}

		var freeBlock *Block = nil
		for head := disk.Sentinel.Next; head != tail; head = head.Next {
			if head.FileId == FREE_BLOCK && head.Size >= tail.Size {
				freeBlock = head
				break
			}
		}

		if freeBlock == nil {
			tail = tail.Prev
			continue
		}

		if freeBlock.Size > tail.Size {
			freeBlock.Split(tail.Size)
		}

		freeBlock.FileId, tail.FileId = tail.FileId, freeBlock.FileId
		tail = tail.TryMergeFree()
	}
}

func (disk *Disk) ComputeChecksum() (sum int64) {
	pos := 0
	for b := disk.Sentinel.Next; b != disk.Sentinel; b = b.Next {
		if b.FileId >= 0 {
			for i := 0; i < b.Size; i++ {
				sum += int64(pos * b.FileId)
				pos++
			}
		} else {
			pos += b.Size
		}
	}
	return
}

func main() {
	disk := MakeDisk(readData("example.txt"))
	disk.Compactify()
	fmt.Println(disk.ComputeChecksum(), 1928)

	disk = MakeDisk(readData("data.txt"))
	disk.Compactify()
	fmt.Println(disk.ComputeChecksum(), 6415184586041)

	disk = MakeDisk(readData("example.txt"))
	disk.Defragment()
	fmt.Println(disk.ComputeChecksum(), 2858)

	disk = MakeDisk(readData("data.txt"))
	disk.Defragment()
	fmt.Println(disk.ComputeChecksum())
}

func readData(fileName string) string {
	resultBytes, err := os.ReadFile(fileName)
	if err != nil {
		panic(err)
	}

	return strings.TrimSpace(string(resultBytes))
}