package main

import (
	"bytes"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"strconv"
	"strings"
	"sync"

	"code.justin.tv/rhys/nursery/ldb"
	"github.com/syndtr/goleveldb/leveldb/iterator"
	"github.com/syndtr/goleveldb/leveldb/storage"
	"github.com/syndtr/goleveldb/leveldb/table"
)

func main() {
	hostport := flag.String("http", "127.0.0.1:8080", "HTTP debug host and port")
	dbdir := flag.String("dbdir", "", "Directory for LevelDB SSTables")
	flag.Parse()

	log.SetFlags(log.LUTC | log.Ldate | log.Ltime)

	go func() {
		err := (&http.Server{
			Addr: *hostport,
		}).ListenAndServe()
		if err != nil {
			log.Fatalf("http err=%q", err)
		}
	}()

	infos, err := ioutil.ReadDir(*dbdir)
	if err != nil {
		log.Fatalf("ReadDir err=%q", err)
	}
	var maxNum int64
	for _, info := range tablesOnly(infos) {
		if n, _ := tableNum(info.Name()); n > maxNum {
			maxNum = n
		}
	}

	sem := make(chan struct{}, 2*runtime.NumCPU())

	for {
		infos, err := ioutil.ReadDir(*dbdir)
		if err != nil {
			log.Fatalf("ReadDir err=%q", err)
		}
		infos = tablesOnly(infos)
		sort.Sort(byFileSize(infos))

		if len(infos) < 2 {
			break
		}

		var wg sync.WaitGroup
		for len(infos) >= 2 {
			sem <- struct{}{}
			wg.Add(1)
			go func(dir string, a, b os.FileInfo, outNum int64) {
				err = mergeFiles(dir, a, b, outNum)
				<-sem
				wg.Done()
				if err != nil {
					log.Fatalf("mergeFiles err=%q", err)
				}
			}(*dbdir, infos[0], infos[1], maxNum+1)

			infos = infos[2:]
			maxNum++
		}
		wg.Wait()
	}
}

func mergeFiles(dir string, a, b os.FileInfo, outNum int64) (err error) {
	af, err := os.Open(filepath.Join(dir, a.Name()))
	if err != nil {
		return err
	}
	defer func() {
		err := af.Close()
		if err != nil {

		}
	}()

	bf, err := os.Open(filepath.Join(dir, b.Name()))
	if err != nil {
		return err
	}
	defer func() {
		err := bf.Close()
		if err != nil {

		}
	}()

	an, _ := tableNum(a.Name())
	bn, _ := tableNum(b.Name())

	ar, err := table.NewReader(af, a.Size(), storage.FileDesc{Type: storage.TypeTable, Num: an}, nil, nil, nil)
	if err != nil {
		return err
	}
	br, err := table.NewReader(bf, b.Size(), storage.FileDesc{Type: storage.TypeTable, Num: bn}, nil, nil, nil)
	if err != nil {
		return err
	}

	ai, bi := ar.NewIterator(nil, nil), br.NewIterator(nil, nil)

	outname := fmt.Sprintf("%06d.ldb", outNum)

	commit := false
	outf, err := ioutil.TempFile(dir, "tmp-"+outname+"-")
	if err != nil {
		return err
	}
	defer func() {
		cerr := outf.Close()
		if err == nil {
			err = cerr
		}
	}()
	defer func() {
		if !commit {
			derr := os.Remove(outf.Name())
			if err == nil {
				err = derr
			}
		}
	}()

	err = os.Chmod(outf.Name(), 0644)
	if err != nil {
		return err
	}

	outw := table.NewWriter(outf, nil)
	defer func() {
		cerr := outw.Close()
		if err == nil {
			err = cerr
		}
	}()

	it := iterator.NewMergedIterator([]iterator.Iterator{ai, bi}, ldb.FastBytewiseComparator, false)

	var prev []byte
	for it.Next() {
		if bytes.Equal(prev, it.Key()) {
			continue
		}
		prev = append([]byte(nil), it.Key()...)

		err := outw.Append(it.Key(), it.Value())
		if err != nil {
			return err
		}
	}

	err = it.Error()
	if err != nil {
		return err
	}

	commit = true

	err = os.Rename(outf.Name(), filepath.Join(dir, outname))
	if err != nil {
		return err
	}

	err = os.Remove(filepath.Join(dir, a.Name()))
	if err != nil {
		return err
	}
	err = os.Remove(filepath.Join(dir, b.Name()))
	if err != nil {
		return err
	}

	return nil
}

func tablesOnly(infos []os.FileInfo) []os.FileInfo {
	var out []os.FileInfo

	for _, info := range infos {
		if _, ok := tableNum(info.Name()); ok {
			out = append(out, info)
		}
	}

	return out
}

func tableNum(name string) (int64, bool) {
	num := strings.TrimSuffix(name, ".ldb")
	if len(name) == len(num) || len(num) < 6 {
		return 0, false
	}

	n, err := strconv.ParseInt(num, 10, 64)
	if err != nil || n < 0 {
		return 0, false
	}

	return n, true
}

type byFileSize []os.FileInfo

func (fs byFileSize) Len() int           { return len(fs) }
func (fs byFileSize) Swap(i, j int)      { fs[i], fs[j] = fs[j], fs[i] }
func (fs byFileSize) Less(i, j int) bool { return fs[i].Size() < fs[j].Size() }
