package sync

import (
	"bytes"
	"fmt"
	"image"
	"image/jpeg"
	"math/rand"
	"net/http"
	"os"
	"path"
	"regexp"
	"strings"
	"unicode"

	"github.com/cenkalti/dominantcolor"
	"github.com/gin-gonic/gin"
	"github.com/yuin/goldmark"
	"go.uber.org/multierr"
	"golang.org/x/image/draw"

	"srs.epita.fr/fic-server/libfic"
)

const StandaloneExercicesDirectory = "exercices"

// GetThemes returns all theme directories in the base directory.
func GetThemes(i Importer) (themes []string, err error) {
	if dirs, err := i.ListDir("/"); err != nil {
		return nil, err
	} else {
		for _, dir := range dirs {
			if !strings.HasPrefix(dir, ".") && !strings.HasPrefix(dir, "_") && dir != StandaloneExercicesDirectory {
				if _, err := i.ListDir(dir); err == nil {
					themes = append(themes, dir)
				}
			}
		}
	}

	return themes, nil
}

// resizePicture makes the given image just fill the given rectangle.
func resizePicture(importedPath string, rect image.Rectangle) error {
	if fl, err := os.Open(importedPath); err != nil {
		return err
	} else {
		if src, _, err := image.Decode(fl); err != nil {
			fl.Close()
			return err
		} else if src.Bounds().Max.X > rect.Max.X && src.Bounds().Max.Y > rect.Max.Y {
			fl.Close()

			mWidth := rect.Max.Y * src.Bounds().Max.X / src.Bounds().Max.Y
			mHeight := rect.Max.X * src.Bounds().Max.Y / src.Bounds().Max.X

			if mWidth > rect.Max.X {
				rect.Max.X = mWidth
			} else {
				rect.Max.Y = mHeight
			}
			dst := image.NewRGBA(rect)
			draw.CatmullRom.Scale(dst, rect, src, src.Bounds(), draw.Over, nil)

			dstFile, err := os.Create(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg")
			if err != nil {
				return err
			}
			defer dstFile.Close()

			if err = jpeg.Encode(dstFile, dst, &jpeg.Options{Quality: 100}); err != nil {
				return err
			}
		} else {
			dstFile, err := os.Create(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg")
			if err != nil {
				return err
			}
			defer dstFile.Close()

			if err = jpeg.Encode(dstFile, src, &jpeg.Options{Quality: 100}); err != nil {
				return err
			}
		}
	}
	return nil
}

type SubImager interface {
	SubImage(r image.Rectangle) image.Image
}

// getBackgroundColor retrieves the most dominant color in the bottom of the image.
func getBackgroundColor(importedPath string) (uint32, error) {
	fl, err := os.Open(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg")
	if err != nil {
		return 0, err
	}

	src, _, err := image.Decode(fl)
	if err != nil {
		fl.Close()
		return 0, err
	}

	bounds := src.Bounds()

	// Test if the right and left corner have the same color
	bottomLeft := src.(SubImager).SubImage(image.Rect(0, bounds.Dy()-10, 40, bounds.Dy()))
	bottomRight := src.(SubImager).SubImage(image.Rect(bounds.Dx()-40, bounds.Dy()-10, bounds.Dx(), bounds.Dy()))

	colorLeft := dominantcolor.Find(bottomLeft)
	colorRight := dominantcolor.Find(bottomRight)
	if uint32(colorLeft.R>>5)<<16+uint32(colorLeft.G>>5)<<8+uint32(colorLeft.B>>5) == uint32(colorRight.R>>5)<<16+uint32(colorRight.G>>5)<<8+uint32(colorRight.B>>5) {
		return uint32(colorLeft.R)<<16 + uint32(colorLeft.G)<<8 + uint32(colorLeft.B), nil
	}

	// Only keep the darkest color of the bottom of the image
	bottomFull := src.(SubImager).SubImage(image.Rect(0, bounds.Dy()-5, bounds.Dx(), bounds.Dy()))
	colors := dominantcolor.FindN(bottomFull, 4)

	color := colors[0]
	for _, c := range colors {
		if uint32(color.R<<2)+uint32(color.G<<2)+uint32(color.B<<2) > uint32(c.R<<2)+uint32(c.G<<2)+uint32(c.B<<2) {
			color = c
		}
	}

	return uint32(color.R)<<16 + uint32(color.G)<<8 + uint32(color.B), nil
}

// getAuthors parses the AUTHORS file.
func getAuthors(i Importer, tname string) ([]string, error) {
	if authors, err := GetFileContent(i, path.Join(tname, "AUTHORS.txt")); err != nil {
		return nil, err
	} else {
		var ret []string
		re := regexp.MustCompile("^([^<]+)(?: +<(.*)>)?$")
		for _, a := range strings.Split(authors, "\n") {
			a = strings.TrimFunc(a, unicode.IsSpace)
			grp := re.FindStringSubmatch(a)
			if len(grp) < 2 || grp[2] == "" {
				ret = append(ret, a)
			} else {
				ret = append(ret, fmt.Sprintf("<a href=\"%s\">%s</a>", grp[2], grp[1]))
			}
		}
		return ret, nil
	}
}

// BuildTheme creates a Theme from a given importer.
func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExceptions, errs error) {
	th = &fic.Theme{}

	th.Path = tdir

	// Get exceptions
	exceptions = LoadThemeException(i, th)

	// Overwrite language
	if language, err := GetFileContent(i, path.Join(tdir, "language.txt")); err == nil {
		language = strings.TrimSpace(language)
		if strings.Contains(language, "\n") {
			errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("language.txt: Language can't contain new lines")))
		} else {
			th.Language = language
		}
	}

	// Extract theme's label
	if tname, err := GetFileContent(i, path.Join(tdir, "title.txt")); err == nil {
		th.Name = fixnbsp(tname)
	} else if f := strings.Index(tdir, "-"); f >= 0 {
		th.Name = fixnbsp(tdir[f+1:])
	} else {
		th.Name = fixnbsp(tdir)
	}
	th.URLId = fic.ToURLid(th.Name)

	if authors, err := getAuthors(i, tdir); err != nil {
		errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get AUTHORS.txt: %w", err)))
		return nil, nil, errs
	} else {
		// Format authors
		th.Authors = strings.Join(authors, ", ")
	}

	var err error
	if i.Exists(path.Join(tdir, "headline.txt")) {
		th.Headline, err = GetFileContent(i, path.Join(tdir, "headline.txt"))
	} else if i.Exists(path.Join(tdir, "headline.md")) {
		th.Headline, err = GetFileContent(i, path.Join(tdir, "headline.md"))
	}
	if err != nil {
		errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get theme's headline: %w", err)))
	}
	if th.Headline != "" {
		// Call checks hooks
		for _, h := range hooks.mdTextHooks {
			for _, err := range multierr.Errors(h(th.Headline, th.Language, exceptions.GetFileExceptions("headline.md", "headline.txt"))) {
				errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("headline.md: %w", err)))
			}
		}
	}

	var intro string
	if i.Exists(path.Join(tdir, "overview.txt")) {
		intro, err = GetFileContent(i, path.Join(tdir, "overview.txt"))
	} else if i.Exists(path.Join(tdir, "overview.md")) {
		intro, err = GetFileContent(i, path.Join(tdir, "overview.md"))
	} else {
		err = fmt.Errorf("unable to find overview.txt nor overview.md")
	}
	if err != nil {
		errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get theme's overview: %w", err)))
	} else {
		// Call checks hooks
		for _, h := range hooks.mdTextHooks {
			for _, err := range multierr.Errors(h(intro, th.Language, exceptions.GetFileExceptions("overview.md", "overview.txt"))) {
				errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("overview.md: %w", err)))
			}
		}

		// Split headline from intro
		if th.Headline == "" {
			ovrvw := strings.Split(fixnbsp(intro), "\n")
			th.Headline = ovrvw[0]
			if len(ovrvw) > 1 {
				intro = strings.Join(ovrvw[1:], "\n")
			}
		}

		// Format overview (markdown)
		th.Intro, err = ProcessMarkdown(i, intro, tdir)
		if err != nil {
			errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("overview.txt: an error occurs during markdown formating: %w", err)))
		}
		var buf bytes.Buffer
		err := goldmark.Convert([]byte(th.Headline), &buf)
		if err != nil {
			errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("overview.txt: an error occurs during markdown formating of the headline: %w", err)))
		} else {
			th.Headline = string(buf.Bytes())
		}
	}

	if i.Exists(path.Join(tdir, "heading.jpg")) {
		th.Image = path.Join(tdir, "heading.jpg")
	} else if i.Exists(path.Join(tdir, "heading.png")) {
		th.Image = path.Join(tdir, "heading.png")
	} else {
		errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("heading.jpg: No such file")))
	}

	if i.Exists(path.Join(tdir, "partner.jpg")) {
		th.PartnerImage = path.Join(tdir, "partner.jpg")
	} else if i.Exists(path.Join(tdir, "partner.png")) {
		th.PartnerImage = path.Join(tdir, "partner.png")
	}

	if i.Exists(path.Join(tdir, "partner.txt")) {
		if txt, err := GetFileContent(i, path.Join(tdir, "partner.txt")); err != nil {
			errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get partner's text: %w", err)))
		} else {
			th.PartnerText, err = ProcessMarkdown(i, txt, tdir)
			if err != nil {
				errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("partner.txt: an error occurs during markdown formating: %w", err)))
			}
		}
	}
	return
}

// SyncThemes imports new or updates existing themes.
func SyncThemes(i Importer) (exceptions map[string]*CheckExceptions, errs error) {
	if themes, err := GetThemes(i); err != nil {
		errs = multierr.Append(errs, fmt.Errorf("Unable to list themes: %w", err))
	} else {
		rand.Shuffle(len(themes), func(i, j int) {
			themes[i], themes[j] = themes[j], themes[i]
		})

		exceptions = map[string]*CheckExceptions{}

		for _, tdir := range themes {
			btheme, excepts, berrs := BuildTheme(i, tdir)
			errs = multierr.Append(errs, berrs)

			if btheme == nil {
				continue
			}

			exceptions[tdir] = excepts

			if len(btheme.Image) > 0 {
				if _, err := i.importFile(btheme.Image,
					func(filePath string, origin string) (interface{}, error) {
						if err := resizePicture(filePath, image.Rect(0, 0, 500, 300)); err != nil {
							return nil, err
						}

						btheme.Image = strings.TrimPrefix(filePath, fic.FilesDir)
						btheme.BackgroundColor, _ = getBackgroundColor(filePath)
						return nil, nil
					}); err != nil {
					errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import heading image: %w", err)))
				}
			}

			if len(btheme.PartnerImage) > 0 {
				if _, err := i.importFile(btheme.PartnerImage,
					func(filePath string, origin string) (interface{}, error) {
						btheme.PartnerImage = strings.TrimPrefix(filePath, fic.FilesDir)
						return nil, nil
					}); err != nil {
					errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import partner image: %w", err)))
				}
			}

			var theme *fic.Theme
			if theme, err = fic.GetThemeByPath(btheme.Path); err != nil {
				if _, err := fic.CreateTheme(btheme); err != nil {
					errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("an error occurs during add: %w", err)))
					continue
				}
			}

			if !fic.CmpTheme(theme, btheme) {
				btheme.Id = theme.Id
				if _, err := btheme.Update(); err != nil {
					errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("an error occurs during update: %w", err)))
					continue
				}
			}
		}
	}

	return
}

func LoadThemeExceptions(i Importer, theme *fic.Theme) (*CheckExceptions, error) {
	return nil, nil
}

// ApiListRemoteThemes is an accessor letting foreign packages to access remote themes list.
func ApiListRemoteThemes(c *gin.Context) {
	themes, err := GetThemes(GlobalImporter)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
		return
	}

	c.JSON(http.StatusOK, themes)
}

// ApiListRemoteTheme is an accessor letting foreign packages to access remote main theme attributes.
func ApiGetRemoteTheme(c *gin.Context) {
	if c.Params.ByName("thid") == "_" {
		c.Status(http.StatusNoContent)
		return
	}

	r, _, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
	if r == nil {
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
		return
	}

	c.JSON(http.StatusOK, r)
}
