// 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 hugolib

import (
	"bytes"
	"context"
	"fmt"
	"runtime/trace"

	"github.com/gohugoio/hugo/config"
	"github.com/gohugoio/hugo/output"
	"golang.org/x/sync/errgroup"
	"golang.org/x/sync/semaphore"

	"github.com/pkg/errors"

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

// Build builds all sites. If filesystem events are provided,
// this is considered to be a potential partial rebuild.
func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {

	if h.running {
		// Make sure we don't trigger rebuilds in parallel.
		h.runningMu.Lock()
		defer h.runningMu.Unlock()
	}

	ctx, task := trace.NewTask(context.Background(), "Build")
	defer task.End()

	errCollector := h.StartErrorCollector()
	errs := make(chan error)

	go func(from, to chan error) {
		var errors []error
		i := 0
		for e := range from {
			i++
			if i > 50 {
				break
			}
			errors = append(errors, e)
		}
		to <- h.pickOneAndLogTheRest(errors)

		close(to)

	}(errCollector, errs)

	if h.Metrics != nil {
		h.Metrics.Reset()
	}

	// Need a pointer as this may be modified.
	conf := &config

	if conf.whatChanged == nil {
		// Assume everything has changed
		conf.whatChanged = &whatChanged{source: true, other: true}
	}

	var prepareErr error

	if !config.PartialReRender {
		prepare := func() error {
			init := func(conf *BuildCfg) error {
				for _, s := range h.Sites {
					s.Deps.BuildStartListeners.Notify()
				}

				if len(events) > 0 {
					// Rebuild
					if err := h.initRebuild(conf); err != nil {
						return errors.Wrap(err, "initRebuild")
					}
				} else {
					if err := h.initSites(conf); err != nil {
						return errors.Wrap(err, "initSites")
					}
				}

				return nil
			}

			var err error

			f := func() {
				err = h.process(conf, init, events...)
			}
			trace.WithRegion(ctx, "process", f)
			if err != nil {
				return errors.Wrap(err, "process")
			}

			f = func() {
				err = h.assemble(conf)
			}
			trace.WithRegion(ctx, "assemble", f)
			if err != nil {
				return err
			}

			return nil
		}

		f := func() {
			prepareErr = prepare()
		}
		trace.WithRegion(ctx, "prepare", f)
		if prepareErr != nil {
			h.SendError(prepareErr)
		}

	}

	if prepareErr == nil {
		var err error
		f := func() {
			err = h.render(conf)
		}
		trace.WithRegion(ctx, "render", f)
		if err != nil {
			h.SendError(err)
		}
	}

	if h.Metrics != nil {
		var b bytes.Buffer
		h.Metrics.WriteMetrics(&b)

		h.Log.FEEDBACK.Printf("\nTemplate Metrics:\n\n")
		h.Log.FEEDBACK.Print(b.String())
		h.Log.FEEDBACK.Println()
	}

	select {
	// Make sure the channel always gets something.
	case errCollector <- nil:
	default:
	}
	close(errCollector)

	err := <-errs
	if err != nil {
		return err
	}

	if err := h.fatalErrorHandler.getErr(); err != nil {
		return err
	}

	errorCount := h.Log.ErrorCounter.Count()
	if errorCount > 0 {
		return fmt.Errorf("logged %d error(s)", errorCount)
	}

	return nil

}

// Build lifecycle methods below.
// The order listed matches the order of execution.

func (h *HugoSites) initSites(config *BuildCfg) error {
	h.reset(config)

	if config.NewConfig != nil {
		if err := h.createSitesFromConfig(config.NewConfig); err != nil {
			return err
		}
	}

	return nil
}

func (h *HugoSites) initRebuild(config *BuildCfg) error {
	if config.NewConfig != nil {
		return errors.New("rebuild does not support 'NewConfig'")
	}

	if config.ResetState {
		return errors.New("rebuild does not support 'ResetState'")
	}

	if !h.running {
		return errors.New("rebuild called when not in watch mode")
	}

	for _, s := range h.Sites {
		s.resetBuildState(config.whatChanged.source)
	}

	h.reset(config)
	h.resetLogs()
	helpers.InitLoggers()

	return nil
}

func (h *HugoSites) process(config *BuildCfg, init func(config *BuildCfg) error, events ...fsnotify.Event) error {
	// We should probably refactor the Site and pull up most of the logic from there to here,
	// but that seems like a daunting task.
	// So for now, if there are more than one site (language),
	// we pre-process the first one, then configure all the sites based on that.

	firstSite := h.Sites[0]

	if len(events) > 0 {
		// This is a rebuild
		return firstSite.processPartial(config, init, events)
	}

	return firstSite.process(*config)

}

func (h *HugoSites) assemble(bcfg *BuildCfg) error {

	if len(h.Sites) > 1 {
		// The first is initialized during process; initialize the rest
		for _, site := range h.Sites[1:] {
			if err := site.initializeSiteInfo(); err != nil {
				return err
			}
		}
	}

	if !bcfg.whatChanged.source {
		return nil
	}

	numWorkers := config.GetNumWorkerMultiplier()
	sem := semaphore.NewWeighted(int64(numWorkers))
	g, ctx := errgroup.WithContext(context.Background())

	for _, s := range h.Sites {
		s := s
		g.Go(func() error {
			err := sem.Acquire(ctx, 1)
			if err != nil {
				return err
			}
			defer sem.Release(1)

			if err := s.assemblePagesMap(s); err != nil {
				return err
			}

			if err := s.pagesMap.assemblePageMeta(); err != nil {
				return err
			}

			if err := s.pagesMap.assembleTaxonomies(s); err != nil {
				return err
			}

			if err := s.createWorkAllPages(); err != nil {
				return err
			}

			return nil

		})
	}

	if err := g.Wait(); err != nil {
		return err
	}

	if err := h.createPageCollections(); err != nil {
		return err
	}

	return nil

}

func (h *HugoSites) render(config *BuildCfg) error {
	siteRenderContext := &siteRenderContext{cfg: config, multihost: h.multihost}

	if !config.PartialReRender {
		h.renderFormats = output.Formats{}
		for _, s := range h.Sites {
			s.initRenderFormats()
			h.renderFormats = append(h.renderFormats, s.renderFormats...)
		}
	}

	i := 0
	for _, s := range h.Sites {
		for siteOutIdx, renderFormat := range s.renderFormats {
			siteRenderContext.outIdx = siteOutIdx
			siteRenderContext.sitesOutIdx = i
			i++

			select {
			case <-h.Done():
				return nil
			default:
				// For the non-renderable pages, we use the content iself as
				// template and we may have to re-parse and execute it for
				// each output format.
				h.TemplateHandler().RebuildClone()

				for _, s2 := range h.Sites {
					// We render site by site, but since the content is lazily rendered
					// and a site can "borrow" content from other sites, every site
					// needs this set.
					s2.rc = &siteRenderingContext{Format: renderFormat}

					if err := s2.preparePagesForRender(s == s2, siteRenderContext.sitesOutIdx); err != nil {
						return err
					}
				}

				if !config.SkipRender {
					if config.PartialReRender {
						if err := s.renderPages(siteRenderContext); err != nil {
							return err
						}
					} else {
						if err := s.render(siteRenderContext); err != nil {
							return err
						}
					}
				}
			}

		}

	}

	if !config.SkipRender {
		if err := h.renderCrossSitesArtifacts(); err != nil {
			return err
		}
	}

	return nil
}
