/**
 * Copyright © Mnemosyne LLC
 *
 * This file is licensed under the GPLv2.
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 */

function Torrent(data) {
    this.initialize(data);
};

/***
 ****
 ****  Constants
 ****
 ***/

// Torrent.fields.status
Torrent._StatusStopped = 0;
Torrent._StatusCheckWait = 1;
Torrent._StatusCheck = 2;
Torrent._StatusDownloadWait = 3;
Torrent._StatusDownload = 4;
Torrent._StatusSeedWait = 5;
Torrent._StatusSeed = 6;

// Torrent.fields.seedRatioMode
Torrent._RatioUseGlobal = 0;
Torrent._RatioUseLocal = 1;
Torrent._RatioUnlimited = 2;

// Torrent.fields.error
Torrent._ErrNone = 0;
Torrent._ErrTrackerWarning = 1;
Torrent._ErrTrackerError = 2;
Torrent._ErrLocalError = 3;

// TrackerStats' announceState
Torrent._TrackerInactive = 0;
Torrent._TrackerWaiting = 1;
Torrent._TrackerQueued = 2;
Torrent._TrackerActive = 3;

Torrent.Fields = {};

// commonly used fields which only need to be loaded once,
// either on startup or when a magnet finishes downloading its metadata
// finishes downloading its metadata
Torrent.Fields.Metadata = [
    'addedDate',
    'name',
    'totalSize'
];

// commonly used fields which need to be periodically refreshed
Torrent.Fields.Stats = [
    'error',
    'errorString',
    'eta',
    'isFinished',
    'isStalled',
    'leftUntilDone',
    'metadataPercentComplete',
    'peersConnected',
    'peersGettingFromUs',
    'peersSendingToUs',
    'percentDone',
    'queuePosition',
    'rateDownload',
    'rateUpload',
    'recheckProgress',
    'seedRatioMode',
    'seedRatioLimit',
    'sizeWhenDone',
    'status',
    'trackers',
    'downloadDir',
    'uploadedEver',
    'uploadRatio',
    'webseedsSendingToUs'
];

// fields used by the inspector which only need to be loaded once
Torrent.Fields.InfoExtra = [
    'comment',
    'creator',
    'dateCreated',
    'files',
    'hashString',
    'isPrivate',
    'pieceCount',
    'pieceSize'
];

// fields used in the inspector which need to be periodically refreshed
Torrent.Fields.StatsExtra = [
    'activityDate',
    'corruptEver',
    'desiredAvailable',
    'downloadedEver',
    'fileStats',
    'haveUnchecked',
    'haveValid',
    'peers',
    'startDate',
    'trackerStats'
];

/***
 ****
 ****  Methods
 ****
 ***/

Torrent.prototype = {
    initialize: function (data) {
        this.fields = {};
        this.fieldObservers = {};
        this.refresh(data);
    },

    notifyOnFieldChange: function (field, callback) {
        this.fieldObservers[field] = this.fieldObservers[field] || [];
        this.fieldObservers[field].push(callback);
    },

    setField: function (o, name, value) {
        var i, observer;

        if (o[name] === value) {
            return false;
        };
        if (o == this.fields && this.fieldObservers[name] && this.fieldObservers[name].length) {
            for (i = 0; observer = this.fieldObservers[name][i]; ++i) {
                observer.call(this, value, o[name], name);
            };
        };
        o[name] = value;
        return true;
    },

    // fields.files is an array of unions of RPC's "files" and "fileStats" objects.
    updateFiles: function (files) {
        var changed = false;
        var myfiles = this.fields.files || [];
        var keys = ['length', 'name', 'bytesCompleted', 'wanted', 'priority'];
        var i, f, j, key, myfile;

        for (i = 0; f = files[i]; ++i) {
            myfile = myfiles[i] || {};
            for (j = 0; key = keys[j]; ++j) {
                if (key in f) {
                    changed |= this.setField(myfile, key, f[key]);
                };
            };
            myfiles[i] = myfile;
        }
        this.fields.files = myfiles;
        return changed;
    },

    collateTrackers: function (trackers) {
        var i, t, announces = [];

        for (i = 0; t = trackers[i]; ++i) {
            announces.push(t.announce.toLowerCase());
        };
        return announces.join('\t');
    },

    refreshFields: function (data) {
        var key;
        var changed = false;

        for (key in data) {
            switch (key) {
            case 'files':
            case 'fileStats': // merge files and fileStats together
                changed |= this.updateFiles(data[key]);
                break;
            case 'trackerStats': // 'trackerStats' is a superset of 'trackers'...
                changed |= this.setField(this.fields, 'trackers', data[key]);
                break;
            case 'trackers': // ...so only save 'trackers' if we don't have it already
                if (!(key in this.fields)) {
                    changed |= this.setField(this.fields, key, data[key]);
                };
                break;
            default:
                changed |= this.setField(this.fields, key, data[key]);
            };
        };

        return changed;
    },

    refresh: function (data) {
        if (this.refreshFields(data)) {
            $(this).trigger('dataChanged', this);
        };
    },

    /****
     *****
     ****/

    // simple accessors
    getComment: function () {
        return this.fields.comment;
    },
    getCreator: function () {
        return this.fields.creator;
    },
    getDateAdded: function () {
        return this.fields.addedDate;
    },
    getDateCreated: function () {
        return this.fields.dateCreated;
    },
    getDesiredAvailable: function () {
        return this.fields.desiredAvailable;
    },
    getDownloadDir: function () {
        return this.fields.downloadDir;
    },
    getDownloadSpeed: function () {
        return this.fields.rateDownload;
    },
    getDownloadedEver: function () {
        return this.fields.downloadedEver;
    },
    getError: function () {
        return this.fields.error;
    },
    getErrorString: function () {
        return this.fields.errorString;
    },
    getETA: function () {
        return this.fields.eta;
    },
    getFailedEver: function (i) {
        return this.fields.corruptEver;
    },
    getFile: function (i) {
        return this.fields.files[i];
    },
    getFileCount: function () {
        return this.fields.files ? this.fields.files.length : 0;
    },
    getHashString: function () {
        return this.fields.hashString;
    },
    getHave: function () {
        return this.getHaveValid() + this.getHaveUnchecked()
    },
    getHaveUnchecked: function () {
        return this.fields.haveUnchecked;
    },
    getHaveValid: function () {
        return this.fields.haveValid;
    },
    getId: function () {
        return this.fields.id;
    },
    getLastActivity: function () {
        return this.fields.activityDate;
    },
    getLeftUntilDone: function () {
        return this.fields.leftUntilDone;
    },
    getMetadataPercentComplete: function () {
        return this.fields.metadataPercentComplete;
    },
    getName: function () {
        return this.fields.name || 'Unknown';
    },
    getPeers: function () {
        return this.fields.peers;
    },
    getPeersConnected: function () {
        return this.fields.peersConnected;
    },
    getPeersGettingFromUs: function () {
        return this.fields.peersGettingFromUs;
    },
    getPeersSendingToUs: function () {
        return this.fields.peersSendingToUs;
    },
    getPieceCount: function () {
        return this.fields.pieceCount;
    },
    getPieceSize: function () {
        return this.fields.pieceSize;
    },
    getPrivateFlag: function () {
        return this.fields.isPrivate;
    },
    getQueuePosition: function () {
        return this.fields.queuePosition;
    },
    getRecheckProgress: function () {
        return this.fields.recheckProgress;
    },
    getSeedRatioLimit: function () {
        return this.fields.seedRatioLimit;
    },
    getSeedRatioMode: function () {
        return this.fields.seedRatioMode;
    },
    getSizeWhenDone: function () {
        return this.fields.sizeWhenDone;
    },
    getStartDate: function () {
        return this.fields.startDate;
    },
    getStatus: function () {
        return this.fields.status;
    },
    getTotalSize: function () {
        return this.fields.totalSize;
    },
    getTrackers: function () {
        return this.fields.trackers;
    },
    getUploadSpeed: function () {
        return this.fields.rateUpload;
    },
    getUploadRatio: function () {
        return this.fields.uploadRatio;
    },
    getUploadedEver: function () {
        return this.fields.uploadedEver;
    },
    getWebseedsSendingToUs: function () {
        return this.fields.webseedsSendingToUs;
    },
    isFinished: function () {
        return this.fields.isFinished;
    },

    // derived accessors
    hasExtraInfo: function () {
        return 'hashString' in this.fields;
    },
    isSeeding: function () {
        return this.getStatus() === Torrent._StatusSeed;
    },
    isStopped: function () {
        return this.getStatus() === Torrent._StatusStopped;
    },
    isChecking: function () {
        return this.getStatus() === Torrent._StatusCheck;
    },
    isDownloading: function () {
        return this.getStatus() === Torrent._StatusDownload;
    },
    isQueued: function () {
        return this.getStatus() === Torrent._StatusDownloadWait || this.getStatus() === Torrent._StatusSeedWait;
    },
    isDone: function () {
        return this.getLeftUntilDone() < 1;
    },
    needsMetaData: function () {
        return this.getMetadataPercentComplete() < 1;
    },
    getActivity: function () {
        return this.getDownloadSpeed() + this.getUploadSpeed();
    },
    getPercentDoneStr: function () {
        return Transmission.fmt.percentString(100 * this.getPercentDone());
    },
    getPercentDone: function () {
        return this.fields.percentDone;
    },
    getStateString: function () {
        switch (this.getStatus()) {
        case Torrent._StatusStopped:
            return this.isFinished() ? 'Seeding complete' : 'Paused';
        case Torrent._StatusCheckWait:
            return 'Queued for verification';
        case Torrent._StatusCheck:
            return 'Verifying local data';
        case Torrent._StatusDownloadWait:
            return 'Queued for download';
        case Torrent._StatusDownload:
            return 'Downloading';
        case Torrent._StatusSeedWait:
            return 'Queued for seeding';
        case Torrent._StatusSeed:
            return 'Seeding';
        case null:
        case undefined:
            return 'Unknown';
        default:
            return 'Error';
        }
    },
    seedRatioLimit: function (controller) {
        switch (this.getSeedRatioMode()) {
        case Torrent._RatioUseGlobal:
            return controller.seedRatioLimit();
        case Torrent._RatioUseLocal:
            return this.getSeedRatioLimit();
        default:
            return -1;
        }
    },
    getErrorMessage: function () {
        var str = this.getErrorString();
        switch (this.getError()) {
        case Torrent._ErrTrackerWarning:
            return 'Tracker returned a warning: ' + str;
        case Torrent._ErrTrackerError:
            return 'Tracker returned an error: ' + str;
        case Torrent._ErrLocalError:
            return 'Error: ' + str;
        default:
            return null;
        }
    },
    getCollatedName: function () {
        var f = this.fields;
        if (!f.collatedName && f.name) {
            f.collatedName = f.name.toLowerCase();
        };
        return f.collatedName || '';
    },
    getCollatedTrackers: function () {
        var f = this.fields;
        if (!f.collatedTrackers && f.trackers) {
            f.collatedTrackers = this.collateTrackers(f.trackers);
        };
        return f.collatedTrackers || '';
    },

    /****
     *****
     ****/

    testState: function (state) {
        var s = this.getStatus();

        switch (state) {
        case Prefs._FilterActive:
            return this.getPeersGettingFromUs() > 0 || this.getPeersSendingToUs() > 0 || this.getWebseedsSendingToUs() > 0 || this.isChecking();
        case Prefs._FilterSeeding:
            return (s === Torrent._StatusSeed) || (s === Torrent._StatusSeedWait);
        case Prefs._FilterDownloading:
            return (s === Torrent._StatusDownload) || (s === Torrent._StatusDownloadWait);
        case Prefs._FilterPaused:
            return this.isStopped();
        case Prefs._FilterFinished:
            return this.isFinished();
        default:
            return true;
        }
    },

    /**
     * @param filter one of Prefs._Filter*
     * @param search substring to look for, or null
     * @return true if it passes the test, false if it fails
     */
    test: function (state, search, tracker) {
        // flter by state...
        var pass = this.testState(state);

        // maybe filter by text...
        if (pass && search && search.length) {
            pass = this.getCollatedName().indexOf(search.toLowerCase()) !== -1;
        };

        // maybe filter by tracker...
        if (pass && tracker && tracker.length) {
            pass = this.getCollatedTrackers().indexOf(tracker) !== -1;
        };

        return pass;
    }
};

/***
 ****
 ****  SORTING
 ****
 ***/

Torrent.compareById = function (ta, tb) {
    return ta.getId() - tb.getId();
};
Torrent.compareByName = function (ta, tb) {
    return ta.getCollatedName().localeCompare(tb.getCollatedName()) || Torrent.compareById(ta, tb);
};
Torrent.compareByQueue = function (ta, tb) {
    return ta.getQueuePosition() - tb.getQueuePosition();
};
Torrent.compareByAge = function (ta, tb) {
    var a = ta.getDateAdded();
    var b = tb.getDateAdded();

    return (b - a) || Torrent.compareByQueue(ta, tb);
};
Torrent.compareByState = function (ta, tb) {
    var a = ta.getStatus();
    var b = tb.getStatus();

    return (b - a) || Torrent.compareByQueue(ta, tb);
};
Torrent.compareByActivity = function (ta, tb) {
    var a = ta.getActivity();
    var b = tb.getActivity();

    return (b - a) || Torrent.compareByState(ta, tb);
};
Torrent.compareByRatio = function (ta, tb) {
    var a = ta.getUploadRatio();
    var b = tb.getUploadRatio();

    if (a < b) {
        return 1;
    };
    if (a > b) {
        return -1;
    };
    return Torrent.compareByState(ta, tb);
};
Torrent.compareByProgress = function (ta, tb) {
    var a = ta.getPercentDone();
    var b = tb.getPercentDone();

    return (a - b) || Torrent.compareByRatio(ta, tb);
};
Torrent.compareBySize = function (ta, tb) {
    var a = ta.getTotalSize();
    var b = tb.getTotalSize();

    return (a - b) || Torrent.compareByName(ta, tb);
};

Torrent.compareTorrents = function (a, b, sortMethod, sortDirection) {
    var i;

    switch (sortMethod) {
    case Prefs._SortByActivity:
        i = Torrent.compareByActivity(a, b);
        break;
    case Prefs._SortByAge:
        i = Torrent.compareByAge(a, b);
        break;
    case Prefs._SortByQueue:
        i = Torrent.compareByQueue(a, b);
        break;
    case Prefs._SortByProgress:
        i = Torrent.compareByProgress(a, b);
        break;
    case Prefs._SortBySize:
        i = Torrent.compareBySize(a, b);
        break;
    case Prefs._SortByState:
        i = Torrent.compareByState(a, b);
        break;
    case Prefs._SortByRatio:
        i = Torrent.compareByRatio(a, b);
        break;
    default:
        i = Torrent.compareByName(a, b);
        break;
    };

    if (sortDirection === Prefs._SortDescending) {
        i = -i;
    };

    return i;
};

/**
 * @param torrents an array of Torrent objects
 * @param sortMethod one of Prefs._SortBy*
 * @param sortDirection Prefs._SortAscending or Prefs._SortDescending
 */
Torrent.sortTorrents = function (torrents, sortMethod, sortDirection) {
    switch (sortMethod) {
    case Prefs._SortByActivity:
        torrents.sort(this.compareByActivity);
        break;
    case Prefs._SortByAge:
        torrents.sort(this.compareByAge);
        break;
    case Prefs._SortByQueue:
        torrents.sort(this.compareByQueue);
        break;
    case Prefs._SortByProgress:
        torrents.sort(this.compareByProgress);
        break;
    case Prefs._SortBySize:
        torrents.sort(this.compareBySize);
        break;
    case Prefs._SortByState:
        torrents.sort(this.compareByState);
        break;
    case Prefs._SortByRatio:
        torrents.sort(this.compareByRatio);
        break;
    default:
        torrents.sort(this.compareByName);
        break;
    };

    if (sortDirection === Prefs._SortDescending) {
        torrents.reverse();
    };

    return torrents;
};