var groove = require('groove');
var semver = require('semver');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var mkdirp = require('mkdirp');
var fs = require('fs');
var uuid = require('./uuid');
var path = require('path');
var Pend = require('pend');
var DedupedQueue = require('./deduped_queue');
var findit = require('findit2');
var shuffle = require('mess');
var mv = require('mv');
var MusicLibraryIndex = require('music-library-index');
var keese = require('keese');
var safePath = require('./safe_path');
var PassThrough = require('stream').PassThrough;
var url = require('url');
var dbIterate = require('./db_iterate');
var log = require('./log');
var importUrlFilters = require('./import_url_filters');
var youtubeSearch = require('./youtube_search');
var yauzl = require('yauzl');

var importFileFilters = [
  {
    name: 'zip',
    fn: importFileAsZip,
  },
  {
    name: 'song',
    fn: importFileAsSong,
  },
];

module.exports = Player;

ensureGrooveVersionIsOk();

var cpuCount = 1;

var PLAYER_KEY_PREFIX = "Player.";
var LIBRARY_KEY_PREFIX = "Library.";
var LIBRARY_DIR_PREFIX = "LibraryDir.";
var QUEUE_KEY_PREFIX = "Playlist.";
var PLAYLIST_KEY_PREFIX = "StoredPlaylist.";
var LABEL_KEY_PREFIX = "Label.";
var PLAYLIST_META_KEY_PREFIX = "StoredPlaylistMeta.";

// db: store in the DB
var DB_PROPS = {
  key: {
    db: true,
    clientVisible: true,
    clientCanModify: false,
    type: 'string',
  },
  name: {
    db: true,
    clientVisible: true,
    clientCanModify: true,
    type: 'string',
  },
  artistName: {
    db: true,
    clientVisible: true,
    clientCanModify: true,
    type: 'string',
  },
  albumArtistName: {
    db: true,
    clientVisible: true,
    clientCanModify: true,
    type: 'string',
  },
  albumName: {
    db: true,
    clientVisible: true,
    clientCanModify: true,
    type: 'string',
  },
  compilation: {
    db: true,
    clientVisible: true,
    clientCanModify: true,
    type: 'boolean',
  },
  track: {
    db: true,
    clientVisible: true,
    clientCanModify: true,
    type: 'integer',
  },
  trackCount: {
    db: true,
    clientVisible: true,
    clientCanModify: true,
    type: 'integer',
  },
  disc: {
    db: true,
    clientVisible: true,
    clientCanModify: true,
    type: 'integer',
  },
  discCount: {
    db: true,
    clientVisible: true,
    clientCanModify: true,
    type: 'integer',
  },
  duration: {
    db: true,
    clientVisible: true,
    clientCanModify: false,
    type: 'float',
  },
  year: {
    db: true,
    clientVisible: true,
    clientCanModify: true,
    type: 'integer',
  },
  genre: {
    db: true,
    clientVisible: true,
    clientCanModify: true,
    type: 'string',
  },
  file: {
    db: true,
    clientVisible: true,
    clientCanModify: false,
    type: 'string',
  },
  mtime: {
    db: true,
    clientVisible: false,
    clientCanModify: false,
    type: 'integer',
  },
  replayGainAlbumGain: {
    db: true,
    clientVisible: false,
    clientCanModify: false,
    type: 'float',
  },
  replayGainAlbumPeak: {
    db: true,
    clientVisible: false,
    clientCanModify: false,
    type: 'float',
  },
  replayGainTrackGain: {
    db: true,
    clientVisible: false,
    clientCanModify: false,
    type: 'float',
  },
  replayGainTrackPeak: {
    db: true,
    clientVisible: false,
    clientCanModify: false,
    type: 'float',
  },
  composerName: {
    db: true,
    clientVisible: true,
    clientCanModify: true,
    type: 'string',
  },
  performerName: {
    db: true,
    clientVisible: true,
    clientCanModify: true,
    type: 'string',
  },
  lastQueueDate: {
    db: true,
    clientVisible: false,
    clientCanModify: false,
    type: 'date',
  },
  fingerprint: {
    db: true,
    clientVisible: false,
    clientCanModify: false,
    type: 'array_of_integer',
  },
  playCount: {
    db: true,
    clientVisible: false,
    clientCanModify: false,
    type: 'integer',
  },
  labels: {
    db: true,
    clientVisible: true,
    clientCanModify: false,
    type: 'set',
  },
};

var PROP_TYPE_PARSERS = {
  'string': function(value) {
    return value ? String(value) : "";
  },
  'date': function(value) {
    if (!value) return null;
    var date = new Date(value);
    if (isNaN(date.getTime())) return null;
    return date;
  },
  'integer': parseIntOrNull,
  'float': parseFloatOrNull,
  'boolean': function(value) {
    return value == null ? null : !!value;
  },
  'array_of_integer': function(value) {
    if (!Array.isArray(value)) return null;
    value = value.map(parseIntOrNull);
    for (var i = 0; i < value.length; i++) {
      if (value[i] == null) return null;
    }
    return value;
  },
  'set': function(value) {
    var result = {};
    for (var key in value) {
      result[key] = 1;
    }
    return result;
  },
};

var labelColors = [
  "#e11d21", "#eb6420", "#fbca04", "#009800",
  "#006b75", "#207de5", "#0052cc", "#5319e7",
  "#f7c6c7", "#fad8c7", "#fef2c0", "#bfe5bf",
  "#bfdadc", "#bfd4f2", "#c7def8", "#d4c5f9",
];

// how many GrooveFiles to keep open, ready to be decoded
var OPEN_FILE_COUNT = 8;
var PREV_FILE_COUNT = Math.floor(OPEN_FILE_COUNT / 2);
var NEXT_FILE_COUNT = OPEN_FILE_COUNT - PREV_FILE_COUNT;

var DB_SCALE = Math.log(10.0) * 0.05;
var REPLAYGAIN_PREAMP = 0.75;
var REPLAYGAIN_DEFAULT = 0.25;

Player.REPEAT_OFF = 0;
Player.REPEAT_ALL = 1;
Player.REPEAT_ONE = 2;

Player.trackWithoutIndex = trackWithoutIndex;
Player.setGrooveLoggingLevel = setGrooveLoggingLevel;

util.inherits(Player, EventEmitter);
function Player(db, config) {
  EventEmitter.call(this);
  this.setMaxListeners(0);

  this.db = db;
  this.musicDirectory = config.musicDirectory;
  this.dbFilesByPath = {};
  this.dbFilesByLabel = {};
  this.libraryIndex = new MusicLibraryIndex({
    searchFields: MusicLibraryIndex.defaultSearchFields.concat('file'),
  });
  this.addQueue = new DedupedQueue({
    processOne: this.addToLibrary.bind(this),
    // limit to 1 async operation because we're blocking on the hard drive,
    // it's faster to read one file at a time.
    maxAsync: 1,
  });

  this.dirs = {};
  this.dirScanQueue = new DedupedQueue({
    processOne: this.refreshFilesIndex.bind(this),
    // only 1 dir scanning can happen at a time
    // we'll pass the dir to scan as the ID so that not more than 1 of the
    // same dir can queue up
    maxAsync: 1,
  });
  this.dirScanQueue.on('error', function(err) {
    log.error("library scanning error:", err.stack);
  });
  this.disableFsRefCount = 0;

  this.playlist = {};
  this.playlists = {};
  this.currentTrack = null;
  this.tracksInOrder = []; // another way to look at playlist
  this.grooveItems = {}; // maps groove item id to track
  this.seekRequestPos = -1; // set to >= 0 when we want to seek
  this.invalidPaths = {}; // files that could not be opened
  this.playlistItemDeleteQueue = [];
  this.dontBelieveTheEndOfPlaylistSentinelItsATrap = false;
  this.queueClearEncodedBuffers = false;

  this.repeat = Player.REPEAT_OFF;
  this.desiredPlayerHardwareState = null; // true: normal hardware playback. false: dummy
  this.pendingPlayerAttachDetach = null;
  this.isPlaying = false;
  this.trackStartDate = null;
  this.pausedTime = 0;
  this.autoDjOn = false;
  this.autoDjHistorySize = 10;
  this.autoDjFutureSize = 10;

  this.ongoingScans =