package main

import (
	"bytes"
	"io"
	"log"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/aymerick/raymond"
	"github.com/fogleman/gg"
)

const (
	blogTitle   = "Prose"
	blogURL     = "https://prose.nsood.in"
	blogSummary = "Where I infodump in Markdown and nobody can stop me."
)

type server struct {
	staticHandler http.Handler

	mu        sync.RWMutex
	templates map[string]*raymond.Template
	postList
	styles    map[string]string
	homeImage []byte
}

func newServer() (*server, error) {
	s := &server{
		staticHandler: http.FileServer(http.Dir("static/")),
	}

	var imgBuffer bytes.Buffer
	err := createImage(blogTitle, blogSummary, blogURL, &imgBuffer)
	if err != nil {
		return nil, err
	}
	s.homeImage, err = io.ReadAll(&imgBuffer)
	if err != nil {
		return nil, err
	}

	posts, err := newPostList()
	if err != nil {
		return nil, err
	}
	s.mu.Lock()
	s.postList = posts
	s.mu.Unlock()

	tpls, err := loadTemplates([]string{
		"page.html",
		"fullpost.html",
		"summary.html",
		"notfound.html",
		"error.html",
		"rss-channel.xml",
		"rss-item.xml",
	})
	if err != nil {
		return nil, err
	}
	s.mu.Lock()
	s.templates = tpls
	s.mu.Unlock()

	styles, err := newStylesMap()
	if err != nil {
		return nil, err
	}
	s.mu.Lock()
	s.styles = styles
	s.mu.Unlock()

	postsLn := newPostListener(func(updateFn func(postList) postList) {
		s.mu.Lock()
		defer s.mu.Unlock()
		s.postList = updateFn(s.postList)
	})
	go postsLn.listen()

	templatesLn := newTemplateListener(func(updateFn func(map[string]*raymond.Template)) {
		s.mu.Lock()
		defer s.mu.Unlock()
		updateFn(s.templates)
	})
	go templatesLn.listen()

	stylesLn := newStylesListener(func(updateFn func(map[string]string)) {
		s.mu.Lock()
		defer s.mu.Unlock()
		updateFn(s.styles)
	})
	go stylesLn.listen()

	return s, nil
}

func (s *server) logRequest(req *http.Request) {
	log.Printf("%s %s from %s", req.Method, req.URL.Path, req.RemoteAddr)
}

func (s *server) router(res http.ResponseWriter, req *http.Request) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	s.logRequest(req)
	res = &errorCatcher{
		res:          res,
		req:          req,
		errorTpl:     s.templates["error.html"],
		notFoundTpl:  s.templates["notfound.html"],
		handledError: false,
	}
	slug := req.URL.Path[1:]

	if slug == "" {
		s.homePage(res, req)
		return
	}
	if slug == "about.png" {
		s.renderImage(res, req, s.homeImage)
		return
	}
	if slug == "rss.xml" {
		s.renderRSS(res, req)
		return
	}

	for _, p := range s.postList {
		if slug == p.Slug {
			s.postPage(p, res, req)
			return
		} else if slug == p.Slug+"/about.png" {
			s.renderImage(res, req, p.Image)
			return
		}
	}

	if strings.HasPrefix(slug, "css/") {
		filename := strings.TrimPrefix(slug, "css/")
		ok := s.loadStylesheet(res, req, filename)
		if ok {
			return
		}
	}

	s.staticHandler.ServeHTTP(res, req)
}

func (s *server) errorInRequest(res http.ResponseWriter, req *http.Request, err error) {
	res.WriteHeader(http.StatusInternalServerError)
	res.Write([]byte("oh no"))
	log.Printf("ERR %s: %s", req.URL.Path, err)
}

func (s *server) createWebPage(title, subtitle, contents, path string) (string, error) {
	ctx := map[string]interface{}{
		"title":    title,
		"subtitle": subtitle,
		"contents": contents,
		"path":     blogURL + path,
	}
	return s.templates["page.html"].Exec(ctx)
}

func (s *server) postPage(p *Post, res http.ResponseWriter, req *http.Request) {
	res.Header().Add("content-type", "text/html; charset=utf-8")
	contents, err := p.render(s.templates["fullpost.html"])
	if err != nil {
		s.errorInRequest(res, req, err)
	}
	page, err := s.createWebPage(p.Metadata.Title, p.Metadata.Summary, contents, req.URL.Path)
	if err != nil {
		s.errorInRequest(res, req, err)
	}
	res.Write([]byte(page))
}

func (s *server) homePage(res http.ResponseWriter, req *http.Request) {
	res.Header().Add("content-type", "text/html; charset=utf-8")

	var posts string

	for _, p := range s.postList {
		summary, err := p.render(s.templates["summary.html"])
		if err != nil {
			log.Printf("could not render post summary for %s", p.Slug)
		}
		posts = posts + summary
	}

	page, err := s.createWebPage("Home", blogSummary, posts, "")

	if err != nil {
		s.errorInRequest(res, req, err)
	}

	res.Write([]byte(page))
}

func (s *server) renderImage(res http.ResponseWriter, req *http.Request, img []byte) {
	res.Header().Add("content-type", "image/png")
	res.Write(img)
}

func (s *server) renderRSS(res http.ResponseWriter, req *http.Request) {
	res.Header().Add("content-type", "text/xml; charset=utf-8")

	var posts string

	for _, p := range s.postList {
		summary, err := p.render(s.templates["rss-item.xml"])
		if err != nil {
			log.Printf("could not render post summary for %s", p.Slug)
		}
		posts = posts + summary
	}

	var pubDate string
	if len(posts) > 0 {
		pubDate = rssDatetime(s.postList[0].Metadata.Time)
	} else {
		pubDate = rssDatetime(0)
	}

	page, err := s.templates["rss-channel.xml"].Exec(map[string]string{
		"title":       blogTitle,
		"description": blogSummary,
		"link":        blogURL,
		"pubDate":     pubDate,
		"items":       posts,
	})

	if err != nil {
		s.errorInRequest(res, req, err)
	}

	res.Write([]byte(page))
}

func (s *server) loadStylesheet(res http.ResponseWriter, req *http.Request, filename string) (ok bool) {
	contents, ok := s.styles[filename]
	if !ok {
		return false
	}
	res.Header().Add("content-type", "text/css")
	res.Write([]byte(contents))
	return ok
}

func rssDatetime(timestamp int64) string {
	return time.Unix(timestamp, 0).Format("Mon, 02 Jan 2006 15:04:05 MST")
}

func createImage(title, summary, url string, out io.Writer) error {
	imgWidth, imgHeight, imgPaddingX, imgPaddingY := 1200, 600, 50, 100
	accentHeight, spacerHeight := 12.5, 20.0
	titleSize, summarySize, urlSize := 63.0, 42.0, 27.0
	lineHeight := 1.05
	textWidth := float64(imgWidth - 2*imgPaddingX)

	draw := gg.NewContext(imgWidth, imgHeight)

	titleFont, err := gg.LoadFontFace("static/fonts/Nunito-Bold.ttf", titleSize)
	if err != nil {
		return err
	}
	summaryFont, err := gg.LoadFontFace("static/fonts/Nunito-LightItalic.ttf", summarySize)
	if err != nil {
		return err
	}
	urlFont, err := gg.LoadFontFace("static/fonts/JetBrainsMono-ExtraLight.ttf", urlSize)
	if err != nil {
		return err
	}

	draw.SetFontFace(titleFont)
	wrappedTitle := draw.WordWrap(title, textWidth)
	draw.SetFontFace(summaryFont)
	wrappedSummary := draw.WordWrap(summary, textWidth)

	draw.SetHexColor("#fff")
	draw.DrawRectangle(0, 0, float64(imgWidth), float64(imgHeight))
	draw.Fill()
	draw.SetHexColor("#3498db")
	draw.DrawRectangle(0, float64(imgHeight)-accentHeight, float64(imgWidth), accentHeight)
	draw.Fill()

	offset := float64(imgPaddingY)

	draw.SetFontFace(titleFont)
	draw.SetHexColor("#333")
	for _, line := range wrappedTitle {
		draw.DrawString(line, float64(imgPaddingX), offset)
		offset += lineHeight * titleSize
	}
	offset += spacerHeight

	draw.SetFontFace(summaryFont)
	draw.SetHexColor("#999")
	for _, line := range wrappedSummary {
		draw.DrawString(line, float64(imgPaddingX), offset)
		offset += lineHeight * summarySize
	}

	draw.SetHexColor("#333")
	draw.SetFontFace(urlFont)
	urlY := float64(imgHeight - imgPaddingY)
	draw.DrawStringWrapped(url, float64(imgPaddingX), urlY, 0, 0, textWidth, lineHeight, gg.AlignRight)

	return draw.EncodePNG(out)
}
