/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

function debug(aMsg) {
  //dump("[TVSimulatorService] " + aMsg + "\n");
}

const Cc = Components.classes;
const Cu = Components.utils;
const Ci = Components.interfaces;
const Cr = Components.returnCode;

Cu.importGlobalProperties(["File"]);

const TV_SIMULATOR_DUMMY_DIRECTORY = "dummy";
const TV_SIMULATOR_DUMMY_FILE      = "settings.json";

// See http://seanyhlin.github.io/TV-Manager-API/#idl-def-TVSourceType
const TV_SOURCE_TYPES = ["dvb-t","dvb-t2","dvb-c","dvb-c2","dvb-s",
                         "dvb-s2","dvb-h","dvb-sh","atsc","atsc-m/h",
                         "isdb-t","isdb-tb","isdb-s","isdb-c","1seg",
                         "dtmb","cmmb","t-dmb","s-dmb"];
function containInvalidSourceType(aElement, aIndex, aArray) {
  return !TV_SOURCE_TYPES.includes(aElement);
}

// See http://seanyhlin.github.io/TV-Manager-API/#idl-def-TVChannelType
const TV_CHANNEL_TYPES = ["tv","radio","data"];

Cu.import("resource://gre/modules/XPCOMUtils.jsm");

function TVSimulatorService() {
  this._internalTuners = null;
  this._scanCompleteTimer = null;
  this._scanningWrapTunerData = null;
  this._init();
}

TVSimulatorService.prototype = {
  classID: Components.ID("{94b065ad-d45a-436a-b394-6dabc3cf110f}"),
  QueryInterface: XPCOMUtils.generateQI([Ci.nsITVSimulatorService,
                                         Ci.nsITVService,
                                         Ci.nsITimerCallback]),

  _init: function TVSimInit() {
    if (this._internalTuners) {
      return;
    }

    // Load the setting file from local JSON file.
    // Synchrhronous File Reading.
    let file = Cc["@mozilla.org/file/local;1"]
                 .createInstance(Ci.nsILocalFile);

    file.initWithPath(this._getFilePath(TV_SIMULATOR_DUMMY_FILE));

    let fstream = Cc["@mozilla.org/network/file-input-stream;1"]
                    .createInstance(Ci.nsIFileInputStream);
    let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"]
                    .createInstance(Ci.nsIConverterInputStream);

    let settingStr = "";

    try {
      fstream.init(file, -1, 0, 0);
      cstream.init(fstream,
                   "UTF-8",
                   1024,
                   Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);

      let str = {};
      while (cstream.readString(0xffffffff, str) != 0) {
        settingStr += str.value;
      }
    } catch(e) {
      debug("Error occurred : " + e );
      return;
    } finally {
      cstream.close();
    }

    let settingsObj;
    try {
      /*
       *
       * Setting JSON file format:
       *
       * Note: This setting JSON is not allow empty array.
       *       If set the empty array, _init() will fail.
       *       e.g.
       *        - "tuners": []
       *        - "channels":[]
       * Format:
       *   {
       *    "tuners": [{
       *      "id":                     "The ID of the tuner",
       *      "supportedType":          ["The array of source type to be used."],
       *      "sources": [{
       *        "type":                 "The source type to be used",
       *        "channels" : [{
       *          "networkId":          "The ID of the channel network",
       *          "transportStreamId":  "The ID of channel transport stream",
       *          "serviceId":          "The ID of channel service",
       *          "type":               "The type of channel",
       *          "name":               "The channel name",
       *          "number" :            The LCN (Logical Channel Number) of the channel,
       *          "isEmergency" :       Whether this channel is emergency status,
       *          "isFree":             Whether this channel is free or not,
       *          "videoFilePath":      "The path of the fake video file",
       *          "programs":[{
       *            "eventId":          "The ID of this program event",
       *            "title" :           "This program's title",
       *            "startTime":        "The start time of this program",
       *            "duration":         "The duration of this program",
       *            "description":      "The description of this program",
       *            "rating":           "The rating of this program",
       *            "audioLanugages":   ["The array of audio language"],
       *            "subtitleLanguages":["The array of subtitle language"],
       *           },]
       *         },]
       *       },]
       *     },]
       *   }
       */
      settingsObj = JSON.parse(settingStr);
    } catch(e) {
      debug("File load error: " + e);
      return;
    }

    // validation
    if (!this._validateSettings(settingsObj)) {
      debug("Failed to validate settings.");
      return;
    }

    // Key is as follow
    // {'tunerId':tunerId, 'sourceType':sourceType}
    this._internalTuners = new Map();

    // TVTunerData
    for (let tunerData of settingsObj.tuners) {
      let tuner = Cc["@mozilla.org/tv/tvtunerdata;1"]
                    .createInstance(Ci.nsITVTunerData);
      tuner.id = tunerData.id;
      tuner.streamType = tuner.TV_STREAM_TYPE_SIMULATOR;
      tuner.setSupportedSourceTypes(tunerData.supportedType.length,
                                    tunerData.supportedType);

      let wrapTunerData = {
        'tuner': tuner,
        'channels': new Map(),
        'sourceType': undefined,
      };

      // TVSource
      for (let sourceData of tunerData.sources) {
        wrapTunerData.sourceType = sourceData.type;

        // TVChannel
        for (let channelData of sourceData.channels) {
          let channel = Cc["@mozilla.org/tv/tvchanneldata;1"]
                          .createInstance(Ci.nsITVChannelData);
          channel.networkId         = channelData.networkId;
          channel.transportStreamId = channelData.transportStreamId;
          channel.serviceId         = channelData.serviceId;
          channel.type              = channelData.type;
          channel.name              = channelData.name;
          channel.number            = channelData.number;
          channel.isEmergency       = channelData.isEmergency;
          channel.isFree            = channelData.isFree;

          let wrapChannelData = {
            'channel': channel,
            'programs': new Array(),
            'videoFilePath': channelData.videoFilePath,
          };

          // TVProgram
          for (let programData of channelData.programs) {
            let program = Cc["@mozilla.org/tv/tvprogramdata;1"]
                            .createInstance(Ci.nsITVProgramData);
            program.eventId     = programData.eventId;
            program.title       = programData.title;
            program.startTime   = programData.startTime;
            program.duration    = programData.duration;
            program.description = programData.description;
            program.rating      = programData.rating;
            program.setAudioLanguages(programData.audioLanguages.length,
                                      programData.audioLanguages);
            program.setSubtitleLanguages(programData.subtitleLanguages.length,
                                         programData.subtitleLanguages);
            wrapChannelData.programs.push(program);
          }

          // Sort the program according to the startTime
	  wrapChannelData.programs.sort(function(a, b) {
            return a.startTime - b.startTime;
          });
          wrapTunerData.channels.set(channel.number, wrapChannelData);
        }

        // Sort the channel according to the channel number
        wrapTunerData.channels = new Map([...wrapTunerData.channels.entries()].sort(function(a, b) {
          return a[0] - b[0];
        }));
        this._internalTuners.set(
                this._getTunerMapKey(tuner.id, sourceData.type),
                wrapTunerData);
      }
    }
  },

  getTuners: function TVSimGetTuners(aCallback) {
    if (!aCallback) {
      debug("aCallback is null\n");
      return Cr.NS_ERROR_INVALID_ARG;
    }

    let tuners = Cc["@mozilla.org/array;1"]
                   .createInstance(Ci.nsIMutableArray);

    for (let [k,wrapTunerData] of this._internalTuners) {
      tuners.appendElement(wrapTunerData.tuner, false);
    }

    return aCallback.notifySuccess(tuners);
  },

  setSource: function TVSimSetSource(aTunerId, aSourceType, aCallback) {
    if (!aCallback) {
      debug("aCallback is null\n");
      return NS_ERROR_INVALID_ARG;
    }

    let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
    if (!wrapTunerData) {
      return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
    }
    return aCallback.notifySuccess(null);
  },

  startScanningChannels: function TVSimStartScanningChannels(aTunerId, aSourceType, aCallback) {
    if (!aCallback) {
      debug("aCallback is null\n");
      return Cr.NS_ERROR_INVALID_ARG;
    }

    let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
    if (!wrapTunerData || !wrapTunerData.channels) {
      return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
    }

    if (this._scanningWrapTunerData) {
      return aCallback.notifyError(Cr.NS_ERROR_DOM_INVALID_STATE_ERR);
    }

    this._scanningWrapTunerData = wrapTunerData;

    aCallback.notifySuccess(null);

    for (let [key, wrapChannelData] of wrapTunerData.channels) {
      this._sourceListener.notifyChannelScanned(
                                        wrapTunerData.tuner.id,
                                        wrapTunerData.sourceType,
                                        wrapChannelData.channel);
    }

    this._scanCompleteTimer = Cc["@mozilla.org/timer;1"]
                                .createInstance(Ci.nsITimer);
    rv = this._scanCompleteTimer.initWithCallback(this, 10,
                                                  Ci.nsITimer.TYPE_ONE_SHOT);
    return Cr.NS_OK;
  },

  notify: function TVSimTimerCallback(aTimer) {
    if (!this._scanningWrapTunerData) {
      return;
    }

    this._scanCompleteTimer = null;
    this._scanningWrapTunerData = null;
    return this._sourceListener.notifyChannelScanComplete(
                                       this._scanningWrapTunerData.tuner.id,
                                       this._scanningWrapTunerData.sourceType);
  },

  stopScanningChannels: function TVSimStopScanningChannels(aTunerId, aSourceType, aCallback) {
    if (!aCallback) {
      debug("aCallback is null\n");
      return Cr.NS_ERROR_INVALID_ARG;
    }

    if (!this._scanningWrapTunerData) {
      return aCallback.notifyError(Cr.NS_ERROR_DOM_INVALID_STATE_ERR);
    }

    let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
    if (!wrapTunerData) {
      return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
    }

    if (this._scanCompleteTimer) {
      this._scanCompleteTimer.cancel();
      this._scanCompleteTimer = null;
    }

    if (wrapTunerData.tuner.id === this._scanningWrapTunerData.tuner.id &&
        wrapTunerData.sourceType === this._scanningWrapTunerData.sourceType) {
      this._scanningWrapTunerData = null;
      this._sourceListener.notifyChannelScanStopped(
                                 wrapTunerData.tuner.id,
                                 wrapTunerData.sourceType);
    }

    return aCallback.notifySuccess(null);
  },

  clearScannedChannelsCache: function TVSimClearScannedChannelsCache(aTunerId, aSourceType, aCallback) {
    // Doesn't support for this method.
    return Cr.NS_OK;
  },

  setChannel: function TVSimSetChannel(aTunerId, aSourceType, aChannelNumber, aCallback) {
    if (!aCallback) {
      debug("aCallback is null\n");
      return Cr.NS_ERROR_INVALID_ARG;
    }

    let channel = Cc["@mozilla.org/array;1"]
                    .createInstance(Ci.nsIMutableArray);

    let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
    if (!wrapTunerData || !wrapTunerData.channels) {
      return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
    }

    let wrapChannelData = wrapTunerData.channels.get(aChannelNumber);
    if (!wrapChannelData) {
      return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
    }

    channel.appendElement(wrapChannelData.channel, false);
    return aCallback.notifySuccess(channel);

  },

  getChannels: function TVSimGetChannels(aTunerId, aSourceType, aCallback) {
    if (!aCallback) {
      debug("aCallback is null\n");
      return Cr.NS_ERROR_INVALID_ARG;
    }

    let channelArray = Cc["@mozilla.org/array;1"]
                         .createInstance(Ci.nsIMutableArray);

    let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
    if (!wrapTunerData || !wrapTunerData.channels) {
      return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
    }

    for (let [key, wrapChannelData] of wrapTunerData.channels) {
      channelArray.appendElement(wrapChannelData.channel, false);
    }

    return aCallback.notifySuccess(channelArray);
  },

  getPrograms: function TVSimGetPrograms(aTunerId, aSourceType, aChannelNumber, aStartTime, aEndTime, aCallback) {
    if (!aCallback) {
      debug("aCallback is null\n");
      return Cr.NS_ERROR_INVALID_ARG;
    }
    let programArray = Cc["@mozilla.org/array;1"]
                         .createInstance(Ci.nsIMutableArray);

    let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
    if (!wrapTunerData || !wrapTunerData.channels) {
      return aCallback.notifyError(Ci.nsITVServiceCallback.TV_ERROR_FAILURE);
    }

    let wrapChannelData = wrapTunerData.channels.get(aChannelNumber);
    if (!wrapChannelData || !wrapChannelData.programs) {
      return Cr.NS_ERROR_INVALID_ARG;
    }

    for (let program of wrapChannelData.programs) {
      programArray.appendElement(program, false);
    }

    return aCallback.notifySuccess(programArray);

  },

  getOverlayId: function TVSimGetOverlayId(aTunerId, aCallback) {
    if (!aCallback) {
      debug("aCallback is null\n");
      return Cr.NS_ERROR_INVALID_ARG;
    }

    // TVSimulatorService does not use this parameter.
    overlayIds = Cc["@mozilla.org/array;1"]
                  .createInstance(Ci.nsIMutableArray);
    return aCallback.notifySuccess(overlayIds);
  },

  set sourceListener(aListener) {
    this._sourceListener = aListener;
  },

  get sourceListener() {
      return this._sourceListener;
  },

  getSimulatorVideoBlobURL: function TVSimGetSimulatorVideoBlob(aTunerId,
                                                                aSourceType,
                                                                aChannelNumber,
                                                                aWin) {
    let wrapTunerData = this._getWrapTunerData(aTunerId, aSourceType);
    if (!wrapTunerData || !wrapTunerData.channels) {
      return "";
    }

    let wrapChannelData = wrapTunerData.channels.get(aChannelNumber);
    if (!wrapChannelData || !wrapChannelData.videoFilePath) {
      return "";
    }

    let videoFile = new File(this._getFilePath(wrapChannelData.videoFilePath));
    let videoBlobURL = aWin.URL.createObjectURL(videoFile);

    return videoBlobURL;
  },

  _getTunerMapKey: function TVSimGetTunerMapKey(aTunerId, aSourceType) {
    return JSON.stringify({'tunerId': aTunerId, 'sourceType': aSourceType});
  },

  _getWrapTunerData: function TVSimGetWrapTunerData(aTunerId, aSourceType) {
    if (!this._internalTuners || this._internalTuners.size <= 0) {
      return null;
    }
    return this._internalTuners.get(this._getTunerMapKey(aTunerId, aSourceType));
  },

  _getFilePath: function TVSimGetFilePathFromDummyDirectory(fileName) {
    let dsFile = Cc["@mozilla.org/file/directory_service;1"]
                   .getService(Ci.nsIProperties)
                   .get("ProfD", Ci.nsIFile);
    dsFile.append(TV_SIMULATOR_DUMMY_DIRECTORY);
    dsFile.append(fileName);

    return dsFile.path;
  },

  _validateSettings: function TVSimValidateSettings(aSettingsObject) {
    return this._validateTuners(aSettingsObject.tuners);
  },

  _validateTuners: function TVSimValidateTuners(aTunersObject) {
    let tunerIds = new Array();
    for (let tuner of aTunersObject) {
      if (!tuner.id ||
          !tuner.supportedType ||
          !tuner.supportedType.length ||
          tuner.supportedType.some(containInvalidSourceType) ||
          tunerIds.includes(tuner.id)) {
        debug("invalid tuner data.");
        return false;
      }
      tunerIds.push(tuner.id);

      if (!this._validateSources(tuner.sources)) {
        return false;
      }
    }
    return true;
  },

  _validateSources: function TVSimValidateSources(aSourcesObject) {
    for (let source of aSourcesObject) {
      if (!source.type ||
          !TV_SOURCE_TYPES.includes(source.type)) {
        debug("invalid source data.");
        return false;
      }

      if (!this._validateChannels(source.channels)) {
        return false;
      }
    }
    return true;
  },

  _validateChannels: function TVSimValidateChannels(aChannelsObject) {
    let channelNumbers = new Array();
    for (let channel of aChannelsObject) {
      if (!channel.networkId ||
          !channel.transportStreamId ||
          !channel.serviceId ||
          !channel.type ||
          !TV_CHANNEL_TYPES.includes(channel.type) ||
          !channel.number ||
          channelNumbers.includes(channel.number) ||
          !channel.name) {
        debug("invalid channel data.");
        return false;
      }
      channelNumbers.push(channel.number);

      if (!this._validatePrograms(channel.programs)) {
        return false;
      }
    }
    return true;
  },

  _validatePrograms: function TVSimValidatePrograms(aProgramsObject) {
    let eventIds = new Array();
    for (let program of aProgramsObject) {
      if (!program.eventId ||
          eventIds.includes(program.eventId) ||
          !program.title ||
          !program.startTime ||
          !program.duration) {
        debug("invalid program data.");
        return false;
      }
      eventIds.push(program.eventId);
    }
    return true;
  },
};

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TVSimulatorService]);
