//=============================================================================
// MapTiler for Train Simulator
// Written 2022 by Wutknubbel
// Based on the Original work from Ren-Chang
//=============================================================================
var stream = require('stream');
var express = require('express');
var request = require('request');
var iServer = express();
const util = require('node:util');
const { off } = require('process');
var querystr = require('querystring');
var im = require('images');

//=============================================================================
// Global Strings
//=============================================================================
var welcomeMsg  = 'Welcome to Map-Tiler v1.01a a Train Simulator Overlay Proxy Server';
var waitMsg     = 'Please wait until you see "Sending Proxy Script" before you starting RailWorks or use the Windows Proxy trick (See Manual)';
var tileMsgA    = 'Using %s as TileService';
var tileMsgB    = null;
var googleMsg   = 'Google';
var osmMsg      = 'OpenStreetMap';
var mBoxMsg     = 'MapBox Satellite';
var ua_modern   = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36';
var ua_legacy   = 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729)';

//=============================================================================
// Global Values
//=============================================================================
var tsLat       = null;
var tsLon       = null;
var tsZoom      = null;
var tsCenter    = null;
var maxZoom     = null;
var tileSize    = null;
var renderSize  = null;
var googleSvc   = false;
var tsMapType   = null;     // Only needed for Google
var pxy         = null;
var tsSize      = null;

//=============================================================================
// Tile Services
//=============================================================================
var tileOSM         = "https://tile.openstreetmap.org/%s/%s/%s.png";                // zxy
var googleSat       = 'https://mt1.google.com/vt/lyrs=s&x=%s&y=%s&z=%s';            // xyz
var googleRoad      = 'https://mt1.google.com/vt/lyrs=m&x=%s&y=%s&z=%s';            // xyz
var googleHybrid    = 'https://mt1.google.com/vt/lyrs=y&x=%s&y=%s&z=%s';            // xyz
var googleTerrain   = 'https://mt1.google.com/vt/lyrs=p&x=%s&y=%s&z=%s';            // xyz
var mapBoxSat       = 'https://a.tiles.mapbox.com/v4/mapbox.satellite/%s/%s/%s.jpg?access_token=pk.eyJ1Ijoib3BlbnN0cmVldG1hcCIsImEiOiJja2w5YWt5bnYwNjZmMnFwZjhtbHk1MnA1In0.eq2aumBK6JuRoIuBMm6Gew';

var tempTileService = null;

//=============================================================================
// Commandline Arguments
//=============================================================================
const conArgs = process.argv.slice(2);

switch(conArgs[0]) {
    case 'osm':
        tileSize = 256;
        renderSize = 256;
        tileMsgB = util.format(tileMsgA, osmMsg);
        tempTileService = tileOSM;
        break;
    case 'mbox':
        tileSize = 256;
        renderSize = 256;
        tileMsgB = util.format(tileMsgA, mBoxMsg);
        tempTileService = mapBoxSat;
        break;
    case 'google':
        tileSize = 256;
        renderSize = 256;
        googleSvc = true;
        tileMsgB = util.format(tileMsgA, googleMsg);
        break;
    default:
        tileSize = 256;
        renderSize = 256;
        tileMsgB = util.format(tileMsgA, googleMsg);
        googleSvc = true;
        break;
}

console.clear();
console.log(welcomeMsg);
console.log(tileMsgB);

//=============================================================================
// Server Startup
//=============================================================================
iServer.listen(6789, function() {
    console.log("Listening on Port : 6789");
    console.log(waitMsg);
});

iServer.get('/pac.txt', function(req, res) {
    console.log('Sending Proxy Script');
    res.header('Content-Type:text/plain');
    res.sendFile(__dirname+'/gmaps.pac');
});

//=============================================================================
// Error Handler
//=============================================================================
function onError(err, res, msg) {
    res.statusCode = 500;
    res.header("Content-Type", "text/plain");
    res.write([msg, err.message, err.stack].join('\n'));
    console.log(err.message);
    res.end();
}

//=============================================================================
// Capture GoogleAPI Url
//=============================================================================
iServer.get('/maps/api/staticmap', function(req, res) {

    // Extract needed Informations
    tsZoom = parseInt(req.query.zoom);
    tsMapType = req.query.maptype;
    tsCenter = req.query.center.split(',').map(parseFloat);
    tsLat = tsCenter[0], tsLon = tsCenter[1];
    tsSize = req.query.size.split('x').map(s=>parseInt(s, 10));

    if (googleSvc == true)
        procGoogleTiles(req, res);
    else
        procOSMStyleTiles(req, res);
});

//=============================================================================
// Tile Processing
//=============================================================================
function procGoogleTiles(req, res) {
    
    switch(tsMapType) {
        case 'hybrid':
            tempTileService = googleHybrid;
            break;
        case 'roadmap':
            tempTileService = googleRoad;
            break;
        case 'terrain':
            tempTileService = googleTerrain;
            break;
        case 'satellite':
            tempTileService = googleSat;
            break;
        default:
            break;
    } 

    centerInfo = getCenterTile(tsLon, tsLat, tsZoom, tileSize); // tileSize = 256
    x = centerInfo[0], y = centerInfo[1], i = centerInfo[2], j = centerInfo[3];
    targetInfo = getPixelRange(i, j, tsSize, tileSize); // tileSize = 256

     // request and process relevant tiles
     loadTileSet(tempTileService, getTilesParam(
        x, y, targetInfo.xdelta, targetInfo.ydelta
    ), tsZoom, pxy).then(function(tiles) {
        procTileSet(tiles, targetInfo, centerInfo, tsSize)
            .then(function (result){
                // send result
                res.header("Content-Type", "image/jpeg");
                var restream = stream.PassThrough();
                restream.end(result);
                restream.pipe(res);
            }).catch(err=>{onError(err, res, 'Error - Send result ');});
    }).catch(err=>{onError(err, res, 'Error - Process tiles');});
}

function procOSMStyleTiles(req, res) {

    centerInfo = getCenterTile(tsLon, tsLat, tsZoom, tileSize); // tileSize = 256
    x = centerInfo[0], y = centerInfo[1], i = centerInfo[2], j = centerInfo[3];
    targetInfo = getPixelRange(i, j, tsSize, tileSize); // tileSize = 256;

     // request and process relevant tiles
     loadTileSet(tempTileService, getTilesParam(
        x, y, targetInfo.xdelta, targetInfo.ydelta
    ), tsZoom, pxy).then(function(tiles) {
        procTileSet(tiles, targetInfo, centerInfo, tsSize)
            .then(function (result){
                // send result
                res.header("Content-Type", "image/jpeg");
                var restream = stream.PassThrough();
                restream.end(result);
                restream.pipe(res);
            }).catch(err=>{onError(err, res, 'Error - Send result ');});
    }).catch(err=>{onError(err, res, 'Error - Process tiles');});
}

function getCenterTile(lon, lat, zoom, tsize) {
    var xf = (lon+180)/360*Math.pow(2,zoom);
    var yf = (1-Math.log(Math.tan(lat*Math.PI/180) + 1/Math.cos(lat*Math.PI/180))/Math.PI)/2 *Math.pow(2,zoom);
    var i = Math.floor(xf % 1 * tsize);
    var j = Math.floor(yf % 1 * tsize)
    return [Math.floor(xf), Math.floor(yf), i, j];
}

function getPixelRange(i, j, psize, tsize) {
    var hsize = psize.map(s=>0.5*s);
    var imin = i - hsize[0];
    var jmin = j - hsize[1];
    var imax = i + hsize[0];
    var jmax = j + hsize[1];
    var xdelta = [Math.floor(imin / tsize), Math.floor(imax / tsize)];
    var ydelta = [Math.floor(jmin / tsize), Math.floor(jmax / tsize)];
    var idelta = xdelta[0] * tsize;
    var jdelta = ydelta[0] * tsize;
    imin -= idelta; imax -= idelta;
    jmin -= jdelta; jmax -= jdelta;
    return {'xdelta': xdelta, 'ydelta': ydelta, bbox: [imin, jmin, imax, jmax]};
}

function concatTileSet(tiles, targetInfo, tsize) {
    
    var tsize = tsize ? tsize : 256;
    // create canvas
    var ncol = targetInfo.xdelta[1] - targetInfo.xdelta[0] + 1;
    var nrow = targetInfo.ydelta[1] - targetInfo.ydelta[0] + 1;
    //console.log(util.format('Width.: %s Height.: %s', ncol * tsize, nrow * tsize))
    var canvas = im(ncol * tsize, nrow * tsize);
    
    // merge tiles
    var ntile = 0;
    for (var j = 0; j < nrow; j++) {
        var top = tsize * j;
        for (i = 0; i < ncol; i++) {
            var left = tsize * i;
            if (tiles[ntile]) {
                var tile = im(tiles[ntile]);
                
                    canvas.draw(tile, left, top);
                
                ntile += 1;
            }
        }
    }
    return canvas;
}

function cropConcatImage(canvas, istart, jstart, psize) {
    return new Promise(function (resolve) {
        // clip tiles
        
        if (psize.length == 1) {
            var cropped = im(psize, psize);
            cropped = im(canvas, istart, jstart, psize, psize);
            resolve(cropped.encode('jpg', {operation: 100}));
        }
        else {
            var cropped = im(psize[0], psize[1]);
            cropped = im(canvas, istart, jstart, psize[0], psize[1]);
            resolve(cropped.encode('jpg', {operation: 100}));
        }
    });
}

function procTileSet(tiles, targetInfo, tileInfo, psize, tsize) {
   
    return cropConcatImage(
        concatTileSet(tiles, targetInfo, tsize?tsize:256),
        targetInfo.bbox[0], targetInfo.bbox[1], psize
    );
}

function loadTileSet(api, params, zoom, proxy) {
    var tasks = params.map((query)=>{
        query.z = zoom;
        //console.log(api);
        //console.log(query);
        return getTile(api, query, proxy);
    });
    return Promise.all(tasks);
}

function getTile(api, params, proxy) {
    return new Promise(function (resolve, reject) {
        //var url = api + querystr.stringify(params);
        
        if(googleSvc == true)
            var url = util.format(api, params.x, params.y, params.z);
        else
            var url = util.format(api,params.z, params.x, params.y);

        //console.log(util.format("API.: %s", api));
        //console.log(util.format("URL.: %s",url))
        //console.log(util.format("Params.: %s",params));

        request.get(url, {
            'headers': {
                'Accept': '*/*',
                'Accept-Encoding': 'gzip',
                'User-Agent': ua_legacy,
            },
            'proxy': proxy,
            'gzip': true,
            'encoding': null
        }, function (err, res, body) {
            if (err)
                reject(err);
            else
                resolve(body);
        })
    });
}

function getTilesParam(x, y, xdelta, ydelta) {
    var params = [];
    for (var j = y + ydelta[0]; j <= y + ydelta[1]; j++) {
        for (var i = x + xdelta[0]; i <= x + xdelta[1]; i++) {
            params.push({'x': i, 'y': j});
        }
    }
    return params;
}