package main

import (
	"encoding/json"
	"flag"
	"log"
	"net/http"
	_ "net/http/pprof"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
)

var (
	root = flag.String("root", filepath.Join(os.Getenv("HOME"), "Maildir"),
		"root directory to watch")
	poll = flag.Duration("poll", time.Second*time.Duration(10),
		"poll interval between new file check")
	addr = flag.String("addr", ":8080",
		"address to listen for HTTP connections")
)

func folderFromPath(path string) string {
	const sep = string(filepath.Separator)
	parts := strings.Split(path, sep)
	var folder string
	if isMailDir(path) {
		folder = parts[len(parts)-2]
	} else {
		folder = parts[len(parts)-1]
	}
	if folder[0] == '.' {
		return strings.Replace(folder[1:], ".", "/", -1)
	}
	return "Inbox"
}

// isMailDirParent determines if path is a Maildir folder's
// cur/new/tmp parent directory.
func isMailDirParent(path string) bool {
	if isMailDir(path) {
		return false
	}
	fi, err := os.Stat(filepath.Join(path, "new"))
	if err != nil {
		return false
	}
	return fi.IsDir()
}

// isMailDir determins if path is one of Maildir's cur/new/tmp mail folders.
func isMailDir(path string) bool {
	return strings.HasSuffix(path, "new") || strings.HasSuffix(path, "cur") || strings.HasSuffix(path, "tmp")
}

// isNewEmail parses path looking for Maildir filename patterns for the flags
// indicating if a message has been seen.
func isNewEmail(path string) bool {
	// More docs on Maildir flags at http://cr.yp.to/proto/maildir.html
	//
	// Seen:
	// 1340520791.M843511P28536.sagan.sf.xinu.tv,S=1467,W=1503:2,RS
	// Unseen:
	// 1377995543.50996_2.sagan.sf.xinu.tv:2,Rb
	seen := false
	i := strings.LastIndex(path, ":")
	if i != -1 {
		info := path[i:]
		i = strings.Index(info, ",")
		if i == -1 {
			return true
		}

		flags := info[i:]
		seen = strings.Contains(flags, "S")
	}
	return !seen
}

type mailCounter struct {
	lastrun    time.Time
	counts     map[string]int
	needUpdate []string
	root       string
}

func newMailCounter() *mailCounter {
	return &mailCounter{
		counts: map[string]int{},
	}
}

func (mc *mailCounter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Content-Type", "application/json")
	c := map[string]int{}
	for k, v := range mc.counts {
		if v != 0 {
			c[k] = v
		}
	}
	enc := json.NewEncoder(w)
	err := enc.Encode(c)
	if err != nil {
		log.Println("Error printing JSON:", err)
	}
}

func (mc *mailCounter) updateCount(path string) error {
	if !isMailDir(path) {
		return nil
	}

	d, err := os.Open(path)
	if err != nil {
		return err
	}

	fi, err := d.Stat()
	if err != nil {
		return err
	}

	if !fi.IsDir() {
		return nil
	}

	folder := folderFromPath(filepath.Dir(path))
	paths, err := d.Readdirnames(-1)
	for _, path := range paths {
		if isNewEmail(path) {
			mc.counts[folder]++
		}
	}
	return nil
}

func (mc *mailCounter) walk(path string, info os.FileInfo, err error) error {
	if err != nil {
		return err
	}

	if info.IsDir() {
		return nil
	}

	dn := filepath.Dir(path)
	if isMailDir(dn) {
		if isNewEmail(path) {
			folder := folderFromPath(dn)
			mc.counts[folder]++
		}
	}
	return nil
}

func skip(path string) bool {
	switch filepath.Base(path) {
	case "cur", "new", "tmp":
		return true
	}
	return false
}

func (mc *mailCounter) fillNeedUpdate() error {
	mc.needUpdate = mc.needUpdate[:0]
	//	log.Println("BEGIN fillNeedUpdate", mc.needUpdate)
	//	defer func() {
	//		log.Println("END fillNeedUpdate", mc.needUpdate)
	//	}()
	return filepath.Walk(mc.root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if info.IsDir() {
			if info.ModTime().After(mc.lastrun) && isMailDir(path) {
				mc.needUpdate = append(mc.needUpdate, path)
			}
			if skip(path) {
				return filepath.SkipDir
			}
		}
		return nil
	})
}

func (mc *mailCounter) watch(root string, poll time.Duration) error {
	mc.root = root
	for {
		thisrun := time.Now()
		if err := mc.fillNeedUpdate(); err != nil {
			return err
		}
		modCnt := len(mc.needUpdate)
		if modCnt > 0 {
			log.Println(modCnt, "folders need update")
			update := map[string]struct{}{}
			for _, dir := range mc.needUpdate {
				// Store parent directory, which is the IMAP folder
				update[filepath.Dir(dir)] = struct{}{}
			}
			for u := range update {
				folder := folderFromPath(u)
				mc.counts[folder] = 0
			}
			for u := range update {
				if err := mc.updateCount(filepath.Join(u, "cur")); err != nil {
					return err
				}
				if err := mc.updateCount(filepath.Join(u, "new")); err != nil {
					return err
				}
				//if err := filepath.Walk(filepath.Join(u, "cur"), mc.walk); err != nil {
				//	return err
				//}
				//if err := filepath.Walk(filepath.Join(u, "new"), mc.walk); err != nil {
				//	return err
				//}
			}
		}
		mc.lastrun = thisrun
		time.Sleep(poll)
	}
	return nil
}

type countList []count

type count struct {
	f string
	c int
}

func (cl countList) Len() int { return len(cl) }
func (cl countList) Less(i, j int) bool {
	if cl[i].c == cl[j].c {
		return cl[i].f < cl[j].f
	}
	return cl[i].c > cl[j].c
}
func (cl countList) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }

func countListFromMap(counts map[string]int) countList {
	cl := make([]count, len(counts))
	i := 0
	for k, v := range counts {
		cl[i].f = k
		cl[i].c = v
		i++
	}
	return cl
}

// printCounts prints the folders with unread messages in descending order,
// sorting alphabetically for tie-breakers
func printCounts(counts map[string]int) {
	cl := countListFromMap(counts)
	sort.Sort(cl)
	for _, c := range cl {
		if c.c > 0 {
			log.Printf("%s (%d)", c.f, c.c)
		}
	}
}

func main() {
	flag.Parse()

	mc := newMailCounter()
	go func() {
		http.Handle("/unread", mc)
		err := http.ListenAndServe(*addr, nil)
		if err != nil {
			log.Fatal(err)
		}
	}()

	err := mc.watch(*root, *poll)
	if err != nil {
		log.Fatal(err)
	}
}
