// Copyright 2019 The Hugo Authors. All rights reserved.
//
// 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 hugofs

import (
	"fmt"
	"io/fs"
	"os"
	"path/filepath"

	"github.com/spf13/afero"
)

var _ FilesystemUnwrapper = (*baseFileDecoratorFs)(nil)

func decorateDirs(fs afero.Fs, meta *FileMeta) afero.Fs {
	ffs := &baseFileDecoratorFs{Fs: fs}

	decorator := func(fi FileNameIsDir, name string) (FileNameIsDir, error) {
		if !fi.IsDir() {
			// Leave regular files as they are.
			return fi, nil
		}

		return decorateFileInfo(fi, nil, "", meta), nil
	}

	ffs.decorate = decorator

	return ffs
}

// NewBaseFileDecorator decorates the given Fs to provide the real filename
// and an Opener func.
func NewBaseFileDecorator(fs afero.Fs, callbacks ...func(fi FileMetaInfo)) afero.Fs {
	ffs := &baseFileDecoratorFs{Fs: fs}

	decorator := func(fi FileNameIsDir, filename string) (FileNameIsDir, error) {
		// Store away the original in case it's a symlink.
		meta := NewFileMeta()
		meta.Name = fi.Name()

		if fi.IsDir() {
			meta.JoinStatFunc = func(name string) (FileMetaInfo, error) {
				joinedFilename := filepath.Join(filename, name)
				fi, err := fs.Stat(joinedFilename)
				if err != nil {
					return nil, err
				}
				fim, err := ffs.decorate(fi, joinedFilename)
				if err != nil {
					return nil, err
				}

				return fim.(FileMetaInfo), nil
			}
		}

		opener := func() (afero.File, error) {
			return ffs.open(filename)
		}

		fim := decorateFileInfo(fi, opener, filename, meta)

		for _, cb := range callbacks {
			cb(fim)
		}

		return fim, nil
	}

	ffs.decorate = decorator
	return ffs
}

type baseFileDecoratorFs struct {
	afero.Fs
	decorate func(fi FileNameIsDir, name string) (FileNameIsDir, error)
}

func (fs *baseFileDecoratorFs) UnwrapFilesystem() afero.Fs {
	return fs.Fs
}

func (fs *baseFileDecoratorFs) Stat(name string) (os.FileInfo, error) {
	fi, err := fs.Fs.Stat(name)
	if err != nil {
		return nil, err
	}

	fim, err := fs.decorate(fi, name)
	if err != nil {
		return nil, err
	}
	return fim.(os.FileInfo), nil
}

func (fs *baseFileDecoratorFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
	if lstater, ok := fs.Fs.(afero.Lstater); ok {
		fi, ok, err := lstater.LstatIfPossible(name)
		if err != nil {
			return nil, false, err
		}
		fim, err := fs.decorate(fi, name)
		if err != nil {
			return nil, false, err
		}
		return fim.(os.FileInfo), ok, nil
	}
	fi, err := fs.Stat(name)
	return fi, false, err
}

func (fs *baseFileDecoratorFs) Open(name string) (afero.File, error) {
	return fs.open(name)
}

func (fs *baseFileDecoratorFs) open(name string) (afero.File, error) {
	f, err := fs.Fs.Open(name)
	if err != nil {
		return nil, err
	}
	return &baseFileDecoratorFile{File: f, fs: fs}, nil
}

type baseFileDecoratorFile struct {
	afero.File
	fs *baseFileDecoratorFs
}

func (l *baseFileDecoratorFile) ReadDir(n int) ([]fs.DirEntry, error) {
	fis, err := l.File.(fs.ReadDirFile).ReadDir(-1)
	if err != nil {
		return nil, err
	}

	fisp := make([]fs.DirEntry, len(fis))

	for i, fi := range fis {
		filename := fi.Name()
		if l.Name() != "" {
			filename = filepath.Join(l.Name(), fi.Name())
		}

		fid, err := l.fs.decorate(fi, filename)
		if err != nil {
			return nil, fmt.Errorf("decorate: %w", err)
		}

		fisp[i] = fid.(fs.DirEntry)

	}

	return fisp, err
}

func (l *baseFileDecoratorFile) Readdir(c int) (ofi []os.FileInfo, err error) {
	panic("not supported: Use ReadDir")
}
