func main() {

	var host = flag.String("host", "localhost", "The hostname to listen for requests on")
	var port = flag.Int("port", 8080, "The port number to listen for requests on")
	var data = flag.String("data", "", "The data directory where WOF data lives, required")
	var cache_size = flag.Int("cache_size", 1024, "The number of WOF records with large geometries to cache")
	var cache_trigger = flag.Int("cache_trigger", 2000, "The minimum number of coordinates in a WOF record that will trigger caching")
	var strict = flag.Bool("strict", false, "Enable strict placetype checking")
	var logs = flag.String("logs", "", "Where to write logs to disk")
	var metrics = flag.String("metrics", "", "Where to write (@rcrowley go-metrics style) metrics to disk")
	var format = flag.String("metrics-as", "plain", "Format metrics as... ? Valid options are \"json\" and \"plain\"")
	var cors = flag.Bool("cors", false, "Enable CORS headers")
	var verbose = flag.Bool("verbose", false, "Enable verbose logging, or log level \"info\"")
	var verboser = flag.Bool("verboser", false, "Enable really verbose logging, or log level \"debug\"")

	flag.Parse()
	args := flag.Args()

	if *data == "" {
		panic("missing data")
	}

	_, err := os.Stat(*data)

	if os.IsNotExist(err) {
		panic("data does not exist")
	}

	loglevel := "status"

	if *verbose {
		loglevel = "info"
	}

	if *verboser {
		loglevel = "debug"
	}

	var l_writer io.Writer
	var m_writer io.Writer

	l_writer = io.MultiWriter(os.Stdout)

	if *logs != "" {

		l_file, l_err := os.OpenFile(*logs, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0660)

		if l_err != nil {
			panic(l_err)
		}

		l_writer = io.MultiWriter(os.Stdout, l_file)
	}

	logger := log.NewWOFLogger("[wof-pip-server] ")
	logger.AddLogger(l_writer, loglevel)

	p, p_err := pip.NewPointInPolygon(*data, *cache_size, *cache_trigger, logger)

	if p_err != nil {
		panic(p_err)
	}

	if *metrics != "" {

		m_file, m_err := os.OpenFile(*metrics, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0660)

		if m_err != nil {
			panic(m_err)
		}

		m_writer = io.MultiWriter(m_file)
		_ = p.SendMetricsTo(m_writer, 60e9, *format)
	}

	t1 := time.Now()

	for _, path := range args {
		p.IndexMetaFile(path)
	}

	t2 := float64(time.Since(t1)) / 1e9

	p.Logger.Status("indexed %d records in %.3f seconds", p.Rtree.Size(), t2)

	for pt, count := range p.Placetypes {
		p.Logger.Status("indexed %s: %d", pt, count)
	}

	handler := func(rsp http.ResponseWriter, req *http.Request) {

		query := req.URL.Query()

		str_lat := query.Get("latitude")
		str_lon := query.Get("longitude")
		placetype := query.Get("placetype")

		if str_lat == "" {
			http.Error(rsp, "Missing latitude parameter", http.StatusBadRequest)
			return
		}

		if str_lon == "" {
			http.Error(rsp, "Missing longitude parameter", http.StatusBadRequest)
			return
		}

		lat, lat_err := strconv.ParseFloat(str_lat, 64)
		lon, lon_err := strconv.ParseFloat(str_lon, 64)

		if lat_err != nil {
			http.Error(rsp, "Invalid latitude parameter", http.StatusBadRequest)
			return
		}

		if lon_err != nil {
			http.Error(rsp, "Invalid longitude parameter", http.StatusBadRequest)
			return
		}

		if lat > 90.0 || lat < -90.0 {
			http.Error(rsp, "E_IMPOSSIBLE_LATITUDE", http.StatusBadRequest)
			return
		}

		if lon > 180.0 || lon < -180.0 {
			http.Error(rsp, "E_IMPOSSIBLE_LONGITUDE", http.StatusBadRequest)
			return
		}

		if placetype != "" {

			if *strict && !p.IsKnownPlacetype(placetype) {
				http.Error(rsp, "Unknown placetype", http.StatusBadRequest)
				return
			}
		}

		results, timings := p.GetByLatLonForPlacetype(lat, lon, placetype)

		count := len(results)
		ttp := 0.0

		for _, t := range timings {
			ttp += t.Duration
		}

		if placetype != "" {
			p.Logger.Debug("time to reverse geocode %f, %f @%s: %d results in %f seconds ", lat, lon, placetype, count, ttp)
		} else {
			p.Logger.Debug("time to reverse geocode %f, %f: %d results in %f seconds ", lat, lon, count, ttp)
		}

		js, err := json.Marshal(results)

		if err != nil {
			http.Error(rsp, err.Error(), http.StatusInternalServerError)
			return
		}

		// maybe this although it seems like it adds functionality for a lot of
		// features this server does not need - https://github.com/rs/cors
		// (20151022/thisisaaronland)

		if *cors {
			rsp.Header().Set("Access-Control-Allow-Origin", "*")
		}

		rsp.Header().Set("Content-Type", "application/json")
		rsp.Write(js)
	}

	endpoint := fmt.Sprintf("%s:%d", *host, *port)

	http.HandleFunc("/", handler)
	http.ListenAndServe(endpoint, nil)
}
func main() {

	var host = flag.String("host", "localhost", "The hostname to listen for requests on")
	var port = flag.Int("port", 8080, "The port number to listen for requests on")
	var data = flag.String("data", "", "The data directory where WOF data lives, required")
	var cache_all = flag.Bool("cache_all", false, "Just cache everything, regardless of size")
	var cache_size = flag.Int("cache_size", 1024, "The number of WOF records with large geometries to cache")
	var cache_trigger = flag.Int("cache_trigger", 2000, "The minimum number of coordinates in a WOF record that will trigger caching")
	var strict = flag.Bool("strict", false, "Enable strict placetype checking")
	var loglevel = flag.String("loglevel", "info", "Log level for reporting")
	var logs = flag.String("logs", "", "Where to write logs to disk")
	var metrics = flag.String("metrics", "", "Where to write (@rcrowley go-metrics style) metrics to disk")
	var format = flag.String("metrics-as", "plain", "Format metrics as... ? Valid options are \"json\" and \"plain\"")
	var cors = flag.Bool("cors", false, "Enable CORS headers")
	var procs = flag.Int("procs", (runtime.NumCPU() * 2), "The number of concurrent processes to clone data with")
	var pidfile = flag.String("pidfile", "", "Where to write a PID file for wof-pip-server. If empty the PID file will be written to wof-pip-server.pid in the current directory")
	var nopid = flag.Bool("nopid", false, "Do not try to write a PID file")

	flag.Parse()
	args := flag.Args()

	if *data == "" {
		panic("missing data")
	}

	_, err := os.Stat(*data)

	if os.IsNotExist(err) {
		panic("data does not exist")
	}

	runtime.GOMAXPROCS(*procs)

	var l_writer io.Writer
	var m_writer io.Writer

	l_writer = io.MultiWriter(os.Stdout)

	if *logs != "" {

		l_file, l_err := os.OpenFile(*logs, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0660)

		if l_err != nil {
			panic(l_err)
		}

		l_writer = io.MultiWriter(os.Stdout, l_file)
	}

	logger := log.NewWOFLogger("[wof-pip-server] ")
	logger.AddLogger(l_writer, *loglevel)

	if *cache_all {

		*cache_size = 0
		*cache_trigger = 1

		mu := new(sync.Mutex)
		wg := new(sync.WaitGroup)

		for _, path := range args {

			wg.Add(1)

			go func(path string) {
				defer wg.Done()

				count := 0

				fh, err := os.Open(path)

				if err != nil {
					logger.Error("failed to open %s for reading, because %v", path, err)
					os.Exit(1)
				}

				scanner := bufio.NewScanner(fh)

				for scanner.Scan() {
					count += 1
				}

				mu.Lock()
				*cache_size += count
				mu.Unlock()

			}(path)
		}

		wg.Wait()

		logger.Status("set cache_size to %d and cache_trigger to %d", *cache_size, *cache_trigger)
	}

	p, p_err := pip.NewPointInPolygon(*data, *cache_size, *cache_trigger, logger)

	if p_err != nil {
		panic(p_err)
	}

	if *metrics != "" {

		m_file, m_err := os.OpenFile(*metrics, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0660)

		if m_err != nil {
			panic(m_err)
		}

		m_writer = io.MultiWriter(m_file)
		_ = p.SendMetricsTo(m_writer, 60e9, *format)
	}

	indexing := true
	ch := make(chan bool)

	go func() {
		<-ch
		indexing = false
	}()

	go func() {

		if *nopid {

			t1 := time.Now()

			for _, path := range args {
				p.Logger.Status("indexing %s", path)
				p.IndexMetaFile(path)
			}

			t2 := float64(time.Since(t1)) / 1e9
			p.Logger.Status("indexed %d records in %.3f seconds", p.Rtree.Size(), t2)

			ch <- true
			return
		}

		if *pidfile == "" {

			cwd, err := os.Getwd()

			if err != nil {
				panic(err)
			}

			fname := fmt.Sprintf("%s.pid", os.Args[0])

			*pidfile = filepath.Join(cwd, fname)
		}

		fh, err := os.Create(*pidfile)

		if err != nil {
			panic(err)
		}

		defer fh.Close()

		t1 := time.Now()

		for _, path := range args {
			p.Logger.Status("indexing %s", path)
			p.IndexMetaFile(path)
		}

		t2 := float64(time.Since(t1)) / 1e9
		p.Logger.Status("indexed %d records in %.3f seconds", p.Rtree.Size(), t2)

		pid := os.Getpid()
		strpid := strconv.Itoa(pid)

		fh.Write([]byte(strpid))

		p.Logger.Status("create PID file %s", *pidfile)

		sigs := make(chan os.Signal, 1)
		signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

		go func() {
			<-sigs

			p.Logger.Status("remove PID file %s", *pidfile)

			os.Remove(*pidfile)
			os.Exit(0)
		}()

		ch <- true
	}()

	handler := func(rsp http.ResponseWriter, req *http.Request) {

		if indexing == true {
			http.Error(rsp, "indexing records", http.StatusServiceUnavailable)
			return
		}

		query := req.URL.Query()

		str_lat := query.Get("latitude")
		str_lon := query.Get("longitude")
		placetype := query.Get("placetype")
		excluded := query["exclude"] // see the way we're accessing the map directly to get a list? yeah, that

		if str_lat == "" {
			http.Error(rsp, "Missing latitude parameter", http.StatusBadRequest)
			return
		}

		if str_lon == "" {
			http.Error(rsp, "Missing longitude parameter", http.StatusBadRequest)
			return
		}

		lat, lat_err := strconv.ParseFloat(str_lat, 64)
		lon, lon_err := strconv.ParseFloat(str_lon, 64)

		if lat_err != nil {
			http.Error(rsp, "Invalid latitude parameter", http.StatusBadRequest)
			return
		}

		if lon_err != nil {
			http.Error(rsp, "Invalid longitude parameter", http.StatusBadRequest)
			return
		}

		if lat > 90.0 || lat < -90.0 {
			http.Error(rsp, "E_IMPOSSIBLE_LATITUDE", http.StatusBadRequest)
			return
		}

		if lon > 180.0 || lon < -180.0 {
			http.Error(rsp, "E_IMPOSSIBLE_LONGITUDE", http.StatusBadRequest)
			return
		}

		filters := pip.WOFPointInPolygonFilters{}

		if placetype != "" {

			if *strict && !p.IsKnownPlacetype(placetype) {
				http.Error(rsp, "Unknown placetype", http.StatusBadRequest)
				return
			}

			filters["placetype"] = placetype
		}

		for _, what := range excluded {

			if what == "deprecated" || what == "superseded" {
				filters[what] = false
			}
		}

		results, timings := p.GetByLatLonFiltered(lat, lon, filters)

		count := len(results)
		ttp := 0.0

		for _, t := range timings {
			ttp += t.Duration
		}

		if placetype != "" {
			p.Logger.Debug("time to reverse geocode %f, %f @%s: %d results in %f seconds ", lat, lon, placetype, count, ttp)
		} else {
			p.Logger.Debug("time to reverse geocode %f, %f: %d results in %f seconds ", lat, lon, count, ttp)
		}

		js, err := json.Marshal(results)

		if err != nil {
			http.Error(rsp, err.Error(), http.StatusInternalServerError)
			return
		}

		// maybe this although it seems like it adds functionality for a lot of
		// features this server does not need - https://github.com/rs/cors
		// (20151022/thisisaaronland)

		if *cors {
			rsp.Header().Set("Access-Control-Allow-Origin", "*")
		}

		rsp.Header().Set("Content-Type", "application/json")
		rsp.Write(js)
	}

	endpoint := fmt.Sprintf("%s:%d", *host, *port)

	mux := http.NewServeMux()
	mux.HandleFunc("/", handler)

	gracehttp.Serve(&http.Server{Addr: endpoint, Handler: mux})

	os.Exit(0)
}