package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"os/user"
	"path/filepath"
	"strconv"
	"strings"

	"tildegit.org/sloum/pbmview/termios"
)

const (
	grays float64 = 23.0
	colors float64 = 255.0
	grayOffset int = 232
)

var maxFactor float64 = 23.0

type rgb struct {
	r int
	g int
	b int
}

type colorCell struct {
	top    rgb
	bottom rgb
}

func (c *colorCell) String() string {
	format := "\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm▀\033[0m"
	topR := int(float64(c.top.r) * maxFactor)
	topG := int(float64(c.top.g) * maxFactor)
	topB := int(float64(c.top.b) * maxFactor)
	bottomR := int(float64(c.bottom.r) * maxFactor)
	bottomG := int(float64(c.bottom.g) * maxFactor)
	bottomB := int(float64(c.bottom.b) * maxFactor)
	return fmt.Sprintf(format, topR, topG, topB, bottomR, bottomG, bottomB)
}

type cell struct {
	top    int
	bottom int
}

func (c *cell) String() string {
	format := "\033[38;5;%dm\033[48;5;%dm▀\033[0m"
	top := int(float64(c.top) * maxFactor) + grayOffset
	bottom := int(float64(c.bottom) * maxFactor) + grayOffset
	return fmt.Sprintf(format, top, bottom)
}


type image struct {
	width      int
	height     int
	pbmType    string
	maxVal     int
	maxFactor  float64

	colorImage [][]colorCell
	image      [][]cell
	// screenCols int
	// screenRows int
	// colOff     int
	// rowOff     int
}

func (i *image) String() string {
	var out strings.Builder
	if i.pbmType != "P3" {
		for r := range i.image {
			for _, c := range i.image[r] {
				out.WriteString(c.String())
			}
			out.WriteRune('\n')
		}
	} else {
		for r := range i.colorImage {
			for _, c := range i.colorImage[r] {
				out.WriteString(c.String())
			}
			out.WriteRune('\n')
		}
	}
	return out.String()
}

func (i *image) SetType(t string) error {
	if i.pbmType != "" {
		return fmt.Errorf("PBM type already set")
	}

	t = strings.TrimSpace(strings.ToUpper(t))
	switch t {
	case "P1", "P2", "P3":
		i.pbmType = t
		i.maxVal = 1
		return nil
	case "P4", "P5", "P6", "P7":
		return fmt.Errorf("%s is not yet supported", t)
	default:
		return fmt.Errorf("Invalid pbm type: %q", t)
	}
}

func (i *image) SetSize(s string, height bool) error {
	v, err := strconv.Atoi(s)
	if err != nil || v < 1 {
		return fmt.Errorf("Invalid dimension value: %s", s)
	}
	if height {
		i.height = v
	} else {
		i.width =  v
	}
	return nil
}

func (im *image) BuildColorCells(bm []int) {
	colorSlice := make([]rgb, 0, len(bm)/3)
	for i := 0; i < len(bm); i+=3 {
		colorSlice = append(colorSlice, rgb{bm[i], bm[i+1], bm[i+2]})
	}
	im.colorImage = make([][]colorCell, im.height/2)
	for row := 0; row < len(im.colorImage); row++ {
		for col := 0; col < im.width; col++ {
			im.colorImage[row] = append(im.colorImage[row], colorCell{colorSlice[row*2*im.width+col], rgb{}})
			bottomIndex := (row*2+1)*im.width+col
			if bottomIndex < len(colorSlice) {
				im.colorImage[row][col].bottom = colorSlice[bottomIndex]
			}
		}
	}
}

func (i *image) BuildCells(bm []int) {
	i.image = make([][]cell, i.height/2)
	for row := 0; row < len(i.image); row++ {
		for col := 0; col < i.width; col++ {
			i.image[row] = append(i.image[row], cell{bm[row*2*i.width+col], 0})
			bottomIndex := (row*2+1)*i.width+col
			if bottomIndex < len(bm) {
				i.image[row][col].bottom = bm[bottomIndex]
			}
		}
	}
}

func (i *image) ScaleColor(newWidth int) {
	newHeight := int(float64(newWidth) * (float64(i.height) / float64(i.width))) / 2

	out := make([][]colorCell, newHeight)
	for i := range out {
		out[i] = make([]colorCell, newWidth)
	}

	xRatio := float64(i.width) / float64(newWidth)
	yRatio := float64(i.height/2) / float64(newHeight)
	var px, py int
	for x := 0; x < newHeight; x++ {
		for y := 0; y < newWidth; y++ {
			px = int(float64(y) * xRatio)
			py = int(float64(x) * yRatio)
			out[x][y] = i.colorImage[py][px]
		}
	}
	i.colorImage = out
}

func (i *image) Scale(newWidth int) {
	newHeight := int(float64(newWidth) * (float64(i.height) / float64(i.width))) / 2

	out := make([][]cell, newHeight)
	for i := range out {
		out[i] = make([]cell, newWidth)
	}

	xRatio := float64(i.width) / float64(newWidth)
	yRatio := float64(i.height/2) / float64(newHeight)
	var px, py int
	for x := 0; x < newHeight; x++ {
		for y := 0; y < newWidth; y++ {
			px = int(float64(y) * xRatio)
			py = int(float64(x) * yRatio)
			out[x][y] = i.image[py][px]
		}
	}
	i.image = out
}

func NewImage(path string) (image, error) {
	if path == "" {
		HandleError(fmt.Errorf("No filepath provided"))
	}
	path = ExpandedAbsFilepath(path)
	fbytes, err := ioutil.ReadFile(path)
	if err != nil {
		fmt.Fprint(os.Stderr, "Unable to read file\n")
		os.Exit(1)
	}
	img := image{}
	img.maxVal = 1
	fdata := strings.Split(string(fbytes), "\n")
	var bitmap []int
	var intval int
	maxSet := false
	for _, line := range fdata {
		if len(line) < 1 || line[0] == '#' {
			continue
		}
		if ind := strings.Index(line, "#"); ind != -1 {
			line = line[:ind]
		}
		fields := strings.Fields(line)
		for _, field := range fields {
			if img.pbmType == "" {
				HandleError(img.SetType(field))
			} else if img.width == 0 {
				HandleError(img.SetSize(field, false))
			} else if img.height == 0 {
				HandleError(img.SetSize(field, true))
				bitmap = make([]int, 0, img.width*img.height)
			} else if img.pbmType != "P1" && !maxSet {
				maxSet = true
				mv, err := strconv.Atoi(field)
				if err != nil {
					HandleError(fmt.Errorf("Invalud max value: %s", field))
				}
				img.maxVal = mv
				if img.pbmType == "P3" {
					maxFactor = colors / float64(img.maxVal)
				} else {
					maxFactor = grays / float64(img.maxVal)
				}
			} else {
				intval, err = strconv.Atoi(field)
				if err != nil {
					HandleError(err)
				}
				bitmap = append(bitmap, intval)
			}
		}
	}
	multiplier := 1
	if img.pbmType == "P3" {
		multiplier = 3
	}
	if len(bitmap) != img.width * img.height * multiplier {
		// TODO remove this Printf once debugging is complete
		fmt.Printf("W: %d  H: %d  Vals: %d\n%v", img.width, img.height, len(bitmap), bitmap)
		HandleError(fmt.Errorf("Corrupt bitmap data"))
	}
	if img.pbmType == "P3" {
		img.BuildColorCells(bitmap)
	} else {
		img.BuildCells(bitmap)
	}

	return img, nil
}

func ExpandedAbsFilepath(p string) string {
	if strings.HasPrefix(p, "~/") {
		usr, _ := user.Current()
		homedir := usr.HomeDir
		p = filepath.Join(homedir, p[2:])
	}

	path, _ := filepath.Abs(p)
	return path
}

func HandleError(err error) {
	if err == nil {
		return
	}
	fmt.Fprintf(os.Stderr, err.Error())
	os.Exit(1)
}


func main() {
	fitToWidth := flag.Bool("fit", false, "Scale the image to fit the terminal width")
	scaleToWidth := flag.Int("scale", -1, "Scale the image to the given width")
	flag.Parse()
	img, err := NewImage(flag.Arg(0))
	if err != nil {
		HandleError(err)
	}
	if *fitToWidth || *scaleToWidth != -1 {
		cols, _ := termios.GetWindowSize()
		if img.width < cols {
			cols = img.width
		}
		if *scaleToWidth != -1 {
			cols = *scaleToWidth
		}
		if img.pbmType == "P3" {
			img.ScaleColor(cols)
		} else {
			img.Scale(cols)
		}
	}
	fmt.Print(img.String())
}
