// ==UserScript==
// @name     GaiaGps Uploader v2
// @version  2.0
// @grant    none
// @match    https://www.gaiagps.com/upload/
// @require  https://ajax.googleapis.com/ajax/libs/jquery/3.5.0/jquery.min.js
// ==/UserScript==

var gpxParser = function () {
    this.xmlSource = "";
    this.metadata  = {};
    this.waypoints = [];
    this.tracks    = [];
    this.routes    = [];
};

gpxParser.prototype.parse = function (gpxstring) {
    var keepThis = this;
    var domParser = new window.DOMParser();
    this.xmlSource = domParser.parseFromString(gpxstring, 'text/xml');

    metadata = this.xmlSource.querySelector('metadata');
    if(metadata != null){
        this.metadata.name  = this.getElementValue(metadata, "name");
        this.metadata.desc  = this.getElementValue(metadata, "desc");
        this.metadata.time  = this.getElementValue(metadata, "time");

        let author = {};
        let authorElem = metadata.querySelector('author');
        if(authorElem != null){
            author.name = this.getElementValue(authorElem, "name");
            author.email  = {};
            let emailElem = authorElem.querySelector('email');
            if(emailElem != null){
                author.email.id     = emailElem.getAttribute("id");
                author.email.domain = emailElem.getAttribute("domain");
            }

            let link     = {};
            let linkElem = authorElem.querySelector('link');
            if(linkElem != null){
                link.href = linkElem.getAttribute('href');
                link.text = this.getElementValue(linkElem, "text");
                link.type = this.getElementValue(linkElem, "type");
            }
            author.link = link;
        }
        this.metadata.author = author;

        let link = {};
        let linkElem = this.queryDirectSelector(metadata, 'link');
        if(linkElem != null){
            link.href = linkElem.getAttribute('href');
            link.text = this.getElementValue(linkElem, "text");
            link.type = this.getElementValue(linkElem, "type");
            this.metadata.link = link;
        }
    }

    var wpts = [].slice.call(this.xmlSource.querySelectorAll('wpt'));
    for (let idx in wpts){
        var wpt = wpts[idx];
        let pt  = {};
        pt.name = keepThis.getElementValue(wpt, "name")
        pt.lat  = parseFloat(wpt.getAttribute("lat"));
        pt.lon  = parseFloat(wpt.getAttribute("lon"));
        pt.ele  = parseFloat(keepThis.getElementValue(wpt, "ele")) || null;
        pt.cmt  = keepThis.getElementValue(wpt, "cmt");
        pt.desc = keepThis.getElementValue(wpt, "desc");
        pt.sym = keepThis.getElementValue(wpt, "sym");
        keepThis.waypoints.push(pt);
    }

    var rtes = [].slice.call(this.xmlSource.querySelectorAll('rte'));
    for (let idx in rtes){
        var rte = rtes[idx];
        let route = {};
        route.name   = keepThis.getElementValue(rte, "name");
        route.cmt    = keepThis.getElementValue(rte, "cmt");
        route.desc   = keepThis.getElementValue(rte, "desc");
        route.src    = keepThis.getElementValue(rte, "src");
        route.number = keepThis.getElementValue(rte, "number");

        let type     = keepThis.queryDirectSelector(rte, "type");
        route.type   = type != null ? type.innerHTML : null;

        let link     = {};
        let linkElem = rte.querySelector('link');
        if(linkElem != null){
            link.href = linkElem.getAttribute('href');
            link.text = keepThis.getElementValue(linkElem, "text");
            link.type = keepThis.getElementValue(linkElem, "type");
        }
        route.link = link;

        let routepoints = [];
        var rtepts = [].slice.call(rte.querySelectorAll('rtept'));

        for (let idxIn in rtepts){
            var rtept = rtepts[idxIn];
            let pt    = {};
            pt.lat    = parseFloat(rtept.getAttribute("lat"));
            pt.lon    = parseFloat(rtept.getAttribute("lon"));
            pt.ele    = parseFloat(keepThis.getElementValue(rtept, "ele"));
            routepoints.push(pt);
        }

        route.points = routepoints;
        keepThis.routes.push(route);
    }

    var trks = [].slice.call(this.xmlSource.querySelectorAll('trk'));
    for (let idx in trks){
        var trk = trks[idx];
        let track = {};

        track.name   = keepThis.getElementValue(trk, "name");
        track.cmt    = keepThis.getElementValue(trk, "cmt");
        track.desc   = keepThis.getElementValue(trk, "desc");
        track.src    = keepThis.getElementValue(trk, "src");
        track.number = keepThis.getElementValue(trk, "number");
        track.color  = keepThis.getElementValue(trk, "DisplayColor");

        let type     = keepThis.queryDirectSelector(trk, "type");

        let link     = {};
        let linkElem = trk.querySelector('link');
        if(linkElem != null){
            link.href = linkElem.getAttribute('href');
            link.text = keepThis.getElementValue(linkElem, "text");
            link.type = keepThis.getElementValue(linkElem, "type");
        }
        track.link = link;

        let trackpoints = [];
        var trkpts = [].slice.call(trk.querySelectorAll('trkpt'));
        for (let idxIn in trkpts){
            var trkpt = trkpts[idxIn];
            let pt = {};
            pt.lat = parseFloat(trkpt.getAttribute("lat"));
            pt.lon = parseFloat(trkpt.getAttribute("lon"));
            pt.ele = parseFloat(keepThis.getElementValue(trkpt, "ele")) || null;
            trackpoints.push(pt);
        }
        track.points = trackpoints;

        keepThis.tracks.push(track);
    }
};

gpxParser.prototype.getElementValue = function(parent, needle){
    let elem = parent.querySelector(needle);
    if(elem != null){
        return elem.innerHTML != undefined ? elem.innerHTML : elem.childNodes[0].data;
    }
    return elem;
}

gpxParser.prototype.queryDirectSelector = function(parent, needle) {

    let elements  = parent.querySelectorAll(needle);
    let finalElem = elements[0];

    if(elements.length > 1) {
        let directChilds = parent.childNodes;

        for(idx in directChilds) {
            elem = directChilds[idx];
            if(elem.tagName === needle) {
                finalElem = elem;
            }
        }
    }

    return finalElem;
}

gpxParser.prototype.getGPX = function(sName) {
    var sTrack = '';

    for(var i=0 ; i < this.tracks.length ; i++) {
        var sTrkSeg = '';
        for(var j=0 ; j < this.tracks[i].points.length ; j++) {
            sTrkSeg +=  '<trkpt lat="'+this.tracks[i].points[j].lat+'" lon="'+this.tracks[i].points[j].lon+'">'+
                            '<ele>'+(this.tracks[i].points[j].ele || '')+'</ele>'+
                        '</trkpt>';
        }
        sTrack +=   '<trk>'+
                        '<name>'+(this.tracks[i].name || '')+'</name>'+
                        '<desc>'+(this.tracks[i].desc || '')+'</desc>'+
                        '<src>'+(this.tracks[i].src || '')+'</src>'+
                        '<trkseg>'+sTrkSeg+'</trkseg>'+
                    '</trk>';
    }

    var sGPX =	'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>'+
                '<gpx version="1.1" creator="Franzz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'+
                    '<metadata><name>'+sName+'</name></metadata>'+
                    sTrack+
                '</gpx>';

    return sGPX;
};

class Gaia {
    static get URL() { return 'https://www.gaiagps.com'; }
    static get API() { return Gaia.URL+'/api/objects'; }

    constructor($Form) {
        this.aoWaypoints = [];
        this.aoTracks = [];
        this.sFolderId = '';
        this.sFolderName = '';

        this.$Form = $Form;
        this.$InputName = $Form.find('input[name=name]');
        this.$InputFile = $Form.find('input[type=file]');
        this.$Feedback = $Form.after($('<div>'));
        this.setLayout();
    }

    feedback(sType, sMsg) {
        var sFormattedMsg = sType.charAt(0).toUpperCase()+sType.slice(1)+': '+sMsg+'.';
        console.log(sFormattedMsg);

        let sColor = 'black';
        switch(sType) {
            case 'error':   sColor = 'red';     break;
            case 'warning': sColor = 'orange';  break;
            case 'info':    sColor = 'green';   break;
        }
        this.$Feedback.append($('<p>', {'style': 'color: '+sColor+';'}).text(sFormattedMsg));
    }

    //Modify Gaia DOM Interface
    setLayout() {
        //Set event on file selection
        this.$InputFile
            .attr('multiple', 'multiple')
            .attr('name', 'files[]')
            .change(() => {this.readInputFiles();});

        //Remove submit button & edit label
        this.$Form.find('#fileuploadtext').text('Select files');
        this.$Form.find('button[type=submit]').hide();

        //Set default Name
        this.$InputName.val('PCT');

		//Clear all upload notifications
		this.resetNotif();
    }

    //Parse files from input (multiple)
    readInputFiles() {
        if (!window.File || !window.FileReader || !window.FileList || !window.Blob) {
            this.feedback('error', 'File APIs are not fully supported in this browser');
            return;
        }
        else this.feedback('info', 'Parsing input files');

        //Get Folder Name (text input)
        this.sFolderName = this.$InputName.val();

        let aoReaders = [];
        let asFiles = this.$InputFile.prop('files');
        let asContents = [];
        for(var i=0 ; i < asFiles.length ; i++) {
            aoReaders[i] = new FileReader();
            aoReaders[i].onload = ((asResult) => {
                asContents.push(asResult.target.result);
                this.feedback('info', 'Reading file '+asContents.length+'/'+asFiles.length);
                if(asContents.length == asFiles.length) this.parseFile(asContents);
            });
            aoReaders[i].readAsText(asFiles[i]);
        }
    }

    //Parse GPX files to consolidate tracks & waypoints
    parseFile(asContents) {
      	this.feedback('info', 'Merging files');
        for(var i in asContents) {
            var oGPX = new gpxParser();
            oGPX.parse(asContents[i]);

          	//Waypoints
          	for(var w in oGPX.waypoints) {
              var sWaypointName = oGPX.waypoints[w].name;
              if(sWaypointName.indexOf('Milestone - ') != -1 && sWaypointName.substr(-2) != '00') { //1 milestone every 100 miles
                  this.feedback('info', 'Ignoring milestone waypoint "'+sWaypointName+'"');
              }
              else this.aoWaypoints.push(oGPX.waypoints[w]);
            }

          	//Tracks
            for(var t in oGPX.tracks) {
                this.aoTracks.push(oGPX.tracks[t]);
            }
        }

        this.deleteFolder();
    }

    //Delete existing folder with same name
    deleteFolder() {
        this.feedback('info', 'Looking for existing "'+this.sFolderName+'" folder...');

        $.get(Gaia.API+'/folder/?routepoints=false&show_archived=true&show_filed=true').done((asFolders) => {
            var bCalled = false;
            for(var f in asFolders) {
                if(asFolders[f].title == this.sFolderName) {
                    bCalled = true;
                    this.feedback('info', 'Deleting "'+this.sFolderName+'" folder');
                    $.ajax({
                        url: Gaia.API+'/folder/'+asFolders[f].id+'/',
                        type: 'DELETE'
                    });
                }
            }
            if(!bCalled) this.feedback('info', 'No folder named "'+this.sFolderName+'" found');

            this.uploadTrack();
        });
    }

    //Marking all upload notifications as read
    resetNotif() {
        this.feedback('info', 'Marking all upload notifications as read');
        $.get(Gaia.URL+'/social/notifications/popup/').done((asNotifs) => {
            for(var i in asNotifs) {
                if(!asNotifs[i].isViewed && asNotifs[i].html.indexOf('has completed') != -1) {
                    $.post(Gaia.URL+'/social/notifications/'+asNotifs[i].id+'/markviewed/');
                }
            }
        });
    }

    //Build & Upload Track File
    uploadTrack() {
        //Build track file
        this.feedback('info', 'Building consolidated track');
        var oGPX = new gpxParser();
        oGPX.tracks = this.aoTracks;
        var sTrack = oGPX.getGPX(this.sFolderName);

        //Send consolidated Tracks as one file
        var oForm = new FormData();
        oForm.append('name', this.sFolderName);
        oForm.append('files', new Blob([sTrack]), 'Consolidated.gpx');

        this.feedback('info', 'Uploading consolidated track');
        $.ajax({
            url: Gaia.URL+'/upload/',
            type: 'POST',
            data: oForm,
            enctype: 'multipart/form-data',
            processData: false,
            contentType: false,
            cache: false
        }).done(() => {this.checkNotif();});
    }

    //Wait for file to be processed by Gaia
    checkNotif() {
        this.feedback('info', 'Waiting for Gaia to process consolidated track');
        $.get(Gaia.URL+'/social/notifications/popup/').done((asNotifs) => {
            for(var i in asNotifs) {
                if(!asNotifs[i].isViewed && asNotifs[i].html.indexOf('has completed') != -1) {
                    this.feedback('info', 'Notification '+asNotifs[i].id+' found. Marking as read');
                    var $Notif = $('<span>').html(asNotifs[i].html);
                    this.sFolderId = $Notif.find('a').attr('href').split('/')[3];
                    $.post(Gaia.URL+'/social/notifications/'+asNotifs[i].id+'/markviewed/');
                }
            }

            if(this.sFolderId != '') {
                this.setTracksColor();
                this.uploadWayPoints();
            }
            else setTimeout((() => {this.checkNotif();}), 1000);
        });
    }

    //Convert QMapshack Track Colors into Gaia Colors
    setTracksColor() {
        this.feedback('info', 'Track Color - Matching Gaia track IDs with GPX tracks');
        var iCount = 0;

        $.get(Gaia.API+'/folder/'+this.sFolderId+'/').done((asFolder) => {
            var self = this;
            for(var iGaiaIndex in asFolder.properties.tracks) {
                for(var iGpxIndex in this.aoTracks) {
                    if(asFolder.properties.tracks[iGaiaIndex].title == this.aoTracks[iGpxIndex].name) {
                        this.aoTracks[iGpxIndex].id = asFolder.properties.tracks[iGaiaIndex].id;
                        this.feedback('info', 'Track Color - Track "'+this.aoTracks[iGpxIndex].name+'" found a match ('+(++iCount)+'/'+this.aoTracks.length+')');

                        $.ajax({
                            url: Gaia.API+'/track/'+this.aoTracks[iGpxIndex].id+'/',
                            color: this.aoTracks[iGpxIndex].color
                        }).done(function(asGaiaTrackDisplay) {
                            var asGaiaTrack = asGaiaTrackDisplay.features[0];
                            delete asGaiaTrack.id;
                            delete asGaiaTrack.style;

                            //Set color
                            var sColor = '#ff0000';
                            switch(this.color) {
                                case 'DarkBlue':	sColor = '#2D3FC7';	break;
                                case 'Magenta':		sColor = '#B60DC3';	break;
                            }
                            asGaiaTrack.properties.color = sColor;
                            asGaiaTrack.properties.hexcolor = sColor;

                            self.feedback('info', 'Track Color - Setting track color "'+sColor+'" ('+this.color+') to "'+asGaiaTrack.properties.title+'"');
                            $.ajax({
                                url: Gaia.API+'/track/'+asGaiaTrack.properties.id+'/',
                                type: 'PUT',
                                contentType: 'application/json',
                                data: JSON.stringify(asGaiaTrack)
                            });
                        });
                    }
                }
            }
        });
    }

    uploadWayPoints(iIndex, bSecondTry) {
        iIndex = iIndex || 0;
        bSecondTry = bSecondTry || false;

        //Upload waypoints
        if(iIndex < this.aoWaypoints.length) {
            var sWaypointName = this.aoWaypoints[iIndex].name;

			this.feedback('info', 'Waypoints Upload - Uploading waypoint '+(iIndex + 1)+'/'+this.aoWaypoints.length+' ('+sWaypointName+')');
			var asPost = {
				type: 'Feature',
				geometry: {
					type: 'Point',
					coordinates: [
						this.aoWaypoints[iIndex].lon,
						this.aoWaypoints[iIndex].lat,
						this.aoWaypoints[iIndex].ele
					]
				},
				properties: {
					title: sWaypointName,
					time_created: "2020-06-07T14:01:03.944Z",
					icon: Gaia.getIconName(this.aoWaypoints[iIndex].sym),
					writable: true,
					localId: iIndex+'',
					archived: false,
					isTitleLoaded: true,
					isLocallyCreated: true,
					type: 'waypoint'
				}
			};
			if(this.aoWaypoints[iIndex].desc) asPost.properties.notes = this.aoWaypoints[iIndex].desc;

			$.ajax({
				url: Gaia.API+'/waypoint/',
				type: 'POST',
				contentType: 'application/json',
				data: JSON.stringify(asPost)
			}).done((asWaypoint) => {
				//Update local waypoint with all server info (including ID)
				this.aoWaypoints[iIndex] = asWaypoint;

				$.get(Gaia.URL+'/public/generate?type=waypoint&objectid='+asWaypoint.properties.id).always(() => {
					this.uploadWayPoints(++iIndex);
				});
			}).fail(function(){
				this.feedback('error', 'Waypoints Upload - Failed to upload waypoint #'+(iIndex + 1)+' ('+sWaypointName+'). Trying again...');
				this.uploadWayPoints(iIndex, true);
			});
        }

        //Done uploading, assigning waypoints to folder
        else {
            this.feedback('info', 'Waypoints Upload - Getting folders list');
            $.get(Gaia.API+'/folder/').done((asFolders) => {
                //Find Folder
                for(var f in asFolders) {
                    if(asFolders[f].id == this.sFolderId) {
                        var asFolder = asFolders[f];
                    }
                }

                //Assign waypoints to folder
                for(var i in this.aoWaypoints) asFolder.waypoints.push(this.aoWaypoints[i].properties.id);

              	this.feedback('info', 'Waypoints Upload - Assigning waypoints to folder '+this.sFolderId);
                $.ajax({
                    url: Gaia.API+'/folder/'+this.sFolderId+'/',
                    type: 'PUT',
                    contentType: 'application/json',
                    data: JSON.stringify(asFolder)
                }).done(() => {
                    this.feedback('info', 'Waypoints Upload - Finished successfully');
                }).fail(() => {
                    this.feedback('error', 'Waypoints Upload - Failed to assign waypoints to folder "'+sFolderId+'"');
                });
            });
        }
    }

    static getIconName(sGarminName) {
        var asMapping = {
            'Trail Head': 'trailhead',
            'Water Source': 'water-24.png',
            'Truck': 'car-24.png',
            'Post Office': 'resupply',
            'Flag, Blue': 'blue-pin-down.png',
            'Car': 'car-24.png',
            'Flag, Red': 'red-pin-down.png',
            'Campground': 'campsite-24.png',
            'Flag, Green': 'green-pin.png',
            'Powerline': 'petroglyph',
            'Shopping Center': 'cafe-24.png',
            'Lodging': 'lodging-24.png',
            'Water Hydrant': 'red-pin-down.png',
            'Drinking Water': 'potable-water',
            'Toll Booth': 'ranger-station',
            'Summit': 'peak',
            'Park': 'park-24.png',
            'Forest': 'forest',
            'Cemetery': 'cemetery-24.png',
            'Bridge': 'dam-24.png',
            'Restaurant': 'restaurant-24.png',
            'Picnic Area': 'picnic',
            'Residence': 'city-24.png',
            'City (Capitol)': 'city-24.png',
            'Ski Resort': 'skiing-24.png',
            'Restroom': 'toilets-24.png',
            'Ground Transportation': 'car-24.png',
            'Church': 'ghost-town'
        };
        return (sGarminName in asMapping)?asMapping[sGarminName]:'red-pin-down.png';
    }
}

console.log('computes');

let oGaia = new Gaia($('#uploadForm'));
