/*
Node-OpenDroneMap Node.js App and REST API to access OpenDroneMap.
Copyright (C) 2016 Node-OpenDroneMap Contributors

This program 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 3 of the License, or
(at your option) any later version.

This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";

const fs = require('fs');
const config = require('./config.js');
const packageJson = JSON.parse(fs.readFileSync('./package.json'));

const logger = require('./libs/logger');
const async = require('async');
const mime = require('mime');

const cors = require('cors')
const express = require('express');
const app = express();

const bodyParser = require('body-parser');
const multer = require('multer');

const TaskManager = require('./libs/TaskManager');
const odmInfo = require('./libs/odmInfo');
const si = require('systeminformation');
const S3 = require('./libs/S3');

const auth = require('./libs/auth/factory').fromConfig(config);
const authCheck = auth.getMiddleware();
const taskNew = require('./libs/taskNew');

app.use(cors())
app.options('*', cors())

app.use(express.static('public'));
app.use('/swagger.json', express.static('docs/swagger.json'));

const formDataParser = multer().none();
const urlEncodedBodyParser = bodyParser.urlencoded({extended: false});
const jsonBodyParser = bodyParser.json();

let taskManager;
let server;

/** @swagger
 *  /task/new/init:
 *    post:
 *      description: Initialize the upload of a new task. If successful, a user can start uploading files via /task/new/upload. The task will not start until /task/new/commit is called.
 *      tags: [task]
 *      parameters:
 *        -
 *          name: name
 *          in: formData
 *          description: An optional name to be associated with the task
 *          required: false
 *          type: string
 *        -
 *          name: options
 *          in: formData
 *          description: 'Serialized JSON string of the options to use for processing, as an array of the format: [{name: option1, value: value1}, {name: option2, value: value2}, ...]. For example, [{"name":"cmvs-maxImages","value":"500"},{"name":"time","value":true}]. For a list of all options, call /options'
 *          required: false
 *          type: string
 *        -
 *          name: skipPostProcessing
 *          in: formData
 *          description: 'When set, skips generation of point cloud tiles.'
 *          required: false
 *          type: boolean
 *        -
 *          name: webhook
 *          in: formData
 *          description: Optional URL to call when processing has ended (either successfully or unsuccessfully).
 *          required: false
 *          type: string
 *        -
 *          name: outputs
 *          in: formData
 *          description: 'An optional serialized JSON string of paths relative to the project directory that should be included in the all.zip result file, overriding the default behavior.'
 *          required: false
 *          type: string
 *        -
 *          name: dateCreated
 *          in: formData
 *          description: 'An optional timestamp overriding the default creation date of the task.'
 *          required: false
 *          type: integer
 *        -
 *          name: token
 *          in: query
 *          description: 'Token required for authentication (when authentication is required).'
 *          required: false
 *          type: string
 *        -
 *          name: set-uuid
 *          in: header
 *          description: 'An optional UUID string that will be used as UUID for this task instead of generating a random one.'
 *          required: false
 *          type: string
 *      responses:
 *        200:
 *          description: Success
 *          schema:
 *            type: object
 *            required: [uuid]
 *            properties:
 *              uuid:
 *                type: string
 *                description: UUID of the newly created task
 *        default:
 *          description: Error
 *          schema:
 *            $ref: '#/definitions/Error'
 */
app.post('/task/new/init', authCheck, taskNew.assignUUID, formDataParser, taskNew.handleInit);

/** @swagger
 *  /task/new/upload/{uuid}:
 *    post:
 *      description: Adds one or more files to the task created via /task/new/init. It does not start the task. To start the task, call /task/new/commit.
 *      tags: [task]
 *      consumes:
 *        - multipart/form-data
 *      parameters:
 *        -
 *           name: uuid
 *           in: path
 *           description: UUID of the task
 *           required: true
 *           type: string
 *        -
 *          name: images
 *          in: formData
 *          description: Images to process, plus optional files such as a GEO file (geo.txt), image groups file (image_groups.txt), GCP file (*.txt), seed file (seed.zip) or alignment files (align.las, align.laz, align.tif). If included, the GCP file should have .txt extension. If included, the seed archive pre-polulates the task directory with its contents.
 *          required: true
 *          type: file
 *        -
 *          name: token
 *          in: query
 *          description: 'Token required for authentication (when authentication is required).'
 *          required: false
 *          type: string
 *      responses:
 *        200:
 *          description: File Received
 *          schema:
 *            $ref: "#/definitions/Response"
 *        default:
 *          description: Error
 *          schema:
 *            $ref: '#/definitions/Error'
 */
app.post('/task/new/upload/:uuid', authCheck, taskNew.getUUID, taskNew.preUpload, taskNew.uploadImages, taskNew.handleUpload);

/** @swagger
 *  /task/new/commit/{uuid}:
 *    post:
 *      description: Creates a new task for which images have been uploaded via /task/new/upload.
 *      tags: [task]
 *      parameters:
 *        -
 *           name: uuid
 *           in: path
 *           description: UUID of the task
 *           required: true
 *           type: string
 *        -
 *          name: token
 *          in: query
 *          description: 'Token required for authentication (when authentication is required).'
 *          required: false
 *          type: string
 *      responses:
 *        200:
 *          description: Success
 *          schema:
 *            type: object
 *            required: [uuid]
 *            properties:
 *              uuid:
 *                type: string
 *                description: UUID of the newly created task
 *        default:
 *          description: Error
 *          schema:
 *            $ref: '#/definitions/Error'
 */
app.post('/task/new/commit/:uuid', authCheck, taskNew.getUUID, taskNew.handleCommit, taskNew.createTask);

/** @swagger
 *  /task/new:
 *    post:
 *      description: Creates a new task and places it at the end of the processing queue. For uploading really large tasks, see /task/new/init instead.
 *      tags: [task]
 *      consumes:
 *        - multipart/form-data
 *      parameters:
 *        -
 *          name: images
 *          in: formData
 *          description: Images to process, plus optional files such as a GEO file (geo.txt), image groups file (image_groups.txt), GCP file (*.txt), seed file (seed.zip) or alignment files (align.las, align.laz, align.tif). If included, the GCP file should have .txt extension. If included, the seed archive pre-polulates the task directory with its contents.
 *          required: false
 *          type: file
 *        -
 *          name: zipurl
 *          in: formData
 *          description: URL of the zip file containing the images to process, plus an optional GEO file and/or an optional GCP file. If included, the GCP file should have .txt extension
 *          required: false
 *          type: string
 *        -
 *          name: name
 *          in: formData
 *          description: An optional name to be associated with the task
 *          required: false
 *          type: string
 *        -
 *          name: options
 *          in: formData
 *          description: 'Serialized JSON string of the options to use for processing, as an array of the format: [{name: option1, value: value1}, {name: option2, value: value2}, ...]. For example, [{"name":"cmvs-maxImages","value":"500"},{"name":"time","value":true}]. For a list of all options, call /options'
 *          required: false
 *          type: string
 *        -
 *          name: skipPostProcessing
 *          in: formData
 *          description: 'When set, skips generation of point cloud tiles.'
 *          required: false
 *          type: boolean
 *        -
 *          name: webhook
 *          in: formData
 *          description: Optional URL to call when processing has ended (either successfully or unsuccessfully).
 *          required: false
 *          type: string
 *        -
 *          name: outputs
 *          in: formData
 *          description: 'An optional serialized JSON string of paths relative to the project directory that should be included in the all.zip result file, overriding the default behavior.'
 *          required: false
 *          type: string
 *        -
 *          name: dateCreated
 *          in: formData
 *          description: 'An optional timestamp overriding the default creation date of the task.'
 *          required: false
 *          type: integer
 *        -
 *          name: token
 *          in: query
 *          description: 'Token required for authentication (when authentication is required).'
 *          required: false
 *          type: string
 *        -
 *          name: set-uuid
 *          in: header
 *          description: 'An optional UUID string that will be used as UUID for this task instead of generating a random one.'
 *          required: false
 *          type: string
 *      responses:
 *        200:
 *          description: Success
 *          schema:
 *            type: object
 *            required: [uuid]
 *            properties:
 *              uuid:
 *                type: string
 *                description: UUID of the newly created task
 *        default:
 *          description: Error
 *          schema:
 *            $ref: '#/definitions/Error'
 */
app.post('/task/new', authCheck, taskNew.assignUUID, taskNew.uploadImages, (req, res, next) => {
    req.body = req.body || {};
    if ((!req.files || req.files.length === 0) && !req.body.zipurl) req.error = "Need at least 1 file or a zip file url.";
    else if (config.maxImages && req.files && req.files.length > config.maxImages) req.error = `${req.files.length} images uploaded, but this node can only process up to ${config.maxImages}.`;
    next();
}, taskNew.createTask);

let getTaskFromUuid = (req, res, next) => {
    let task = taskManager.find(req.params.uuid);
    if (task) {
        req.task = task;
        next();
    } else res.json({ error: `${req.params.uuid} not found` });
};

/** @swagger
 *  /task/list:
 *     get:
 *       description: Gets the list of tasks available on this node.
 *       tags: [task]
 *       parameters:
 *        -
 *          name: token
 *          in: query
 *          description: 'Token required for authentication (when authentication is required).'
 *          required: false
 *          type: string
 *       responses:
 *        200:
 *          description: Task List
 *          schema:
 *            title: TaskList
 *            type: array
 *            items:
 *              type: object
 *              required: [uuid]
 *              properties:
 *                uuid:
 *                  type: string
 *                  description: UUID
 *        default:
 *          description: Error
 *          schema:
 *            $ref: '#/definitions/Error'
 */
app.get('/task/list', authCheck, (req, res) => {
    const tasks = [];
    for (let uuid in taskManager.tasks){
        tasks.push({uuid});
    }
    res.json(tasks);
});

/** @swagger
 *  /task/{uuid}/info:
 *     get:
 *       description: Gets information about this task, such as name, creation date, processing time, status, command line options and number of images being processed. See schema definition for a full list.
 *       tags: [task]
 *       parameters:
 *        -
 *           name: uuid
 *           in: path
 *           description: UUID of the task
 *           required: true
 *           type: string
 *        -
 *          name: token
 *          in: query
 *          description: 'Token required for authentication (when authentication is required).'
 *          required: false
 *          type: string
 *        -
 *          name: with_output
 *          in: query
 *          description: Optionally retrieve the console output for this task. The parameter specifies the line number that the console output should be truncated from. For example, passing a value of 100 will retrieve the console output starting from line 100. By default no console output is added to the response.
 *          default: 0
 *          required: false
 *          type: integer
 *       responses:
 *        200:
 *         description: Task Information
 *         schema:
 *           title: TaskInfo
 *           type: object
 *           required: [uuid, name, dateCreated, processingTime, status, options, imagesCount, progress]
 *           properties:
 *            uuid:
 *              type: string
 *              description: UUID
 *            name:
 *              type: string
 *              description: Name
 *            dateCreated:
 *              type: integer
 *              description: Timestamp
 *            processingTime:
 *              type: integer
 *              description: Milliseconds that have elapsed since the task started being processed.
 *            status:
 *              type: object
 *              required: [code]
 *              properties:
 *                code:
 *                  type: integer
 *                  description: Status code (10 = QUEUED, 20 = RUNNING, 30 = FAILED, 40 = COMPLETED, 50 = CANCELED)
 *                  enum: [10, 20, 30, 40, 50]
 *            options:
 *              type: array
 *              description: List of options used to process this task
 *              items:
 *                type: object
 *                required: [name, value]
 *                properties:
 *                  name:
 *                    type: string
 *                    description: 'Option name (example: "odm_meshing-octreeDepth")'
 *                  value:
 *                    type: string
 *                    description: 'Value (example: 9)'
 *            imagesCount:
 *              type: integer
 *              description: Number of images
 *            progress:
 *              type: float
 *              description: Percentage progress (estimated) of the task
 *            output:
 *              type: array
 *              description: Console output for the task (only if requested via ?output=<linenum>)
 *              items:
 *                type: string
 *        default:
 *          description: Error
 *          schema:
 *            $ref: '#/definitions/Error'
 */
app.get('/task/:uuid/info', authCheck, getTaskFromUuid, (req, res) => {
    const info = req.task.getInfo();
    if (req.query.with_output !== undefined) info.output = req.task.getOutput(req.query.with_output);
    res.json(info);
});

/** @swagger
 *  /task/{uuid}/output:
 *     get:
 *       description: Retrieves the console output of the OpenDroneMap's process. Useful for monitoring execution and to provide updates to the user.
 *       tags: [task]
 *       parameters:
 *        -
 *           name: uuid
 *           in: path
 *           description: UUID of the task
 *           required: true
 *           type: string
 *        -
 *         name: line
 *         in: query
 *         description: Optional line number that the console output should be truncated from. For example, passing a value of 100 will retrieve the console output starting from line 100. Defaults to 0 (retrieve all console output).
 *         default: 0
 *         required: false
 *         type: integer
 *        -
 *          name: token
 *          in: query
 *          description: 'Token required for authentication (when authentication is required).'
 *          required: false
 *          type: string
 *       responses:
 *        200:
 *         description: Console Output
 *         schema:
 *           type: string
 *        default:
 *          description: Error
 *          schema:
 *            $ref: '#/definitions/Error'
 */
app.get('/task/:uuid/output', authCheck, getTaskFromUuid, (req, res) => {
    res.json(req.task.getOutput(req.query.line));
});

/** @swagger
 *  /task/{uuid}/download/{asset}:
 *    get:
 *      description: Retrieves an asset (the output of OpenDroneMap's processing) associated with a task
 *      tags: [task]
 *      produces: [application/zip]
 *      parameters:
 *        - name: uuid
 *          in: path
 *          type: string
 *          description: UUID of the task
 *          required: true
 *        - name: asset
 *          in: path
 *          type: string
 *          description: Type of asset to download. Use "all.zip" for zip file containing all assets.
 *          required: true
 *          enum:
 *            - all.zip
 *        -
 *          name: token
 *          in: query
 *          description: 'Token required for authentication (when authentication is required).'
 *          required: false
 *          type: string
 *      responses:
 *        200:
 *          description: Asset File
 *          schema:
 *            type: file
 *        default:
 *          description: Error message
 *          schema:
 *            $ref: '#/definitions/Error'
 */
app.get('/task/:uuid/download/:asset', authCheck, getTaskFromUuid, (req, res) => {
    let asset = req.params.asset !== undefined ? req.params.asset : "all.zip";
    let filePath = req.task.getAssetsArchivePath(asset);
    if (filePath) {
        if (fs.existsSync(filePath)) {
            res.setHeader('Content-Disposition', `attachment; filename=${asset}`);
            res.setHeader('Content-Type', mime.getType(filePath));
            res.setHeader('Content-Length', fs.statSync(filePath).size);

            const filestream = fs.createReadStream(filePath);
            filestream.pipe(res);
        } else {
            res.json({ error: "Asset not ready" });
        }
    } else {
        res.json({ error: "Invalid asset" });
    }
});

/** @swagger
 * definition:
 *   Error:
 *     type: object
 *     required:
 *       - error
 *     properties:
 *       error:
 *         type: string
 *         description: Description of the error
 *   Response:
 *     type: object
 *     required:
 *       - success
 *     properties:
 *       success:
 *         type: boolean
 *         description: true if the command succeeded, false otherwise
 *       error:
 *         type: string
 *         description: Error message if an error occured
 */
let uuidCheck = (req, res, next) => {
    if (!req.body.uuid) res.json({ error: "uuid param missing." });
    else next();
};

let successHandler = res => {
    return err => {
        if (!err) res.json({ success: true });
        else res.json({ success: false, error: err.message });
    };
};

/** @swagger
 * /task/cancel:
 *    post:
 *      description: Cancels a task (stops its execution, or prevents it from being executed)
 *      parameters:
 *        -
 *          name: uuid
 *          in: body
 *          description: UUID of the task
 *          required: true
 *          schema:
 *            type: string
 *        -
 *          name: token
 *          in: query
 *          description: 'Token required for authentication (when authentication is required).'
 *          required: false
 *          type: string
 *      responses:
 *        200:
 *          description: Command Received
 *          schema:
 *            $ref: "#/definitions/Response"
 */
app.post('/task/cancel', urlEncodedBodyParser, jsonBodyParser, authCheck, uuidCheck, (req, res) => {
    taskManager.cancel(req.body.uuid, successHandler(res));
});

/** @swagger
 * /task/remove:
 *    post:
 *      description: Removes a task and deletes all of its assets
 *      parameters:
 *        -
 *          name: uuid
 *          in: body
 *          description: UUID of the task
 *          required: true
 *          schema:
 *            type: string
 *        -
 *          name: token
 *          in: query
 *          description: 'Token required for authentication (when authentication is required).'
 *          required: false
 *          type: string
 *      responses:
 *        200:
 *          description: Command Received
 *          schema:
 *            $ref: "#/definitions/Response"
 */
app.post('/task/remove', urlEncodedBodyParser, jsonBodyParser, authCheck, uuidCheck, (req, res) => {
    taskManager.remove(req.body.uuid, successHandler(res));
});

/** @swagger
 * /task/restart:
 *    post:
 *      description: Restarts a task that was previously canceled, that had failed to process or that successfully completed
 *      parameters:
 *        -
 *          name: uuid
 *          in: body
 *          description: UUID of the task
 *          required: true
 *          schema:
 *            type: string
 *        -
 *          name: options
 *          in: body
 *          description: 'Serialized JSON string of the options to use for processing, as an array of the format: [{name: option1, value: value1}, {name: option2, value: value2}, ...]. For example, [{"name":"cmvs-maxImages","value":"500"},{"name":"time","value":true}]. For a list of all options, call /options. Overrides the previous options set for this task.'
 *          required: false
 *          schema:
 *            type: string
 *        -
 *          name: token
 *          in: query
 *          description: 'Token required for authentication (when authentication is required).'
 *          required: false
 *          type: string
 *      responses:
 *        200:
 *          description: Command Received
 *          schema:
 *            $ref: "#/definitions/Response"
 */
app.post('/task/restart', urlEncodedBodyParser, jsonBodyParser, authCheck, uuidCheck, (req, res, next) => {
    if (req.body.options){
        odmInfo.filterOptions(req.body.options, (err, options) => {
            if (err) res.json({ error: err.message });
            else {
                req.body.options = options;
                next();
            }
        });
    } else next();
}, (req, res) => {
    taskManager.restart(req.body.uuid, req.body.options, successHandler(res));
});

/** @swagger
 * /options:
 *   get:
 *     description: Retrieves the command line options that can be passed to process a task
 *     parameters:
 *       -
 *         name: token
 *         in: query
 *         description: 'Token required for authentication (when authentication is required).'
 *         required: false
 *         type: string
 *     tags: [server]
 *     responses:
 *       200:
 *         description: Options
 *         schema:
 *           type: array
 *           items:
 *             title: Option
 *             type: object
 *             required: [name, type, value, domain, help]
 *             properties:
 *               name:
 *                 type: string
 *                 description: Command line option (exactly as it is passed to the OpenDroneMap process, minus the leading '--')
 *               type:
 *                 type: string
 *                 description: Datatype of the value of this option
 *                 enum:
 *                   - int
 *                   - float
 *                   - string
 *                   - bool
 *               value:
 *                 type: string
 *                 description: Default value of this option
 *               domain:
 *                 type: string
 *                 description: Valid range of values (for example, "positive integer" or "float > 0.0")
 *               help:
 *                 type: string
 *                 description: Description of what this option does
 */
app.get('/options', authCheck, (req, res) => {
    odmInfo.getOptions((err, options) => {
        if (err) res.json({ error: err.message });
        else res.json(options);
    });
});

/** @swagger
 * /info:
 *   get:
 *     description: Retrieves information about this node
 *     parameters:
 *       -
 *         name: token
 *         in: query
 *         description: 'Token required for authentication (when authentication is required).'
 *         required: false
 *         type: string
 *     tags: [server]
 *     responses:
 *       200:
 *         description: Info
 *         schema:
 *           type: object
 *           required: [version, taskQueueCount, maxImages, engineVersion, engine]
 *           properties:
 *             version:
 *               type: string
 *               description: Current API version
 *             taskQueueCount:
 *               type: integer
 *               description: Number of tasks currently being processed or waiting to be processed
 *             availableMemory:
 *               type: integer
 *               description: Amount of RAM available in bytes
 *             totalMemory:
 *               type: integer
 *               description: Amount of total RAM in the system in bytes
 *             cpuCores:
 *               type: integer
 *               description: Number of CPU cores (virtual)
 *             maxImages:
 *               type: integer
 *               description: Maximum number of images allowed for new tasks or null if there's no limit.
 *             maxParallelTasks:
 *               type: integer
 *               description: Maximum number of tasks that can be processed simultaneously
 *             engineVersion:
 *               type: string
 *               description: Current version of processing engine
 *             engine:
 *               type: string
 *               description: Lowercase identifier of processing engine
 */
app.get('/info', authCheck, (req, res) => {
    async.parallel({
        cpu: cb => si.cpu(data => cb(null, data)),
        mem: cb => si.mem(data => cb(null, data)),
        engineVersion: odmInfo.getVersion,
        engine: odmInfo.getEngine
    }, (_, data) => {
        const { cpu, mem, engineVersion, engine } = data;

        // For testing
        if (req.query._debugUnauthorized){
            res.writeHead(401, "unauthorized")
            res.end();
            return;
        }

        res.json({
            version: packageJson.version,
            taskQueueCount: taskManager.getQueueCount(),
            totalMemory: mem.total,
            availableMemory: mem.available,
            cpuCores: cpu.cores,
            maxImages: config.maxImages,
            maxParallelTasks: config.parallelQueueProcessing,
            engineVersion,
            engine
        });
    });
});

/** @swagger
 * /auth/info:
 *   get:
 *     description: Retrieves login information for this node.
 *     tags: [auth]
 *     responses:
 *       200:
 *         description: LoginInformation
 *         schema:
 *           type: object
 *           required: [message, loginUrl, registerUrl]
 *           properties:
 *             message:
 *               type: string
 *               description: Message to be displayed to the user prior to login/registration. This might include instructions on how to register or login, or to communicate that authentication is not available.
 *             loginUrl:
 *               type: string
 *               description: URL (absolute or relative) where to make a POST request to obtain a token, or null if login is disabled.
 *             registerUrl:
 *               type: string
 *               description: URL (absolute or relative) where to make a POST request to register a user, or null if registration is disabled.
 */
app.get('/auth/info', (req, res) => {
    res.json({
        message: "Authentication not available on this node", 
        loginUrl: null,
        registerUrl: null
    });
});

/** @swagger
 * /auth/login:
 *    post:
 *      description: Retrieve a token from a username/password pair.
 *      parameters:
 *        -
 *          name: username
 *          in: body
 *          description: Username
 *          required: true
 *          schema:
 *            type: string
 *        -
 *          name: password
 *          in: body
 *          description: Password
 *          required: true
 *          type: string
 *      responses:
 *        200:
 *          description: Login Succeeded
 *          schema:
 *            type: object
 *            required: [token]
 *            properties:
 *              token:
 *                type: string
 *                description: Token to be passed as a query parameter to other API calls.
 *        default:
 *          description: Error
 *          schema:
 *            $ref: '#/definitions/Error'
 */
app.post('/auth/login', (req, res) => {
    res.json({error: "Not available"});
});

/** @swagger
 * /auth/register:
 *    post:
 *      description: Register a new username/password.
 *      parameters:
 *        -
 *          name: username
 *          in: body
 *          description: Username
 *          required: true
 *          schema:
 *            type: string
 *        -
 *          name: password
 *          in: body
 *          description: Password
 *          required: true
 *          type: string
 *      responses:
 *        200:
 *          description: Response
 *          schema:
 *            $ref: "#/definitions/Response"
 */
app.post('/auth/register', (req, res) => {
    res.json({error: "Not available"});
});


app.use((err, req, res, next) => {
    logger.error(err.stack);
    res.json({error: err.message});
});

let gracefulShutdown = done => {
    async.series([
        cb => taskManager.dumpTaskList(cb),
        cb => auth.cleanup(cb),
        cb => {
            logger.info("Closing server");
            server.close();
            logger.info("Exiting...");
            process.exit(0);
        }
    ], done);
};

// listen for TERM signal .e.g. kill
process.on('SIGTERM', gracefulShutdown);

// listen for INT signal e.g. Ctrl-C
process.on('SIGINT', gracefulShutdown);

// Startup
if (config.test) {
    logger.info("Running in test mode");
    if (config.testSkipOrthophotos) logger.info("Orthophotos will be skipped");
    if (config.testSkipDems) logger.info("DEMs will be skipped");
    if (config.testDropUploads) logger.info("Uploads will drop at random");
}

if (!config.hasUnzip) logger.warn("The unzip program is not installed, (certain unzip operations might be slower)");
if (!config.has7z) logger.warn("The 7z program is not installed, falling back to legacy (zipping will be slower)");


let commands = [
    cb => odmInfo.initialize(cb),
    cb => auth.initialize(cb),
    cb => S3.initialize(cb),
    cb => { 
        TaskManager.initialize(cb);
        taskManager = TaskManager.singleton();
    },
    cb => {
        const startServer = (port, cb) => {
            server = app.listen(parseInt(port), (err) => {
                if (!err) logger.info('Server has started on port ' + String(port));
                cb(err);
            });
            server.on("error", cb);
        };

        const tryStartServer = (port, cb) => {
            startServer(port, (err) => {
                if (err && err.code === 'EADDRINUSE' && port < 5000){
                    tryStartServer(port + 1, cb);
                }else cb(err);
            });
        };

        if (Number.isInteger(parseInt(config.port))){
            startServer(config.port, cb);
        }else if (config.port === "auto"){
            tryStartServer(3000, cb);
        }else{
            cb(new Error(`Invalid port: ${config.port}`));
        }
    }
];

if (config.powercycle) {
    commands.push(cb => {
        logger.info("Power cycling is set, application will shut down...");
        process.exit(0);
    });
}

async.series(commands, err => {
    if (err) {
        logger.error(err.message);
        process.exit(1);
    }
});
