// Copyright 2017 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 commands

import (
	"os"
	"path/filepath"

	"github.com/gohugoio/hugo/hugolib/filesystems"

	"github.com/fsnotify/fsnotify"
	"github.com/gohugoio/hugo/helpers"
	"github.com/spf13/fsync"
)

type staticSyncer struct {
	c *commandeer
}

func newStaticSyncer(c *commandeer) (*staticSyncer, error) {
	return &staticSyncer{c: c}, nil
}

func (s *staticSyncer) isStatic(filename string) bool {
	return s.c.hugo().BaseFs.SourceFilesystems.IsStatic(filename)
}

func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
	c := s.c

	syncFn := func(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
		publishDir := c.hugo().PathSpec.PublishDir
		// If root, remove the second '/'
		if publishDir == "//" {
			publishDir = helpers.FilePathSeparator
		}

		if sourceFs.PublishFolder != "" {
			publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
		}

		syncer := fsync.NewSyncer()
		syncer.NoTimes = c.Cfg.GetBool("noTimes")
		syncer.NoChmod = c.Cfg.GetBool("noChmod")
		syncer.ChmodFilter = chmodFilter
		syncer.SrcFs = sourceFs.Fs
		syncer.DestFs = c.Fs.Destination

		// prevent spamming the log on changes
		logger := helpers.NewDistinctErrorLogger()

		for _, ev := range staticEvents {
			// Due to our approach of layering both directories and the content's rendered output
			// into one we can't accurately remove a file not in one of the source directories.
			// If a file is in the local static dir and also in the theme static dir and we remove
			// it from one of those locations we expect it to still exist in the destination
			//
			// If Hugo generates a file (from the content dir) over a static file
			// the content generated file should take precedence.
			//
			// Because we are now watching and handling individual events it is possible that a static
			// event that occupies the same path as a content generated file will take precedence
			// until a regeneration of the content takes places.
			//
			// Hugo assumes that these cases are very rare and will permit this bad behavior
			// The alternative is to track every single file and which pipeline rendered it
			// and then to handle conflict resolution on every event.

			fromPath := ev.Name

			relPath, found := sourceFs.MakePathRelative(fromPath)

			if !found {
				// Not member of this virtual host.
				continue
			}

			// Remove || rename is harder and will require an assumption.
			// Hugo takes the following approach:
			// If the static file exists in any of the static source directories after this event
			// Hugo will re-sync it.
			// If it does not exist in all of the static directories Hugo will remove it.
			//
			// This assumes that Hugo has not generated content on top of a static file and then removed
			// the source of that static file. In this case Hugo will incorrectly remove that file
			// from the published directory.
			if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove {
				if _, err := sourceFs.Fs.Stat(relPath); os.IsNotExist(err) {
					// If file doesn't exist in any static dir, remove it
					toRemove := filepath.Join(publishDir, relPath)

					logger.Println("File no longer exists in static dir, removing", toRemove)
					_ = c.Fs.Destination.RemoveAll(toRemove)
				} else if err == nil {
					// If file still exists, sync it
					logger.Println("Syncing", relPath, "to", publishDir)

					if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
						c.logger.Errorln(err)
					}
				} else {
					c.logger.Errorln(err)
				}

				continue
			}

			// For all other event operations Hugo will sync static.
			logger.Println("Syncing", relPath, "to", publishDir)
			if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
				c.logger.Errorln(err)
			}
		}

		return 0, nil
	}

	_, err := c.doWithPublishDirs(syncFn)
	return err
}
