/*
    This file is part of Kismet

    Kismet is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    Kismet is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Kismet; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

/*
 * Convert a kismetdb log to KML, with several different options
 */

#include "config.h"

#include <map>
#include <iomanip>
#include <ctime>
#include <iostream>
#include <regex>
#include <tuple>

#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdbool.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <sqlite3.h>

#include "getopt.h"
#include "nlohmann/json.hpp"
#include "sqlite3_cpp11.h"
#include "fmt.h"
#include "packet_ieee80211.h"

// Aggressive additional mangle of text to handle converting to hexcode for XML
std::string MungeForXML(const std::string& in_data) {
	std::string ret;

	for (size_t i = 0; i < in_data.length(); i++) {
        if (in_data[i] == '<') {
            ret += "&lt;";
        } else if (in_data[i] == '>') {
            ret += "&gt;";
        } else if (in_data[i] == '&') {
            ret += "&amp;";
        } else if (in_data[i] == '"') { 
            ret += "&quot;";
        } else if (in_data[i] == '\'') {
            ret += "&apos;";
        } else if ((unsigned char) in_data[i] >= 32 && (unsigned char) in_data[i] <= 126) {
			ret += in_data[i];
		} else {
			ret += '\\';
			ret += ((in_data[i] >> 6) & 0x03) + '0';
			ret += ((in_data[i] >> 3) & 0x07) + '0';
			ret += ((in_data[i] >> 0) & 0x07) + '0';
		}
	}

	return ret;
}

// Some conversion functions from Kismet for Wi-Fi channels
int FrequencyToWifiChannel(double in_freq) {
    if (in_freq == 0)
        return 0;

    in_freq = in_freq / 1000;

    if (in_freq == 2484)
        return 14;
    else if (in_freq < 2484)
        return (in_freq - 2407) / 5;
    else if (in_freq >= 4910 && in_freq <= 4980)
        return (in_freq - 4000) / 5;
    else if (in_freq <= 45000)
        return (in_freq - 5000) / 5;
    else if (in_freq >= 58320 && in_freq <= 64800)
        return (in_freq - 56160) / 2160;
    else
        return in_freq;
}

// we can't do this is as a sqlite function, as cool as that would be, because sqlite functions
// can't be part of a `where`, only a `having`, which introduces tons of problems.
double distance_meters(double lat0, double lon0, double lat1, double lon1) {
    lat0 = (M_PI / 180) * lat0;
    lon0 = (M_PI / 180) * lon0;
    lat1 = (M_PI / 180) * lat1;
    lon1 = (M_PI / 180) * lon1;

    double diff_lon = lon1 - lon0;
    double diff_lat = lat1 - lat0;

    double ret = 
        (2 * asin(sqrt(pow(sin(diff_lat / 2), 2) +
                       cos(lat0) * cos(lat1) * pow(sin(diff_lon / 2), 2)))) * 6731000.0f;

    return ret;
}

std::string WifiCryptToString(unsigned long cryptset) {
    std::stringstream ss;

    if (cryptset & crypt_wps)
        ss << "[WPS] ";

    if ((cryptset & crypt_protectmask) == crypt_wep) 
        ss << "[WEP] ";

    if (cryptset & crypt_wpa) {

        std::string cryptver = "";

        if (cryptset & crypt_tkip) {
            if (cryptset & crypt_aes_ccm) {
                cryptver = "CCMP+TKIP";
            } else {
                cryptver = "TKIP";
            }
        } else if (cryptset & crypt_aes_ccm) {
            cryptver = "CCMP";
        }

        std::string authver = "";

        if (cryptset & crypt_psk) {
            authver = "PSK";
        } else if (cryptset & crypt_eap) {
            authver = "EAP";
        }

        if ((cryptset & crypt_version_wpa) && (cryptset & crypt_version_wpa2)) {
            ss << "[WPA-" << authver << "-" << cryptver << "] ";
            ss << "[WPA2-" << authver << "-" << cryptver << "] ";
        } else if (cryptset & crypt_version_wpa2) {
            ss << "[WPA2-" << authver << "-" << cryptver << "] ";
        } else {
            ss << "[WPA-" << authver << "-" << cryptver << "] ";
        }
    }

    auto retstr = ss.str();

    if (retstr.length() > 0)
        return retstr.substr(0, retstr.length() - 1);

    return "";
}

// Specific points
class kml_point {
public:
    double lat, lon, alt;
};

// Placemark cache
class kml_placemark {
public:
    // Running lat, lon, and alt, if we're computing it
    double avg_lat, avg_lon, avg_alt;
    double avg_2d_num, avg_alt_num;

    // We might want to support multiple points per AP
    std::vector<kml_point> point_vec;

    // Name (last known name, user name, or mac address)
    std::string name;
    std::string phy_layer;
    std::string description;
    std::string channel;
    std::string crypt;
};

void print_help(char *argv) {
    printf("Kismetdb to KML\n");
    printf("A simple tool for converting the packet data from a KismetDB log file to\n"
           "a KML file for plotting in Google Earth\n");
    printf("usage: %s [OPTION]\n", argv);
    printf(" -i, --in [filename]          Input kismetdb file\n"
           " -o, --out [filename]         Output KML file\n"
           " -f, --force                  Force writing to the target file, even if it exists.\n"
           " -v, --verbose                Verbose output\n"
           " -s, --skip-clean             Don't clean (sql vacuum) input database\n"
           " -e, --exclude lat,lon,dist   Exclude records within 'dist' *meters* of the lat,lon\n"
           "                              provided.  This can be used to exclude packets close to\n"
           "                              your home, or other sensitive locations.\n"
           " --basic-location             Use basic average location information instead of computing a\n"
           "                              high-precision location; faster, but less accurate\n"
           " -g, --group                  Group by type into folders\n"
          );
}

// Helper method to add placemarks from a vector to the KML file when grouping placemarks.
void add_placemarks_from_vec(FILE* ofile, std::vector<kml_placemark>& placemark_vec, std::string folder_name, 
        std::string style_name, unsigned long& point_num, unsigned long& place_num) {
    fmt::print(ofile, "<Folder>");
    fmt::print(ofile, "<name>{}</name>", folder_name);
    for (auto pl : placemark_vec) {
        fmt::print(ofile, "<Placemark id=\"{}\">", place_num++);
        fmt::print(ofile, "<name>{}</name>", MungeForXML(pl.name));

        // style based on phy layer
        fmt::print(ofile, "<styleUrl>{}</styleUrl>", style_name);

        // add description
        pl.description = "Name:" + MungeForXML(pl.name) + "\n";
        pl.description += pl.phy_layer + "\n";
        pl.description += "Channel:" + pl.channel;

        if (pl.crypt.length() > 0)
            pl.description += "\nCrypt:" + pl.crypt;

        fmt::print(ofile, "<description>{}</description>", pl.description);

        for (auto p : pl.point_vec) {
            fmt::print(ofile,
                "<Point "
                "id=\"{}\"><coordinates>{:3.10f},{:3.10f},{:3.10f}</coordinates></Point>",
                point_num++, p.lon, p.lat, p.alt);
        }

        fmt::print(ofile, "</Placemark>\n");
    }
    fmt::print(ofile, "</Folder>\n");
}

int main(int argc, char *argv[]) {
    static struct option longopt[] = {
        { "in", required_argument, 0, 'i' },
        { "out", required_argument, 0, 'o' },
        { "verbose", no_argument, 0, 'v' },
        { "force", no_argument, 0, 'f' },
        { "help", no_argument, 0, 'h' },
        { "skip-clean", no_argument, 0, 's' },
        { "exclude", required_argument, 0, 'e'},
        { "basic-location", no_argument, 0, 'B'},
        { "group", no_argument, 0, 'g' },
        { 0, 0, 0, 0 }
    };

    int option_idx = 0;
    optind = 0;
    opterr = 0;

    const std::regex mac_pattern("^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$"); // regex pattern for MAC address
    std::string in_fname, out_fname;
    bool verbose = false;
    bool force = false;
    bool skipclean = false;
    bool basiclocation = false;
    bool group_in_folder = false;

    std::vector<std::tuple<double, double, double>> exclusion_zones;

    int sql_r = 0;
    char *sql_errmsg = NULL;
    sqlite3 *db = NULL;

    FILE *ofile = NULL;

    struct stat statbuf;

    while (1) {
        int r = getopt_long(argc, argv, 
                            "-hi:o:r:c:e:vfs", 
                            longopt, &option_idx);
        if (r < 0) break;

        if (r == 'h') {
            print_help(argv[0]);
            exit(1);
        } else if (r == 'i') {
            in_fname = std::string(optarg);
        } else if (r == 'o') {
            out_fname = std::string(optarg);
        } else if (r == 'v') { 
            verbose = true;
        } else if (r == 'f') {
            force = true;
        } else if (r == 's') {
            skipclean = true;
        } else if (r == 'e') {
            double lat, lon, distance;

            if (sscanf(optarg, "%lf,%lf,%lf", &lat, &lon, &distance) != 3) {
                fmt::print(stderr, "ERROR:  Expected an exclusion zone of lat,lon,distance_in_meters.\n");
                exit(1);
            }

            exclusion_zones.push_back(std::make_tuple(lat, lon, distance));
        } else if (r == 'B') {
            basiclocation = true;
        } else if (r == 'g') {
            group_in_folder = true;
        }
    }

    if (out_fname == "" || in_fname == "") {
        fmt::print(stderr, "ERROR: Expected --in [kismetdb file] and --out [KML file]\n");
        exit(1);
    }

    if (stat(in_fname.c_str(), &statbuf) < 0) {
        if (errno == ENOENT) 
            fmt::print(stderr, "ERROR:  Input file '{}' does not exist.\n", in_fname);
        else
            fmt::print(stderr, "ERROR:  Unexpected problem checking input "
                    "file '{}': {}\n", in_fname, strerror(errno));

        exit(1);
    }

    if (out_fname != "-") {
        if (stat(out_fname.c_str(), &statbuf) < 0) {
            if (errno != ENOENT) {
                fmt::print(stderr, "ERROR:  Unexpected problem checking output "
                        "file '{}': {}\n", out_fname, strerror(errno));
                exit(1);
            }
        } else if (force == false) {
            fmt::print(stderr, "ERROR:  Output file '{}' exists already; use --force to "
                    "clobber the file.\n", out_fname);
            exit(1);
        }
    }

    /* Open the database and run the vacuum command to clean up any stray journals */
    if (!skipclean)
        sql_r = sqlite3_open(in_fname.c_str(), &db);
    else
        sql_r = sqlite3_open_v2(in_fname.c_str(), &db, SQLITE_OPEN_READONLY, nullptr);

    if (sql_r) {
        fmt::print(stderr, "ERROR:  Unable to open '{}': {}\n", in_fname, sqlite3_errmsg(db));
        exit(1);
    }

    if (!skipclean) {
        if (verbose)
            fmt::print(stderr, "* Preparing input database '{}'...\n", in_fname);


        sql_r = sqlite3_exec(db, "VACUUM;", NULL, NULL, &sql_errmsg);

        if (sql_r != SQLITE_OK) {
            fmt::print(stderr, "ERROR:  Unable to clean up (vacuum) database before copying: {}\n", sql_errmsg);
            sqlite3_close(db);
            exit(1);
        }
    }

    // Use our sql adapters

    using namespace kissqlite3;

    int db_version = 0;
    long int n_total_packets_db = 0L;
    long int n_packets_db = 0L;
    long int n_devices_db = 0L;
    long int n_devices_gps_db = 0L;
    long int n_data_gps_db = 0L;

    try {
        // Get the version
        auto version_query = _SELECT(db, "KISMET", {"db_version"});
        auto version_ret = version_query.begin();
        if (version_ret == version_query.end()) {
            fmt::print(stderr, "ERROR:  Unable to fetch database version.\n");
            sqlite3_close(db);
            exit(1);
        }
        db_version = sqlite3_column_as<int>(*version_ret, 0);

        if (verbose)
            fmt::print(stderr, "* Found KismetDB version {}\n", db_version);

        // Get the total counts
        auto npackets_q = _SELECT(db, "packets", 
                {"count(*), sum(case when (sourcemac != '00:00:00:00:00:00' "
                "and lat != 0 and lon != 0) then 1 else 0 end)"});
        auto npackets_ret = npackets_q.begin();
        if (npackets_ret == npackets_q.end()) {
            fmt::print(stderr, "ERROR:  Unable to fetch packet count.\n");
            sqlite3_close(db);
            exit(1);
        }
        n_total_packets_db = sqlite3_column_as<unsigned long>(*npackets_ret, 0);
        n_packets_db = sqlite3_column_as<unsigned long>(*npackets_ret, 1);

        auto ndata_q = _SELECT(db, "data", {"count(*)"}, _WHERE("lat", NEQ, 0, AND, "lon", NEQ, 0));
        auto ndata_ret = ndata_q.begin();
        if (npackets_ret == npackets_q.end()) {
            fmt::print(stderr, "ERROR:  Unable to fetch data count.\n");
            sqlite3_close(db);
            exit(1);
        }
        n_data_gps_db = sqlite3_column_as<unsigned long>(*ndata_ret, 0);

        auto ndevices_q = _SELECT(db, "devices", {"count(*)"});
        auto ndevices_ret = ndevices_q.begin();
        if (ndevices_ret == ndevices_q.end()) {
            fmt::print(stderr, "ERROR:  Unable to fetch device count.\n");
            sqlite3_close(db);
            exit(1);
        }
        n_devices_db = sqlite3_column_as<unsigned long>(*ndevices_ret, 0);

        auto ndevices_gps_q = _SELECT(db, "devices", {"count(*)"}, 
                _WHERE("avglat", NEQ, 0,
                    AND,
                    "avglon", NEQ, 0));
        auto ndevices_gps_ret = ndevices_q.begin();
        if (ndevices_gps_ret == ndevices_gps_q.end()) {
            fmt::print(stderr, "ERROR: Unable to fetch device-with-gps count.\n");
            sqlite3_close(db);
            exit(1);
        }
        n_devices_gps_db = sqlite3_column_as<unsigned long>(*ndevices_gps_ret, 0);

        if (verbose) 
            fmt::print(stderr, "* Found {} devices, {} devices with gps, {} usable packets, {} total packets\n", 
                    n_devices_db, n_devices_gps_db, n_packets_db, n_total_packets_db);

        if (n_devices_gps_db && basiclocation) {
            fmt::print(stderr, "ERROR:  No usable devices in the log file; devices must have GPS information\n"
                               "        to be usable with KML.  You can try without --basic-location to use the\n"
                               "        packets to derive a more precise location, but you may have no GPS data\n"
                               "        in this log.\n");
            sqlite3_close(db);
            exit(1);
        }

        if (n_packets_db == 0 && n_data_gps_db == 0 && !basiclocation) {
            fmt::print(stderr, "ERROR:  No usable packets in the log file; packets must have GPS information\n"
                               "        to be usable with KML.  You can try with --basic-location to use the\n"
                               "        device averages.\n");
            sqlite3_close(db);
            exit(1);
        }
    } catch (const std::exception& e) {
        fmt::print(stderr, "ERROR:  Could not get database information from '{}': {}\n", in_fname, e.what());
        exit(0);
    }

    if (out_fname == "-") {
        ofile = stdout;
    } else {
        ofile = fopen(out_fname.c_str(), "w");
        if (ofile == NULL) {
            fmt::print(stderr, "ERROR:  Unable to open output file for writing: {}\n", strerror(errno));
            exit(1);
        }
    }

    std::vector<kml_placemark> placemark_vec;

    // placemarks by types
    std::vector<kml_placemark> ap_open_placemark_vec;
    std::vector<kml_placemark> ap_wep_placemark_vec;
    std::vector<kml_placemark> ap_wpa_placemark_vec;
    std::vector<kml_placemark> client_placemark_vec;
    std::vector<kml_placemark> other_placemark_vec;
    std::vector<kml_placemark> zigbee_placemark_vec;
    std::vector<kml_placemark> bluetooth_placemark_vec;

    if (basiclocation) {
        auto basic_q = 
            _SELECT(db, "devices", 
                    {"min_lat", "min_lon", "max_lat", "max_lon", "avg_lat", "avg_lon", "device"}, 
                    _WHERE("avglat", NEQ, 0, AND, "avglon", NEQ, 0));

        for (auto d : basic_q) {
            double avg_lat, avg_lon;

            // Handle the different versions
            if (db_version < 5) {
                avg_lat = sqlite3_column_as<double>(d, 4) / 100000;
                avg_lon = sqlite3_column_as<double>(d, 5) / 100000;
            } else {
                avg_lat = sqlite3_column_as<double>(d, 4);
                avg_lon = sqlite3_column_as<double>(d, 5);
            }

            // Check to see if we lie in any exclusion zones
            bool violates_exclusion = false;
            for (auto ez : exclusion_zones) {
                if (distance_meters(avg_lat, avg_lon, std::get<0>(ez), std::get<1>(ez)) <= std::get<2>(ez)) {
                    violates_exclusion = true;
                    break;
                }
            }

            if (violates_exclusion) {
                continue;
            }

            nlohmann::json json;
            std::stringstream ss(sqlite3_column_as<std::string>(d, 6));

            try {
                ss >> json;

                kml_point p;
                p.lat = avg_lat;
                p.lon = avg_lon;
                p.alt = 0;

                kml_placemark pl;
                pl.name = json["kismet.device.base.commonname"].get<std::string>();
                pl.phy_layer = json["kismet.device.base.phyname"].get<std::string>();
                pl.channel = json["kismet.device.base.channel"].get<std::string>();
                pl.crypt = json["kismet.device.base.crypt"].get<std::string>();
                pl.point_vec.push_back(p);

                if (group_in_folder) {
                    // style based on phy layer
                    if (pl.phy_layer == "Bluetooth" || pl.phy_layer == "BTLE") {
                        bluetooth_placemark_vec.push_back(pl);
                    } else if (pl.phy_layer == "802.15.4") {
                        zigbee_placemark_vec.push_back(pl);
                    } else {
                        if (regex_match(pl.name, mac_pattern)) {
                            client_placemark_vec.push_back(pl);
                        } else {
                            if (pl.crypt == "Open") {
                                ap_open_placemark_vec.push_back(pl);
                            } else if (pl.crypt.find("WEP") != std::string::npos) {
                                ap_wep_placemark_vec.push_back(pl);
                            } else if (pl.crypt.find("WPA") != std::string::npos) {
                                ap_wpa_placemark_vec.push_back(pl);
                            } else {
                                other_placemark_vec.push_back(pl);
                            }
                        }
                    }
                } else {
                    placemark_vec.push_back(pl);
                }
            } catch (const std::exception &e) {
                std::cerr << fmt::format(
                                 "WARNING:  Could not process device info for "
                                 "'{}', skipping",
                                 json.dump())
                          << std::endl;
            }
        }
    } else {
        auto basic_q = 
            _SELECT(db, "devices", {"phyname", "devmac", "device"});

        for (auto d : basic_q) {
            // Prep the packet list for different kismetdb versions
            std::list<std::string> packet_fields;

            if (db_version < 5) {
                packet_fields = std::list<std::string>{"lat", "lon" };
            } else {
                packet_fields = std::list<std::string>{"lat", "lon", "alt"};
            }

            auto phyname = sqlite3_column_as<std::string>(d, 0);
            auto devmac = sqlite3_column_as<std::string>(d, 1);
            nlohmann::json json;

            std::stringstream ss(sqlite3_column_as<std::string>(d, 2));

            kml_placemark pl;

            try {
                ss >> json;
                pl.name = json["kismet.device.base.commonname"].get<std::string>();
                pl.phy_layer = json["kismet.device.base.phyname"].get<std::string>();
                pl.channel = json["kismet.device.base.channel"].get<std::string>();
                pl.crypt = json["kismet.device.base.crypt"].get<std::string>();
            } catch (const std::exception& e) {
                fmt::print(stderr, "WARNING:  Could not process device info for '{}', skipping\n", json.dump());
                continue;
            }

            pl.avg_alt = 0;
            pl.avg_lat = 0;
            pl.avg_lon = 0;
            pl.avg_2d_num = 0;
            pl.avg_alt = 0;

            auto packet_q = _SELECT(db, "packets", packet_fields,
                    _WHERE("sourcemac", EQ, devmac, AND, "phyname", EQ, phyname, AND, "lat", NEQ, 0, AND, "lon", NEQ, 0));

            for (auto p : packet_q) {
                double lat, lon, alt;

                // Handle the different versions
                if (db_version < 5) {
                    lat = sqlite3_column_as<double>(p, 0) / 100000;
                    lon = sqlite3_column_as<double>(p, 1) / 100000;
                    alt = 0;
                } else {
                    lat = sqlite3_column_as<double>(p, 0);
                    lon = sqlite3_column_as<double>(p, 1);
                    alt = sqlite3_column_as<double>(p, 2);
                }

                // Check to see if we lie in any exclusion zones
                bool violates_exclusion = false;
                for (auto ez : exclusion_zones) {
                    if (distance_meters(lat, lon, std::get<0>(ez), std::get<1>(ez)) <= std::get<2>(ez)) {
                        violates_exclusion = true;
                        break;
                    }
                }

                if (violates_exclusion) {
                    continue;
                }

                pl.avg_lat += lat;
                pl.avg_lon += lon;
                pl.avg_2d_num++;

                if (alt != 0) {
                    pl.avg_alt += alt;
                    pl.avg_alt_num++;
                }
            }

            auto data_q = _SELECT(db, "data", packet_fields,
                    _WHERE("devmac", EQ, devmac, AND, "phyname", EQ, phyname, AND, "lat", NEQ, 0, AND, "lon", NEQ, 0));

            for (auto p : data_q) {
                double lat, lon, alt;

                // Handle the different versions
                if (db_version < 5) {
                    lat = sqlite3_column_as<double>(p, 0) / 100000;
                    lon = sqlite3_column_as<double>(p, 1) / 100000;
                    alt = 0;
                } else {
                    lat = sqlite3_column_as<double>(p, 0);
                    lon = sqlite3_column_as<double>(p, 1);
                    alt = sqlite3_column_as<double>(p, 2);
                }

                // Check to see if we lie in any exclusion zones
                bool violates_exclusion = false;
                for (auto ez : exclusion_zones) {
                    if (distance_meters(lat, lon, std::get<0>(ez), std::get<1>(ez)) <= std::get<2>(ez)) {
                        violates_exclusion = true;
                        break;
                    }
                }

                if (violates_exclusion) {
                    continue;
                }

                pl.avg_lat += lat;
                pl.avg_lon += lon;
                pl.avg_2d_num++;

                if (alt != 0) {
                    pl.avg_alt += alt;
                    pl.avg_alt_num++;
                }
            }

            if (pl.avg_2d_num == 0) {
                fmt::print(stderr, "WARNING:  No packets with GPS info for '{}', skipping\n", pl.name);
                continue;
            }

            kml_point p;
            p.lat = pl.avg_lat / pl.avg_2d_num;
            p.lon = pl.avg_lon / pl.avg_2d_num;

            if (pl.avg_alt_num)
                p.alt = pl.avg_alt / pl.avg_alt_num;

            pl.point_vec.push_back(p);

            if(group_in_folder) {
                // style based on phy layer
                if(pl.phy_layer == "Bluetooth" || pl.phy_layer == "BTLE") {
                    bluetooth_placemark_vec.push_back(pl);
                }
                else if(pl.phy_layer == "802.15.4") {
                    zigbee_placemark_vec.push_back(pl);
                }
                else {
                    if (regex_match(pl.name, mac_pattern)) {
                        client_placemark_vec.push_back(pl);
                    } else {
                        if (pl.crypt == "Open") {
                            ap_open_placemark_vec.push_back(pl);
                        } else if (pl.crypt.find("WEP") != std::string::npos) {
                            ap_wep_placemark_vec.push_back(pl);
                        } else if (pl.crypt.find("WPA") != std::string::npos) {
                            ap_wpa_placemark_vec.push_back(pl);
                        } else {
                            other_placemark_vec.push_back(pl);
                        }
                    }
                }
            }
            else {
                placemark_vec.push_back(pl);
            }

        }
    }

    unsigned long place_num = 0;
    unsigned long point_num = 0;

    fmt::print(ofile, 
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
            "<kml xmlns=\"http://www.opengis.net/kml/2.2\" xmlns:gx=\"http://www.google.com/kml/ext/2.2\">\n"
            "<Document id=\"1\">\n"
            "<Style id=\"btle\"><LabelStyle><color>#ffFF0000</color></LabelStyle><IconStyle><color>#ffFF0000</color></IconStyle></Style>\n"
            "<Style id=\"zigbee\"><LabelStyle><color>#ffFF00FF</color></LabelStyle><IconStyle><color>#ffFF00FF</color></IconStyle></Style>\n"
            "<Style id=\"wifi-ap-open\"><LabelStyle><color>#ff00FF00</color></LabelStyle><IconStyle><color>#ff00FF00</color></IconStyle></Style>\n"
            "<Style id=\"wifi-ap-wep\"><LabelStyle><color>#ff0080FF</color></LabelStyle><IconStyle><color>#ff0080FF</color></IconStyle></Style>\n"
            "<Style id=\"wifi-ap-wpa\"><LabelStyle><color>#ff0000FF</color></LabelStyle><IconStyle><color>#ff0000FF</color></IconStyle></Style>\n"
            "<Style id=\"wifi-client\"><LabelStyle><color>#ff00FFFF</color></LabelStyle><IconStyle><color>#ff00FFFF</color></IconStyle></Style>\n"
            "<Style id=\"other\"><LabelStyle><color>#ff00AAFF</color></LabelStyle><IconStyle><color>#ff00AAFF</color></IconStyle></Style>\n"
            "<name>Kismet</name>\n"
            "<open>1</open>");

    if (group_in_folder) {
        if (!bluetooth_placemark_vec.empty()) {
            add_placemarks_from_vec(ofile, bluetooth_placemark_vec, "Bluetooth", "btle", point_num, place_num);
        }

        if (!zigbee_placemark_vec.empty()) {
            add_placemarks_from_vec(ofile, zigbee_placemark_vec, "802.15.4(Zigbee)", "zigbee", point_num, place_num);
        }

        if (!client_placemark_vec.empty() || !ap_wpa_placemark_vec.empty() || !ap_wep_placemark_vec.empty() || 
                !ap_open_placemark_vec.empty()) {
            fmt::print(ofile, "<Folder>");
            fmt::print(ofile, "<name>Wifi</name>");
            fmt::print(ofile, "<open>1</open>");

            if (!client_placemark_vec.empty()) {
                add_placemarks_from_vec(ofile, client_placemark_vec, "Client", "wifi-client", point_num, place_num);
            }

            if (!ap_wpa_placemark_vec.empty() || !ap_wep_placemark_vec.empty() || !ap_open_placemark_vec.empty()) {
                fmt::print(ofile, "<Folder>");
                fmt::print(ofile, "<name>AP</name>");
                fmt::print(ofile, "<open>1</open>");

                if (!ap_open_placemark_vec.empty()) {
                    add_placemarks_from_vec(ofile, ap_open_placemark_vec, "Open", "wifi-ap-open", point_num, place_num);
                }

                if (!ap_wep_placemark_vec.empty()) {
                    add_placemarks_from_vec(ofile, ap_wep_placemark_vec, "WEP", "wifi-ap-wep", point_num, place_num);
                }

                if (!ap_wpa_placemark_vec.empty()) {
                    add_placemarks_from_vec(ofile, ap_wpa_placemark_vec, "WPA", "wifi-ap-wpa", point_num, place_num);
                }

                fmt::print(ofile, "</Folder>\n");
            }

            fmt::print(ofile, "</Folder>\n");
        }

        if (!other_placemark_vec.empty()) {
            add_placemarks_from_vec(ofile, other_placemark_vec, "Other", "other", point_num, place_num);
        }

    } else {
        for (auto pl : placemark_vec) {
            fmt::print(ofile, "<Placemark id=\"{}\">", place_num++);
            fmt::print(ofile, "<name>{}</name>", MungeForXML(pl.name));

            // style based on phy layer
            if (pl.phy_layer == "Bluetooth" || pl.phy_layer == "BTLE") {
                fmt::print(ofile, "<styleUrl>btle</styleUrl>");
            } else if (pl.phy_layer == "802.15.4") {
                fmt::print(ofile, "<styleUrl>zigbee</styleUrl>");
            }

            // add description
            pl.description = "Name:" + MungeForXML(pl.name) + "\n";
            pl.description += pl.phy_layer + "\n";
            pl.description += "Channel:" + pl.channel;

            if (pl.crypt.length() > 0)
                pl.description += "\nCrypt:" + pl.crypt;

            fmt::print(ofile, "<description>{}</description>", pl.description);

            for (auto p : pl.point_vec) {
                fmt::print(ofile,
                    "<Point "
                    "id=\"{}\"><coordinates>{:3.10f},{:3.10f},{:3.10f}</coordinates></Point>",
                    point_num++, p.lon, p.lat, p.alt);
            }

            fmt::print(ofile, "</Placemark>\n");
        }
    }

    fmt::print(ofile, "</Document>\n");
    fmt::print(ofile, "</kml>\n");

    if (ofile != stdout) {
        fclose(ofile);
    }

    sqlite3_close(db);

    return 0;
}

