package rice

import (
	"errors"
	"io"
	"os"
	"path/filepath"
	"sort"

	"github.com/GeertJohan/go.rice/embedded"
)

//++ TODO: IDEA: merge virtualFile and virtualDir, this decreases work done by rice.File

// virtualFile is a 'stateful' virtual file.
// virtualFile wraps an *EmbeddedFile for a call to Box.Open() and virtualizes 'read cursor' (offset) and 'closing'.
// virtualFile is only internally visible and should be exposed through rice.File
type virtualFile struct {
	*embedded.EmbeddedFile       // the actual embedded file, embedded to obtain methods
	offset                 int64 // read position on the virtual file
	closed                 bool  // closed when true
}

// create a new virtualFile for given EmbeddedFile
func newVirtualFile(ef *embedded.EmbeddedFile) *virtualFile {
	vf := &virtualFile{
		EmbeddedFile: ef,
		offset:       0,
		closed:       false,
	}
	return vf
}

//++ TODO check for nil pointers in all these methods. When so: return os.PathError with Err: os.ErrInvalid

func (vf *virtualFile) close() error {
	if vf.closed {
		return &os.PathError{
			Op:   "close",
			Path: vf.EmbeddedFile.Filename,
			Err:  errors.New("already closed"),
		}
	}
	vf.EmbeddedFile = nil
	vf.closed = true
	return nil
}

func (vf *virtualFile) stat() (os.FileInfo, error) {
	if vf.closed {
		return nil, &os.PathError{
			Op:   "stat",
			Path: vf.EmbeddedFile.Filename,
			Err:  errors.New("bad file descriptor"),
		}
	}
	return (*embeddedFileInfo)(vf.EmbeddedFile), nil
}

func (vf *virtualFile) readdir(count int) ([]os.FileInfo, error) {
	if vf.closed {
		return nil, &os.PathError{
			Op:   "readdir",
			Path: vf.EmbeddedFile.Filename,
			Err:  errors.New("bad file descriptor"),
		}
	}
	return nil, os.ErrInvalid
}

func (vf *virtualFile) readdirnames(count int) ([]string, error) {
	if vf.closed {
		return nil, &os.PathError{
			Op:   "readdirnames",
			Path: vf.EmbeddedFile.Filename,
			Err:  errors.New("bad file descriptor"),
		}
	}
	return nil, os.ErrInvalid
}

func (vf *virtualFile) read(bts []byte) (int, error) {
	if vf.closed {
		return 0, &os.PathError{
			Op:   "read",
			Path: vf.EmbeddedFile.Filename,
			Err:  errors.New("bad file descriptor"),
		}
	}

	end := vf.offset + int64(len(bts))

	if end >= int64(len(vf.Content)) {
		// end of file, so return what we have + EOF
		n := copy(bts, vf.Content[vf.offset:])
		vf.offset = 0
		return n, io.EOF
	}

	n := copy(bts, vf.Content[vf.offset:end])
	vf.offset += int64(n)
	return n, nil

}

func (vf *virtualFile) seek(offset int64, whence int) (int64, error) {
	if vf.closed {
		return 0, &os.PathError{
			Op:   "seek",
			Path: vf.EmbeddedFile.Filename,
			Err:  errors.New("bad file descriptor"),
		}
	}
	var e error

	//++ TODO: check if this is correct implementation for seek
	switch whence {
	case os.SEEK_SET:
		//++ check if new offset isn't out of bounds, set e when it is, then break out of switch
		vf.offset = offset
	case os.SEEK_CUR:
		//++ check if new offset isn't out of bounds, set e when it is, then break out of switch
		vf.offset += offset
	case os.SEEK_END:
		//++ check if new offset isn't out of bounds, set e when it is, then break out of switch
		vf.offset = int64(len(vf.EmbeddedFile.Content)) - offset
	}

	if e != nil {
		return 0, &os.PathError{
			Op:   "seek",
			Path: vf.Filename,
			Err:  e,
		}
	}

	return vf.offset, nil
}

// virtualDir is a 'stateful' virtual directory.
// virtualDir wraps an *EmbeddedDir for a call to Box.Open() and virtualizes 'closing'.
// virtualDir is only internally visible and should be exposed through rice.File
type virtualDir struct {
	*embedded.EmbeddedDir
	offset int // readdir position on the directory
	closed bool
}

// create a new virtualDir for given EmbeddedDir
func newVirtualDir(ed *embedded.EmbeddedDir) *virtualDir {
	vd := &virtualDir{
		EmbeddedDir: ed,
		offset:      0,
		closed:      false,
	}
	return vd
}

func (vd *virtualDir) close() error {
	//++ TODO: needs sync mutex?
	if vd.closed {
		return &os.PathError{
			Op:   "close",
			Path: vd.EmbeddedDir.Filename,
			Err:  errors.New("already closed"),
		}
	}
	vd.closed = true
	return nil
}

func (vd *virtualDir) stat() (os.FileInfo, error) {
	if vd.closed {
		return nil, &os.PathError{
			Op:   "stat",
			Path: vd.EmbeddedDir.Filename,
			Err:  errors.New("bad file descriptor"),
		}
	}
	return (*embeddedDirInfo)(vd.EmbeddedDir), nil
}

func (vd *virtualDir) readdir(n int) ([]os.FileInfo, error) {

	if vd.closed {
		return nil, &os.PathError{
			Op:   "readdir",
			Path: vd.EmbeddedDir.Filename,
			Err:  errors.New("bad file descriptor"),
		}
	}

	// Build up the array of our contents
	var files []os.FileInfo

	// Add the child directories
	for _, child := range vd.ChildDirs {
		child.Filename = filepath.Base(child.Filename)
		files = append(files, (*embeddedDirInfo)(child))
	}

	// Add the child files
	for _, child := range vd.ChildFiles {
		child.Filename = filepath.Base(child.Filename)
		files = append(files, (*embeddedFileInfo)(child))
	}

	// Sort it by filename (lexical order)
	sort.Sort(SortByName(files))

	// Return all contents if that's what is requested
	if n <= 0 {
		vd.offset = 0
		return files, nil
	}

	// If user has requested past the end of our list
	// return what we can and send an EOF
	if vd.offset+n >= len(files) {
		offset := vd.offset
		vd.offset = 0
		return files[offset:], io.EOF
	}

	offset := vd.offset
	vd.offset += n
	return files[offset : offset+n], nil

}

func (vd *virtualDir) readdirnames(n int) ([]string, error) {

	if vd.closed {
		return nil, &os.PathError{
			Op:   "readdir",
			Path: vd.EmbeddedDir.Filename,
			Err:  errors.New("bad file descriptor"),
		}
	}

	// Build up the array of our contents
	var files []string

	// Add the child directories
	for _, child := range vd.ChildDirs {
		files = append(files, filepath.Base(child.Filename))
	}

	// Add the child files
	for _, child := range vd.ChildFiles {
		files = append(files, filepath.Base(child.Filename))
	}

	// Sort it by filename (lexical order)
	sort.Strings(files)

	// Return all contents if that's what is requested
	if n <= 0 {
		vd.offset = 0
		return files, nil
	}

	// If user has requested past the end of our list
	// return what we can and send an EOF
	if vd.offset+n >= len(files) {
		offset := vd.offset
		vd.offset = 0
		return files[offset:], io.EOF
	}

	offset := vd.offset
	vd.offset += n
	return files[offset : offset+n], nil
}

func (vd *virtualDir) read(bts []byte) (int, error) {
	if vd.closed {
		return 0, &os.PathError{
			Op:   "read",
			Path: vd.EmbeddedDir.Filename,
			Err:  errors.New("bad file descriptor"),
		}
	}
	return 0, &os.PathError{
		Op:   "read",
		Path: vd.EmbeddedDir.Filename,
		Err:  errors.New("is a directory"),
	}
}

func (vd *virtualDir) seek(offset int64, whence int) (int64, error) {
	if vd.closed {
		return 0, &os.PathError{
			Op:   "seek",
			Path: vd.EmbeddedDir.Filename,
			Err:  errors.New("bad file descriptor"),
		}
	}
	return 0, &os.PathError{
		Op:   "seek",
		Path: vd.Filename,
		Err:  errors.New("is a directory"),
	}
}
