// 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 provides the file systems used by Hugo.
package hugofs

import (
	"context"
	"fmt"
	iofs "io/fs"
	"os"
	"strings"

	"github.com/bep/overlayfs"
	"github.com/gohugoio/hugo/common/paths"
	"github.com/gohugoio/hugo/config"
	"github.com/spf13/afero"
)

// Os points to the (real) Os filesystem.
var Os = &afero.OsFs{}

// Fs holds the core filesystems used by Hugo.
type Fs struct {
	// Source is Hugo's source file system.
	// Note that this will always be a "plain" Afero filesystem:
	// * afero.OsFs when running in production
	// * afero.MemMapFs for many of the tests.
	Source afero.Fs

	// PublishDir is where Hugo publishes its rendered content.
	// It's mounted inside publishDir (default /public).
	PublishDir afero.Fs

	// PublishDirStatic is the file system used for static files.
	PublishDirStatic afero.Fs

	// PublishDirServer is the file system used for serving the public directory with Hugo's development server.
	// This will typically be the same as PublishDir, but not if --renderStaticToDisk is set.
	PublishDirServer afero.Fs

	// Os is an OS file system.
	// NOTE: Field is currently unused.
	Os afero.Fs

	// WorkingDirReadOnly is a read-only file system
	// restricted to the project working dir.
	WorkingDirReadOnly afero.Fs

	// WorkingDirWritable is a writable file system
	// restricted to the project working dir.
	WorkingDirWritable afero.Fs
}

func NewDefault(cfg config.Provider) *Fs {
	workingDir, publishDir := getWorkingPublishDir(cfg)
	fs := Os
	return newFs(fs, fs, workingDir, publishDir)
}

// NewFrom creates a new Fs based on the provided Afero Fs
// as source and destination file systems.
// Useful for testing.
func NewFrom(fs afero.Fs, conf config.BaseConfig) *Fs {
	return newFs(fs, fs, conf.WorkingDir, conf.PublishDir)
}

func NewFromOld(fs afero.Fs, cfg config.Provider) *Fs {
	workingDir, publishDir := getWorkingPublishDir(cfg)
	return newFs(fs, fs, workingDir, publishDir)
}

// NewFromSourceAndDestination creates a new Fs based on the provided Afero Fss
// as the source and destination file systems.
func NewFromSourceAndDestination(source, destination afero.Fs, cfg config.Provider) *Fs {
	workingDir, publishDir := getWorkingPublishDir(cfg)
	return newFs(source, destination, workingDir, publishDir)
}

func getWorkingPublishDir(cfg config.Provider) (string, string) {
	workingDir := cfg.GetString("workingDir")
	publishDir := cfg.GetString("publishDirDynamic")
	if publishDir == "" {
		publishDir = cfg.GetString("publishDir")
	}
	return workingDir, publishDir
}

func newFs(source, destination afero.Fs, workingDir, publishDir string) *Fs {
	if publishDir == "" {
		panic("publishDir is empty")
	}

	if workingDir == "." {
		workingDir = ""
	}

	// Sanity check
	if IsOsFs(source) && len(workingDir) < 2 {
		panic("workingDir is too short")
	}

	// If this does not exist, it will be created later.
	absPublishDir := paths.AbsPathify(workingDir, publishDir)

	pubFs := NewBasePathFs(destination, absPublishDir)

	return &Fs{
		Source:             source,
		PublishDir:         pubFs,
		PublishDirServer:   pubFs,
		PublishDirStatic:   pubFs,
		Os:                 &afero.OsFs{},
		WorkingDirReadOnly: getWorkingDirFsReadOnly(source, workingDir),
		WorkingDirWritable: getWorkingDirFsWritable(source, workingDir),
	}
}

func getWorkingDirFsReadOnly(base afero.Fs, workingDir string) afero.Fs {
	if workingDir == "" {
		return NewReadOnlyFs(base)
	}
	return NewBasePathFs(NewReadOnlyFs(base), workingDir)
}

func getWorkingDirFsWritable(base afero.Fs, workingDir string) afero.Fs {
	if workingDir == "" {
		return base
	}
	return NewBasePathFs(base, workingDir)
}

func isWrite(flag int) bool {
	return flag&os.O_RDWR != 0 || flag&os.O_WRONLY != 0
}

// MakeReadableAndRemoveAllModulePkgDir makes any subdir in dir readable and then
// removes the root.
// TODO(bep) move this to a more suitable place.
func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) {
	// Safe guard
	// Note that the base directory changed from pkg to gomod_cache in Go 1.23.
	if !strings.Contains(dir, "pkg") && !strings.Contains(dir, "gomod") {
		panic(fmt.Sprint("invalid dir:", dir))
	}

	counter := 0
	afero.Walk(fs, dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil
		}
		if info.IsDir() {
			counter++
			fs.Chmod(path, 0o777)
		}
		return nil
	})

	return counter, fs.RemoveAll(dir)
}

// IsOsFs returns whether fs is an OsFs or if it fs wraps an OsFs.
// TODO(bep) make this more robust.
func IsOsFs(fs afero.Fs) bool {
	var isOsFs bool
	WalkFilesystems(fs, func(fs afero.Fs) bool {
		switch fs.(type) {
		case *afero.MemMapFs:
			isOsFs = false
		case *afero.OsFs:
			isOsFs = true
		}
		return isOsFs
	})
	return isOsFs
}

// FilesystemsUnwrapper returns the underlying filesystems.
type FilesystemsUnwrapper interface {
	UnwrapFilesystems() []afero.Fs
}

// FilesystemUnwrapper returns the underlying filesystem.
type FilesystemUnwrapper interface {
	UnwrapFilesystem() afero.Fs
}

// WalkFn is the walk func for WalkFilesystems.
type WalkFn func(fs afero.Fs) bool

// WalkFilesystems walks fs recursively and calls fn.
// If fn returns true, walking is stopped.
func WalkFilesystems(fs afero.Fs, fn WalkFn) bool {
	if fn(fs) {
		return true
	}

	if afs, ok := fs.(FilesystemUnwrapper); ok {
		if WalkFilesystems(afs.UnwrapFilesystem(), fn) {
			return true
		}
	} else if bfs, ok := fs.(FilesystemsUnwrapper); ok {
		for _, sf := range bfs.UnwrapFilesystems() {
			if WalkFilesystems(sf, fn) {
				return true
			}
		}
	} else if cfs, ok := fs.(overlayfs.FilesystemIterator); ok {
		for i := range cfs.NumFilesystems() {
			if WalkFilesystems(cfs.Filesystem(i), fn) {
				return true
			}
		}
	}

	return false
}

var _ FilesystemUnwrapper = (*filesystemsWrapper)(nil)

// NewBasePathFs creates a new BasePathFs.
func NewBasePathFs(source afero.Fs, path string) afero.Fs {
	return WrapFilesystem(afero.NewBasePathFs(source, path), source)
}

// NewReadOnlyFs creates a new ReadOnlyFs.
func NewReadOnlyFs(source afero.Fs) afero.Fs {
	return WrapFilesystem(afero.NewReadOnlyFs(source), source)
}

// WrapFilesystem is typically used to wrap a afero.BasePathFs to allow
// access to the underlying filesystem if needed.
func WrapFilesystem(container, content afero.Fs) afero.Fs {
	return filesystemsWrapper{Fs: container, content: content}
}

type filesystemsWrapper struct {
	afero.Fs
	content afero.Fs
}

func (w filesystemsWrapper) UnwrapFilesystem() afero.Fs {
	return w.content
}

type ReadDirWithContextDir interface {
	ReadDirWithContext(context context.Context, count int) ([]iofs.DirEntry, context.Context, error)
}

func ReadDirWithContext(ctx context.Context, f DirOnlyOps, count int) ([]iofs.DirEntry, context.Context, error) {
	if ff, ok := f.(ReadDirWithContextDir); ok {
		return ff.ReadDirWithContext(ctx, count)
	}
	v, err := f.(iofs.ReadDirFile).ReadDir(count)
	if err != nil {
		return nil, ctx, err
	}
	return v, ctx, nil
}

// LstatIfPossible tries to use LstatIfPossible if the filesystem supports it, otherwise it falls back to Stat.
func LstatIfPossible(fs afero.Fs, name string) (os.FileInfo, error) {
	if lstater, ok := fs.(afero.Lstater); ok {
		fi, _, err := lstater.LstatIfPossible(name)
		return fi, err
	}
	fi, err := fs.Stat(name)
	return fi, err
}
