package fastpath

import (
	"unsafe"
)

// Clean: see Builder.Clean(). Analogous to path.Clean().
func Clean(path string) string {
	return (&Builder{}).Clean(path)
}

// Join: see Builder.Join(). Analogous to path.Join().
func Join(elems ...string) string {
	return (&Builder{}).Join(elems...)
}

// Builder provides a means of cleaning and joining system paths,
// while retaining a singular underlying byte buffer for performance.
type Builder struct {
	// B is the underlying byte buffer
	B []byte

	dd  int  // pos of last '..' appended to builder
	abs bool // abs stores whether path passed to first .Append() is absolute
	set bool // set stores whether b.abs has been set i.e. not first call to .Append()
}

// Reset resets the Builder object
func (b *Builder) Reset() {
	b.B = b.B[:0]
	b.dd = 0
	b.abs = false
	b.set = false
}

// Len returns the number of
// accumulated bytes in the Builder.
func (b Builder) Len() int {
	return len(b.B)
}

// Cap returns the capacity of
// the underlying Builder buffer.
func (b Builder) Cap() int {
	return cap(b.B)
}

// Bytes returns the accumulated path bytes.
func (b Builder) Bytes() []byte {
	return b.B
}

// String returns the accumulated path string.
func (b Builder) String() string {
	return unsafe.String(unsafe.SliceData(b.B), len(b.B))
}

// Absolute returns whether current path is absolute (not relative).
func (b Builder) Absolute() bool {
	return b.abs
}

// SetAbsolute converts the current path to-or-from absolute.
func (b *Builder) SetAbsolute(enabled bool) {
	if !b.set {
		// Ensure 1B avail
		b.Guarantee(1)

		if enabled {
			// Set empty 'abs'
			b.appendByte('/')
			b.abs = true
		} else {
			// Set empty 'rel'
			b.appendByte('.')
			b.abs = false
		}

		b.set = true
		return
	}

	if !enabled && b.abs {
		// set && absolute
		// -> update
		b.abs = false

		// If empty, set to '.' (empty rel path)
		if len(b.B) == 0 || (len(b.B) == 1 && b.B[0] == '/') {
			b.Guarantee(1)
			b.B = b.B[:1]
			b.B[0] = '.'
			return
		}

		if b.B[0] != '/' {
			// No need to change
			return
		}

		if len(b.B) > 1 {
			// Shift bytes 1 left
			copy(b.B, b.B[1:])
		}

		// and drop the '/' prefix'
		b.B = b.B[:len(b.B)-1]
	} else if enabled && !b.abs {
		// set && !absolute
		// -> update
		b.abs = true

		// Ensure 1B avail
		b.Guarantee(1)

		// If empty, set to '/' (empty abs path)
		if len(b.B) == 0 || (len(b.B) == 1 && b.B[0] == '.') {
			b.Guarantee(1)
			b.B = b.B[:1]
			b.B[0] = '/'
			return
		}

		// Increase length
		l := len(b.B)
		b.B = b.B[:l+1]

		// Shift bytes 1 right
		copy(b.B[1:], b.B[:l])

		// Set first byte '/'
		b.B[0] = '/'
	}
}

// AppendBytes adds and cleans the supplied path bytes to the
// builder's internal buffer, growing the buffer if necessary
// to accomodate the extra path length.
func (b *Builder) AppendBytes(path []byte) {
	if len(path) == 0 {
		return
	}
	b.Guarantee(len(path) + 1)
	b.append(unsafe.String(unsafe.SliceData(path), len(path)))
}

// Append adds and cleans the supplied path string to the
// builder's internal buffer, growing the buffer if necessary
// to accomodate the extra path length.
func (b *Builder) Append(path string) {
	if len(path) == 0 {
		return
	}
	b.Guarantee(len(path) + 1)
	b.append(path)
}

// Clean creates the shortest possible functional equivalent
// to the supplied path, resetting the builder before performing
// this operation. The builder object is NOT reset after return.
func (b *Builder) Clean(path string) string {
	if path == "" {
		return "."
	}
	b.Reset()
	b.Guarantee(len(path) + 1)
	b.append(path)
	return string(b.B)
}

// Join connects and cleans multiple paths, resetting the builder before
// performing this operation and returning the shortest possible combination
// of all the supplied paths. The builder object is NOT reset after return.
func (b *Builder) Join(elems ...string) string {
	var size int
	for _, elem := range elems {
		size += len(elem)
	}
	if size == 0 {
		return ""
	}
	b.Reset()
	b.Guarantee(size + 1)
	for _, elem := range elems {
		if elem == "" {
			continue
		}
		b.append(elem)
	}
	return string(b.B)
}

// append performs the main logic of 'Append()' but without an empty path check or preallocation.
func (b *Builder) append(path string) {
	if !b.set {
		// Set if absolute or not
		b.abs = path[0] == '/'
		b.set = true
	} else if !b.abs && len(b.B) == 1 && b.B[0] == '.' {
		// Empty non-abs path segment, drop
		// the period so not prefixed './'
		b.B = b.B[:0]
	}

	for i := 0; i < len(path); {
		switch {
		// Empty path segment
		case path[i] == '/':
			i++

		// Singular '.' path segment, treat as empty
		case path[i] == '.' && (i+1 == len(path) || path[i+1] == '/'):
			i++

		// Backtrack segment
		case path[i] == '.' && path[i+1] == '.' && (i+2 == len(path) || path[i+2] == '/'):
			i += 2

			switch {
			// Check if it's possible to backtrack with
			// our current state of the buffer. i.e. is
			// our buffer length longer than the last
			// '..' we placed?
			case len(b.B) > b.dd:
				b.backtrack()

			// If we reached here, need to check if
			// we can append '..' to the path buffer,
			// which is ONLY when path is NOT absolute
			case !b.abs:
				if len(b.B) > 0 {
					b.appendByte('/')
				}
				b.appendByte('.')
				b.appendByte('.')
				b.dd = len(b.B)
			}

		default:
			if (b.abs && len(b.B) != 1) || (!b.abs && len(b.B) > 0) {
				// Append path separator
				b.appendByte('/')
			}

			// Append slice up to next '/'
			i += b.appendSlice(path[i:])
		}
	}

	if len(b.B) > 0 {
		return
	}

	if b.abs {
		// Empty absolute path => /
		b.appendByte('/')
	} else {
		// Empty relative path => .
		b.appendByte('.')
	}
}

// Guarantee ensures there is at least the requested size
// free bytes available in the buffer, reallocating if necessary
func (b *Builder) Guarantee(size int) {
	if size > cap(b.B)-len(b.B) {
		nb := make([]byte, 2*cap(b.B)+size)
		copy(nb, b.B)
		b.B = nb[:len(b.B)]
	}
}

// Truncate reduces the length of the buffer by the requested
// number of bytes. If the byte slice is *effectively* empty,
// i.e. absolute and "/" or relative and ".", it won't be truncated.
func (b *Builder) Truncate(size int) {
	if len(b.B) == 0 {
		return
	}

	if len(b.B) == 1 && ((b.abs && b.B[0] == '/') ||
		(!b.abs && b.B[0] == '.')) {
		// *effectively* empty
		return
	}

	// Truncate requested bytes
	b.B = b.B[:len(b.B)-size]
}

// appendByte appends the supplied byte to the end of
// the buffer. appending is achieved by continually reslicing the
// buffer and setting the next byte-at-index, this is safe as guarantee()
// will have been called beforehand
func (b *Builder) appendByte(c byte) {
	b.B = b.B[:len(b.B)+1]
	b.B[len(b.B)-1] = c
}

// appendSlice appends the supplied string slice to
// the end of the buffer and returns the number of indices
// we were able to iterate before hitting a path separator '/'.
// appending is achieved by continually reslicing the buffer
// and setting the next byte-at-index, this is safe as guarantee()
// will have been called beforehand
func (b *Builder) appendSlice(slice string) int {
	i := 0
	for i < len(slice) && slice[i] != '/' {
		b.B = b.B[:len(b.B)+1]
		b.B[len(b.B)-1] = slice[i]
		i++
	}
	return i
}

// backtrack reduces the end of the buffer back to the last
// separating '/', or end of buffer
func (b *Builder) backtrack() {
	b.B = b.B[:len(b.B)-1]

	for len(b.B)-1 > b.dd && b.B[len(b.B)-1] != '/' {
		b.B = b.B[:len(b.B)-1]
	}

	if len(b.B) > 0 {
		b.B = b.B[:len(b.B)-1]
	}
}
