/*
    This file is part of web3.js.
    web3.js is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    web3.js 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 Lesser General Public License for more details.
    You should have received a copy of the GNU Lesser General Public License
    along with web3.js.  If not, see <http://www.gnu.org/licenses/>.
*/
/**
 * @file ENS.js
 *
 * @author Samuel Furter <samuel@ethereum.org>
 * @date 2018
 */

"use strict";

var config = require('./config');
var formatters = require('web3-core-helpers').formatters;
var utils = require('web3-utils');
var Registry = require('./contracts/Registry');
var ResolverMethodHandler = require('./lib/ResolverMethodHandler');
var contenthash = require('./lib/contentHash');

/**
 * Constructs a new instance of ENS
 *
 * @param {Eth} eth
 *
 * @constructor
 */
function ENS(eth) {
    this.eth = eth;
    var registryAddress = null;
    this._detectedAddress = null;
    this._lastSyncCheck = null;

    Object.defineProperty(this, 'registry', {
        get: function () {
            return new Registry(this);
        },
        enumerable: true
    });

    Object.defineProperty(this, 'resolverMethodHandler', {
        get: function () {
            return new ResolverMethodHandler(this.registry);
        },
        enumerable: true
    });

    Object.defineProperty(this, 'registryAddress', {
        get: function () {
            return registryAddress;
        },
        set: function (value) {
            if (value === null) {
                registryAddress = value;

                return;
            }

            registryAddress = formatters.inputAddressFormatter(value);
        },
        enumerable: true
    });
}

/**
 * Returns true if the given interfaceId is supported and otherwise false.
 *
 * @method supportsInterface
 *
 * @param {string} name
 * @param {string} interfaceId
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {Promise<boolean>}
 */
ENS.prototype.supportsInterface = function (name, interfaceId, callback) {
    return this.getResolver(name).then(function (resolver) {
        if (!utils.isHexStrict(interfaceId)) {
            interfaceId = utils.sha3(interfaceId).slice(0,10);
        }

        return resolver.methods.supportsInterface(interfaceId).call(callback);
    }).catch(function(error) {
        if (typeof callback === 'function') {
            callback(error, null);

            return;
        }

        throw error;
    });
};

/**
 * Returns the Resolver by the given address
 *
 * @deprecated Please use the "getResolver" method instead of "resolver"
 *
 * @method resolver
 *
 * @param {string} name
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {Promise<Contract>}
 */
ENS.prototype.resolver = function (name, callback) {
    return this.registry.resolver(name, callback);
};

/**
 * Returns the Resolver by the given address
 *
 * @method getResolver
 *
 * @param {string} name
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {Promise<Contract>}
 */
ENS.prototype.getResolver = function (name, callback) {
    return this.registry.getResolver(name, callback);
};

/**
 * Does set the resolver of the given name
 *
 * @method setResolver
 *
 * @param {string} name
 * @param {string} address
 * @param {TransactionConfig} txConfig
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.setResolver = function (name, address, txConfig, callback) {
    return this.registry.setResolver(name, address, txConfig, callback);
};

/**
 * Sets the owner, resolver, and TTL for an ENS record in a single operation.
 *
 * @method setRecord
 *
 * @param {string} name
 * @param {string} owner
 * @param {string} resolver
 * @param {string | number} ttl
 * @param {TransactionConfig} txConfig
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.setRecord = function (name, owner, resolver, ttl, txConfig, callback) {
    return this.registry.setRecord(name, owner, resolver, ttl, txConfig, callback);
};

/**
 * Sets the owner, resolver and TTL for a subdomain, creating it if necessary.
 *
 * @method setSubnodeRecord
 *
 * @param {string} name
 * @param {string} label
 * @param {string} owner
 * @param {string} resolver
 * @param {string | number} ttl
 * @param {TransactionConfig} txConfig
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.setSubnodeRecord = function (name, label, owner, resolver, ttl, txConfig, callback) {
    return this.registry.setSubnodeRecord(name, label, owner, resolver, ttl, txConfig, callback);
};

/**
 * Sets or clears an approval by the given operator.
 *
 * @method setApprovalForAll
 *
 * @param {string} operator
 * @param {boolean} approved
 * @param {TransactionConfig} txConfig
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.setApprovalForAll = function (operator, approved, txConfig, callback) {
    return this.registry.setApprovalForAll(operator, approved, txConfig, callback);
};

/**
 * Returns true if the operator is approved
 *
 * @method isApprovedForAll
 *
 * @param {string} owner
 * @param {string} operator
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {Promise<boolean>}
 */
ENS.prototype.isApprovedForAll = function (owner, operator, callback) {
    return this.registry.isApprovedForAll(owner, operator, callback);
};

/**
 * Returns true if the record exists
 *
 * @method recordExists
 *
 * @param {string} name
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {Promise<boolean>}
 */
ENS.prototype.recordExists = function (name, callback) {
    return this.registry.recordExists(name, callback);
};

/**
 * Returns the address of the owner of an ENS name.
 *
 * @method setSubnodeOwner
 *
 * @param {string} name
 * @param {string} label
 * @param {string} address
 * @param {TransactionConfig} txConfig
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.setSubnodeOwner = function (name, label, address, txConfig, callback) {
    return this.registry.setSubnodeOwner(name, label, address, txConfig, callback);
};

/**
 * Returns the address of the owner of an ENS name.
 *
 * @method getTTL
 *
 * @param {string} name
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.getTTL = function (name, callback) {
    return this.registry.getTTL(name, callback);
};

/**
 * Returns the address of the owner of an ENS name.
 *
 * @method setTTL
 *
 * @param {string} name
 * @param {number} ttl
 * @param {TransactionConfig} txConfig
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.setTTL = function (name, ttl, txConfig, callback) {
    return this.registry.setTTL(name, ttl, txConfig, callback);
};

/**
 * Returns the owner by the given name and current configured or detected Registry
 *
 * @method getOwner
 *
 * @param {string} name
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.getOwner = function (name, callback) {
    return this.registry.getOwner(name, callback);
};

/**
 * Returns the address of the owner of an ENS name.
 *
 * @method setOwner
 *
 * @param {string} name
 * @param {string} address
 * @param {TransactionConfig} txConfig
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.setOwner = function (name, address, txConfig, callback) {
    return this.registry.setOwner(name, address, txConfig, callback);
};

/**
 * Returns the address record associated with a name.
 *
 * @method getAddress
 *
 * @param {string} name
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.getAddress = function (name, callback) {
    return this.resolverMethodHandler.method(name, 'addr', []).call(callback);
};

/**
 * Sets a new address
 *
 * @method setAddress
 *
 * @param {string} name
 * @param {string} address
 * @param {TransactionConfig} txConfig
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.setAddress = function (name, address, txConfig, callback) {
    return this.resolverMethodHandler.method(name, 'setAddr', [address]).send(txConfig, callback);
};

/**
 * Returns the public key
 *
 * @method getPubkey
 *
 * @param {string} name
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.getPubkey = function (name, callback) {
    return this.resolverMethodHandler.method(name, 'pubkey', [], null, callback).call(callback);
};

/**
 * Set the new public key
 *
 * @method setPubkey
 *
 * @param {string} name
 * @param {string} x
 * @param {string} y
 * @param {TransactionConfig} txConfig
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.setPubkey = function (name, x, y, txConfig, callback) {
    return this.resolverMethodHandler.method(name, 'setPubkey', [x, y]).send(txConfig, callback);
};

/**
 * Returns the content
 *
 * @method getContent
 *
 * @param {string} name
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.getContent = function (name, callback) {
    return this.resolverMethodHandler.method(name, 'content', []).call(callback);
};

/**
 * Set the content
 *
 * @method setContent
 *
 * @param {string} name
 * @param {string} hash
 * @param {function} callback
 * @param {TransactionConfig} txConfig
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.setContent = function (name, hash, txConfig, callback) {
    return this.resolverMethodHandler.method(name, 'setContent', [hash]).send(txConfig, callback);
};

/**
 * Returns the contenthash
 *
 * @method getContenthash
 *
 * @param {string} name
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<ContentHash>}
 */
ENS.prototype.getContenthash = function (name, callback) {
    return this.resolverMethodHandler.method(name, 'contenthash', [], contenthash.decode).call(callback);
};

/**
 * Set the contenthash
 *
 * @method setContent
 *
 * @param {string} name
 * @param {string} hash
 * @param {function} callback
 * @param {TransactionConfig} txConfig
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.setContenthash = function (name, hash, txConfig, callback) {
    var encoded;
    try {
        encoded = contenthash.encode(hash);
    } catch(err){
        var error = new Error('Could not encode ' + hash + '. See docs for supported hash protocols.');

        if (typeof callback === 'function') {
            callback(error, null);

            return;
        }

        throw error;
    }
    return this.resolverMethodHandler.method(name, 'setContenthash', [encoded]).send(txConfig, callback);
};

/**
 * Get the multihash
 *
 * @method getMultihash
 *
 * @param {string} name
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.getMultihash = function (name, callback) {
    return this.resolverMethodHandler.method(name, 'multihash', []).call(callback);
};

/**
 * Set the multihash
 *
 * @method setMultihash
 *
 * @param {string} name
 * @param {string} hash
 * @param {TransactionConfig} txConfig
 * @param {function} callback
 *
 * @callback callback callback(error, result)
 * @returns {PromiEvent<TransactionReceipt | TransactionRevertInstructionError>}
 */
ENS.prototype.setMultihash = function (name, hash, txConfig, callback) {
    return this.resolverMethodHandler.method(name, 'multihash', [hash]).send(txConfig, callback);
};

/**
 * Checks if the current used network is synced and looks for ENS support there.
 * Throws an error if not.
 *
 * @returns {Promise<string>}
 */
ENS.prototype.checkNetwork = async function () {
    var now = new Date() / 1000;

    if (!this._lastSyncCheck || (now - this._lastSyncCheck) > 3600) {
        var block = await this.eth.getBlock('latest');
        var headAge = now - block.timestamp;

        if (headAge > 3600) {
            throw new Error("Network not synced; last block was " + headAge + " seconds ago");
        }

        this._lastSyncCheck = now;
    }

    if (this.registryAddress) {
        return this.registryAddress;
    }

    if (!this._detectedAddress) {
        var networkType = await this.eth.net.getNetworkType();
        var addr = config.addresses[networkType];

        if (typeof addr === 'undefined') {
            throw new Error("ENS is not supported on network " + networkType);
        }

        this._detectedAddress = addr;

        return this._detectedAddress;
    }

    return this._detectedAddress;
};

module.exports = ENS;
