// Copyright (C) 2018 Marius Schellenberger

package main

import (
	"encoding/binary"
	"errors"
	"flag"
	"fmt"
	"image"
	"image/color"
	_ "image/gif"
	_ "image/jpeg"
	"image/png"
	"io/ioutil"
	"os"
)

const offset = 4

func encodeLen(l int) []byte {
	b := make([]byte, offset)
	binary.LittleEndian.PutUint32(b, uint32(l))
	return b
}

func getLen(p image.Image) int {
	z := make([]byte, offset)
	for i := 0; i < offset; i++ {
		pix := color.NRGBAModel.Convert(p.At(i, 0)).(color.NRGBA)
		z[i] = (pix.R << 6)
		z[i] |= (pix.G << 6) >> 2
		z[i] |= (pix.B << 6) >> 4
		z[i] |= (pix.A << 6) >> 6
	}
	return int(binary.LittleEndian.Uint32(z))
}

func unseal(p image.Image) (b []byte) {
	rect := p.Bounds()
	l := getLen(p)
	c := 0
	for i := 0; i < rect.Max.Y; i++ {
		for j := 0; j < rect.Max.X; j++ {
			if i == 0 && j == 0 {
				j = offset
			}
			pix := color.NRGBAModel.Convert(p.At(j, i)).(color.NRGBA)
			z := (pix.R << 6)
			z |= (pix.G << 6) >> 2
			z |= (pix.B << 6) >> 4
			z |= (pix.A << 6) >> 6
			if c == l {
				return
			}
			b = append(b, z)
			c++
		}
	}
	return
}

func seal(p image.Image, s []byte) (image.Image, error) {
	rect := p.Bounds()
	n := image.NewNRGBA(rect)
	c := 0
	arr := append(encodeLen(len(s)), s...)
	l := len(arr)
	max := (rect.Max.Y * rect.Max.X)
	if l > max {
		return nil, errors.New("not enough space in image")
	}
	for i := 0; i < rect.Max.Y; i++ {
		for j := 0; j < rect.Max.X; j++ {
			pix := color.NRGBAModel.Convert(p.At(j, i)).(color.NRGBA)
			if c < l {
				r := (pix.R &^ 3) | (arr[c] >> 6)
				g := (pix.G &^ 3) | (arr[c]<<2)>>6
				b := (pix.B &^ 3) | (arr[c]<<4)>>6
				a := (pix.A &^ 3) | (arr[c]<<6)>>6
				n.Set(j, i, color.NRGBA{r, g, b, a})
				c++
			} else {
				n.Set(j, i, pix)
			}
		}
	}
	return n, nil
}

var (
	inFile  string
	outFile string
	secret  string
)

func errf(err error) {
	fmt.Fprintln(os.Stderr, err)
	os.Exit(1)
}

func main() {
	flag.StringVar(&inFile, "in", "-", "input file (default: stdin)")
	flag.StringVar(&outFile, "out", "-", "output file, always png (default: stdout)")
	flag.StringVar(&secret, "secret", "", "secret file to hide")
	flag.Parse()
	var (
		err error
		in  = os.Stdin
		out = os.Stdout
	)
	if inFile != "-" {
		in, err = os.Open(inFile)
		if err != nil {
			errf(err)
		}
	}
	if outFile != "-" {
		out, err = os.OpenFile(outFile, os.O_RDWR|os.O_CREATE, 0644)
		if err != nil {
			errf(err)
		}
	}
	p, _, err := image.Decode(in)
	if err != nil {
		errf(err)
	}
	if secret != "" {
		b, err := ioutil.ReadFile(secret)
		if err != nil {
			errf(err)
		}
		n, err := seal(p, b)
		if err != nil {
			errf(err)
		}
		err = png.Encode(out, n)
		if err != nil {
			errf(err)
		}
		return
	}
	_, err = out.Write(unseal(p))
	if err != nil {
		errf(err)
	}
}
