package identicon

import (
	"errors"
	"fmt"
	"image"
	"math"
	"strings"

	"git.mstar.dev/mstar/canvas"
	"git.mstar.dev/mstar/canvas/backend/softwarebackend"
	mathutils "git.mstar.dev/mstar/goutils/math"
	"git.mstar.dev/mstar/goutils/sliceutils"
)

type identicon struct {
	canvas             *canvas.Canvas
	primary            string
	secondary          string
	margin             int
	scale              float64
	cellSize           int
	hash               int
	blocks             []block
	shape              shape
	hasStroke          bool
	strokeWeight       int
	strokeColor        string
	compositeOperation string
	palette            []string
}

const canvasSize = 500

var (
	palette = []string{
		"#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3",
		"#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39",
		"#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#607D8B",
	}
	validOperations = []string{
		"source-over", "source-in", "source-out", "source-atop", "destination-over",
		"destination-in", "destination-out", "destination-atop", "lighter", "copy",
		"xor", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge",
		"color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue",
		"saturation", "color", "luminosity",
	}
)

// Deprecated: Doesn't work
func GenerateIdenticon(text string) *image.RGBA {
	backend := softwarebackend.New(canvasSize, canvasSize)
	canvas := canvas.New(backend)
	i := identicon{
		canvas: canvas,
		hash:   hash(text),
	}
	i.Init()
	i.Draw()
	tmp := *backend.Image
	return &tmp
}

func hash(text string) int {
	runes := []rune(text)
	return sliceutils.Compact(
		sliceutils.Map(runes, func(t rune) int { return int(t) }),
		func(acc, next int) int {
			acc = ((acc << 5) - acc) + next
			return acc & acc
		},
	)
}

func (i *identicon) SetUsername(username string) {
	i.hash = hash(username)
}

func (i *identicon) SetPalette(pal []string) error {
	if len(pal) < 2 {
		return errors.New("palette to small")
	}
	i.palette = pal
	return nil
}

func (i *identicon) SetCompositeOperation(newOp string) error {
	newOp = strings.ToLower(newOp)
	if !sliceutils.Contains(validOperations, newOp) {
		return errors.New("invalid operation")
	}
	i.compositeOperation = newOp
	return nil
}

func (i *identicon) Draw() {
	i.Init()
	i.canvas.Save()
	i.canvas.Translate(float64(canvasSize/2), float64(canvasSize/2))
	i.canvas.Rotate(-math.Pi / 4)
	i.canvas.Translate(float64(canvasSize/2), float64(canvasSize/2))
	i.GenerateBlocks()
	i.DrawBlocks()
	if i.hasStroke {
		i.DrawOutline()
	}
	i.shape = shape{
		canvas:    i.canvas,
		hash:      i.hash,
		primary:   i.primary,
		secondary: i.secondary,
		pos: vec2{
			X: float64(i.margin*int(i.scale)) + 1.5*float64(i.cellSize),
			Y: float64(i.margin)*i.scale + 0.5*float64(i.cellSize),
		},
		scale:       i.scale,
		cellSize:    i.cellSize,
		strokeColor: i.strokeColor,
	}
	i.shape.Draw(i.hasStroke, i.strokeWeight)
	i.canvas.Restore()
}

func (i *identicon) Init() {
	i.blocks = []block{}
	i.shape = shape{}
	i.primary = palette[mathutils.Abs(i.hash%len(palette))]
	i.secondary = palette[mathutils.Abs(hash(fmt.Sprint(i.hash))%len(palette))]
	// i.canvas.SetGlobalCompositeOperation("source-over")
	i.canvas.ClearRect(0, 0, canvasSize, canvasSize)
	// i.canvas.SetGlobalCompositeOperation(i.compositeOperation)
}

func (i *identicon) Offset() {
	i.canvas.Save()
	i.canvas.Translate(0.6*i.scale, -0.6*i.scale)
}

func (i *identicon) ResetOffset() {
	i.canvas.Restore()
}

func (i *identicon) GenerateBlocks() {
	i.blocks = append(i.blocks, block{
		canvas:    i.canvas,
		blockType: 1,
		primary:   i.primary,
		secondary: i.secondary,
		hash:      i.hash,
		pos:       vec2{float64(i.margin) * i.scale, float64(i.margin) * i.scale},
		cellSize:  i.cellSize,
		margin:    i.margin,
		scale:     i.scale,
	}, block{
		canvas:    i.canvas,
		blockType: 2,
		primary:   i.primary,
		secondary: i.primary,
		hash:      i.hash,
		pos:       vec2{float64(i.margin) * i.scale, float64(i.canvas.Height()) / 2},
		cellSize:  i.cellSize,
		margin:    i.margin,
		scale:     i.scale,
	}, block{
		canvas:    i.canvas,
		blockType: 3,
		primary:   i.primary,
		secondary: i.secondary,
		hash:      i.hash,
		pos:       vec2{float64(i.canvas.Width()) / 2, float64(i.canvas.Height()) / 2},
		cellSize:  i.cellSize,
		margin:    i.margin,
		scale:     i.scale,
	},
	)
}

func (i *identicon) DrawOutline() {
	i.Offset()
	// i.canvas.SetGlobalCompositeOperation("source-over")
	i.canvas.BeginPath()
	i.canvas.MoveTo(float64(i.margin)*i.scale, float64(i.margin)*i.scale)
	i.canvas.LineTo(float64(i.margin)*i.scale, float64(canvasSize)-float64(i.margin)*i.scale)
	i.canvas.LineTo(
		float64(i.canvas.Width())-float64(i.margin)*i.scale,
		float64(i.canvas.Height())-float64(i.margin)*i.scale,
	)
	i.canvas.LineTo(
		float64(i.canvas.Width())-float64(i.margin)*i.scale,
		float64(i.canvas.Height())/2,
	)
	i.canvas.LineTo(float64(i.canvas.Width())/2, float64(i.canvas.Height())/2)
	i.canvas.LineTo(float64(i.canvas.Width())/2, float64(i.margin)*i.scale)
	i.canvas.ClosePath()

	i.canvas.SetStrokeStyle(i.strokeColor)
	i.canvas.SetLineWidth(i.scale * (float64(i.strokeWeight) / float64(i.canvas.Width())))
	i.canvas.SetLineJoin(canvas.Round)
	i.canvas.SetLineCap(canvas.Round)
	i.canvas.Stroke()

	i.canvas.BeginPath()
	i.canvas.MoveTo(float64(i.canvas.Width())/2, float64(i.canvas.Height())/2)
	i.canvas.LineTo(float64(i.margin)*i.scale, float64(i.canvas.Height())/2)
	i.canvas.MoveTo(float64(i.canvas.Width())/2, float64(i.canvas.Height())/2)
	i.canvas.LineTo(
		float64(i.canvas.Width())/2,
		float64(i.canvas.Height())-(float64(i.margin)*i.scale),
	)
	i.canvas.Stroke()
	i.ResetOffset()
}

func (i *identicon) DrawBlocks() {
	for _, b := range i.blocks {
		b.Draw()
	}
}
