// Copyright ©2015 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package functions

import (
	"fmt"
	"math"
)

// MinimalSurface implements a finite element approximation to a minimal
// surface problem: determine the surface with minimal area and given boundary
// values in a unit square centered at the origin.
//
// References:
//
//	Averick, M.B., Carter, R.G., Moré, J.J., Xue, G.-L.: The Minpack-2 Test
//	Problem Collection. Preprint MCS-P153-0692, Argonne National Laboratory (1992)
type MinimalSurface struct {
	bottom, top  []float64
	left, right  []float64
	origin, step [2]float64
}

// NewMinimalSurface creates a new discrete minimal surface problem and
// precomputes its boundary values. The problem is discretized on a rectilinear
// grid with nx×ny nodes which means that the problem dimension is (nx-2)(ny-2).
func NewMinimalSurface(nx, ny int) *MinimalSurface {
	ms := &MinimalSurface{
		bottom: make([]float64, nx),
		top:    make([]float64, nx),
		left:   make([]float64, ny),
		right:  make([]float64, ny),
		origin: [2]float64{-0.5, -0.5},
		step:   [2]float64{1 / float64(nx-1), 1 / float64(ny-1)},
	}

	ms.initBoundary(ms.bottom, ms.origin[0], ms.origin[1], ms.step[0], 0)
	startY := ms.origin[1] + float64(ny-1)*ms.step[1]
	ms.initBoundary(ms.top, ms.origin[0], startY, ms.step[0], 0)
	ms.initBoundary(ms.left, ms.origin[0], ms.origin[1], 0, ms.step[1])
	startX := ms.origin[0] + float64(nx-1)*ms.step[0]
	ms.initBoundary(ms.right, startX, ms.origin[1], 0, ms.step[1])

	return ms
}

// Func returns the area of the surface represented by the vector x.
func (ms *MinimalSurface) Func(x []float64) (area float64) {
	nx, ny := ms.Dims()
	if len(x) != (nx-2)*(ny-2) {
		panic("functions: problem size mismatch")
	}

	hx, hy := ms.Steps()
	for j := 0; j < ny-1; j++ {
		for i := 0; i < nx-1; i++ {
			vLL := ms.at(i, j, x)
			vLR := ms.at(i+1, j, x)
			vUL := ms.at(i, j+1, x)
			vUR := ms.at(i+1, j+1, x)

			dvLdx := (vLR - vLL) / hx
			dvLdy := (vUL - vLL) / hy
			dvUdx := (vUR - vUL) / hx
			dvUdy := (vUR - vLR) / hy

			fL := math.Sqrt(1 + dvLdx*dvLdx + dvLdy*dvLdy)
			fU := math.Sqrt(1 + dvUdx*dvUdx + dvUdy*dvUdy)
			area += fL + fU
		}
	}
	area *= 0.5 * hx * hy
	return area
}

// Grad evaluates the area gradient of the surface represented by the vector.
func (ms *MinimalSurface) Grad(grad, x []float64) []float64 {
	nx, ny := ms.Dims()
	if len(x) != (nx-2)*(ny-2) {
		panic("functions: problem size mismatch")
	}
	if grad == nil {
		grad = make([]float64, len(x))
	}
	if len(x) != len(grad) {
		panic("functions: unexpected size mismatch")
	}

	for i := range grad {
		grad[i] = 0
	}
	hx, hy := ms.Steps()
	for j := 0; j < ny-1; j++ {
		for i := 0; i < nx-1; i++ {
			vLL := ms.at(i, j, x)
			vLR := ms.at(i+1, j, x)
			vUL := ms.at(i, j+1, x)
			vUR := ms.at(i+1, j+1, x)

			dvLdx := (vLR - vLL) / hx
			dvLdy := (vUL - vLL) / hy
			dvUdx := (vUR - vUL) / hx
			dvUdy := (vUR - vLR) / hy

			fL := math.Sqrt(1 + dvLdx*dvLdx + dvLdy*dvLdy)
			fU := math.Sqrt(1 + dvUdx*dvUdx + dvUdy*dvUdy)

			if grad != nil {
				if i > 0 {
					if j > 0 {
						grad[ms.index(i, j)] -= (dvLdx/hx + dvLdy/hy) / fL
					}
					if j < ny-2 {
						grad[ms.index(i, j+1)] += (dvLdy/hy)/fL - (dvUdx/hx)/fU
					}
				}
				if i < nx-2 {
					if j > 0 {
						grad[ms.index(i+1, j)] += (dvLdx/hx)/fL - (dvUdy/hy)/fU
					}
					if j < ny-2 {
						grad[ms.index(i+1, j+1)] += (dvUdx/hx + dvUdy/hy) / fU
					}
				}
			}
		}

	}
	cellSize := 0.5 * hx * hy
	for i := range grad {
		grad[i] *= cellSize
	}
	return grad
}

// InitX returns a starting location for the minimization problem. Length of
// the returned slice is (nx-2)(ny-2).
func (ms *MinimalSurface) InitX() []float64 {
	nx, ny := ms.Dims()
	x := make([]float64, (nx-2)*(ny-2))
	for j := 1; j < ny-1; j++ {
		for i := 1; i < nx-1; i++ {
			x[ms.index(i, j)] = (ms.left[j] + ms.bottom[i]) / 2
		}
	}
	return x
}

// ExactX returns the exact solution to the _continuous_ minimization problem
// projected on the interior nodes of the grid. Length of the returned slice is
// (nx-2)(ny-2).
func (ms *MinimalSurface) ExactX() []float64 {
	nx, ny := ms.Dims()
	v := make([]float64, (nx-2)*(ny-2))
	for j := 1; j < ny-1; j++ {
		for i := 1; i < nx-1; i++ {
			v[ms.index(i, j)] = ms.ExactSolution(ms.x(i), ms.y(j))
		}
	}
	return v
}

// ExactSolution returns the value of the exact solution to the minimal surface
// problem at (x,y). The exact solution is
//
//	F_exact(x,y) = U^2(x,y) - V^2(x,y),
//
// where U and V are the unique solutions to the equations
//
//	x =  u + uv^2 - u^3/3,
//	y = -v - u^2v + v^3/3.
func (ms *MinimalSurface) ExactSolution(x, y float64) float64 {
	var u = [2]float64{x, -y}
	var f [2]float64
	var jac [2][2]float64
	for k := 0; k < 100; k++ {
		f[0] = u[0] + u[0]*u[1]*u[1] - u[0]*u[0]*u[0]/3 - x
		f[1] = -u[1] - u[0]*u[0]*u[1] + u[1]*u[1]*u[1]/3 - y
		fNorm := math.Hypot(f[0], f[1])
		if fNorm < 1e-13 {
			break
		}
		jac[0][0] = 1 + u[1]*u[1] - u[0]*u[0]
		jac[0][1] = 2 * u[0] * u[1]
		jac[1][0] = -2 * u[0] * u[1]
		jac[1][1] = -1 - u[0]*u[0] + u[1]*u[1]
		det := jac[0][0]*jac[1][1] - jac[0][1]*jac[1][0]
		u[0] -= (jac[1][1]*f[0] - jac[0][1]*f[1]) / det
		u[1] -= (jac[0][0]*f[1] - jac[1][0]*f[0]) / det
	}
	return u[0]*u[0] - u[1]*u[1]
}

// Dims returns the size of the underlying rectilinear grid.
func (ms *MinimalSurface) Dims() (nx, ny int) {
	return len(ms.bottom), len(ms.left)
}

// Steps returns the spatial step sizes of the underlying rectilinear grid.
func (ms *MinimalSurface) Steps() (hx, hy float64) {
	return ms.step[0], ms.step[1]
}

func (ms *MinimalSurface) x(i int) float64 {
	return ms.origin[0] + float64(i)*ms.step[0]
}

func (ms *MinimalSurface) y(j int) float64 {
	return ms.origin[1] + float64(j)*ms.step[1]
}

func (ms *MinimalSurface) at(i, j int, x []float64) float64 {
	nx, ny := ms.Dims()
	if i < 0 || i >= nx {
		panic(fmt.Sprintf("node [%v,%v] not on grid", i, j))
	}
	if j < 0 || j >= ny {
		panic(fmt.Sprintf("node [%v,%v] not on grid", i, j))
	}

	if i == 0 {
		return ms.left[j]
	}
	if j == 0 {
		return ms.bottom[i]
	}
	if i == nx-1 {
		return ms.right[j]
	}
	if j == ny-1 {
		return ms.top[i]
	}
	return x[ms.index(i, j)]
}

// index maps an interior grid node (i, j) to a one-dimensional index and
// returns it.
func (ms *MinimalSurface) index(i, j int) int {
	nx, ny := ms.Dims()
	if i <= 0 || i >= nx-1 {
		panic(fmt.Sprintf("[%v,%v] is not an interior node", i, j))
	}
	if j <= 0 || j >= ny-1 {
		panic(fmt.Sprintf("[%v,%v] is not an interior node", i, j))
	}

	return i - 1 + (j-1)*(nx-2)
}

// initBoundary initializes with the exact solution the boundary b whose i-th
// element b[i] is located at [startX+i×hx, startY+i×hy].
func (ms *MinimalSurface) initBoundary(b []float64, startX, startY, hx, hy float64) {
	for i := range b {
		x := startX + float64(i)*hx
		y := startY + float64(i)*hy
		b[i] = ms.ExactSolution(x, y)
	}
}
