package sync

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

	"github.com/gin-gonic/gin"
	"github.com/yuin/goldmark"
	"golang.org/x/image/draw"

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

// 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, "_") {
				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
}

// 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 = LoadException(i, th)

	// 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 = 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 intro string
	var err error
	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 = 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 h(intro, exceptions.GetFileExceptions("overview.md", "overview.txt")) {
				errs = append(errs, NewThemeError(th, fmt.Errorf("overview.md: %w", err)))
			}
		}

		// Split headline from intro
		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 = 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 = 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 = 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 = 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 = 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 = 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)
			for _, e := range berrs {
				errs = append(errs, e)
			}

			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)
						return nil, nil
					}); err != nil {
					errs = 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 = 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 = 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 = 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) {
	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)
}
