'use strict';

const fs = require('fs');
const path = require('path');
const fileUtils = require('./utils/files');
const compressionUtils = require('./utils/compression');
const promiseUtils = require('./utils/promises');
const YAML = require('yamljs');
const rimraf = require('rimraf');
const assert = require("assert");
const mkdirp = require('mkdirp');
const compareVersions = require('compare-versions');

/**
 * Details about the container specification
 */
const spec = {
    version: '0.1',
    file_ext: 'tsrc',
    base_mime_type: 'application/tsrc'
};

const content_dir = 'content';

/**
 * Returns a properly formatted container slug.
 *
 * @param language_slug {string}
 * @param project_slug {string}
 * @param resource_slug {string}
 * @returns {string}
 */
function containerSlug(language_slug, project_slug, resource_slug) {
    if(!language_slug || !project_slug || !resource_slug) throw new Error('Invalid resource container slug parameters');
    return language_slug
        + '_' + project_slug
        + '_' + resource_slug;
}

/**
 * Represents an instance of a resource container.
 *
 * @param container_directory
 * @param package_json {{}}
 * @constructor
 */
function Container(container_directory, package_json) {
    let config_yaml = {};
    let toc_yaml = {};

    assert(container_directory, 'resource-container: missing container_directory');
    assert.equal(typeof container_directory, 'string', 'resource-container: container_directory should be a string');

    try {
        let configFile = path.join(container_directory, content_dir, 'config.yml');
        if(fileUtils.fileExists(configFile)) config_yaml = YAML.parse(fs.readFileSync(configFile, {encoding: 'utf8'}));
    } catch (err){
        console.warn(err);
    }
    try {
        let tocFile = path.join(container_directory, content_dir, 'toc.yml');
        if(fileUtils.fileExists(tocFile)) toc_yaml = YAML.parse(fs.readFileSync(tocFile, {encoding: 'utf8'}));
    } catch (err){
        console.warn(err);
    }

    assert(package_json, 'resource-container: missing package json');
    assert(package_json.language, 'resource-container: missing language');
    assert(package_json.project, 'resource-container: missing project');
    assert(package_json.resource, 'resource-container: missing resource');
    assert(package_json.resource.type, 'resource-container: missing resource type');

    return {

        get language () {
            return package_json.language;
        },

        get project () {
            return package_json.project;
        },

        get resource () {
            return package_json.resource;
        },

        /**
         * Returns an array of chapters in this resource container
         */
        chapters: function () {
            let dir = path.join(container_directory, content_dir);
            return fs.readdirSync(dir).filter(function(file) {
                try {
                    return fs.statSync(file).isDirectory();
                } catch (err) {
                    console.log(err);
                    return false;
                }
            });
        },

        /**
         * Returns an array of chunks in the chapter
         */
        chunks: function (chapterSlug) {
            let dir = path.join(container_directory, content_dir, chapterSlug);
            return fs.readdirSync(dir).filter(function(file) {
                try {
                    return fs.statSync(file).isFile();
                } catch (err) {
                    console.log(err);
                    return false;
                }
            });
        },

        /**
         * Returns the contents of a chunk.
         * If the chunk does not exist or there is an exception an empty string will be returned.
         * @param chapterSlug
         * @param chunkSlug
         * @returns string the contents of the chunk
         */
        readChunk: function(chapterSlug, chunkSlug) {
            let file = path.join(container_directory, content_dir, chapterSlug, chunkSlug + '.' + this.chunkExt);
            return fs.readFileSync(file, {encoding: 'utf8'});
        },

        /**
         * Returns the file extension to use for content files (chunks)
         * @returns {*}
         */
        get chunkExt() {
            switch(package_json['content_mime_type']) {
                case 'text/usx':
                    return 'usx';
                case 'text/usfm':
                    return 'usfm';
                case 'text/markdown':
                    return 'md';
                default:
                    return 'txt';
            }
        },

        /**
         * Returns the path to the resource container directory
         * @returns {string}
         */
        get path () {
            return container_directory;
        },

        /**
         * Returns the slug of the resource container
         * @returns {string}
         */
        get slug () {
            return containerSlug(
                package_json.language.slug,
                package_json.project.slug,
                package_json.resource.slug);
        },

        /**
         * Returns the type of the resource container.
         * Shorthand for info.resource.type
         * @returns {string}
         */
        get type() {
            return package_json.resource.type;
        },

        /**
         * Returns the resource container package information.
         * This is the package.json file
         * @returns {{}}
         */
        get info() {
            return package_json;
        },

        /**
         * Returns the resource container data configuration.
         * This is the config.yml file under the content/ directory
         *
         * @returns {{}}
         */
        get config() {
            return config_yaml;
        },

        /**
         * Returns the table of contents.
         * This is the toc.yml file under the content/ directory
         *
         * @returns {{}|[]}
         */
        get toc() {
            return toc_yaml;
        }
    };
}

/**
 * Loads a resource container from the disk.
 * Rejects with an error if the container is not supported. Or does not exist, or is not a directory.
 *
 * @param container_directory {string}
 * @returns {Promise<Container>}
 */
function loadContainer(container_directory) {
    return new Promise(function(resolve, reject) {
        let package_json;
        try {
            if(!fs.statSync(container_directory).isDirectory()) {
                reject(new Error('Not an open resource container'));
                return;
            }
        } catch(err) {
            reject(new Error('The resource container does not exist'));
            return;
        }
        try {
            package_json = JSON.parse(fs.readFileSync(path.join(container_directory, 'package.json'), {encoding: 'utf8'}));
        } catch (err) {
            console.error(err);
        }
        if(package_json == null || package_json.package_version == null) {
            reject(new Error('Not a resource container'));
            return;
        }
        if (compareVersions(package_json.package_version, spec.version) === 1) {
            reject(new Error('Unsupported container version'));
            return;
        }
        if(compareVersions(package_json.package_version, spec.version) === -1) {
            reject(new Error('Outdated container version'));
            return;
        }

        resolve(new Container(container_directory, package_json));
    });
}

/**
 * Creates a new resource container.
 * Rejects with an error if the container exists.
 *
 * @param container_directory {string}
 * @param opts {{}}
 * @returns {Promise<Container>}
 */
function makeContainer(container_directory, opts) {
    return new Promise(function(resolve, reject) {
        if(fileUtils.fileExists(container_directory)) {
            reject(new Error('Container already exists'));
            return;
        }

        let package_json = {};
        // TODO: build the container
        reject(new Error('Not implemented yet.'));

        // resolve(new Container(container_directory, package_json));
    });
}

/**
 * Opens an archived resource container.
 * If the container is already opened it will be loaded
 *
 * @param container_archive {string}
 * @param container_directory {string}
 * @param opts {{}} extra options such as compression_method (zip or tar)
 * @returns {Promise<Container>}
 */
function openContainer(container_archive, container_directory, opts) {
    opts = opts || { compression_method : 'tar' };
    if(fileUtils.fileExists(container_directory)) {
        return loadContainer(container_directory);
    }
    if(!fileUtils.fileExists(container_archive)) return Promise.reject(new Error('Missing resource container'));
    if(opts.compression_method === 'zip') {
        return compressionUtils.unzip(container_archive, container_directory)
            .then(function(dir) {
                return loadContainer(dir);
            });
    } else {
        return compressionUtils.untar(container_archive, container_directory)
            .then(function(dir) {
                return loadContainer(dir);
            });
    }
}

/**
 * Closes (archives) a resource container.
 *
 * @param container_directory {string}
 * @param opts {{}} extra options such as compression_method (zip or tar)
 * @returns {Promise<string>} the path to the resource container archive
 */
function closeContainer(container_directory, opts) {
    opts = opts || { compression_method : 'tar', clean : true };
    if(!fileUtils.fileExists(container_directory)) return Promise.reject(new Error('Missing resource container'));
    var container_archive = container_directory + '.' + spec.file_ext;
    var compressPromise = Promise.resolve(container_archive);

    // create archive if it's missing
    if(!fileUtils.fileExists(container_archive)) {
        if(opts.compression_method === 'zip') {
            compressPromise = compressionUtils.zip(container_directory, container_archive);
        } else {
            compressPromise = compressionUtils.tar(container_directory, container_archive);
        }
    }
    return compressPromise.then(function(path) {
        // remove directory
        if(opts.clean) rimraf.sync(container_directory);
        return Promise.resolve(path);
    });
}

/**
 * TODO: this will eventually be abstracted to call makeContainer after processing the data
 * Converts a legacy resource into a resource container.
 * Rejects with an error if the container exists.
 *
 * @param data {string} the raw resource data
 * @param dir {string} the destination directory
 * @param props {{language, project, resource}} properties of the resource content.
 * @returns {Promise.<Container>} the newly converted container
 */
function convertResource(data, dir, props) {
    // validate opts
    if(!props.language || !props.project || !props.resource || !props.resource.type) {
        return Promise.reject(new Error('Missing required parameters'));
    }

    const writeFileSync = promiseUtils.promisify(fs, 'writeFileSync');
    // TRICKY: the old content was in usx.
    const mimeType = props.project.slug !== 'obs' && props.resource.type === 'book' ? 'text/usx' : 'text/markdown';
    const chunk_ext = mimeType === 'text/usx' ? 'usx' : 'md';

    return new Promise(function(resolve, reject) {
            if(fileUtils.fileExists(path.join(dir + '.' + spec.file_ext))) {
                reject(new Error('Resource container already exists'));
            } else {
                // clean opened container
                rimraf.sync(dir);
                resolve();
            }
        })
        .then(function() {
            // package.json
            mkdirp.sync(dir);
            let packageData = {
                package_version: spec.version,
                modified_at: props.modified_at,
                content_mime_type: mimeType,
                language: props.language,
                project: props.project,
                resource: props.resource,
                chunk_status: []
            };
            return writeFileSync(path.join(dir, 'package.json'), new Buffer(JSON.stringify(packageData, null, 2)));
        })
        .then(function() {
            // license
            // TODO: use a proper license based on the resource license
            return writeFileSync(path.join(dir, 'LICENSE.md'), new Buffer(props.resource.status.license));
        })
        .then(function() {
            // content
            return new Promise(function(resolve, reject) {
                let contentDir = path.join(dir, 'content');
                let config = {};
                let toc = [];

                try {
                    // front matter
                    if(props.resource.type !== 'help' && props.resource.type !== 'dict') {
                        // TRICKY: helps do not have a translatable title
                        let frontDir = path.join(contentDir, 'front');
                        mkdirp.sync(frontDir);
                        fs.writeFileSync(path.join(frontDir, 'title.' + chunk_ext), props.project.name.trim());
                        toc.push({
                            chapter: 'front',
                            chunks: ['title']
                        });
                    }

                    // main content
                    mkdirp.sync(contentDir);
                    if(props.resource.type === 'book') {
                        config.content = {};
                        if (props.project.slug === 'obs') {
                            // add obs images
                            if(!config.media) config.media = {};
                            config.media.image = {
                                mime_type: 'image/jpg',
                                size: 37620940,
                                url: 'https://api.unfoldingword.org/obs/jpg/1/en/obs-images-360px.zip'
                            };
                        }
                        data = JSON.parse(data);
                        for (let chapter of data.chapters) {
                            chapter.number = normalizeSlug(chapter.number);
                            let chapterConfig = {};
                            let chapterTOC = {
                                chapter: chapter.number,
                                chunks: []
                            };
                            let chapterDir = path.join(contentDir, chapter.number);
                            mkdirp.sync(chapterDir);

                            // chapter title
                            chapterTOC.chunks.push('title');
                            if (chapter.title) {
                                fs.writeFileSync(path.join(chapterDir, 'title.' + chunk_ext), chapter.title);
                            } else {
                                var title = localizeChapterTitle(props.language.slug, chapter.number);
                                fs.writeFileSync(path.join(chapterDir, 'title.' + chunk_ext), title);
                            }

                            // frames
                            for (let frame of chapter.frames) {
                                let frameSlug = normalizeSlug(frame.id.split('-')[1].trim());
                                if(frameSlug.trim() === '00') {
                                    // fix for chunk 00.txt bug
                                    let firstVerseRange = /<verse\s+number="(\d+(-\d+)?)"\s+style="v"\s*\/>/.exec(frame.text)[1];
                                    // TRICKY: verses can be num-num
                                    if(firstVerseRange) frameSlug = normalizeSlug(firstVerseRange.split('-')[0]);
                                }

                                // build chunk config
                                let questions = [];
                                let notes = [];
                                let images = [];
                                let words = [];
                                // TODO: add questions, notes, and images to the config for the chunk
                                if(props.tw_assignments) {
                                    try {
                                        let temp = props.tw_assignments[chapter.number][frameSlug];
                                        words = temp ? temp : words;
                                    } catch(e) {}
                                }
                                if(questions.length || notes.length || images.length || words.length) {
                                    chapterConfig[frameSlug] = {};
                                }
                                if(questions.length) {
                                    chapterConfig[frameSlug]['questions'] = questions;
                                }
                                if(notes.length) {
                                    chapterConfig[frameSlug]['notes'] = notes;
                                }
                                if(images.length) {
                                    chapterConfig[frameSlug]['images'] = images;
                                }
                                if(words.length) {
                                    chapterConfig[frameSlug]['words'] = words;
                                }

                                chapterTOC.chunks.push(frameSlug);
                                fs.writeFileSync(path.join(chapterDir, frameSlug + '.' + chunk_ext), frame.text);
                            }

                            // chapter reference
                            if (chapter.ref) {
                                chapterTOC.chunks.push('reference');
                                fs.writeFileSync(path.join(chapterDir, 'reference.' + chunk_ext), chapter.ref)
                            }
                            if(Object.keys(chapterConfig).length) {
                                // TRICKY: only non-empty config
                                config.content[chapter.number] = chapterConfig;
                            }
                            toc.push(chapterTOC);
                        }
                    } else if(props.resource.type === 'help') {
                        if(props.resource.slug === 'tn') {
                            data = JSON.parse(data);
                            for(let chunk of data) {
                                if(!chunk.tn) continue;
                                let slugs = chunk.id.split('-');
                                if(slugs.length !== 2) continue;
                                let chapterSlug = normalizeSlug(slugs[0]);
                                let chunkSlug = normalizeSlug(slugs[1]);

                                // fix for chunk 00.txt bug
                                if(chunkSlug === '00') continue;

                                let chapterDir = path.join(contentDir, chapterSlug);
                                mkdirp.sync(chapterDir);
                                let body = '';
                                for(let note of chunk.tn) {
                                    body += '\n\n#' + note.ref + '\n\n' + note.text;
                                }
                                if(body.trim() !== '') {
                                    fs.writeFileSync(path.join(chapterDir, chunkSlug + '.' + chunk_ext), body.trim());
                                }
                            }
                        } else if(props.resource.slug == 'tq') {
                            data = JSON.parse(data);
                            for(let chapter of data) {
                                if(!chapter.cq) continue;
                                chapter.id = normalizeSlug(chapter.id);
                                let chapterDir = path.join(contentDir, chapter.id);
                                mkdirp.sync(chapterDir);
                                let normalizedChunks = {};
                                for(let question of chapter.cq) {
                                    let text = '\n\n#' + question.q + '\n\n' + question.a;
                                    for(let slug of question.ref) {
                                        let slugs = slug.split('-');
                                        if(slugs.length !== 2) continue;
                                        let chapterSlug = normalizeSlug(slugs[0]);
                                        let chunkSlug = normalizeSlug(slugs[1]);

                                        if(!normalizedChunks[chunkSlug]) normalizedChunks[chunkSlug] = '';
                                        normalizedChunks[chunkSlug] += text;
                                    }
                                }
                                for(let chunk in normalizedChunks) {
                                    fs.writeFileSync(path.join(chapterDir, chunk + '.' + chunk_ext), normalizedChunks[chunk].trim())
                                }
                            }
                        } else {
                            reject(new Error('Unsupported resource ' + props.resource.slug));
                            return;
                        }
                    } else if(props.resource.type === 'dict') {
                        data = JSON.parse(data);
                        for(let word of data) {
                            if(!word.id) continue;
                            let wordDir = path.join(contentDir, word.id);
                            mkdirp.sync(wordDir);
                            let body = '#' + word.term + '\n\n' + word.def;
                            fs.writeFileSync(path.join(wordDir, '01.' + chunk_ext), body);

                            config[word.id] = {
                                def_title: word.def_title
                            };

                            if(word.cf) {
                                let foundIds = {}; // track found related id's so we don't get duplicates
                                for (let related of word.cf) {
                                    if (!config[word.id]['see_also']) config[word.id]['see_also'] = [];
                                    let parts = related.split('|');
                                    let id = parts[0].toLowerCase();
                                    if(!foundIds[id]) {
                                        foundIds[id] = true;
                                        config[word.id]['see_also'].push(id);
                                    }
                                }
                            }
                            if(word.aliases) {
                                for (let alias of word.aliases) {
                                    if (!config[word.id]['aliases']) config[word.id]['aliases'] = [];
                                    let aliases = alias.split(',');
                                    for(let a of aliases) {
                                        config[word.id]['aliases'].push(a.trim());
                                    }
                                }
                            }
                            if(word.ex) {
                                for (let example of word.ex) {
                                    if (!config[word.id]['examples']) config[word.id]['examples'] = [];
                                    config[word.id]['examples'].push(example.ref);
                                }
                            }
                        }
                    } else if (props.resource.type === 'man') {
                        config.content = {};
                        data = JSON.parse(data);

                        let tocMap = {};

                        for(let article of data.articles) {
                            let articleConfig = {
                                recommended: [],
                                dependencies: []
                            };
                            let articleTOC = {

                            };

                            // TRICKY: fix the id's
                            article.id = article.id.replace(/\_/g, '-');
                            if(article.recommend) {
                                article.recommend.forEach(function (id) {
                                    articleConfig.recommended.push(id.replace(/\_/g, '-'));
                                });
                            }
                            if(article.depend) {
                                article.depend.forEach(function (id) {
                                    articleConfig.dependencies.push(id.replace(/\_/g, '-'));
                                });
                            }

                            let articleDir = path.join(contentDir, article.id);
                            mkdirp.sync(articleDir);

                            // article title
                            fs.writeFileSync(path.join(articleDir, 'title.' + chunk_ext), article.title);

                            // article sub-title
                            fs.writeFileSync(path.join(articleDir, 'sub-title.' + chunk_ext), article.question);

                            // article body
                            fs.writeFileSync(path.join(articleDir, '01.' + chunk_ext), article.text);

                            if(articleConfig.recommended.length || articleConfig.dependencies.length) {
                                // TRICKY: only non-empty config
                                config.content[article.id] = articleConfig;
                            }

                            articleTOC['chapter'] = article.id;
                            articleTOC['chunks'] = [
                                'title',
                                'sub-title',
                                '01'
                            ];
                            tocMap[article.id] = articleTOC;
                        }

                        // build toc from what we see in the api
                        let tocRegex = /\[[^\[\]]*\]\s*\(([^\(\)]*)\)/g;
                        let match = tocRegex.exec(data.toc);
                        while(match != null) {
                            let key = match[1].replace(/\_/g, '-');
                            let val = tocMap[key];
                            if (val) toc.push(val);

                            match = tocRegex.exec(data.toc);
                        }
                    } else {
                        reject(new Error('Unsupported resource container type ' + props.resource.type));
                        return;
                    }

                    // write config
                    let configPath = path.join(contentDir, 'config.yml');
                    fs.writeFileSync(configPath, new Buffer(YAML.stringify(config, 10, 2, null, null)));
                    // write toc
                    let tocPath = path.join(contentDir, 'toc.yml');
                    fs.writeFileSync(tocPath, new Buffer(YAML.stringify(toc, 10, 2, null, null)));
                } catch(err) {
                    console.error(data);
                    reject(err);
                    return;
                }
                resolve();
            });
        })
        .then(function() {
            return loadContainer(dir);
        })
        .catch(function(err) {
            // clean up after an error
            rimraf.sync(dir);
            return Promise.reject(err);
        });
}

/**
 * Returns a localized chapter title. e.g. "Chapter 1"
 * If the language does not have a match a default localization will be used.
 *
 * If chapter_number is a number it will be parsed as an int to strip leading 0's
 *
 * @param language_slug the language into which the chapter title will be localized
 * @param chapter_number the chapter number that is being localized
 */
function localizeChapterTitle(language_slug, chapter_number) {
    var translations = {
        'ar': 'الفصل %',
        'en': 'Chapter %',
        'ru': 'Глава %',
        'hu': '%. fejezet',
        'sr-Latin': 'Поглавље %',
        'default': 'Chapter %'
    };
    var title = translations[language_slug];
    if(!title) title = translations['default'];
    var num = parseInt(chapter_number);
    if (isNaN(num)) num = chapter_number;
    return title.replace('%', num);
}

/**
 * Pads a slug to 2 significant digits.
 * Examples:
 * '1'    -> '01'
 * '001'  -> '01'
 * '12'   -> '12'
 * '123'  -> '123'
 * '0123' -> '123'
 * Words are not padded:
 * 'a' -> 'a'
 * '0word' -> '0word'
 * And as a matter of consistency:
 * '0'  -> '00'
 * '00' -> '00'
 *
 * @param slug
 * @returns {*}
 */
function normalizeSlug(slug) {
    if(typeof slug !== 'string') throw new Error('slug must be a string');
    if(slug === '') throw new Error('slug cannot be an empty string');
    if(isNaN(Number(slug))) return slug;
    slug = slug.replace(/^(0+)/, '').trim();
    while(slug.length < 2) {
        slug = '0' + slug;
    }
    return slug;
}

/**
 * Retrieves the resource container type by parsing the resource container mime type.
 *
 * @param mime_type {string} a mime type value e.g. "application/tsrc+book". where the type is "book"
 * @returns {string} The resource container type
 */
function mimeToType(mime_type) {
    return mime_type.split('+')[1];
}

/**
 * Returns a resource container mime type based on the given container type.
 *
 * @param resource_type {string} the resource container type. e.g. "book", "help" etc.
 * @returns {string} The mime type. e.g. "application/tsrc+type"
 */
function typeToMime(resource_type) {
    return spec.base_mime_type + '+' + resource_type;
}

/**
 * Reads the resource container info without opening it.
 * This will however, work on containers that are both open and closed.
 *
 * @param container_path {string} path to the container archive or directory
 * @param opts {{}}
 * @returns {Promise.<{}>} the resource container info (package.json)
 */
function inspectContainer(container_path, opts) {
    return new Promise(function(resolve, reject) {
            if(path.extname(container_path) !== '.' + spec.file_ext) {
                reject(new Error('Invalid resource container file extension'));
                return;
            }
            try {
                resolve(fs.statSync(container_path).isFile());
            } catch(err) {
                reject(new Error('The resource container does not exist at', container_path));
            }
        })
        .then(function(isFile) {
            if(isFile) {
                // TODO: For now we are just opening the container then closing it.
                // Eventually it would be nice if we can inspect the archive without extracting everything.
                let containerDir = path.join(path.dirname(container_path), path.basename(container_path, '.' + spec.file_ext));
                return openContainer(container_path, containerDir, opts)
                    .then(function(container) {
                        return closeContainer(containerDir, opts)
                            .then(function() {
                                return Promise.resolve(container.info);
                            });
                    });
            } else {
                loadContainer(container_path)
                    .then(function(container) {
                        return Promise.resolve(container.info);
                    });
            }
        });
}

module.exports = {
    load: loadContainer,
    make: makeContainer,
    open: openContainer,
    close: closeContainer,
    tools: {
        inspect: inspectContainer,
        convertResource: convertResource,
        makeSlug: containerSlug,
        mimeToType: mimeToType,
        typeToMime: typeToMime,
        localizeChapterTitle: localizeChapterTitle,
        normalizeSlug: normalizeSlug,
        spec: spec
    }
};


"use strict"
var Url = require('url')
var _ = require('underscore')

module.exports = function(ngin) {
  var Model = ngin.NginModel
  var Super = Model.prototype
  var config = ngin.config

  /**
   * Scopes the url to the tournament or flight
   *
   * @param {Object} options
   * @returns {String}
   * @api public
   */

  function scopeUrl(options, inst, action) {
    var opts = _.extend({}, inst, options)
    if (!opts.id && !opts.tournament_id && !opts.league_id && !opts.season_id)
      throw new Error('id, season_id, tournament_id or league_id required to make survey api calls')
    return Survey.urlRoot() + '/' + action + (opts.id ? '/' + opts.id : '') + '.json'
  }

  /**
   * Survey Class
   *
   * @param {Object} attr
   * @param {Object} options
   * @api public
   */

  var Survey = Model.extend({

    fetch: function(options, callback) {
      options || (options = {})
      if (typeof options == 'function') callback = options, options = {}

      var url = scopeUrl(options, this, 'show')
      options.query = _.extend(_.pick(options, 'season_id', 'tournament_id', 'league_id'), options.query)
      return Super.fetch.call(this, url, options, callback)
    }

  }, {

    urlRoot: function() {
      var base = config.urls && config.urls.ngin || config.url
      return Url.resolve(base, '/api2/registration/survey')
    }

  })

  return Survey

}


/*
 * Copyright (C) 2008 Apple Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *        notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *        notice, this list of conditions and the following disclaimer in the
 *        documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.         IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @constructor
 * @extends {WebInspector.Object}
 * @param {!Array.<!WebInspector.DataGrid.ColumnDescriptor>} columnsArray
 * @param {function(!WebInspector.DataGridNode, string, string, string)=} editCallback
 * @param {function(!WebInspector.DataGridNode)=} deleteCallback
 * @param {function()=} refreshCallback
 * @param {function(!WebInspector.ContextMenu, !WebInspector.DataGridNode)=} contextMenuCallback
 */
WebInspector.DataGrid = function(columnsArray, editCallback, deleteCallback, refreshCallback, contextMenuCallback)
{
    this.element = createElementWithClass("div", "data-grid");
    WebInspector.appendStyle(this.element, "ui_lazy/dataGrid.css");
    this.element.tabIndex = 0;
    this.element.addEventListener("keydown", this._keyDown.bind(this), false);

    var headerContainer = createElementWithClass("div", "header-container");
    /** @type {!Element} */
    this._headerTable = headerContainer.createChild("table", "header");
    /** @type {!Object.<string, !Element>} */
    this._headerTableHeaders = {};

    /** @type {!Element} */
    this._scrollContainer = createElementWithClass("div", "data-container");
    /** @type {!Element} */
    this._dataTable = this._scrollContainer.createChild("table", "data");

    this._dataTable.addEventListener("mousedown", this._mouseDownInDataTable.bind(this));
    this._dataTable.addEventListener("click", this._clickInDataTable.bind(this), true);

    this._dataTable.addEventListener("contextmenu", this._contextMenuInDataTable.bind(this), true);

    // FIXME: Add a createCallback which is different from editCallback and has different
    // behavior when creating a new node.
    if (editCallback)
        this._dataTable.addEventListener("dblclick", this._ondblclick.bind(this), false);
    /** @type {function(!WebInspector.DataGridNode, string, string, string)|undefined} */
    this._editCallback = editCallback;
    /** @type {function(!WebInspector.DataGridNode)|undefined} */
    this._deleteCallback = deleteCallback;
    /** @type {function()|undefined} */
    this._refreshCallback = refreshCallback;
    /** @type {function(!WebInspector.ContextMenu, !WebInspector.DataGridNode)|undefined} */
    this._contextMenuCallback = contextMenuCallback;

    this.element.appendChild(headerContainer);
    this.element.appendChild(this._scrollContainer);

    /** @type {!Element} */
    this._headerRow = createElement("tr");
    /** @type {!Element} */
    this._headerTableColumnGroup = createElement("colgroup");
    /** @type {!Element} */
    this._dataTableColumnGroup = createElement("colgroup");

    /** @type {!Element} */
    this._topFillerRow = createElementWithClass("tr", "data-grid-filler-row revealed");
    /** @type {!Element} */
    this._bottomFillerRow = createElementWithClass("tr", "data-grid-filler-row revealed");
    this.setVerticalPadding(0, 0);

    /** @type {boolean} */
    this._inline = false;

    /** @type {!Array.<!WebInspector.DataGrid.ColumnDescriptor>} */
    this._columnsArray = columnsArray;
    /** @type {!Array.<!WebInspector.DataGrid.ColumnDescriptor>} */
    this._visibleColumnsArray = columnsArray;
    /** @type {!Object.<string, !WebInspector.DataGrid.ColumnDescriptor>} */
    this._columns = {};

    /** @type {?string} */
    this._cellClass = null;

    for (var i = 0; i < columnsArray.length; ++i) {
        var column = columnsArray[i];
        var columnIdentifier = column.identifier = column.id || String(i);
        this._columns[columnIdentifier] = column;
        if (column.disclosure)
            this.disclosureColumnIdentifier = columnIdentifier;

        var cell = createElement("th");
        cell.className = columnIdentifier + "-column";
        cell.columnIdentifier = String(columnIdentifier);
        this._headerTableHeaders[columnIdentifier] = cell;

        var div = createElement("div");
        if (column.titleDOMFragment)
            div.appendChild(column.titleDOMFragment);
        else
            div.textContent = column.title;
        cell.appendChild(div);

        if (column.sort) {
            cell.classList.add(column.sort);
            this._sortColumnCell = cell;
        }

        if (column.sortable) {
            cell.addEventListener("click", this._clickInHeaderCell.bind(this), false);
            cell.classList.add("sortable");
            cell.createChild("div", "sort-order-icon-container").createChild("div", "sort-order-icon");
        }
    }

    this._headerTable.appendChild(this._headerTableColumnGroup);
    this.headerTableBody.appendChild(this._headerRow);

    this._dataTable.appendChild(this._dataTableColumnGroup);
    this.dataTableBody.appendChild(this._topFillerRow);
    this.dataTableBody.appendChild(this._bottomFillerRow);

    this._refreshHeader();

    /** @type {boolean} */
    this._editing = false;
    /** @type {?WebInspector.DataGridNode} */
    this.selectedNode = null;
    /** @type {boolean} */
    this.expandNodesWhenArrowing = false;
    this.setRootNode(new WebInspector.DataGridNode());
    /** @type {number} */
    this.indentWidth = 15;
    /** @type {!Array.<!Element|{__index: number, __position: number}>} */
    this._resizers = [];
    /** @type {boolean} */
    this._columnWidthsInitialized = false;
    /** @type {number} */
    this._cornerWidth = WebInspector.DataGrid.CornerWidth;
    /** @type {!WebInspector.DataGrid.ResizeMethod} */
    this._resizeMethod = WebInspector.DataGrid.ResizeMethod.Nearest;
}

// Keep in sync with .data-grid col.corner style rule.
WebInspector.DataGrid.CornerWidth = 14;

/**
 * @typedef {{
 *   id: string,
 *   title: string,
 *   sortable: boolean,
 *   sort: (?WebInspector.DataGrid.Order|undefined),
 *   align: (?WebInspector.DataGrid.Align|undefined),
 *   fixedWidth: (boolean|undefined),
 *   editable: (boolean|undefined),
 *   nonSelectable: (boolean|undefined),
 *   longText: (boolean|undefined),
 *   disclosure: (boolean|undefined),
 *   identifier: (string|undefined),
 *   weight: (number|undefined)
 * }}
 */
WebInspector.DataGrid.ColumnDescriptor;

WebInspector.DataGrid.Events = {
    SelectedNode: "SelectedNode",
    DeselectedNode: "DeselectedNode",
    SortingChanged: "SortingChanged",
    ColumnsResized: "ColumnsResized"
}

/** @enum {string} */
WebInspector.DataGrid.Order = {
    Ascending: "sort-ascending",
    Descending: "sort-descending"
}

/** @enum {string} */
WebInspector.DataGrid.Align = {
    Center: "center",
    Right: "right"
}

WebInspector.DataGrid._preferredWidthSymbol = Symbol("preferredWidth");

WebInspector.DataGrid.prototype = {
    /**
     * @param {string} cellClass
     */
    setCellClass: function(cellClass)
    {
        this._cellClass = cellClass;
    },

    _refreshHeader: function()
    {
        this._headerTableColumnGroup.removeChildren();
        this._dataTableColumnGroup.removeChildren();
        this._headerRow.removeChildren();
        this._topFillerRow.removeChildren();
        this._bottomFillerRow.removeChildren();

        for (var i = 0; i < this._visibleColumnsArray.length; ++i) {
            var column = this._visibleColumnsArray[i];
            var columnIdentifier = column.identifier || String(i);
            var headerColumn = this._headerTableColumnGroup.createChild("col");
            var dataColumn = this._dataTableColumnGroup.createChild("col");
            if (column.width) {
                headerColumn.style.width = column.width;
                dataColumn.style.width = column.width;
            }
            this._headerRow.appendChild(this._headerTableHeaders[columnIdentifier]);
            this._topFillerRow.createChild("td", "top-filler-td");
            this._bottomFillerRow.createChild("td", "bottom-filler-td").columnIdentifier_ = columnIdentifier;
        }

        this._headerRow.createChild("th", "corner");
        this._topFillerRow.createChild("td", "corner").classList.add("top-filler-td");
        this._bottomFillerRow.createChild("td", "corner").classList.add("bottom-filler-td");
        this._headerTableColumnGroup.createChild("col", "corner");
        this._dataTableColumnGroup.createChild("col", "corner");
    },

    /**
     * @param {number} top
     * @param {number} bottom
     * @protected
     */
    setVerticalPadding: function(top, bottom)
    {
        this._topFillerRow.style.height = top + "px";
        if (top || bottom)
            this._bottomFillerRow.style.height = bottom + "px";
        else
            this._bottomFillerRow.style.height = "auto";
    },

    /**
     * @param {!WebInspector.DataGridNode} rootNode
     * @protected
     */
    setRootNode: function(rootNode)
    {
        if (this._rootNode) {
            this._rootNode.removeChildren();
            this._rootNode.dataGrid = null;
            this._rootNode._isRoot = false;
        }
        /** @type {!WebInspector.DataGridNode} */
        this._rootNode = rootNode;
        rootNode._isRoot = true;
        rootNode.hasChildren = false;
        rootNode._expanded = true;
        rootNode._revealed = true;
        rootNode.selectable = false;
        rootNode.dataGrid = this;
    },

    /**
     * @return {!WebInspector.DataGridNode}
     */
    rootNode: function()
    {
        return this._rootNode;
    },

    _ondblclick: function(event)
    {
        if (this._editing || this._editingNode)
            return;

        var columnIdentifier = this.columnIdentifierFromNode(event.target);
        if (!columnIdentifier || !this._columns[columnIdentifier].editable)
            return;
        this._startEditing(event.target);
    },

    /**
     * @param {!WebInspector.DataGridNode} node
     * @param {number} cellIndex
     */
    _startEditingColumnOfDataGridNode: function(node, cellIndex)
    {
        this._editing = true;
        /** @type {?WebInspector.DataGridNode} */
        this._editingNode = node;
        this._editingNode.select();

        var element = this._editingNode._element.children[cellIndex];
        WebInspector.InplaceEditor.startEditing(element, this._startEditingConfig(element));
        element.getComponentSelection().setBaseAndExtent(element, 0, element, 1);
    },

    _startEditing: function(target)
    {
        var element = target.enclosingNodeOrSelfWithNodeName("td");
        if (!element)
            return;

        this._editingNode = this.dataGridNodeFromNode(target);
        if (!this._editingNode) {
            if (!this.creationNode)
                return;
            this._editingNode = this.creationNode;
        }

        // Force editing the 1st column when editing the creation node
        if (this._editingNode.isCreationNode)
            return this._startEditingColumnOfDataGridNode(this._editingNode, this._nextEditableColumn(-1));

        this._editing = true;
        WebInspector.InplaceEditor.startEditing(element, this._startEditingConfig(element));

        element.getComponentSelection().setBaseAndExtent(element, 0, element, 1);
    },

    renderInline: function()
    {
        this.element.classList.add("inline");
        this._cornerWidth = 0;
        this._inline = true;
        this.updateWidths();
    },

    _startEditingConfig: function(element)
    {
        return new WebInspector.InplaceEditor.Config(this._editingCommitted.bind(this), this._editingCancelled.bind(this), element.textContent);
    },

    _editingCommitted: function(element, newText, oldText, context, moveDirection)
    {
        var columnIdentifier = this.columnIdentifierFromNode(element);
        if (!columnIdentifier) {
            this._editingCancelled(element);
            return;
        }
        var column = this._columns[columnIdentifier];
        var cellIndex = this._visibleColumnsArray.indexOf(column);
        var textBeforeEditing = this._editingNode.data[columnIdentifier];
        var currentEditingNode = this._editingNode;

        /**
         * @param {boolean} wasChange
         * @this {WebInspector.DataGrid}
         */
        function moveToNextIfNeeded(wasChange)
        {
            if (!moveDirection)
                return;

            if (moveDirection === "forward") {
                var firstEditableColumn = this._nextEditableColumn(-1);
                if (currentEditingNode.isCreationNode && cellIndex === firstEditableColumn && !wasChange)
                    return;

                var nextEditableColumn = this._nextEditableColumn(cellIndex);
                if (nextEditableColumn !== -1)
                    return this._startEditingColumnOfDataGridNode(currentEditingNode, nextEditableColumn);

                var nextDataGridNode = currentEditingNode.traverseNextNode(true, null, true);
                if (nextDataGridNode)
                    return this._startEditingColumnOfDataGridNode(nextDataGridNode, firstEditableColumn);
                if (currentEditingNode.isCreationNode && wasChange) {
                    this.addCreationNode(false);
                    return this._startEditingColumnOfDataGridNode(this.creationNode, firstEditableColumn);
                }
                return;
            }

            if (moveDirection === "backward") {
                var prevEditableColumn = this._nextEditableColumn(cellIndex, true);
                if (prevEditableColumn !== -1)
                    return this._startEditingColumnOfDataGridNode(currentEditingNode, prevEditableColumn);

                var lastEditableColumn = this._nextEditableColumn(this._visibleColumnsArray.length, true);
                var nextDataGridNode = currentEditingNode.traversePreviousNode(true, true);
                if (nextDataGridNode)
                    return this._startEditingColumnOfDataGridNode(nextDataGridNode, lastEditableColumn);
                return;
            }
        }

        if (textBeforeEditing == newText) {
            this._editingCancelled(element);
            moveToNextIfNeeded.call(this, false);
            return;
        }

        // Update the text in the datagrid that we typed
        this._editingNode.data[columnIdentifier] = newText;

        // Make the callback - expects an editing node (table row), the column number that is being edited,
        // the text that used to be there, and the new text.
        this._editCallback(this._editingNode, columnIdentifier, textBeforeEditing, newText);

        if (this._editingNode.isCreationNode)
            this.addCreationNode(false);

        this._editingCancelled(element);
        moveToNextIfNeeded.call(this, true);
    },

    _editingCancelled: function(element)
    {
        this._editing = false;
        this._editingNode = null;
    },

    /**
     * @param {number} cellIndex
     * @param {boolean=} moveBackward
     * @return {number}
     */
    _nextEditableColumn: function(cellIndex, moveBackward)
    {
        var increment = moveBackward ? -1 : 1;
        var columns = this._visibleColumnsArray;
        for (var i = cellIndex + increment; (i >= 0) && (i < columns.length); i += increment) {
            if (columns[i].editable)
                return i;
        }
        return -1;
    },

    /**
     * @return {?string}
     */
    sortColumnIdentifier: function()
    {
        if (!this._sortColumnCell)
            return null;
        return this._sortColumnCell.columnIdentifier;
    },

    /**
     * @return {?string}
     */
    sortOrder: function()
    {
        if (!this._sortColumnCell || this._sortColumnCell.classList.contains(WebInspector.DataGrid.Order.Ascending))
            return WebInspector.DataGrid.Order.Ascending;
        if (this._sortColumnCell.classList.contains(WebInspector.DataGrid.Order.Descending))
            return WebInspector.DataGrid.Order.Descending;
        return null;
    },

    /**
     * @return {boolean}
     */
    isSortOrderAscending: function()
    {
        return !this._sortColumnCell || this._sortColumnCell.classList.contains(WebInspector.DataGrid.Order.Ascending);
    },

    get headerTableBody()
    {
        if ("_headerTableBody" in this)
            return this._headerTableBody;

        this._headerTableBody = this._headerTable.getElementsByTagName("tbody")[0];
        if (!this._headerTableBody) {
            this._headerTableBody = this.element.ownerDocument.createElement("tbody");
            this._headerTable.insertBefore(this._headerTableBody, this._headerTable.tFoot);
        }

        return this._headerTableBody;
    },

    get dataTableBody()
    {
        if ("_dataTableBody" in this)
            return this._dataTableBody;

        this._dataTableBody = this._dataTable.getElementsByTagName("tbody")[0];
        if (!this._dataTableBody) {
            this._dataTableBody = this.element.ownerDocument.createElement("tbody");
            this._dataTable.insertBefore(this._dataTableBody, this._dataTable.tFoot);
        }

        return this._dataTableBody;
    },

    /**
     * @param {!Array.<number>} widths
     * @param {number} minPercent
     * @param {number=} maxPercent
     * @return {!Array.<number>}
     */
    _autoSizeWidths: function(widths, minPercent, maxPercent)
    {
        if (minPercent)
            minPercent = Math.min(minPercent, Math.floor(100 / widths.length));
        var totalWidth = 0;
        for (var i = 0; i < widths.length; ++i)
            totalWidth += widths[i];
        var totalPercentWidth = 0;
        for (var i = 0; i < widths.length; ++i) {
            var width = Math.round(100 * widths[i] / totalWidth);
            if (minPercent && width < minPercent)
                width = minPercent;
            else if (maxPercent && width > maxPercent)
                width = maxPercent;
            totalPercentWidth += width;
            widths[i] = width;
        }
        var recoupPercent = totalPercentWidth - 100;

        while (minPercent && recoupPercent > 0) {
            for (var i = 0; i < widths.length; ++i) {
                if (widths[i] > minPercent) {
                    --widths[i];
                    --recoupPercent;
                    if (!recoupPercent)
                        break;
                }
            }
        }

        while (maxPercent && recoupPercent < 0) {
            for (var i = 0; i < widths.length; ++i) {
                if (widths[i] < maxPercent) {
                    ++widths[i];
                    ++recoupPercent;
                    if (!recoupPercent)
                        break;
                }
            }
        }

        return widths;
    },

    /**
     * @param {number} minPercent
     * @param {number=} maxPercent
     * @param {number=} maxDescentLevel
     */
    autoSizeColumns: function(minPercent, maxPercent, maxDescentLevel)
    {
        var widths = [];
        for (var i = 0; i < this._columnsArray.length; ++i)
            widths.push((this._columnsArray[i].title || "").length);

        maxDescentLevel = maxDescentLevel || 0;
        var children = this._enumerateChildren(this._rootNode, [], maxDescentLevel + 1);
        for (var i = 0; i < children.length; ++i) {
            var node = children[i];
            for (var j = 0; j < this._columnsArray.length; ++j) {
                var text = node.data[this._columnsArray[j].identifier] || "";
                if (text.length > widths[j])
                    widths[j] = text.length;
            }
        }

        widths = this._autoSizeWidths(widths, minPercent, maxPercent);

        for (var i = 0; i < this._columnsArray.length; ++i)
            this._columnsArray[i].weight = widths[i];
        this._columnWidthsInitialized = false;
        this.updateWidths();
    },

    _enumerateChildren: function(rootNode, result, maxLevel)
    {
        if (!rootNode._isRoot)
            result.push(rootNode);
        if (!maxLevel)
            return;
        for (var i = 0; i < rootNode.children.length; ++i)
            this._enumerateChildren(rootNode.children[i], result, maxLevel - 1);
        return result;
    },

    onResize: function()
    {
        this.updateWidths();
    },

    // Updates the widths of the table, including the positions of the column
    // resizers.
    //
    // IMPORTANT: This function MUST be called once after the element of the
    // DataGrid is attached to its parent element and every subsequent time the
    // width of the parent element is changed in order to make it possible to
    // resize the columns.
    //
    // If this function is not called after the DataGrid is attached to its
    // parent element, then the DataGrid's columns will not be resizable.
    updateWidths: function()
    {
        var headerTableColumns = this._headerTableColumnGroup.children;

        // Use container size to avoid changes of table width caused by change of column widths.
        var tableWidth = this.element.offsetWidth - this._cornerWidth;
        var numColumns = headerTableColumns.length - 1; // Do not process corner column.

        // Do not attempt to use offsetes if we're not attached to the document tree yet.
        if (!this._columnWidthsInitialized && this.element.offsetWidth) {
            // Give all the columns initial widths now so that during a resize,
            // when the two columns that get resized get a percent value for
            // their widths, all the other columns already have percent values
            // for their widths.
            for (var i = 0; i < numColumns; i++) {
                var columnWidth = this.headerTableBody.rows[0].cells[i].offsetWidth;
                var column = this._visibleColumnsArray[i];
                if (!column.weight)
                    column.weight = 100 * columnWidth / tableWidth;
            }
            this._columnWidthsInitialized = true;
        }
        this._applyColumnWeights();
    },

    /**
     * @param {string} name
     */
    setName: function(name)
    {
        this._columnWeightsSetting = WebInspector.settings.createSetting("dataGrid-" + name + "-columnWeights", {});
        this._loadColumnWeights();
    },

    _loadColumnWeights: function()
    {
        if (!this._columnWeightsSetting)
            return;
        var weights = this._columnWeightsSetting.get();
        for (var i = 0; i < this._columnsArray.length; ++i) {
            var column = this._columnsArray[i];
            var weight = weights[column.identifier];
            if (weight)
                column.weight = weight;
        }
        this._applyColumnWeights();
    },

    _saveColumnWeights: function()
    {
        if (!this._columnWeightsSetting)
            return;
        var weights = {};
        for (var i = 0; i < this._columnsArray.length; ++i) {
            var column = this._columnsArray[i];
            weights[column.identifier] = column.weight;
        }
        this._columnWeightsSetting.set(weights);
    },

    wasShown: function()
    {
        this._loadColumnWeights();
    },

    willHide: function()
    {
    },

    _applyColumnWeights: function()
    {
        var tableWidth = this.element.offsetWidth - this._cornerWidth;
        if (tableWidth <= 0)
            return;

        var sumOfWeights = 0.0;
        var fixedColumnWidths = [];
        for (var i = 0; i < this._visibleColumnsArray.length; ++i) {
            var column = this._visibleColumnsArray[i];
            if (column.fixedWidth) {
                var width = this._headerTableColumnGroup.children[i][WebInspector.DataGrid._preferredWidthSymbol] || this.headerTableBody.rows[0].cells[i].offsetWidth;
                fixedColumnWidths[i] = width;
                tableWidth -= width;
            } else {
                sumOfWeights += this._visibleColumnsArray[i].weight;
            }
        }
        var sum = 0;
        var lastOffset = 0;

        for (var i = 0; i < this._visibleColumnsArray.length; ++i) {
            var column = this._visibleColumnsArray[i];
            var width;
            if (column.fixedWidth) {
                width = fixedColumnWidths[i];
            } else {
                sum += column.weight;
                var offset = (sum * tableWidth / sumOfWeights) | 0;
                width = offset - lastOffset;
                lastOffset = offset;
            }
            this._setPreferredWidth(i, width);
        }

        this._positionResizers();
        this.dispatchEventToListeners(WebInspector.DataGrid.Events.ColumnsResized);
    },

    /**
     * @param {!Object.<string, boolean>} columnsVisibility
     */
    setColumnsVisiblity: function(columnsVisibility)
    {
        this._visibleColumnsArray = [];
        for (var i = 0; i < this._columnsArray.length; ++i) {
            var column = this._columnsArray[i];
            if (columnsVisibility[column.identifier || String(i)])
                this._visibleColumnsArray.push(column);
        }
        this._refreshHeader();
        this._applyColumnWeights();
        var nodes = this._enumerateChildren(this.rootNode(), [], -1);
        for (var i = 0; i < nodes.length; ++i)
            nodes[i].refresh();
    },

    get scrollContainer()
    {
        return this._scrollContainer;
    },

    _positionResizers: function()
    {
        var headerTableColumns = this._headerTableColumnGroup.children;
        var numColumns = headerTableColumns.length - 1; // Do not process corner column.
        var left = [];
        var resizers = this._resizers;

        while (resizers.length > numColumns - 1)
            resizers.pop().remove();

        for (var i = 0; i < numColumns - 1; i++) {
            // Get the width of the cell in the first (and only) row of the
            // header table in order to determine the width of the column, since
            // it is not possible to query a column for its width.
            left[i] = (left[i-1] || 0) + this.headerTableBody.rows[0].cells[i].offsetWidth;
        }

        // Make n - 1 resizers for n columns.
        for (var i = 0; i < numColumns - 1; i++) {
            var resizer = resizers[i];
            if (!resizer) {
                // This is the first call to updateWidth, so the resizers need
                // to be created.
                resizer = createElement("div");
                resizer.__index = i;
                resizer.classList.add("data-grid-resizer");
                // This resizer is associated with the column to its right.
                WebInspector.installDragHandle(resizer, this._startResizerDragging.bind(this), this._resizerDragging.bind(this), this._endResizerDragging.bind(this), "col-resize");
                this.element.appendChild(resizer);
                resizers.push(resizer);
            }
            if (resizer.__position !== left[i]) {
                resizer.__position = left[i];
                resizer.style.left = left[i] + "px";
            }
        }
    },

    addCreationNode: function(hasChildren)
    {
        if (this.creationNode)
            this.creationNode.makeNormal();

        var emptyData = {};
        for (var column in this._columns)
            emptyData[column] = null;
        this.creationNode = new WebInspector.CreationDataGridNode(emptyData, hasChildren);
        this.rootNode().appendChild(this.creationNode);
    },

    _keyDown: function(event)
    {
        if (!this.selectedNode || event.shiftKey || event.metaKey || event.ctrlKey || this._editing)
            return;

        var handled = false;
        var nextSelectedNode;
        if (event.keyIdentifier === "Up" && !event.altKey) {
            nextSelectedNode = this.selectedNode.traversePreviousNode(true);
            while (nextSelectedNode && !nextSelectedNode.selectable)
                nextSelectedNode = nextSelectedNode.traversePreviousNode(true);
            handled = nextSelectedNode ? true : false;
        } else if (event.keyIdentifier === "Down" && !event.altKey) {
            nextSelectedNode = this.selectedNode.traverseNextNode(true);
            while (nextSelectedNode && !nextSelectedNode.selectable)
                nextSelectedNode = nextSelectedNode.traverseNextNode(true);
            handled = nextSelectedNode ? true : false;
        } else if (event.keyIdentifier === "Left") {
            if (this.selectedNode.expanded) {
                if (event.altKey)
                    this.selectedNode.collapseRecursively();
                else
                    this.selectedNode.collapse();
                handled = true;
            } else if (this.selectedNode.parent && !this.selectedNode.parent._isRoot) {
                handled = true;
                if (this.selectedNode.parent.selectable) {
                    nextSelectedNode = this.selectedNode.parent;
                    handled = nextSelectedNode ? true : false;
                } else if (this.selectedNode.parent)
                    this.selectedNode.parent.collapse();
            }
        } else if (event.keyIdentifier === "Right") {
            if (!this.selectedNode.revealed) {
                this.selectedNode.reveal();
                handled = true;
            } else if (this.selectedNode.hasChildren) {
                handled = true;
                if (this.selectedNode.expanded) {
                    nextSelectedNode = this.selectedNode.children[0];
                    handled = nextSelectedNode ? true : false;
                } else {
                    if (event.altKey)
                        this.selectedNode.expandRecursively();
                    else
                        this.selectedNode.expand();
                }
            }
        } else if (event.keyCode === 8 || event.keyCode === 46) {
            if (this._deleteCallback) {
                handled = true;
                this._deleteCallback(this.selectedNode);
            }
        } else if (isEnterKey(event)) {
            if (this._editCallback) {
                handled = true;
                this._startEditing(this.selectedNode._element.children[this._nextEditableColumn(-1)]);
            }
        }

        if (nextSelectedNode) {
            nextSelectedNode.reveal();
            nextSelectedNode.select();
        }
        if (handled)
            event.consume(true);
    },

    /**
     * @param {?WebInspector.DataGridNode} root
     * @param {boolean} onlyAffectsSubtree
     */
    updateSelectionBeforeRemoval: function(root, onlyAffectsSubtree)
    {
        var ancestor = this.selectedNode;
        while (ancestor && ancestor !== root)
            ancestor = ancestor.parent;
        // Selection is not in the subtree being deleted.
        if (!ancestor)
            return;

        var nextSelectedNode;
        // Skip subtree being deleted when looking for the next selectable node.
        for (ancestor = root; ancestor && !ancestor.nextSibling; ancestor = ancestor.parent) { }
        if (ancestor)
            nextSelectedNode = ancestor.nextSibling;
        while (nextSelectedNode && !nextSelectedNode.selectable)
            nextSelectedNode = nextSelectedNode.traverseNextNode(true);

        if (!nextSelectedNode || nextSelectedNode.isCreationNode) {
            nextSelectedNode = root.traversePreviousNode(true);
            while (nextSelectedNode && !nextSelectedNode.selectable)
                nextSelectedNode = nextSelectedNode.traversePreviousNode(true);
        }
        if (nextSelectedNode) {
            nextSelectedNode.reveal();
            nextSelectedNode.select();
        } else {
            this.selectedNode.deselect();
        }
    },

    /**
     * @param {!Node} target
     * @return {?WebInspector.DataGridNode}
     */
    dataGridNodeFromNode: function(target)
    {
        var rowElement = target.enclosingNodeOrSelfWithNodeName("tr");
        return rowElement && rowElement._dataGridNode;
    },

    /**
     * @param {!Node} target
     * @return {?string}
     */
    columnIdentifierFromNode: function(target)
    {
        var cellElement = target.enclosingNodeOrSelfWithNodeName("td");
        return cellElement && cellElement.columnIdentifier_;
    },

    _clickInHeaderCell: function(event)
    {
        var cell = event.target.enclosingNodeOrSelfWithNodeName("th");
        if (!cell || (cell.columnIdentifier === undefined) || !cell.classList.contains("sortable"))
            return;

        var sortOrder = WebInspector.DataGrid.Order.Ascending;
        if ((cell === this._sortColumnCell) && this.isSortOrderAscending())
            sortOrder = WebInspector.DataGrid.Order.Descending;

        if (this._sortColumnCell)
            this._sortColumnCell.classList.remove(WebInspector.DataGrid.Order.Ascending, WebInspector.DataGrid.Order.Descending);
        this._sortColumnCell = cell;

        cell.classList.add(sortOrder);

        this.dispatchEventToListeners(WebInspector.DataGrid.Events.SortingChanged);
    },

    /**
     * @param {string} columnIdentifier
     * @param {!WebInspector.DataGrid.Order} sortOrder
     */
    markColumnAsSortedBy: function(columnIdentifier, sortOrder)
    {
        if (this._sortColumnCell)
            this._sortColumnCell.classList.remove(WebInspector.DataGrid.Order.Ascending, WebInspector.DataGrid.Order.Descending);
        this._sortColumnCell = this._headerTableHeaders[columnIdentifier];
        this._sortColumnCell.classList.add(sortOrder);
    },

    /**
     * @param {string} columnIdentifier
     * @return {!Element}
     */
    headerTableHeader: function(columnIdentifier)
    {
        return this._headerTableHeaders[columnIdentifier];
    },

    _mouseDownInDataTable: function(event)
    {
        var gridNode = this.dataGridNodeFromNode(event.target);
        if (!gridNode || !gridNode.selectable)
            return;

        if (gridNode.isEventWithinDisclosureTriangle(event))
            return;

        var columnIdentifier = this.columnIdentifierFromNode(event.target);
        if (columnIdentifier && this._columns[columnIdentifier].nonSelectable)
            return;

        if (event.metaKey) {
            if (gridNode.selected)
                gridNode.deselect();
            else
                gridNode.select();
        } else
            gridNode.select();
    },

    _contextMenuInDataTable: function(event)
    {
        var contextMenu = new WebInspector.ContextMenu(event);

        var gridNode = this.dataGridNodeFromNode(event.target);
        if (this._refreshCallback && (!gridNode || gridNode !== this.creationNode))
            contextMenu.appendItem(WebInspector.UIString("Refresh"), this._refreshCallback.bind(this));

        if (gridNode && gridNode.selectable && !gridNode.isEventWithinDisclosureTriangle(event)) {
            if (this._editCallback) {
                if (gridNode === this.creationNode)
                    contextMenu.appendItem(WebInspector.UIString.capitalize("Add ^new"), this._startEditing.bind(this, event.target));
                else {
                    var columnIdentifier = this.columnIdentifierFromNode(event.target);
                    if (columnIdentifier && this._columns[columnIdentifier].editable)
                        contextMenu.appendItem(WebInspector.UIString("Edit \"%s\"", this._columns[columnIdentifier].title), this._startEditing.bind(this, event.target));
                }
            }
            if (this._deleteCallback && gridNode !== this.creationNode)
                contextMenu.appendItem(WebInspector.UIString.capitalize("Delete"), this._deleteCallback.bind(this, gridNode));
            if (this._contextMenuCallback)
                this._contextMenuCallback(contextMenu, gridNode);
        }

        contextMenu.show();
    },

    _clickInDataTable: function(event)
    {
        var gridNode = this.dataGridNodeFromNode(event.target);
        if (!gridNode || !gridNode.hasChildren)
            return;

        if (!gridNode.isEventWithinDisclosureTriangle(event))
            return;

        if (gridNode.expanded) {
            if (event.altKey)
                gridNode.collapseRecursively();
            else
                gridNode.collapse();
        } else {
            if (event.altKey)
                gridNode.expandRecursively();
            else
                gridNode.expand();
        }
    },

    /**
     * @param {!WebInspector.DataGrid.ResizeMethod} method
     */
    setResizeMethod: function(method)
    {
        this._resizeMethod = method;
    },

    /**
     * @return {boolean}
     */
    _startResizerDragging: function(event)
    {
        this._currentResizer = event.target;
        return true;
    },

    _resizerDragging: function(event)
    {
        var resizer = this._currentResizer;
        if (!resizer)
            return;

        // Constrain the dragpoint to be within the containing div of the
        // datagrid.
        var dragPoint = event.clientX - this.element.totalOffsetLeft();
        var firstRowCells = this.headerTableBody.rows[0].cells;
        var leftEdgeOfPreviousColumn = 0;
        // Constrain the dragpoint to be within the space made up by the
        // column directly to the left and the column directly to the right.
        var leftCellIndex = resizer.__index;
        var rightCellIndex = leftCellIndex + 1;
        for (var i = 0; i < leftCellIndex; i++)
            leftEdgeOfPreviousColumn += firstRowCells[i].offsetWidth;

        // Differences for other resize methods
        if (this._resizeMethod === WebInspector.DataGrid.ResizeMethod.Last) {
            rightCellIndex = this._resizers.length;
        } else if (this._resizeMethod === WebInspector.DataGrid.ResizeMethod.First) {
            leftEdgeOfPreviousColumn += firstRowCells[leftCellIndex].offsetWidth - firstRowCells[0].offsetWidth;
            leftCellIndex = 0;
        }

        var rightEdgeOfNextColumn = leftEdgeOfPreviousColumn + firstRowCells[leftCellIndex].offsetWidth + firstRowCells[rightCellIndex].offsetWidth;

        // Give each column some padding so that they don't disappear.
        var leftMinimum = leftEdgeOfPreviousColumn + this.ColumnResizePadding;
        var rightMaximum = rightEdgeOfNextColumn - this.ColumnResizePadding;
        if (leftMinimum > rightMaximum)
            return;

        dragPoint = Number.constrain(dragPoint, leftMinimum, rightMaximum);

        var position = (dragPoint - this.CenterResizerOverBorderAdjustment);
        resizer.__position = position;
        resizer.style.left = position + "px";

        this._setPreferredWidth(leftCellIndex, dragPoint - leftEdgeOfPreviousColumn);
        this._setPreferredWidth(rightCellIndex, rightEdgeOfNextColumn - dragPoint);

        var leftColumn = this._visibleColumnsArray[leftCellIndex];
        var rightColumn = this._visibleColumnsArray[rightCellIndex];
        if (leftColumn.weight || rightColumn.weight) {
            var sumOfWeights = leftColumn.weight + rightColumn.weight;
            var delta = rightEdgeOfNextColumn - leftEdgeOfPreviousColumn;
            leftColumn.weight = (dragPoint - leftEdgeOfPreviousColumn) * sumOfWeights / delta;
            rightColumn.weight = (rightEdgeOfNextColumn - dragPoint) * sumOfWeights / delta;
        }

        this._positionResizers();
        event.preventDefault();
        this.dispatchEventToListeners(WebInspector.DataGrid.Events.ColumnsResized);
    },

    /**
     * @param {number} columnIndex
     * @param {number} width
     */
    _setPreferredWidth: function(columnIndex, width)
    {
        var pxWidth = width + "px";
        this._headerTableColumnGroup.children[columnIndex][WebInspector.DataGrid._preferredWidthSymbol] = width;
        this._headerTableColumnGroup.children[columnIndex].style.width = pxWidth;
        this._dataTableColumnGroup.children[columnIndex].style.width = pxWidth;
    },

    /**
     * @param {string} columnId
     * @return {number}
     */
    columnOffset: function(columnId)
    {
        if (!this.element.offsetWidth)
            return 0;
        for (var i = 1; i < this._visibleColumnsArray.length; ++i) {
            if (columnId === this._visibleColumnsArray[i].identifier) {
                if (this._resizers[i - 1])
                    return this._resizers[i - 1].__position;
            }
        }
        return 0;
    },

    _endResizerDragging: function(event)
    {
        this._currentResizer = null;
        this._saveColumnWeights();
        this.dispatchEventToListeners(WebInspector.DataGrid.Events.ColumnsResized);
    },

    /**
     * @return {!WebInspector.DataGridWidget}
     */
    asWidget: function()
    {
        if (!this._dataGridWidget)
            this._dataGridWidget = new WebInspector.DataGridWidget(this);
        return this._dataGridWidget;
    },

    ColumnResizePadding: 24,

    CenterResizerOverBorderAdjustment: 3,

    __proto__: WebInspector.Object.prototype
}

/** @enum {string} */
WebInspector.DataGrid.ResizeMethod = {
    Nearest: "nearest",
    First: "first",
    Last: "last"
}

/**
 * @constructor
 * @extends {WebInspector.Object}
 * @param {?Object.<string, *>=} data
 * @param {boolean=} hasChildren
 */
WebInspector.DataGridNode = function(data, hasChildren)
{
    /** @type {?Element} */
    this._element = null;
    /** @type {boolean} */
    this._expanded = false;
    /** @type {boolean} */
    this._selected = false;
    /** @type {number|undefined} */
    this._depth;
    /** @type {boolean|undefined} */
    this._revealed;
    /** @type {boolean} */
    this._attached = false;
    /** @type {?{parent: !WebInspector.DataGridNode, index: number}} */
    this._savedPosition = null;
    /** @type {boolean} */
    this._shouldRefreshChildren = true;
    /** @type {!Object.<string, *>} */
    this._data = data || {};
    /** @type {boolean} */
    this.hasChildren = hasChildren || false;
    /** @type {!Array.<!WebInspector.DataGridNode>} */
    this.children = [];
    /** @type {?WebInspector.DataGrid} */
    this.dataGrid = null;
    /** @type {?WebInspector.DataGridNode} */
    this.parent = null;
    /** @type {?WebInspector.DataGridNode} */
    this.previousSibling = null;
    /** @type {?WebInspector.DataGridNode} */
    this.nextSibling = null;
    /** @type {number} */
    this.disclosureToggleWidth = 10;
}

WebInspector.DataGridNode.prototype = {
    /** @type {boolean} */
    selectable: true,

    /** @type {boolean} */
    _isRoot: false,

    /**
     * @return {!Element}
     */
    element: function()
    {
        if (!this._element) {
            this.createElement();
            this.createCells();
        }
        return /** @type {!Element} */ (this._element);
    },

    /**
     * @protected
     */
    createElement: function()
    {
        this._element = createElement("tr");
        this._element._dataGridNode = this;

        if (this.hasChildren)
            this._element.classList.add("parent");
        if (this.expanded)
            this._element.classList.add("expanded");
        if (this.selected)
            this._element.classList.add("selected");
        if (this.revealed)
            this._element.classList.add("revealed");
    },

    /**
     * @protected
     */
    createCells: function()
    {
        this._element.removeChildren();
        var columnsArray = this.dataGrid._visibleColumnsArray;
        for (var i = 0; i < columnsArray.length; ++i)
            this._element.appendChild(this.createCell(columnsArray[i].identifier || String(i)));
        this._element.appendChild(this._createTDWithClass("corner"));
    },

    get data()
    {
        return this._data;
    },

    set data(x)
    {
        this._data = x || {};
        this.refresh();
    },

    get revealed()
    {
        if (this._revealed !== undefined)
            return this._revealed;

        var currentAncestor = this.parent;
        while (currentAncestor && !currentAncestor._isRoot) {
            if (!currentAncestor.expanded) {
                this._revealed = false;
                return false;
            }

            currentAncestor = currentAncestor.parent;
        }

        this._revealed = true;
        return true;
    },

    set hasChildren(x)
    {
        if (this._hasChildren === x)
            return;

        this._hasChildren = x;

        if (!this._element)
            return;

        this._element.classList.toggle("parent", this._hasChildren);
        this._element.classList.toggle("expanded", this._hasChildren && this.expanded);
    },

    get hasChildren()
    {
        return this._hasChildren;
    },

    set revealed(x)
    {
        if (this._revealed === x)
            return;

        this._revealed = x;

        if (this._element)
            this._element.classList.toggle("revealed", this._revealed);

        for (var i = 0; i < this.children.length; ++i)
            this.children[i].revealed = x && this.expanded;
    },

    /**
     * @return {number}
     */
    get depth()
    {
        if (this._depth !== undefined)
            return this._depth;
        if (this.parent && !this.parent._isRoot)
            this._depth = this.parent.depth + 1;
        else
            this._depth = 0;
        return this._depth;
    },

    get leftPadding()
    {
        return this.depth * this.dataGrid.indentWidth;
    },

    get shouldRefreshChildren()
    {
        return this._shouldRefreshChildren;
    },

    set shouldRefreshChildren(x)
    {
        this._shouldRefreshChildren = x;
        if (x && this.expanded)
            this.expand();
    },

    get selected()
    {
        return this._selected;
    },

    set selected(x)
    {
        if (x)
            this.select();
        else
            this.deselect();
    },

    get expanded()
    {
        return this._expanded;
    },

    /**
     * @param {boolean} x
     */
    set expanded(x)
    {
        if (x)
            this.expand();
        else
            this.collapse();
    },

    refresh: function()
    {
        if (!this.dataGrid)
            this._element = null;
        if (!this._element)
            return;
        this.createCells();
    },

    /**
     * @param {string} className
     * @return {!Element}
     */
    _createTDWithClass: function(className)
    {
        var cell = createElementWithClass("td", className);
        var cellClass = this.dataGrid._cellClass;
        if (cellClass)
            cell.classList.add(cellClass);
        return cell;
    },

    /**
     * @param {string} columnIdentifier
     * @return {!Element}
     */
    createTD: function(columnIdentifier)
    {
        var cell = this._createTDWithClass(columnIdentifier + "-column");
        cell.columnIdentifier_ = columnIdentifier;

        var alignment = this.dataGrid._columns[columnIdentifier].align;
        if (alignment)
            cell.classList.add(alignment);

        if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) {
            cell.classList.add("disclosure");
            if (this.leftPadding)
                cell.style.setProperty("padding-left", this.leftPadding + "px");
        }

        return cell;
    },

    /**
     * @param {string} columnIdentifier
     * @return {!Element}
     */
    createCell: function(columnIdentifier)
    {
        var cell = this.createTD(columnIdentifier);

        var data = this.data[columnIdentifier];
        if (data instanceof Node) {
            cell.appendChild(data);
        } else {
            cell.textContent = data;
            if (this.dataGrid._columns[columnIdentifier].longText)
                cell.title = data;
        }

        return cell;
    },

    /**
     * @return {number}
     */
    nodeSelfHeight: function()
    {
        return 16;
    },

    /**
     * @param {!WebInspector.DataGridNode} child
     */
    appendChild: function(child)
    {
        this.insertChild(child, this.children.length);
    },

    /**
     * @param {!WebInspector.DataGridNode} child
     * @param {number} index
     */
    insertChild: function(child, index)
    {
        if (!child)
            throw("insertChild: Node can't be undefined or null.");
        if (child.parent === this) {
            var currentIndex = this.children.indexOf(child);
            if (currentIndex < 0)
                console.assert(false, "Inconsistent DataGrid state");
            if (currentIndex === index)
                return;
            if (currentIndex < index)
                --index;
        }

        child.remove();

        this.children.splice(index, 0, child);
        this.hasChildren = true;

        child.parent = this;
        child.dataGrid = this.dataGrid;
        child.recalculateSiblings(index);

        child._depth = undefined;
        child._revealed = undefined;
        child._attached = false;
        child._shouldRefreshChildren = true;

        var current = child.children[0];
        while (current) {
            current.dataGrid = this.dataGrid;
            current._depth = undefined;
            current._revealed = undefined;
            current._attached = false;
            current._shouldRefreshChildren = true;
            current = current.traverseNextNode(false, child, true);
        }

        if (this.expanded)
            child._attach();
        if (!this.revealed)
            child.revealed = false;
    },

    remove: function()
    {
        if (this.parent)
            this.parent.removeChild(this);
    },

    /**
     * @param {!WebInspector.DataGridNode} child
     */
    removeChild: function(child)
    {
        if (!child)
            throw("removeChild: Node can't be undefined or null.");
        if (child.parent !== this)
            throw("removeChild: Node is not a child of this node.");

        if (this.dataGrid)
            this.dataGrid.updateSelectionBeforeRemoval(child, false);
        child._detach();

        this.children.remove(child, true);

        if (child.previousSibling)
            child.previousSibling.nextSibling = child.nextSibling;
        if (child.nextSibling)
            child.nextSibling.previousSibling = child.previousSibling;

        child.dataGrid = null;
        child.parent = null;
        child.nextSibling = null;
        child.previousSibling = null;

        if (this.children.length <= 0)
            this.hasChildren = false;
    },

    removeChildren: function()
    {
        if (this.dataGrid)
            this.dataGrid.updateSelectionBeforeRemoval(this, true);
        for (var i = 0; i < this.children.length; ++i) {
            var child = this.children[i];
            child._detach();
            child.dataGrid = null;
            child.parent = null;
            child.nextSibling = null;
            child.previousSibling = null;
        }

        this.children = [];
        this.hasChildren = false;
    },

    /**
     * @param {number} myIndex
     */
    recalculateSiblings: function(myIndex)
    {
        if (!this.parent)
            return;

        var previousChild = this.parent.children[myIndex - 1] || null;
        if (previousChild)
            previousChild.nextSibling = this;
        this.previousSibling = previousChild;

        var nextChild = this.parent.children[myIndex + 1] || null;
        if (nextChild)
            nextChild.previousSibling = this;
        this.nextSibling = nextChild;
    },

    collapse: function()
    {
        if (this._isRoot)
            return;
        if (this._element)
            this._element.classList.remove("expanded");

        this._expanded = false;

        for (var i = 0; i < this.children.length; ++i)
            this.children[i].revealed = false;
    },

    collapseRecursively: function()
    {
        var item = this;
        while (item) {
            if (item.expanded)
                item.collapse();
            item = item.traverseNextNode(false, this, true);
        }
    },

    populate: function() { },

    expand: function()
    {
        if (!this.hasChildren || this.expanded)
            return;
        if (this._isRoot)
            return;

        if (this.revealed && !this._shouldRefreshChildren)
            for (var i = 0; i < this.children.length; ++i)
                this.children[i].revealed = true;

        if (this._shouldRefreshChildren) {
            for (var i = 0; i < this.children.length; ++i)
                this.children[i]._detach();

            this.populate();

            if (this._attached) {
                for (var i = 0; i < this.children.length; ++i) {
                    var child = this.children[i];
                    if (this.revealed)
                        child.revealed = true;
                    child._attach();
                }
            }

            this._shouldRefreshChildren = false;
        }

        if (this._element)
            this._element.classList.add("expanded");

        this._expanded = true;
    },

    expandRecursively: function()
    {
        var item = this;
        while (item) {
            item.expand();
            item = item.traverseNextNode(false, this);
        }
    },

    reveal: function()
    {
        if (this._isRoot)
            return;
        var currentAncestor = this.parent;
        while (currentAncestor && !currentAncestor._isRoot) {
            if (!currentAncestor.expanded)
                currentAncestor.expand();
            currentAncestor = currentAncestor.parent;
        }

        this.element().scrollIntoViewIfNeeded(false);
    },

    /**
     * @param {boolean=} supressSelectedEvent
     */
    select: function(supressSelectedEvent)
    {
        if (!this.dataGrid || !this.selectable || this.selected)
            return;

        if (this.dataGrid.selectedNode)
            this.dataGrid.selectedNode.deselect();

        this._selected = true;
        this.dataGrid.selectedNode = this;

        if (this._element)
            this._element.classList.add("selected");

        if (!supressSelectedEvent)
            this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Events.SelectedNode);
    },

    revealAndSelect: function()
    {
        if (this._isRoot)
            return;
        this.reveal();
        this.select();
    },

    /**
     * @param {boolean=} supressDeselectedEvent
     */
    deselect: function(supressDeselectedEvent)
    {
        if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected)
            return;

        this._selected = false;
        this.dataGrid.selectedNode = null;

        if (this._element)
            this._element.classList.remove("selected");

        if (!supressDeselectedEvent)
            this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Events.DeselectedNode);
    },

    /**
     * @param {boolean} skipHidden
     * @param {?WebInspector.DataGridNode=} stayWithin
     * @param {boolean=} dontPopulate
     * @param {!Object=} info
     * @return {?WebInspector.DataGridNode}
     */
    traverseNextNode: function(skipHidden, stayWithin, dontPopulate, info)
    {
        if (!dontPopulate && this.hasChildren)
            this.populate();

        if (info)
            info.depthChange = 0;

        var node = (!skipHidden || this.revealed) ? this.children[0] : null;
        if (node && (!skipHidden || this.expanded)) {
            if (info)
                info.depthChange = 1;
            return node;
        }

        if (this === stayWithin)
            return null;

        node = (!skipHidden || this.revealed) ? this.nextSibling : null;
        if (node)
            return node;

        node = this;
        while (node && !node._isRoot && !((!skipHidden || node.revealed) ? node.nextSibling : null) && node.parent !== stayWithin) {
            if (info)
                info.depthChange -= 1;
            node = node.parent;
        }

        if (!node)
            return null;

        return (!skipHidden || node.revealed) ? node.nextSibling : null;
    },

    /**
     * @param {boolean} skipHidden
     * @param {boolean=} dontPopulate
     * @return {?WebInspector.DataGridNode}
     */
    traversePreviousNode: function(skipHidden, dontPopulate)
    {
        var node = (!skipHidden || this.revealed) ? this.previousSibling : null;
        if (!dontPopulate && node && node.hasChildren)
            node.populate();

        while (node && ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null)) {
            if (!dontPopulate && node.hasChildren)
                node.populate();
            node = ((!skipHidden || (node.revealed && node.expanded)) ? node.children[node.children.length - 1] : null);
        }

        if (node)
            return node;

        if (!this.parent || this.parent._isRoot)
            return null;

        return this.parent;
    },

    /**
     * @return {boolean}
     */
    isEventWithinDisclosureTriangle: function(event)
    {
        if (!this.hasChildren)
            return false;
        var cell = event.target.enclosingNodeOrSelfWithNodeName("td");
        if (!cell || !cell.classList.contains("disclosure"))
            return false;

        var left = cell.totalOffsetLeft() + this.leftPadding;
        return event.pageX >= left && event.pageX <= left + this.disclosureToggleWidth;
    },

    _attach: function()
    {
        if (!this.dataGrid || this._attached)
            return;

        this._attached = true;

        var previousNode = this.traversePreviousNode(true, true);
        var previousElement = previousNode ? previousNode.element() : this.dataGrid._topFillerRow;
        this.dataGrid.dataTableBody.insertBefore(this.element(), previousElement.nextSibling);

        if (this.expanded)
            for (var i = 0; i < this.children.length; ++i)
                this.children[i]._attach();
    },

    _detach: function()
    {
        if (!this._attached)
            return;

        this._attached = false;

        if (this._element)
            this._element.remove();

        for (var i = 0; i < this.children.length; ++i)
            this.children[i]._detach();

        this.wasDetached();
    },

    wasDetached: function()
    {
    },

    savePosition: function()
    {
        if (this._savedPosition)
            return;

        if (!this.parent)
            throw("savePosition: Node must have a parent.");
        this._savedPosition = {
            parent: this.parent,
            index: this.parent.children.indexOf(this)
        };
    },

    restorePosition: function()
    {
        if (!this._savedPosition)
            return;

        if (this.parent !== this._savedPosition.parent)
            this._savedPosition.parent.insertChild(this, this._savedPosition.index);

        this._savedPosition = null;
    },

    __proto__: WebInspector.Object.prototype
}

/**
 * @constructor
 * @extends {WebInspector.DataGridNode}
 */
WebInspector.CreationDataGridNode = function(data, hasChildren)
{
    WebInspector.DataGridNode.call(this, data, hasChildren);
    /** @type {boolean} */
    this.isCreationNode = true;
}

WebInspector.CreationDataGridNode.prototype = {
    makeNormal: function()
    {
        this.isCreationNode = false;
    },

    __proto__: WebInspector.DataGridNode.prototype
}

/**
 * @constructor
 * @extends {WebInspector.VBox}
 * @param {!WebInspector.DataGrid} dataGrid
 */
WebInspector.DataGridWidget = function(dataGrid)
{
    WebInspector.VBox.call(this);
    this._dataGrid = dataGrid;
    this.element.appendChild(dataGrid.element);
}

WebInspector.DataGridWidget.prototype = {
    /**
     * @override
     */
    wasShown: function()
    {
        this._dataGrid.wasShown();
    },

    /**
     * @override
     */
    willHide: function()
    {
        this._dataGrid.willHide();
    },

    /**
     * @override
     */
    onResize: function()
    {
        this._dataGrid.onResize();
    },

    /**
     * @override
     * @return {!Array.<!Element>}
     */
    elementsToRestoreScrollPositionsFor: function()
    {
        return [ this._dataGrid._scrollContainer ];
    },

    /**
     * @override
     */
    detachChildWidgets: function()
    {
        WebInspector.Widget.prototype.detachChildWidgets.call(this);
        for (var dataGrid of this._dataGrids)
            this.element.removeChild(dataGrid.element);
        this._dataGrids = [];
    },

    __proto__: WebInspector.VBox.prototype
}


/*!
 * Module dependencies
 */
var util = require('util')
  , Resource = require('./')
  , updateMixin = require('../mixins/update')
  ;

/*!
 * Expose `UserPreference()`.
 */
exports = module.exports = UserPreference;

/**
 * Initialize a new `UserPreference` with the given `parent` and `definition`.
 *
 * @param {Function} parent The parent this resource is attached to.
 * @param {Object} definition The resource definition.
 * @api private
 */
function UserPreference(parent, definition) {
  var key;
  for (key in updateMixin) {
    this[key] = updateMixin[key];
  }
  UserPreference.super_.apply(this, arguments);
}
util.inherits(UserPreference, Resource);