/*Package histogram provides basic histogram types and functions to analyze RGBA images.*/
package histogram

import (
	"image"

	"github.com/anthonynsimon/bild/clone"
)

// RGBAHistogram holds a sub-histogram per RGBA channel.
// Each channel histogram contains 256 bins (8-bit color depth per channel).
type RGBAHistogram struct {
	R Histogram
	G Histogram
	B Histogram
	A Histogram
}

// Histogram holds a variable length slice of bins, which keeps track of sample counts.
type Histogram struct {
	Bins []int
}

// Max returns the highest count found in the histogram bins.
func (h *Histogram) Max() int {
	var max int
	if len(h.Bins) > 0 {
		max = h.Bins[0]
		for i := 1; i < len(h.Bins); i++ {
			if h.Bins[i] > max {
				max = h.Bins[i]
			}
		}
	}
	return max
}

// Min returns the lowest count found in the histogram bins.
func (h *Histogram) Min() int {
	var min int
	if len(h.Bins) > 0 {
		min = h.Bins[0]
		for i := 1; i < len(h.Bins); i++ {
			if h.Bins[i] < min {
				min = h.Bins[i]
			}
		}
	}
	return min
}

// Cumulative returns a new Histogram in which each bin is the cumulative
// value of its previous bins
func (h *Histogram) Cumulative() *Histogram {
	binCount := len(h.Bins)
	out := Histogram{make([]int, binCount)}

	if binCount > 0 {
		out.Bins[0] = h.Bins[0]
	}

	for i := 1; i < binCount; i++ {
		out.Bins[i] = out.Bins[i-1] + h.Bins[i]
	}

	return &out
}

// Image returns a grayscale image representation of the Histogram.
// The width and height of the image will be equivalent to the number of Bins in the Histogram.
func (h *Histogram) Image() *image.Gray {
	dstW, dstH := len(h.Bins), len(h.Bins)
	dst := image.NewGray(image.Rect(0, 0, dstW, dstH))

	max := h.Max()
	if max == 0 {
		max = 1
	}

	for x := 0; x < dstW; x++ {
		value := ((h.Bins[x] << 16 / max) * dstH) >> 16
		// Fill from the bottom up
		for y := dstH - 1; y > dstH-value-1; y-- {
			dst.Pix[y*dst.Stride+x] = 0xFF
		}
	}
	return dst
}

// NewRGBAHistogram constructs a RGBAHistogram out of the provided image.
// A sub-histogram is created per RGBA channel with 256 bins each.
func NewRGBAHistogram(img image.Image) *RGBAHistogram {
	src := clone.AsRGBA(img)

	binCount := 256
	r := Histogram{make([]int, binCount)}
	g := Histogram{make([]int, binCount)}
	b := Histogram{make([]int, binCount)}
	a := Histogram{make([]int, binCount)}

	for y := 0; y < src.Bounds().Dy(); y++ {
		for x := 0; x < src.Bounds().Dx(); x++ {
			pos := y*src.Stride + x*4
			r.Bins[src.Pix[pos+0]]++
			g.Bins[src.Pix[pos+1]]++
			b.Bins[src.Pix[pos+2]]++
			a.Bins[src.Pix[pos+3]]++
		}
	}

	return &RGBAHistogram{R: r, G: g, B: b, A: a}
}

// Cumulative returns a new RGBAHistogram in which each bin is the cumulative
// value of its previous bins per channel.
func (h *RGBAHistogram) Cumulative() *RGBAHistogram {
	binCount := len(h.R.Bins)

	r := Histogram{make([]int, binCount)}
	g := Histogram{make([]int, binCount)}
	b := Histogram{make([]int, binCount)}
	a := Histogram{make([]int, binCount)}

	out := RGBAHistogram{R: r, G: g, B: b, A: a}

	if binCount > 0 {
		out.R.Bins[0] = h.R.Bins[0]
		out.G.Bins[0] = h.G.Bins[0]
		out.B.Bins[0] = h.B.Bins[0]
		out.A.Bins[0] = h.A.Bins[0]
	}

	for i := 1; i < binCount; i++ {
		out.R.Bins[i] = out.R.Bins[i-1] + h.R.Bins[i]
		out.G.Bins[i] = out.G.Bins[i-1] + h.G.Bins[i]
		out.B.Bins[i] = out.B.Bins[i-1] + h.B.Bins[i]
		out.A.Bins[i] = out.A.Bins[i-1] + h.A.Bins[i]
	}

	return &out
}

// Image returns an RGBA image representation of the RGBAHistogram.
// An image width of 256 represents the 256 Bins per channel and the
// image height of 256 represents the max normalized histogram value per channel.
// Each RGB channel from the histogram is mapped to its corresponding channel in the image,
// so that for example if the red channel is extracted from the image, it corresponds to the
// red channel histogram.
func (h *RGBAHistogram) Image() *image.RGBA {
	if len(h.R.Bins) != 256 || len(h.G.Bins) != 256 ||
		len(h.B.Bins) != 256 || len(h.A.Bins) != 256 {
		panic("RGBAHistogram bins length not equal to 256")
	}

	dstW, dstH := 256, 256
	dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH))

	maxR := h.R.Max()
	if maxR == 0 {
		maxR = 1
	}
	maxG := h.G.Max()
	if maxG == 0 {
		maxG = 1
	}
	maxB := h.B.Max()
	if maxB == 0 {
		maxB = 1
	}

	for x := 0; x < dstW; x++ {
		binHeightR := ((h.R.Bins[x] << 16 / maxR) * dstH) >> 16
		binHeightG := ((h.G.Bins[x] << 16 / maxG) * dstH) >> 16
		binHeightB := ((h.B.Bins[x] << 16 / maxB) * dstH) >> 16
		// Fill from the bottom up
		for y := dstH - 1; y >= 0; y-- {
			pos := y*dst.Stride + x*4
			iy := dstH - 1 - y

			if iy < binHeightR {
				dst.Pix[pos+0] = 0xFF
			}
			if iy < binHeightG {
				dst.Pix[pos+1] = 0xFF
			}
			if iy < binHeightB {
				dst.Pix[pos+2] = 0xFF
			}
			dst.Pix[pos+3] = 0xFF
		}
	}

	return dst
}
