"use strict";

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Creates a new Asset object and saves it into the list of created Assets.
 * Initializes job which will load file (from disk/server) with the asset, but the job
 * can also be dummy, which means it does not load anything, it just performs run callback.
 * Execution of the job (i.e. downloading and then running) of the asset must be executed
 * manually via start(). The class is a little mess... Calling constructor is equivalent to calling factory method createJob().
 *
 * @author smrceant (Antonin Smrcek)
 *
 * @param {string}                      assetName           name under which Asset object could be looked up from global list
 * @param {Asset.AssetType}             assetType           type of the created asset
 * @param {*}                           objectInstance      application object, currently empty, but initialized
 * @param {function}                    [runCallback]       function which will be executed during the RUNNING stage of the job
 *                                                          (typically this will process the downloaded data)
 * @constructor
 */
var AssetJob = function (assetName, assetType, objectInstance, runCallback)
{
  /**
   * ID number of the job. Bigger the number, later the job was created.
   *
   * @type {number}
   */
  this.id = AssetJob.cnt++;

  /**
   * Name of the job is same as the name of the asset.
   *
   * @type {string}
   */
  this.name = assetName;

  //////////////////////////////////////////////////////////
  // RUN CALLBACK
  //////////////////////////////////////////////////////////

  /**
   * Function which will be executed during the RUNNING stage of this job (after download is completed, or skipped in the
   * case of the dummy job).
   *
   * @type {function}
   */
  this.runCallback = runCallback;

  // special case when we want to load text file and get it as JSON
  if (assetType === Asset.AssetType.JSON)
  {
    // we need to redefine the run callback
    if (Util.isInitialized (runCallback))
    {
      // save original run callback
      var originalRunCallback = this.runCallback;

      this.runCallback = function()
      {
        // convert text file to JSON
        this.asset.createdObject = JSON.parse (this._downloadedData.data);

        // run original callback
        originalRunCallback();
      }.bind (this);
    }
    else
    {
      this.runCallback = function()
      {
        this.asset.createdObject = JSON.parse (this._downloadedData.data);
      }.bind (this);
    }
  }
  //////////////////////////////////////////////////////////

  /**
   * Current state of the job. The job is in several states (stages) during its lifecycle. The typical lifecycle
   * is NEW-DOWNLOADING-RUNNING-FINISHED, but NEW-DOWNLOADING-RUNNING-FINISHED_BUT_WAITING-FINISHED can happen too.
   *
   * NEW: The job was just created, nothing happened so far.
   * DOWNLOADING: The job didn't have to wait for any other jobs to start (downloading), so it has started (downloading its asset file).
   *              If the job is dummy and doesn't have to download anything, it will immediately jump to the RUNNING state.
   * RUNNING: The job is in this state right after the asset file is downloaded. The run callback is executed in this stage.
   * FINISHED_BUT_WAITING:  The job has finished the execution of its job callback, but during the DOWNLOADING and RUNNING stage,
   *                        another jobs were added as required - the job will remain in this state until these required jobs reach FINISHED state.
   * FINISHED:  The job has finished the execution of its job callback and it didn't have to wait for
   *            any other jobs (or didn't have to wait anymore).
   * ERROR: Downloading of the asset file or run callback has ended with an error.
   *
   * @type {number}
   */
  this.jobState = AssetJob.JobState.NEW;


  this.isDownloadJob =  (assetType === Asset.AssetType.MESH) ||
                        (assetType === Asset.AssetType.IMAGE) ||
                        (assetType === Asset.AssetType.SCENE_DEF) ||
                        (assetType === Asset.AssetType.TEXT) ||
                        (assetType === Asset.AssetType.JSON);

  /**
   * If set to TRUE, the job can start if its own (!!!) start() or startDownload(true) method were ever called.
   * In practise, that means calling assetJob.start() somewhere in the source code. If this condition is not met,
   * the startDownload method will terminate right at the start, doing absolutely nothing. Used exclusively by
   * dummy jobs when constructing a system which correctly loads stuff in order (e.g. loading of world heightmap
   * after the scene is finished loading etc.)
   *
   * If set to FALSE, the job can be also started by the (depending) job which waits for it and there is no need
   * to write assetJob.start() in the source code.
   *
   * @type {boolean}
   */
  this.mustStartItself = (assetType === Asset.AssetType.DUMMY_START_ITSELF);

  //////////////////////////////////////////////////////////
  // DOWNLOADED DATA
  //////////////////////////////////////////////////////////

  /**
   * Data which has been downloaded in the downloading phase of the job.
   * When the downloaded data is an image, its dimensions are stored.
   */
  this._downloadedData =
  {
    data: null,

    imageWidth: null,
    imageHeight: null
  };

  //////////////////////////////////////////////////////////
  // ASSET CREATION
  //////////////////////////////////////////////////////////

  this.asset = null;

  if ((assetType !== Asset.AssetType.DUMMY) && (assetType !== Asset.AssetType.DUMMY_START_ITSELF))
  {
    if (assetName === null)
    {
      throw new Error("Asset loader ERROR: Asset name is null, although it is not a dummy asset.");
    }

    this.asset = new Asset (assetName, assetType, objectInstance, this);

    assetLoader.saveAssetIntoLibrary (this.asset, assetType);
  }
  // asset performs dummy job, do not store it into the library
  else
  {
    this.asset = new Asset (assetName, assetType, objectInstance, this);
  }

  /**
   * Flag indicating whether start() or startDownload() of this job was ever called.
   * It is used so that job won't be started inadvertently, e.g. from the onRequiredFinished() method.
   *
   * @type {boolean}
   */
  this.downloadAndRunOrderIssued = false;

  //////////////////////////////////////////////////////////
  // SUCCESSORS - THE JOB IS WAITING FOR ITS SUCCESSORS
  //////////////////////////////////////////////////////////

  /**
   * The job must wait for these jobs before it launches downloading of its asset file OR before it moves from
   * FINISHED_BUT_WAITING to the FINISHED state (this happens when a required job is added when the job is in the
   * DOWNLOADING or RUNNING state and it means that it won't be considered as finished until the required job is
   * finished as well).
   *
   * [DEV]: The jobs are removed from the list when they finish. Slower, but easier to debug.
   *
   * In production: jobs are NOT REMOVED from the list when they finish - instead, the appropriate counter
   * of unfinished jobs is decreased (cntUnfinishedRequiredJobs).

   * @type {AssetJob[]}
   */
  this.requiredJobs = [];

  /**
   * List of jobs which must be finished before this job can commence.
   *
   * @type {AssetJob[]}
   */
  this.requiredJobsToFinish = [];

  // (note: technically we do not need this since we have those requiredJobs[] lists, but with counters, we do not
  //        have to care about removing finished jobs from the lists)
  //////////////////////////////////////////////////////////
  // PREDECESSORS - THEY ARE WAITING FOR THE JOB
  //////////////////////////////////////////////////////////

  /**
   * These jobs are waiting for this job to finish (i.e. to be in FINISHED state). It is counterpart to the requiredJobs[].
   *
   * @type {AssetJob[]}
   */
  this.dependingJobs = [];
  // (no counters needed for depending jobs, we just need its list)

  ///////////////////////////////////

  AssetJob.jobs.push (this);
};

/**
 * List of all created jobs.
 *
 * @type {AssetJob[]}
 */
AssetJob.jobs = [];

/**
 * Number of all created jobs. Used to assign ID's.
 *
 * @type {number}
 */
AssetJob.cnt = 0;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// FACTORY METHODS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Creates a job that has to download file with the asset before it actually executes itself.
 * Run callback is not defined, because it always will be onFileDownloaded() method of the data object, so it is
 * important to make sure that each object passed as data has defined that method in its prototype chain.
 *
 * @param   {string}                  assetName         name under which Asset object could be looked up from global list
 * @param   {Asset.AssetType}         assetType         type of the created asset
 * @param   {*}                       objectInstance    application object, currently empty, but initialized
 * @returns {AssetJob}
 */
AssetJob.createJobObjectCreation = function (assetName, assetType, objectInstance)
{
  var job = new AssetJob (assetName, assetType, objectInstance, function()
  {
    objectInstance.onFileDownloaded (job);
  });

  // IMPORTANT: each object which is created from downloaded asset must provide onFileDownloaded method!
  //job.runCallback = objectInstance.onFileDownloaded.bind (objectInstance, job);

  return job;
};

/**
 * Same effect as calling the constructor.
 *
 * @param {string}          assetName       name under which Asset object could be looked up from global list
 * @param {Asset.AssetType} assetType       type of the created asset
 * @param {*}               objectInstance  application object, currently empty, but initialized
 * @param {function}        [runCallback]   function which will be executed during the RUNNING stage of the job
 *                                          (typically this will process the downloaded data)
 * @returns {AssetJob}                      newly created job
 */
AssetJob.createJob = function (assetName, assetType, objectInstance, runCallback)
{
  return new AssetJob (assetName, assetType, objectInstance, runCallback);
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// JOB STATES
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

AssetJob.JobState =
{
  NEW:                    "new",
  DOWNLOADING:            "downloading",
  DOWNLOAD_JUST_FINISHED: "downloading just finished",
  WAITING_FOR_OTHERS:     "waiting for finish of others",
  RUNNING:                "running",
  FINISHED_BUT_WAITING:   "finished but waiting",
  JUST_FINISHED:          "just finished, must perform finishing stuff",
  FINISHED:               "finished",
  ERROR:                  "error"
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// GETTERS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Retrieves raw downloaded data of the asset file.
 *
 * @returns {*}
 */
AssetJob.prototype.getDownloadedData = function()
{
  return this._downloadedData.data;
};

/**
 * Gets container with raw downloaded data of the asset file - useful when dealing with downloaded images.
 */
AssetJob.prototype.getDownloadedDataContainer = function()
{
  return this._downloadedData;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Checks if the job must start only by itself.
 */
AssetJob.prototype.startItselfOnly = function()
{
  this.mustStartItself = true;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// JOB ASSIGNMENTS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Asynchronously downloads asset file (or image) and saves it, then sends a message to the system that
 * downloaded has been finished.
 */
AssetJob.prototype.downloadFile = function()
{
  var _this = this;

  switch (this.asset.type)
  {
    ///////////////////////////////////////////////////////////////////
    // IMAGE FILE
    ///////////////////////////////////////////////////////////////////
    case Asset.AssetType.IMAGE:
      var image = new Image();
      if (!image)
      {
        this.jobState = AssetJob.JobState.ERROR;

        console.error ("AssetLoader error: Failed to create an image object for " + this.asset.name);

        return;
      }

      image.onerror = function()
      {
        _this.jobState = AssetJob.JobState.ERROR;

        console.error ("AssetLoader error: Image " + _this.asset.name + " not found.");
      };

      // call event after the image is loaded
      image.onload = function()
      {
        _this._downloadedData.data = image;
        _this._downloadedData.imageWidth = image.naturalWidth;
        _this._downloadedData.imageHeight = image.naturalHeight;

        _this.onDownloadFinished();
      };

      // (JS note: assigning src should be done after assigning onload, otherwise (for cached) images, onload might not get called)
      image.src = this.asset.name;
    break;

    ///////////////////////////////////////////////////////////////////
    // REGULAR FILE
    ///////////////////////////////////////////////////////////////////
    default:
      var request = new XMLHttpRequest();

      request.onreadystatechange = function()
      {
        if (request.readyState === 4)
        {
          if (request.status !== 404)
          {
            // check if the grabbed file is XML or a general file and save it
            _this._downloadedData.data = (_this.name.indexOf(".xml") !== -1) ? request.responseXML : request.responseText;

            _this.onDownloadFinished();
          }
          else
          {
            _this.jobState = AssetJob.JobState.ERROR;

            console.error ("AssetLoader error: File " + _this.asset.name + " not found.");
          }
        }
      };

      // create a request to acquire the file and send that request
      request.open('GET', this.name, true);

      // if we are fetching XML document, change response type to document, otherwise it is just text
      // (note: if we would not do this, we would get warnings about non-well formed documents in case of a text files)
      request.responseType = (this.name.indexOf(".xml") !== -1) ? "document" : "text";

      request.send();
    break;

  }
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ADDING RELATIONS BETWEEN JOBS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Adds a job which is required for this job, which means that the job cannot
 * start unless the required job is finished. Alias method for addRequiredJob(), and also conceptually same method
 * as Asset.waitFor() - it is intended for easier coding, since we can use the waitFor() on both Asset and AssetJob
 * instances.
 *
 * @param {Asset|AssetJob}  requiredAssetOrAssetJob
 * @param {boolean}         waitForFinish             does the job has to wait for this job until it is totally finished?
 */
AssetJob.prototype.waitFor = function (requiredAssetOrAssetJob, waitForFinish)
{
  if (!Util.isInitialized(waitForFinish))
  {
    var waitForFinish = false;
  }

  // we were given an Asset object
  if (requiredAssetOrAssetJob instanceof Asset)
  {
    this.addRequiredJob (requiredAssetOrAssetJob.job, waitForFinish);
  }
  // we were given Asset.job object
  else
  {
    this.addRequiredJob (requiredAssetOrAssetJob, waitForFinish);
  }
};

/**
 *
 * @param {Asset|AssetJob} requiredAssetOrAssetJob
 */
AssetJob.prototype.waitForFinish = function (requiredAssetOrAssetJob)
{
  this.waitFor (requiredAssetOrAssetJob, true);
};

/**
 * Adds a job which is required for this job, which means that the job cannot
 * start unless the required job is finished. For this reason, this job is considered as depending on the required job.
 *
 * @param {AssetJob} requiredJob      the job must wait for required job to finish, it cannot start if required job is not finished
 * @param {boolean}  waitForFinish    does the job has to wait for this job until it is totally finished?
 */
AssetJob.prototype.addRequiredJob = function (requiredJob, waitForFinish)
{
  // this = depending job

  if (!Util.isInitialized(waitForFinish))
  {
    var waitForFinish = false;
  }

  if (!AssetJob.checkDependingRequiredValidity (this, requiredJob)) return;

  this._addRequiredJob (requiredJob, waitForFinish);
  requiredJob._addDependingJob (this);
};

/**
 * Actual addition of the required job into the internal structures.
 *
 * @param {AssetJob} requiredJob
 * @param {boolean}  waitForFinish   does the job has to wait for this job until it is totally finished?
 * @private
 */
AssetJob.prototype._addRequiredJob = function (requiredJob, waitForFinish)
{
  // this = depending job

  if (waitForFinish)
  {
    this.requiredJobsToFinish.push (requiredJob);
  }
  else
  {
    this.requiredJobs.push (requiredJob);
  }
};

/**
 * Actual addition of the depending job into the internal structures. Similar to _addRequiredJob().
 *
 * @param {AssetJob} dependingJob   depending job will wait for this job to finish
 * @private
 */
AssetJob.prototype._addDependingJob = function (dependingJob)
{
  // this = required job

  this.dependingJobs.push (dependingJob);
};

/**
 * Checks if it makes sense to add required/depending relationship between the two jobs.
 *
 * @param   {AssetJob} dependingJob
 * @param   {AssetJob} requiredJob
 * @returns {boolean}   TRUE if the relationship can be carried out, FALSE if it makes no sense
 */
AssetJob.checkDependingRequiredValidity = function (dependingJob, requiredJob)
{
  if ((dependingJob.jobState === AssetJob.JobState.FINISHED) ||
      (dependingJob.jobState === AssetJob.JobState.ERROR)
      )
  {
    if (Preferences.Application.isDevMode())
    {
      console.error("No sense in adding job '" + dependingJob.name + "' as depending on the job '" + requiredJob.name + "'. Job '" + dependingJob.name + "' is already finished (or ended in the error).");
    }
    return false;
  }

  // if required job is fully finished or error, it makes no sense to add it
  if ((requiredJob.jobState === AssetJob.JobState.FINISHED) ||
      (requiredJob.jobState === AssetJob.JobState.ERROR)
      )
  {
    if (Preferences.Application.isDevMode())
    {
      console.error("No sense in adding job '" + requiredJob.name + "' as required for the job '" + dependingJob.name + "'. Job '" + requiredJob.name + "' is already finished (or ended in the error).");
    }
    return false;
  }

  return true;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CHECKING OF THE REQUIRED JOBS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Called when required job (the job for which this job is waiting) has finished.
 *
 * @param {AssetJob} finishedRequired   which required job has finished
 */
AssetJob.prototype.onRequiredFinished = function (finishedRequired)
{
  //////////////////////////////////////////////////////////////////////
  // DETECTING INTO WHICH CATEGORY FALLS THE REQUIRED JOB IN THIS JOB
  //////////////////////////////////////////////////////////////////////

  // if the job needed required for finishing
  var waitForFinish = null;

  // not the most effective solution, but is is sufficient for now
  for (var i = 0; i < this.requiredJobs.length; i++)
  {
    if (this.requiredJobs[i] === finishedRequired)
    {
      this.requiredJobs.splice (i, 1);

      // the required job which has finished is not required for the job to finish
      waitForFinish = false;

      break;
    }
  }

  if (waitForFinish === null)
  {
    for (var i = 0; i < this.requiredJobsToFinish.length; i++)
    {
      if (this.requiredJobsToFinish[i] === finishedRequired)
      {
        this.requiredJobsToFinish.splice (i, 1);

        // the required job which has finished IS required for the job to finish
        waitForFinish = true;

        break;
      }
    }
  }

  //////////////////////////////////////////////////////////////////////

  this.update();
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CHECKING COUNTERS OF THE REQUIRED JOBS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Checks if the job is still waiting for some other jobs to finish.
 *
 * @returns {boolean}   TRUE if job is waiting for jobs to finish, FALSE if the job doesn't wait anymore
 */
AssetJob.prototype.areRequiredJobs = function()
{
  return (this.requiredJobs.length !== 0);
};

/**
 * @returns {boolean}
 */
AssetJob.prototype.areRequiredToFinishJobs = function()
{
  return (this.requiredJobsToFinish.length !== 0);
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// STARTING DOWNLOADING AND RUNNING THE JOB
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Starts the job directly.
 */
AssetJob.prototype.start = function()
{
  this.update();
};


/**
 * Starts the job, i.e. starts its DOWNLOADING phase.
 */
AssetJob.prototype.update = function ()
{
  this.downloadAndRunOrderIssued = true;

  switch (this.jobState)
  {
    case AssetJob.JobState.NEW:
      // still some required jobs
      if (this.areRequiredJobs())
      {
        for (var i = 0; i < this.requiredJobs.length; i++)
        {
          //console.log(this.name + " req job: " + this.requiredJobs[i].name);
          this.requiredJobs[i].update();
        }
      }
      // no jobs required
      else
      {
        this._startDownload();
      }
      break;

    case AssetJob.JobState.DOWNLOAD_JUST_FINISHED:
      this._startRunning();
      break;

    case AssetJob.JobState.FINISHED_BUT_WAITING:
      if (!this.areRequiredToFinishJobs())
      {
        this.onFinished();
      }
      // else: cannot do anything, we cannot force these jobs to start, must wait for them
      break;

    case AssetJob.JobState.JUST_FINISHED:
      this.jobState = AssetJob.JobState.FINISHED;

      // send message to all jobs which are waiting for this job that it has finished
      // (note: waiting jobs do not care which job has finished, just that some job has finished)
      for (var i = 0; i < this.dependingJobs.length; i++)
      {
        //this.dependingJobs[i].onRequiredFinished();
        this.dependingJobs[i].onRequiredFinished (this);
      }

      // fully completed now!

      // mark underlying asset as finished
      if (Util.isInitialized (this.asset))
      {
        this.asset.markFinished();
      }

      break;
  }

};

/**
 * Actually starts the job. Should not ever be called publicly!
 *
 * @private
 */
AssetJob.prototype._startDownload = function()
{
  this.jobState = AssetJob.JobState.DOWNLOADING;

  //console.log("Job " + this.name + " started download, is dl job: " + this.isDownloadJob);

  // download only assets which needs to be downloaded, otherwise skip downloading phase
  if (this.isDownloadJob)
  {
    this.downloadFile();
  }
  else
  {
    //console.log("Job " + this.name + " skipped download (not downloading type of job).");
    this.onDownloadFinished();
  }
};

/**
 * Execute the job, i.e. starts its running phase and calls its run callback method.
 *
 * @private
 */
AssetJob.prototype._startRunning = function()
{
  this.jobState = AssetJob.JobState.RUNNING;

  //console.log("Job " + this.name + " started running.");

  if (Util.isInitialized (this.runCallback))
  {
    this.runCallback(); // must be synchronous!
  }

  this.onRunningFinished();
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CALLED WHEN DOWNLOADING, RUNNING, ENTIRE JOB IS COMPLETED
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Called when downloading of the asset file is finished.
 */
AssetJob.prototype.onDownloadFinished = function()
{
  this.jobState = AssetJob.JobState.DOWNLOAD_JUST_FINISHED;
  this.update();
};

/**
 * Called when running phase of the job is finished, i.e. the run callback has ended.
 */
AssetJob.prototype.onRunningFinished = function()
{
  this.jobState = AssetJob.JobState.FINISHED_BUT_WAITING;

  ////////////////////////////////////////////////

  // we may have used "thisJob.waitFor (someRequiredJob)" during run callback
   // what we meant by that - it is a shortcut for following two lines:
   //   thisJob.waitForFinish (someRequiredJob)
   //   someRequiredJob.startJob()

  // (convert all freshly added required jobs to requiredToFinish jobs and execute them)
  for (var i = 0; i < this.requiredJobs.length; i++)
  {
    this.addRequiredJob (this.requiredJobs[i], true);
    this.requiredJobs[i].update();
  }

  this.requiredJobs = [];

  ////////////////////////////////////////////////

  this.update();
};

/**
 * Called when the job is fully finished.
 */
AssetJob.prototype.onFinished = function()
{
  this.jobState = AssetJob.JobState.JUST_FINISHED;
  this.update();
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// COMMENTS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// job.waitWithRunningForFinish (otherJob);
// - job cannot execute callback() and finish until otherJob is finished, it must wait and not do anything
// - that means otherJob must be started from elsewhere MANUALLY

// job.waitFor (otherJob);
// - job waits for otherJob to finish and then it can execute callback(), it will try to start otherJob
// - if the jobs are properly connected, everything will start themselves

// job.waitForFinish (otherJob);
// - callback() may have been already executed, job waits for otherJob to finish to mark itself as finished, it must wait and not do anything
// - usually used in run callback of job (the usual .waitFor() would have no effect when we are already in run callback) to additionally add otherJob as required for job
