package main

/*
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "extensionCallback.h"
*/
import "C" // This is required to import the C code

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"reflect"
	"strconv"
	"strings"
	"time"
	"unsafe"

	influxdb2 "github.com/influxdata/influxdb-client-go/v2"
	"github.com/jackc/pgx/v4"
	"github.com/jackc/pgx/v4/pgxpool"
)

// declare list of functions available for call
var AVAILABLE_FUNCTIONS = map[string]interface{}{
	"initExtension":        initExtension,
	"deinitExtension":      deinitExtension,
	"loadSettings":         loadSettings,
	"connectToInflux":      connectToInflux,
	"writeToInflux":        writeToInflux,
	"connectToTimescale":   connectToTimescale,
	"initTimescale":        initTimescale,
	"writeToTimescale":     writeToTimescale,
	"getDir":               getDir,
	"sanitizeLineProtocol": sanitizeLineProtocol,
	"version":              version,
	"getUnixTimeNano":      getUnixTimeNano,
}

var EXTENSION_VERSION string = "0.0.2"
var extensionCallbackFnc C.extensionCallback

type ServerPollSetting struct {
	Name        string `json:"name"`
	Enabled     bool   `json:"enabled"`
	ServerOnly  bool   `json:"serverOnly"`
	IntervalMs  int    `json:"intervalMs"`
	Bucket      string `json:"bucket"`
	Measurement string `json:"measurement"`
	Description string `json:"description"`
}

var ServerPollSettingProperties []string = []string{
	"Name",
	"Enabled",
	"ServerOnly",
	"IntervalMs",
	"Bucket",
	"Measurement",
	"Description",
}

type CBAEventHandler struct {
	Name        string `json:"name"`
	Enabled     bool   `json:"enabled"`
	Description string `json:"description"`
}

var CBAEventHandlerProperties []string = []string{
	"Name",
	"Enabled",
	"Description",
}

type settingsJson struct {
	Influx struct {
		Enabled bool   `json:"enabled"`
		Host    string `json:"host"`
		Token   string `json:"token"`
		Org     string `json:"org"`
	} `json:"influxdb"`
	Timescale struct {
		Enabled       bool   `json:"enabled"`
		ConnectionUrl string `json:"connectionUrl"`
		DatabaseName  string `json:"databaseName"`
	} `json:"timescaledb"`
	Arma3 struct {
		RefreshRateMs int  `json:"refreshRateMs"`
		Debug         bool `json:"debug"`
	} `json:"arma3"`
	RecordingSettings map[string]interface{} `json:"recordingSettings"`
}

var activeSettings settingsJson

// InfluxDB variables
var influxClient influxdb2.Client

// TimescaleDB variables
var timescaleDbPool *pgxpool.Pool

// file paths
var ADDON_FOLDER string = getDir() + "\\@RangerMetrics"
var LOG_FILE string = ADDON_FOLDER + "\\rangermetrics.log"
var SETTINGS_FILE string = ADDON_FOLDER + "\\settings.json"
var SETTINGS_FILE_EXAMPLE string = ADDON_FOLDER + "\\settings.example.json"

// var BACKUP_FILE_PATH string = ADDON_FOLDER + "/local_backup.log.gzip"
// var BACKUP_WRITER *gzip.Writer

// configure log output
func init() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	// log to file
	f, err := os.OpenFile(LOG_FILE, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		log.Fatalf("error opening file: %v", err)
	}
	// log to console as well
	// log.SetOutput(io.MultiWriter(f, os.Stdout))
	// log only to file
	log.SetOutput(f)
}

func initExtension() {
	functionName := "initExtension"
	logLine(functionName, `["Initializing RangerMetrics extension", "INFO"]`, false)
	logLine(functionName, fmt.Sprintf(`["RangerMetrics Extension Version: %v", "INFO"]`, EXTENSION_VERSION), false)

	var err error

	activeSettings, err = loadSettings()
	if err != nil {
		logLine(functionName, fmt.Sprintf(`["Error loading settings: %v", "ERROR"]`, err), false)
		return
	}

	if activeSettings.Influx.Enabled {
		influxClient, err = connectToInflux()
		if err != nil {
			logLine(functionName, fmt.Sprintf(`["Error connecting to InfluxDB: %v", "ERROR"]`, err), false)
			return
		}
	}
	if activeSettings.Timescale.Enabled {
		timescaleDbPool, err = connectToTimescale()
		if err != nil {
			logLine(functionName, fmt.Sprintf(`["Error connecting to TimescaleDB: %v", "ERROR"]`, err), false)
			return
		}

		initTimescale()
	}
	logLine("extensionReady", `["Extension ready", "INFO"]`, false)
}

func deinitExtension() {
	functionName := "deinitExtension"
	logLine(functionName, `["Deinitializing RangerMetrics extension", "INFO"]`, false)

	if timescaleDbPool != nil {
		logLine(functionName, `["Closing TimescaleDB connection", "INFO"]`, false)
		timescaleDbPool.Close()
	} else {
		logLine(functionName, `["TimescaleDB connection not open", "INFO"]`, false)
	}

	logLine(functionName, `[true]`, false)
}

// func RVExtensionContext(output *C.char, argc *C.int) {

// }

func version() {
	functionName := "version"
	logLine(functionName, fmt.Sprintf(`["RangerMetrics Extension Version:%s", "INFO"]`, EXTENSION_VERSION), false)
}

// return db client and error
func connectToInflux() (influxdb2.Client, error) {
	if activeSettings.Influx.Host == "" ||
		activeSettings.Influx.Host == "http://host:8086" {

		return nil, errors.New("influxConnectionSettings.Host is empty")
		// logLine("connectToInflux", `["Creating backup file", "INFO"]`)
		// file, err := os.Open(BACKUP_FILE_PATH)
		// if err != nil {
		// 	log.Fatal(err)
		// 	logLine("connectToInflux", `["Error opening backup file", "ERROR"]`)
		// }
		// BACKUP_WRITER = gzip.NewWriter(file)
		// if err != nil {
		// 	log.Fatal(err)
		// 	logLine("connectToInflux", `["Error creating gzip writer", "ERROR"]`)
		// }
		// return "Error connecting to Influx. Using local backup"
	}

	influxClient := influxdb2.NewClientWithOptions(activeSettings.Influx.Host, activeSettings.Influx.Token, influxdb2.DefaultOptions().SetBatchSize(1000).SetFlushInterval(1000))

	return influxClient, nil
}

//////////////////////////////////
// TIMESCALE
//////////////////////////////////

func connectToTimescale() (*pgxpool.Pool, error) {
	functionName := "connectToTimescale"
	var err error

	// urlExample := "postgres://username:password@localhost:5432/database_name"
	// logLine("connectToTimescale", fmt.Sprintf(`["timescaleConnectionSettings.ConnectionUrl: %s", "INFO"]`, timescaleConnectionSettings.ConnectionUrl))
	conn, err := pgx.Connect(context.Background(), activeSettings.Timescale.ConnectionUrl+"/postgres")

	if err != nil {
		logLine(
			functionName,
			fmt.Sprintf(`["Error connecting to Timescale DB: %v", "ERROR"]`, err.Error()),
			false,
		)
		return nil, err
	}

	// ensure database exists
	logLine(
		functionName,
		fmt.Sprintf(`["TimescaleDB: 'CREATE DATABASE %s'", "INFO"]`, activeSettings.Timescale.DatabaseName),
		false,
	)
	conn.Query(context.Background(), fmt.Sprintf(`CREATE DATABASE %s`, activeSettings.Timescale.DatabaseName))

	// close connection
	conn.Close(context.Background())

	// create connection pool
	var dbPool *pgxpool.Pool
	dbPool, err = pgxpool.Connect(
		context.Background(),
		fmt.Sprintf(`%s/%s`, activeSettings.Timescale.ConnectionUrl, activeSettings.Timescale.DatabaseName),
	)
	if err != nil {
		logLine(
			functionName,
			fmt.Sprintf(`["Error connecting to Timescale DB: %v", "ERROR"]`, err.Error()),
			false,
		)
		return nil, err
	}

	logLine("connectToTimescale", `["Connected to Timescale successfully", "INFO"]`, false)
	return dbPool, nil
}

func initTimescale() {

	functionName := "initTimescale"
	var err error

	// schema init sql
	var tableCreationSql string = `
CREATE TABLE IF NOT EXISTS units."State"
(
    "timestamp" timestamp with time zone NOT NULL,
    mission_id integer NOT NULL,
    net_id text COLLATE pg_catalog."default" NOT NULL,
    player_uid text COLLATE pg_catalog."default" NOT NULL DEFAULT '-1'::integer,
    is_alive boolean NOT NULL,
    is_afk boolean,
    is_speaking smallint,
    unit_name text COLLATE pg_catalog."default" NOT NULL,
    side text COLLATE pg_catalog."default" NOT NULL,
    "position" point NOT NULL,
    direction numeric(2, 0) NOT NULL,
    health numeric(2, 0),
    traits text[] COLLATE pg_catalog."default",
    CONSTRAINT "UnitStates_pkey" PRIMARY KEY (net_id, "timestamp", player_uid, mission_id)
);

COMMENT ON TABLE units."State"
    IS 'Reflects a unit''s (soldier''s) state during a mission. Can be AI (player_uid = -1) or a player.';

CREATE TABLE IF NOT EXISTS units."Identity"
(
    "timestamp" timestamp with time zone NOT NULL,
    mission_id integer NOT NULL,
    net_id text COLLATE pg_catalog."default" NOT NULL,
    player_uid text COLLATE pg_catalog."default" NOT NULL DEFAULT '-1'::integer,
    class_name text COLLATE pg_catalog."default" NOT NULL,
    display_name text,
    side text,
    role_description text COLLATE pg_catalog."default",
    traits text[] COLLATE pg_catalog."default",
    CONSTRAINT "Units_pkey" PRIMARY KEY (mission_id, net_id, "timestamp", player_uid)
);

COMMENT ON TABLE units."Identity"
    IS 'This is polled periodically during the mission to gather static and less-dynamic traits of units (soldiers).';

CREATE TABLE IF NOT EXISTS events."Chat"
(
    id bigserial NOT NULL,
    "timestamp" timestamp with time zone NOT NULL,
    mission_id integer NOT NULL,
    player_uid text COLLATE pg_catalog."default" NOT NULL DEFAULT -1,
    channel integer,
    owner integer,
    name text COLLATE pg_catalog."default",
    "from" text COLLATE pg_catalog."default",
    text text COLLATE pg_catalog."default",
    "forcedDisplay" boolean,
    "isPlayerMessage" boolean,
    "sentenceType" integer,
    "chatMessageType" integer,
    CONSTRAINT "Chat_pkey" PRIMARY KEY ("timestamp", mission_id, player_uid, id),
    UNIQUE (id)
);

CREATE TABLE IF NOT EXISTS missions."Played"
(
    id serial NOT NULL,
    start_time_utc timestamp with time zone NOT NULL,
    start_time_game timestamp with time zone NOT NULL,
    world_name text COLLATE pg_catalog."default" NOT NULL,
    briefing_name text COLLATE pg_catalog."default" NOT NULL,
    mission_name text COLLATE pg_catalog."default" NOT NULL,
    mission_name_source text COLLATE pg_catalog."default" NOT NULL,
    on_load_name text NOT NULL,
    author text COLLATE pg_catalog."default" NOT NULL,
    server_name text COLLATE pg_catalog."default" NOT NULL,
    server_mods json,
    ace_medical boolean,
    radio_tfar boolean,
    radio_acre boolean,
    duration interval,
    version_addon integer[] NOT NULL,
    version_extension integer[] NOT NULL,
    tags text[] COLLATE pg_catalog."default",
    CONSTRAINT "MissionsRun_pkey" PRIMARY KEY (id, world_name, start_time_utc),
    CONSTRAINT "MissionsRun_id_start_time_utc_key" UNIQUE (id, start_time_utc),
    UNIQUE (id)
);

COMMENT ON TABLE missions."Played"
    IS 'Contains core mission data, recorded at mission init. The IDs here are referenced by many other areas to "place" them.';

CREATE TABLE IF NOT EXISTS players."ConnectionsMission"
(
    id bigserial NOT NULL,
    "timestamp" timestamp with time zone NOT NULL,
    mission_id integer NOT NULL,
    player_uid text COLLATE pg_catalog."default" NOT NULL,
    event_type text COLLATE pg_catalog."default" NOT NULL,
    profile_name text COLLATE pg_catalog."default" NOT NULL,
    display_name text COLLATE pg_catalog."default" NOT NULL,
    steam_name text COLLATE pg_catalog."default" NOT NULL,
    is_jip boolean,
    CONSTRAINT "ConnectionsMission_pkey" PRIMARY KEY (player_uid, "timestamp", profile_name, mission_id, id),
    UNIQUE ("timestamp", mission_id, player_uid, id)
);

CREATE TABLE IF NOT EXISTS units."Inventory"
(
    "timestamp" timestamp with time zone NOT NULL,
    mission_id integer NOT NULL,
    net_id text NOT NULL,
    player_uid text NOT NULL DEFAULT -1,
    inventory jsonb NOT NULL,
    PRIMARY KEY ("timestamp", net_id, mission_id, player_uid)
);

COMMENT ON TABLE units."Inventory"
    IS 'This is used to track the inventory state of units.';

CREATE TABLE IF NOT EXISTS players."ConnectionsServer"
(
    "timestamp" timestamp with time zone NOT NULL,
    player_uid text COLLATE pg_catalog."default" NOT NULL,
    event_type text COLLATE pg_catalog."default" NOT NULL,
    profile_name text COLLATE pg_catalog."default" NOT NULL,
    display_name text COLLATE pg_catalog."default" NOT NULL,
    steam_name text COLLATE pg_catalog."default" NOT NULL,
    is_jip boolean,
    CONSTRAINT "PlayersObserved_pkey" PRIMARY KEY (player_uid, "timestamp", profile_name)
);

CREATE TABLE IF NOT EXISTS missions."Environment"
(
    "timestamp" timestamp with time zone NOT NULL,
    mission_id integer NOT NULL,
    fog numeric(2),
    overcast numeric(2),
    rain numeric(2),
    humidity numeric(2),
    waves numeric(2),
    "windDir" numeric(2),
    "windStr" numeric(2),
    gusts numeric(2),
    lightnings numeric(2),
    "moonIntensity" numeric(2),
    "moonPhase" numeric(2),
    "sunOrMoon" numeric(2),
    PRIMARY KEY ("timestamp", mission_id)
);

COMMENT ON TABLE missions."Environment"
    IS 'Contains periodically collected environmental information during missions.';

CREATE TABLE IF NOT EXISTS vehicles."Identity"
(
    "timestamp" timestamp with time zone NOT NULL,
    mission_id integer NOT NULL,
    net_id text NOT NULL,
    class_name text NOT NULL,
    display_name text,
    customization jsonb,
    weapons jsonb,
    PRIMARY KEY ("timestamp", mission_id, net_id)
);

COMMENT ON TABLE vehicles."Identity"
    IS 'Periodically polled identity information about a vehicle.';

CREATE TABLE IF NOT EXISTS vehicles."State"
(
    "timestamp" timestamp with time zone NOT NULL,
    mission_id integer NOT NULL,
    net_id text NOT NULL,
    is_alive boolean,
    side text,
    "position" point NOT NULL,
    direction numeric(2),
    health numeric(2),
    crew text[],
    PRIMARY KEY ("timestamp", mission_id, net_id)
);

COMMENT ON TABLE vehicles."State"
    IS 'State information about vehicles. Crew is an array of net_id to be referenced against the Unit tables.';

CREATE TABLE IF NOT EXISTS players."AllObserved"
(
    id serial NOT NULL,
    player_uid text NOT NULL,
    PRIMARY KEY (id, player_uid),
    UNIQUE (player_uid)
);
`

	relationCreationSql := []string{
		`ALTER TABLE IF EXISTS units."State"
    ADD FOREIGN KEY (mission_id)
    REFERENCES missions."Played" (id) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    NOT VALID;
`,

		`ALTER TABLE IF EXISTS units."State"
    ADD FOREIGN KEY (player_uid)
    REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    NOT VALID;
`,

		`ALTER TABLE IF EXISTS units."Identity"
    ADD FOREIGN KEY (mission_id)
    REFERENCES missions."Played" (id) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    NOT VALID;
`,

		`ALTER TABLE IF EXISTS units."Identity"
    ADD FOREIGN KEY (player_uid)
    REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    NOT VALID;
`,

		`ALTER TABLE IF EXISTS events."Chat"
    ADD FOREIGN KEY (mission_id)
    REFERENCES missions."Played" (id) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    NOT VALID;
`,

		`ALTER TABLE IF EXISTS events."Chat"
    ADD FOREIGN KEY (player_uid)
    REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    NOT VALID;
`,

		`ALTER TABLE IF EXISTS players."ConnectionsMission"
    ADD FOREIGN KEY (mission_id)
    REFERENCES missions."Played" (id) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    NOT VALID;
`,

		`ALTER TABLE IF EXISTS players."ConnectionsMission"
    ADD FOREIGN KEY (player_uid)
    REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    NOT VALID;
`,

		`ALTER TABLE IF EXISTS units."Inventory"
    ADD FOREIGN KEY (mission_id)
    REFERENCES missions."Played" (id) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    NOT VALID;
`,

		`ALTER TABLE IF EXISTS units."Inventory"
    ADD FOREIGN KEY (player_uid)
    REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    NOT VALID;
`,

		`ALTER TABLE IF EXISTS missions."Environment"
    ADD FOREIGN KEY (mission_id)
    REFERENCES missions."Played" (id) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    NOT VALID;
`,

		`ALTER TABLE IF EXISTS vehicles."Identity"
    ADD FOREIGN KEY (mission_id)
    REFERENCES missions."Played" (id) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    NOT VALID;
`,

		`ALTER TABLE IF EXISTS vehicles."State"
    ADD FOREIGN KEY (mission_id)
    REFERENCES missions."Played" (id) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    NOT VALID;
		`,
	}

	var tx pgx.Tx
	tx, err = timescaleDbPool.Begin(context.Background())
	if err != nil {
		logLine(functionName, fmt.Sprintf(`["Error creating transaction: %v", "ERROR"]`, err.Error()), false)
		return
	}

	_, err = tx.Exec(context.Background(), tableCreationSql)
	if err != nil {
		logLine(functionName, fmt.Sprintf(`["Error creating tables: %v", "ERROR"]`, err.Error()), false)
		tx.Rollback(context.Background())
		tx.Commit(context.Background())
		return
	}
	err = tx.Commit(context.Background())
	if err != nil {
		logLine(functionName, fmt.Sprintf(`["Error committing table creation transaction: %v", "ERROR"]`, err.Error()), false)
		return
	}

	// run each relation creation sql statement
	for _, sql := range relationCreationSql {
		_, err = tx.Exec(context.Background(), sql)

		if err != nil {
			logLine(functionName, fmt.Sprintf(`["Error creating relation: %v", "ERROR"]`, err.Error()), false)
			tx.Rollback(context.Background())
			tx.Commit(context.Background())
			return
		}
	}

	err = tx.Commit(context.Background())
	if err != nil {
		logLine(functionName, fmt.Sprintf(`["Error committing relation creation transaction: %v", "ERROR"]`, err.Error()), false)
		return
	}

	logLine(functionName, `["Timescale schema initialized", "INFO"]`, false)

}

func writeToTimescale(table string, line string) {
	// logLine("writeToTimescale", fmt.Sprintf(`["line: %s", "INFO"]`, line))
	functionName := "writeToTimescale"
	_, err := timescaleDbPool.Exec(context.Background(), "INSERT INTO %1 (time, line) VALUES (NOW(), $2);", table, line)
	if err != nil {
		logLine(functionName, fmt.Sprintf(`["Error writing to timescale: %v", "ERROR"]`, err.Error()), false)
	}
}

func writeToInflux(a3DataRaw *[]string) string {

	// convert to string array
	a3Data := *a3DataRaw

	logLine("writeToInflux", fmt.Sprintf(`["Received %d params", "DEBUG"]`, len(a3Data)), true)

	MIN_PARAMS_COUNT := 1

	var logData string
	functionName := "writeToInflux"

	if len(a3Data) < MIN_PARAMS_COUNT {
		logData = fmt.Sprintf(`["Not all parameters present (got %d, expected at least %d)", "ERROR"]`, len(a3Data), MIN_PARAMS_COUNT)
		logLine(functionName, logData, false)
		return logData
	}

	// use custom bucket or default
	var bucket string = fixEscapeQuotes(trimQuotes(string(a3Data[0])))

	// Get non-blocking write client
	WRITE_API := influxClient.WriteAPI(activeSettings.Influx.Org, bucket)

	if WRITE_API == nil {
		logData = `["Error creating write API", "ERROR"]`
		logLine(functionName, logData, false)
		return logData
	}

	// Get errors channel
	errorsCh := WRITE_API.Errors()
	go func() {
		for writeErr := range errorsCh {
			logData = fmt.Sprintf(`["Error parsing line protocol: %s", "ERROR"]`, strings.Replace(writeErr.Error(), `"`, `'`, -1))
			logLine(functionName, logData, false)
		}
	}()

	// now we have our write client, we'll go through the rest of the receive array items in line protocol format and write them to influx
	for i := 1; i < len(a3Data); i++ {
		var p string = fixEscapeQuotes(trimQuotes(string(a3Data[i])))

		// write the line to influx
		WRITE_API.WriteRecord(p)

		// TODO: Add backup writer
		// // append backup line to file if BACKUP_WRITER is set
		// //
		// if BACKUP_WRITER != nil {
		// 	_, err = BACKUP_WRITER.Write([]byte(p + "\n"))
		// }

	}

	// schedule cleanup
	WRITE_API.Flush()
	logData = fmt.Sprintf(`["Wrote %d lines to influx", "DEBUG"]`, len(a3Data)-1)
	logLine(functionName, logData, true)

	return "Success"
}

// sanitize line protocol for influx
func sanitizeLineProtocol(line string) string {
	// replace all spaces with underscores
	// line = strings.ReplaceAll(line, ` `, `\ `)
	// replace all commas with underscores
	// line = strings.ReplaceAll(line, `,`, `\,`)
	// replace all equals with underscores
	// line = strings.ReplaceAll(line, "=", "_")
	// replace all quotes with underscores
	// line = strings.ReplaceAll(line, "\"", "_")

	return line
}

func getDir() string {
	dir, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}
	return dir
}

// return true if the program should continue
func loadSettings() (settingsJson, error) {
	functionName := "loadSettings"
	logLine(functionName, fmt.Sprintf(`["ADDON_FOLDER: %s", "INFO"]`, ADDON_FOLDER), false)
	logLine(functionName, fmt.Sprintf(`["LOG_FILE: %s", "INFO"]`, LOG_FILE), false)
	logLine(functionName, fmt.Sprintf(`["SETTINGS_FILE: %s", "INFO"]`, SETTINGS_FILE), false)

	settings := settingsJson{}

	// print the current working directory
	var file *os.File
	var err error
	// read settings from file
	// settings.json should be in the same directory as the .dll
	// see if the file exists
	if _, err = os.Stat(SETTINGS_FILE); os.IsNotExist(err) {
		// file does not exist
		logLine(
			functionName,
			fmt.Sprintf(`["%s does not exist", "ERROR"]`, SETTINGS_FILE),
			false,
		)
		// copy settings.json.example to settings.json
		// load contents
		fileContents, err := ioutil.ReadFile(SETTINGS_FILE_EXAMPLE)
		if err != nil {
			return settings, err
		}
		// write contents to settings.json
		err = ioutil.WriteFile(SETTINGS_FILE, fileContents, 0644)
		if err != nil {
			return settings, err
		}

		// Exit false to discontinue initialization since settings are defaults
		logLine(functionName, `["CREATED SETTINGS"]`, false)
		// return a new error
		return settings, errors.New("settings.json does not exist")
	} else {
		// file exists
		logLine(functionName, `["settings.json found", "DEBUG"]`, true)
		// read the file
		file, err = os.Open(SETTINGS_FILE)
		if err != nil {
			return settings, err
		}
		defer file.Close()
		decoder := json.NewDecoder(file)
		err = decoder.Decode(&settings)
		if err != nil {
			return settings, err
		}

		// send contents of settings file
		// get the file contents
		fileContents, err := ioutil.ReadFile(SETTINGS_FILE)
		if err != nil {
			return settings, err
		}

		// compact the json
		var jsonStr bytes.Buffer
		err = json.Compact(&jsonStr, fileContents)
		if err != nil {
			return settings, err
		}

		logLine(
			"loadSettingsJSON",
			jsonStr.String(),
			false,
		)

	}

	return settings, nil
}

func runExtensionCallback(name *C.char, function *C.char, data *C.char) C.int {
	return C.runExtensionCallback(extensionCallbackFnc, name, function, data)
}

//export goRVExtensionVersion
func goRVExtensionVersion(output *C.char, outputsize C.size_t) {
	result := C.CString(EXTENSION_VERSION)
	defer C.free(unsafe.Pointer(result))
	var size = C.strlen(result) + 1
	if size > outputsize {
		size = outputsize
	}
	C.memmove(unsafe.Pointer(output), unsafe.Pointer(result), size)
}

//export goRVExtensionArgs
func goRVExtensionArgs(output *C.char, outputsize C.size_t, input *C.char, argv **C.char, argc C.int) {
	var offset = unsafe.Sizeof(uintptr(0))
	var out []string
	for index := C.int(0); index < argc; index++ {
		out = append(out, C.GoString(*argv))
		argv = (**C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(argv)) + offset))
	}

	var temp string
	temp = fmt.Sprintf("Function: %s nb params: %d params: %s!", C.GoString(input), argc, out)

	if C.GoString(input) == "sendToInflux" {
		// start a goroutine to send the data to influx
		// param string is argv[0] which is the data to send to influx
		go writeToInflux(&out)
		temp = fmt.Sprintf("Function: %s nb params: %d", C.GoString(input), argc)
	}

	if C.GoString(input) == "setAddonDir" {
		// set the addon directory
		ADDON_FOLDER = out[0]
		LOG_FILE = ADDON_FOLDER + "\\rangermetrics.log"
		SETTINGS_FILE = ADDON_FOLDER + "\\settings.json"
		SETTINGS_FILE_EXAMPLE = ADDON_FOLDER + "\\settings.example.json"
	}

	// Return a result to Arma
	result := C.CString(temp)
	defer C.free(unsafe.Pointer(result))
	var size = C.strlen(result) + 1
	if size > outputsize {
		size = outputsize
	}

	C.memmove(unsafe.Pointer(output), unsafe.Pointer(result), size)
}

func callBackExample() {
	name := C.CString("arma")
	defer C.free(unsafe.Pointer(name))
	function := C.CString("funcToExecute")
	defer C.free(unsafe.Pointer(function))
	// Make a callback to Arma
	for i := 0; i < 3; i++ {
		time.Sleep(2 * time.Second)
		param := C.CString(fmt.Sprintf("Loop: %d", i))
		defer C.free(unsafe.Pointer(param))
		runExtensionCallback(name, function, param)
	}
}

func getUnixTimeNano() int64 {
	// get the current unix timestamp in nanoseconds
	return time.Now().UnixNano()
}

func trimQuotes(s string) string {
	// trim the start and end quotes from a string
	return strings.Trim(s, `"`)
}

func fixEscapeQuotes(s string) string {
	// fix the escape quotes in a string
	return strings.Replace(s, `""`, `"`, -1)
}

func logLine(functionName string, data string, isDebug bool) {
	statusName := C.CString("RangerMetrics")
	defer C.free(unsafe.Pointer(statusName))
	statusFunction := C.CString(functionName)
	defer C.free(unsafe.Pointer(statusFunction))
	statusParam := C.CString(data)
	defer C.free(unsafe.Pointer(statusParam))
	runExtensionCallback(statusName, statusFunction, statusParam)

	if activeSettings.Arma3.Debug && isDebug {
		log.Println(data)
	} else if !isDebug {
		log.Println(data)
	}
}

//export goRVExtension
func goRVExtension(output *C.char, outputsize C.size_t, input *C.char) {

	var temp string

	// logLine("goRVExtension", fmt.Sprintf(`["Input: %s",  "DEBUG"]`, C.GoString(input)), true)

	switch C.GoString(input) {
	case "version":
		temp = EXTENSION_VERSION
	case "getDir":
		temp = getDir()
	case "getUnixTimeNano":
		time := getUnixTimeNano()
		temp = fmt.Sprintf(`["%s"]`, strconv.FormatInt(time, 10))

	default:
		// check if input is in AVAILABLE_FUNCTIONS
		// if not, return error
		// if yes, continue
		if _, ok := AVAILABLE_FUNCTIONS[C.GoString(input)]; !ok {
			temp = fmt.Sprintf(`["Function: %s not found!", "ERROR"]`, C.GoString(input))
		} else {
			// call the function by name
			go reflect.ValueOf(AVAILABLE_FUNCTIONS[C.GoString(input)]).Call([]reflect.Value{})
			temp = fmt.Sprintf(`["Function: %s called successfully", "DEBUG"]`, C.GoString(input))
		}
	}

	// switch C.GoString(input) {
	// case "version":
	// 	logLine("goRVExtension", fmt.Sprintf(`["Input: %s",  "INFO"]`, C.GoString(input)))
	// 	temp = EXTENSION_VERSION
	// case "getDir":
	// 	logLine("goRVExtension", fmt.Sprintf(`["Input: %s",  "INFO"]`, C.GoString(input)))
	// 	temp = getDir()
	// case "loadSettings":
	// 	logLine("goRVExtension", fmt.Sprintf(`["Input: %s",  "INFO"]`, C.GoString(input)))
	// 	cwd, result, influxHost, timescaleUrl := loadSettings()
	// 	log.Println("CWD:", cwd)
	// 	log.Println("RESULT:", result)
	// 	log.Println("INFLUX HOST:", influxHost)
	// 	log.Println("TIMESCALE URL:", timescaleUrl)
	// 	if result != "" {
	// 		logLine("goRVExtension", result)
	// 		temp = fmt.Sprintf(
	// 			`["%s", "%s", "%s", "%d"]`,
	// 			EXTENSION_VERSION,
	// 			influxConnectionSettings.Host,
	// 			influxConnectionSettings.Org,
	// 			a3Settings.RefreshRateMs,
	// 		)
	// 	}
	// case "connectToInflux":
	// 	// logLine("goRVExtension", fmt.Sprintf(`["Input: %s",  "INFO"]`, C.GoString(input)))
	// 	go connectToInflux()
	// 	temp = `["Connecting to InfluxDB", "INFO"]`
	// case "connectToTimescale":
	// 	// logLine("goRVExtension", fmt.Sprintf(`["Input: %s",  "INFO"]`, C.GoString(input)))
	// 	go connectToTimescale()
	// 	temp = `["Connecting to TimescaleDB", "INFO"]`
	// case "getUnixTimeNano":
	// 	temp = fmt.Sprintf(`["%d", "INFO"]`, getUnixTimeNano())
	// case "deinitialize":
	// 	logLine("goRVExtension", fmt.Sprintf(`["Input: %s",  "INFO"]`, C.GoString(input)))
	// 	deinitialize()
	// 	temp = `["Deinitializing", "INFO"]`
	// default:
	// 	temp = fmt.Sprintf(`["Unknown command: %s", "ERROR"]`, C.GoString(input))
	// }

	result := C.CString(temp)
	defer C.free(unsafe.Pointer(result))
	var size = C.strlen(result) + 1
	if size > outputsize {
		size = outputsize
	}

	C.memmove(unsafe.Pointer(output), unsafe.Pointer(result), size)
	// return
}

//export goRVExtensionRegisterCallback
func goRVExtensionRegisterCallback(fnc C.extensionCallback) {
	extensionCallbackFnc = fnc
}

func main() {}
