// Copyright 2018 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package packing offers a packing algorithm in 2D space.
package packing

import (
	"errors"
	"fmt"
	"image"
)

type Page struct {
	root    *Node
	width   int
	height  int
	maxSize int
}

func isPositivePowerOf2(x int) bool {
	if x <= 0 {
		return false
	}
	return x&(x-1) == 0
}

func NewPage(initWidth, initHeight int, maxSize int) *Page {
	if !isPositivePowerOf2(initWidth) {
		panic(fmt.Sprintf("packing: initWidth must be a positive power of 2 but %d", initWidth))
	}
	if !isPositivePowerOf2(initHeight) {
		panic(fmt.Sprintf("packing: initHeight must be a positive power of 2 but %d", initHeight))
	}
	if !isPositivePowerOf2(maxSize) {
		panic(fmt.Sprintf("packing: maxSize must be a positive power of 2 but %d", maxSize))
	}
	return &Page{
		width:   initWidth,
		height:  initHeight,
		maxSize: maxSize,
	}
}

func (p *Page) IsEmpty() bool {
	if p.root == nil {
		return true
	}
	return !p.root.used && p.root.child0 == nil && p.root.child1 == nil
}

type Node struct {
	region image.Rectangle
	used   bool

	parent *Node
	child0 *Node
	child1 *Node
}

func (n *Node) canFree() bool {
	if n.used {
		return false
	}
	if n.child0 == nil && n.child1 == nil {
		return true
	}
	return n.child0.canFree() && n.child1.canFree()
}

func (n *Node) Region() image.Rectangle {
	return n.region
}

// square returns a float value indicating how much the given rectangle is close to a square.
// If the given rectangle is square, this return 1 (maximum value).
// Otherwise, this returns a value in [0, 1).
func square(width, height int) float64 {
	if width == 0 && height == 0 {
		return 0
	}
	if width <= height {
		return float64(width) / float64(height)
	}
	return float64(height) / float64(width)
}

func alloc(n *Node, width, height int) *Node {
	if n.region.Dx() < width || n.region.Dy() < height {
		return nil
	}
	if n.used {
		return nil
	}
	if n.child0 == nil && n.child1 == nil {
		if n.region.Dx() == width && n.region.Dy() == height {
			n.used = true
			return n
		}
		if square(n.region.Dx()-width, n.region.Dy()) >= square(n.region.Dx(), n.region.Dy()-height) {
			// Split vertically
			n.child0 = &Node{
				region: image.Rect(n.region.Min.X, n.region.Min.Y, n.region.Min.X+width, n.region.Max.Y),
				parent: n,
			}
			n.child1 = &Node{
				region: image.Rect(n.region.Min.X+width, n.region.Min.Y, n.region.Max.X, n.region.Max.Y),
				parent: n,
			}
		} else {
			// Split horizontally
			n.child0 = &Node{
				region: image.Rect(n.region.Min.X, n.region.Min.Y, n.region.Max.X, n.region.Min.Y+height),
				parent: n,
			}
			n.child1 = &Node{
				region: image.Rect(n.region.Min.X, n.region.Min.Y+height, n.region.Max.X, n.region.Max.Y),
				parent: n,
			}
		}
		// Note: it now MUST fit, due to above preconditions (repeated here).
		if n.child0.region.Dx() < width || n.child0.region.Dy() < height {
			panic(fmt.Sprintf("packing: the newly created child node (%d, %d) unexpectedly does not contain the requested size (%d, %d)", n.child0.region.Dx(), n.child0.region.Dy(), width, height))
		}
		// Thus, alloc can't return nil, but it may do another split along the other dimension
		// to get a node with the exact size (width, height).
		node := alloc(n.child0, width, height)
		if node == nil {
			panic(fmt.Sprintf("packing: could not allocate the requested size (%d, %d) in the newly created child node (%d, %d)", width, height, n.child0.region.Dx(), n.child0.region.Dy()))
		}
		return node
	}
	if n.child0 == nil || n.child1 == nil {
		panic("packing: both two children must not be nil at alloc")
	}
	if node := alloc(n.child0, width, height); node != nil {
		return node
	}
	if node := alloc(n.child1, width, height); node != nil {
		return node
	}
	return nil
}

func (p *Page) Size() (int, int) {
	return p.width, p.height
}

func (p *Page) Alloc(width, height int) *Node {
	if width <= 0 || height <= 0 {
		panic("packing: width and height must > 0")
	}

	if p.root == nil {
		p.root = &Node{
			region: image.Rect(0, 0, p.width, p.height),
		}
	}
	return p.extendAndAlloc(width, height)
}

func (p *Page) Free(node *Node) {
	if node.child0 != nil || node.child1 != nil {
		panic("packing: can't free the node including children")
	}
	node.used = false
	if node.parent == nil {
		return
	}
	if node.parent.child0 == nil || node.parent.child1 == nil {
		panic("packing: both two children must not be nil at Free: double free happened?")
	}
	if node.parent.child0.canFree() && node.parent.child1.canFree() {
		node.parent.child0 = nil
		node.parent.child1 = nil
		p.Free(node.parent)
	}
}

var (
	skipAll      = errors.New("skip all")
	skipChildren = errors.New("skip children")
)

func walk(n *Node, f func(n *Node) error) error {
	if err := f(n); err != nil {
		if errors.Is(err, skipChildren) {
			return nil
		}
		return err
	}
	if n.child0 != nil {
		if err := walk(n.child0, f); err != nil {
			return err
		}
	}
	if n.child1 != nil {
		if err := walk(n.child1, f); err != nil {
			return err
		}
	}
	return nil
}

func (p *Page) extendAndAlloc(width, height int) *Node {
	if n := alloc(p.root, width, height); n != nil {
		return n
	}

	if p.width >= p.maxSize && p.height >= p.maxSize {
		return nil
	}

	// (1, 0), (0, 1), (2, 0), (1, 1), (0, 2), (3, 0), (2, 1), (1, 2), (0, 3), ...
	for i := 1; ; i++ {
		for j := 0; j <= i; j++ {
			newWidth := p.width
			for k := 0; k < i-j; k++ {
				newWidth *= 2
			}
			newHeight := p.height
			for k := 0; k < j; k++ {
				newHeight *= 2
			}

			if newWidth > p.maxSize || newHeight > p.maxSize {
				if newWidth > p.maxSize && newHeight > p.maxSize {
					panic(fmt.Sprintf("packing: too big extension: allocating size: (%d, %d), current size: (%d, %d), new size: (%d, %d), (i, j): (%d, %d), max size: %d", width, height, p.width, p.height, newWidth, newHeight, i, j, p.maxSize))
				}
				continue
			}

			rollback := p.extend(newWidth, newHeight)
			if n := alloc(p.root, width, height); n != nil {
				return n
			}
			rollback()

			// If the allocation failed even with a maximized page, give up the allocation.
			if newWidth >= p.maxSize && newHeight >= p.maxSize {
				return nil
			}
		}
	}
}

func (p *Page) extend(newWidth int, newHeight int) func() {
	var edgeNodes []*Node
	var edgeAllocated bool
	if p.root != nil {
		_ = walk(p.root, func(n *Node) error {
			if n.region.Max.X < p.width && n.region.Max.Y < p.height {
				return nil
			}
			if n.used {
				edgeAllocated = true
				return skipAll
			}
			edgeNodes = append(edgeNodes, n)
			return nil
		})
	}

	var rollback func()

	if edgeAllocated {
		// As there is at least one allocated on the edge.
		// Extend the page by simply adding a region.

		origRoot := p.root
		origRootCloned := *p.root

		// Extend the page in the vertical direction.
		if newHeight-p.height > 0 {
			upper := p.root
			lower := &Node{
				region: image.Rect(0, p.height, p.width, newHeight),
			}
			p.root = &Node{
				region: image.Rect(0, 0, p.width, newHeight),
				child0: upper,
				child1: lower,
			}
			upper.parent = p.root
			lower.parent = p.root
		}

		// Extend the page in the horizontal direction.
		if newWidth-p.width > 0 {
			left := p.root
			right := &Node{
				region: image.Rect(p.width, 0, newWidth, newHeight),
			}
			p.root = &Node{
				region: image.Rect(0, 0, newWidth, newHeight),
				child0: left,
				child1: right,
			}
			left.parent = p.root
			right.parent = p.root
		}

		origWidth, origHeight := p.width, p.height
		rollback = func() {
			p.width = origWidth
			p.height = origHeight
			// The node address must not be changed, so use the original root node's pointer (#2584).
			// As the root node might be modified, restore the content by the cloned content.
			p.root = origRoot
			*p.root = origRootCloned
		}
	} else {
		// Extend the page by extending edge nodes.

		origWidth, origHeight := p.width, p.height
		origMaxXs := map[*Node]int{}
		origMaxYs := map[*Node]int{}

		for _, n := range edgeNodes {
			if n.region.Max.X == p.width {
				origMaxXs[n] = n.region.Max.X
				n.region.Max.X = newWidth
			}
			if n.region.Max.Y == p.height {
				origMaxYs[n] = n.region.Max.Y
				n.region.Max.Y = newHeight
			}
		}

		rollback = func() {
			p.width = origWidth
			p.height = origHeight
			for n, x := range origMaxXs {
				n.region.Max.X = x
			}
			for n, y := range origMaxYs {
				n.region.Max.Y = y
			}
		}
	}

	p.width = newWidth
	p.height = newHeight

	return rollback
}

func (p *Page) AllocatedRegion() image.Rectangle {
	var r image.Rectangle
	if p.root == nil {
		return r
	}
	_ = walk(p.root, func(n *Node) error {
		if n.region.In(r) {
			return skipChildren
		}
		if n.used {
			r = r.Union(n.region)
			return skipChildren
		}
		return nil
	})
	return r
}
