// Copyright 2018 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"
	"os"
	"path/filepath"
	"strings"

	"github.com/spf13/afero"
)

const hugoFsMarker = "__hugofs"

var (
	_ LanguageAnnouncer = (*LanguageFileInfo)(nil)
	_ FilePather        = (*LanguageFileInfo)(nil)
	_ afero.Lstater     = (*LanguageFs)(nil)
)

// LanguageAnnouncer is aware of its language.
type LanguageAnnouncer interface {
	Lang() string
	TranslationBaseName() string
}

// FilePather is aware of its file's location.
type FilePather interface {
	// Filename gets the full path and filename to the file.
	Filename() string

	// Path gets the content relative path including file name and extension.
	// The directory is relative to the content root where "content" is a broad term.
	Path() string

	// RealName is FileInfo.Name in its original form.
	RealName() string

	BaseDir() string
}

var LanguageDirsMerger = func(lofi, bofi []os.FileInfo) ([]os.FileInfo, error) {
	m := make(map[string]*LanguageFileInfo)

	for _, fi := range lofi {
		fil, ok := fi.(*LanguageFileInfo)
		if !ok {
			return nil, fmt.Errorf("received %T, expected *LanguageFileInfo", fi)
		}
		m[fil.virtualName] = fil
	}

	for _, fi := range bofi {
		fil, ok := fi.(*LanguageFileInfo)
		if !ok {
			return nil, fmt.Errorf("received %T, expected *LanguageFileInfo", fi)
		}
		existing, found := m[fil.virtualName]

		if !found || existing.weight < fil.weight {
			m[fil.virtualName] = fil
		}
	}

	merged := make([]os.FileInfo, len(m))
	i := 0
	for _, v := range m {
		merged[i] = v
		i++
	}

	return merged, nil
}

type LanguageFileInfo struct {
	os.FileInfo
	lang                string
	baseDir             string
	realFilename        string
	relFilename         string
	name                string
	realName            string
	virtualName         string
	translationBaseName string

	// We add some weight to the files in their own language's content directory.
	weight int
}

func (fi *LanguageFileInfo) Filename() string {
	return fi.realFilename
}

func (fi *LanguageFileInfo) Path() string {
	return fi.relFilename
}

func (fi *LanguageFileInfo) RealName() string {
	return fi.realName
}

func (fi *LanguageFileInfo) BaseDir() string {
	return fi.baseDir
}

func (fi *LanguageFileInfo) Lang() string {
	return fi.lang
}

// TranslationBaseName returns the base filename without any extension or language
// identificator.
func (fi *LanguageFileInfo) TranslationBaseName() string {
	return fi.translationBaseName
}

// Name is the name of the file within this filesystem without any path info.
// It will be marked with language information so we can identify it as ours.
func (fi *LanguageFileInfo) Name() string {
	return fi.name
}

type languageFile struct {
	afero.File
	fs *LanguageFs
}

// Readdir creates FileInfo entries by calling Lstat if possible.
func (l *languageFile) Readdir(c int) (ofi []os.FileInfo, err error) {
	names, err := l.File.Readdirnames(c)
	if err != nil {
		return nil, err
	}

	fis := make([]os.FileInfo, len(names))

	for i, name := range names {
		fi, _, err := l.fs.LstatIfPossible(filepath.Join(l.Name(), name))

		if err != nil {
			return nil, err
		}
		fis[i] = fi
	}

	return fis, err
}

type LanguageFs struct {
	// This Fs is usually created with a BasePathFs
	basePath   string
	lang       string
	nameMarker string
	languages  map[string]bool
	afero.Fs
}

func NewLanguageFs(lang string, languages map[string]bool, fs afero.Fs) *LanguageFs {
	if lang == "" {
		panic("no lang set for the language fs")
	}
	var basePath string

	if bfs, ok := fs.(*afero.BasePathFs); ok {
		basePath, _ = bfs.RealPath("")
	}

	marker := hugoFsMarker + "_" + lang + "_"

	return &LanguageFs{lang: lang, languages: languages, basePath: basePath, Fs: fs, nameMarker: marker}
}

func (fs *LanguageFs) Lang() string {
	return fs.lang
}

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

	fi, err := fs.Fs.Stat(name)
	if err != nil {
		return nil, err
	}

	return fs.newLanguageFileInfo(name, fi)
}

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

	if err != nil {
		return nil, err
	}
	return &languageFile{File: f, fs: fs}, nil
}

func (fs *LanguageFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
	name, err := fs.realName(name)
	if err != nil {
		return nil, false, err
	}

	var fi os.FileInfo
	var b bool

	if lif, ok := fs.Fs.(afero.Lstater); ok {
		fi, b, err = lif.LstatIfPossible(name)
	} else {
		fi, err = fs.Fs.Stat(name)
	}

	if err != nil {
		return nil, b, err
	}

	lfi, err := fs.newLanguageFileInfo(name, fi)

	return lfi, b, err
}

func (fs *LanguageFs) realPath(name string) (string, error) {
	if baseFs, ok := fs.Fs.(*afero.BasePathFs); ok {
		return baseFs.RealPath(name)
	}
	return name, nil
}

func (fs *LanguageFs) realName(name string) (string, error) {
	if strings.Contains(name, hugoFsMarker) {
		if !strings.Contains(name, fs.nameMarker) {
			return "", os.ErrNotExist
		}
		return strings.Replace(name, fs.nameMarker, "", 1), nil
	}

	if fs.basePath == "" {
		return name, nil
	}

	return strings.TrimPrefix(name, fs.basePath), nil
}

func (fs *LanguageFs) newLanguageFileInfo(filename string, fi os.FileInfo) (*LanguageFileInfo, error) {
	filename = filepath.Clean(filename)
	_, name := filepath.Split(filename)

	realName := name
	virtualName := name

	realPath, err := fs.realPath(filename)
	if err != nil {
		return nil, err
	}

	lang := fs.Lang()

	baseNameNoExt := ""

	if !fi.IsDir() {

		// Try to extract the language from the file name.
		// Any valid language identificator in the name will win over the
		// language set on the file system, e.g. "mypost.en.md".
		baseName := filepath.Base(name)
		ext := filepath.Ext(baseName)
		baseNameNoExt = baseName

		if ext != "" {
			baseNameNoExt = strings.TrimSuffix(baseNameNoExt, ext)
		}

		fileLangExt := filepath.Ext(baseNameNoExt)
		fileLang := strings.TrimPrefix(fileLangExt, ".")

		if fs.languages[fileLang] {
			lang = fileLang
			baseNameNoExt = strings.TrimSuffix(baseNameNoExt, fileLangExt)
		}

		// This connects the filename to the filesystem, not the language.
		virtualName = baseNameNoExt + "." + lang + ext

		name = fs.nameMarker + name
	}

	weight := 1
	// If this file's language belongs in this directory, add some weight to it
	// to make it more important.
	if lang == fs.Lang() {
		weight = 2
	}

	if fi.IsDir() {
		// For directories we always want to start from the union view.
		realPath = strings.TrimPrefix(realPath, fs.basePath)
	}

	return &LanguageFileInfo{
		lang:                lang,
		weight:              weight,
		realFilename:        realPath,
		realName:            realName,
		relFilename:         strings.TrimPrefix(strings.TrimPrefix(realPath, fs.basePath), string(os.PathSeparator)),
		name:                name,
		virtualName:         virtualName,
		translationBaseName: baseNameNoExt,
		baseDir:             fs.basePath,
		FileInfo:            fi}, nil
}