<commit_before>'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import url from 'url';
import _ from 'underscore';
import ReactTooltip from 'react-tooltip';
var serialize = require('form-serialize');
var jwt = require('jsonwebtoken');
import jsonScriptEscape from '../libs/jsonScriptEscape';
import * as globals from './globals';
import ErrorPage from './static-pages/ErrorPage';
import Navigation from './navigation';
import SubmissionView from './submission/submission-view';
import Footer from './footer';
import * as store from '../store';
import * as origin from '../libs/origin';
import { Filters, ajax, JWT, console, isServerSide, navigate, analytics } from './util';
import Alerts from './alerts';
import { FacetCharts } from './facetcharts';
import { ChartDataController } from './viz/chart-data-controller';
import ChartDetailCursor from './viz/ChartDetailCursor';
import { getTitleStringFromContext } from './item-pages/item';

/**
 * The top-level component for this application.
 *
 * @module {Component} app
 */

/**
 * Used to temporarily store Redux store values for simultaneous dispatch.
 *
 * @memberof module:app
 */
let dispatch_dict = {};

/**
 * Top bar navigation & link schema definition.
 *
 * @memberof module:app
 */
const portal = {
    portal_title: '4DN Data Portal',
    global_sections: [
        {
            id: 'browse',
            sid:'sBrowse',
            title: 'Browse',
            //url: '/browse/?type=ExperimentSetReplicate&experimentset_type=replicate&limit=all',
            url : function(currentUrlParts){
                if (!currentUrlParts) return '/browse/?type=ExperimentSetReplicate&experimentset_type=replicate&limit=all'; // Default/fallback
                return Filters.filtersToHref(
                    store.getState().expSetFilters,
                    currentUrlParts.protocol + '//' + currentUrlParts.host + '/browse/'
                );
            },
            active : function(currentWindowPath){
                if (currentWindowPath && currentWindowPath.indexOf('/browse/') > -1) return true;
                return false;
            }
        },
        {id: 'help', sid:'sHelp', title: 'Help', children: [
            {id: 'gettingstarted', title: 'Getting started', url: '/help', children : [
                {id: 'metadatastructure', title: 'Metadata structure', url: '/help#metadata-structure'},
                {id: 'datasubmission', title: 'Data submission', url: '/help#data-submission'},
                {id: 'restapi', title: 'REST API', url: '/help#rest-api'},
            ]},
            {id: 'submitting-metadata', title: 'Submitting Metadata', url: '/help/submitting'},
            {id: 'about', title: 'About', url: '/about'}
        ]}
    ],
    user_section: [
            {id: 'login', title: 'Log in', url: '/'},
            {id: 'accountactions', title: 'Register', url: '/help'}
            // Remove context actions for now{id: 'contextactions', title: 'Actions', url: '/'}
    ]
};


// See https://github.com/facebook/react/issues/2323
class Title extends React.Component {

    constructor(props){
        super(props);
        this.componentDidMount = this.componentDidMount.bind(this);
        this.render = this.render.bind(this);
    }

    componentDidMount() {
        var node = document.querySelector('title');
        if (node && !node.getAttribute('data-reactid')) {
            node.setAttribute('data-reactid', this._rootNodeID);
        }
    }

    render() {
        return <title {...this.props}>{this.props.children}</title>;
    }

}


/**
 * Creates a promise which completes after a delay, performing no network request.
 * Used to perform a promise.race to see if this timeout or a network requests completes first, which
 * then allows us to set app.state.slow and render a loading icon until long-running network request completes.
 */
class Timeout {
    constructor(timeout) {
        this.promise = new Promise(resolve => setTimeout(resolve.bind(undefined, this), timeout));
    }
}

/**
 * @alias module:app
 */


export default class App extends React.Component {

    static SLOW_REQUEST_TIME = 750

    static contentTypeIsJSON(content) {
        var isJson = true;
        try{
            var json = JSON.parse(JSON.stringify(content));
        }catch(err){
            isJson = false;
        }
        return isJson;
    }

    static scrollTo() {
        var hash = window.location.hash;
        if (hash && document.getElementById(hash.slice(1))) {
            window.location.replace(hash);
        } else {
            window.scrollTo(0, 0);
        }
    }

    static getRenderedPropValues(document, filter = null){
        var returnObj = {};
        var script_props;
        if (typeof filter === 'string') script_props = document.querySelectorAll('script[data-prop-name="' + filter + '"]');
        else script_props = document.querySelectorAll('script[data-prop-name]');
        for (var i = 0; i < script_props.length; i++) {
            var elem = script_props[i];
            var prop_name = elem.getAttribute('data-prop-name');
            if (filter && Array.isArray(filter)){
                if (filter.indexOf(prop_name) === -1) continue;
            }
            var elem_value = elem.text;
            var elem_type = elem.getAttribute('type') || '';
            if (elem_type == 'application/json' || elem_type.slice(-5) == '+json') {
                elem_value = JSON.parse(elem_value);
            }
            //if (elem.getAttribute('data-prop-name') === 'user_details' && !filter){
                // pass; don't include as is not a redux prop
            //} else {
                returnObj[prop_name] = elem_value;
            //}
        }
        return returnObj;
    }

    static getRenderedProps(document, filters = null) {
        return _.extend(App.getRenderedPropValues(document, filters), {
            'href' : document.querySelector('link[rel="canonical"]').getAttribute('href') // Ensure the initial render is exactly the same
        });
    }

    static propTypes = {
        "sessionMayBeSet" : PropTypes.any,    // Whether Auth0 session exists or not.
    }

    static defaultProps = {
        'sessionMayBeSet' : null
    }

    static childContextTypes = {
        dropdownComponent: React.PropTypes.string,
        listActionsFor: React.PropTypes.func,
        currentResource: React.PropTypes.func,
        location_href: React.PropTypes.string,
        onDropdownChange: React.PropTypes.func,
        portal: React.PropTypes.object,
        hidePublicAudits: React.PropTypes.bool,
        fetch: React.PropTypes.func,
        session: React.PropTypes.bool,
        navigate: React.PropTypes.func,
        contentTypeIsJSON: React.PropTypes.func,
        updateUserInfo: React.PropTypes.func,
        schemas: React.PropTypes.object
    }

    constructor(props){
        super(props);
        this.componentDidMount = this.componentDidMount.bind(this);
        this.componentDidUpdate = this.componentDidUpdate.bind(this);
        this.fetch = this.fetch.bind(this);
        this.getChildContext = this.getChildContext.bind(this);
        this.listActionsFor = this.listActionsFor.bind(this);
        this.currentResource = this.currentResource.bind(this);
        this.currentAction = this.currentAction.bind(this);
        this.loadSchemas = this.loadSchemas.bind(this);
        this.getStatsComponent = this.getStatsComponent.bind(this);
        this.updateStats = this.updateStats.bind(this);

        // Global event handlers. These will catch events unless they are caught and prevented from bubbling up earlier.
        this.handleDropdownChange = this.handleDropdownChange.bind(this);
        this.handleAutocompleteChosenChange = this.handleAutocompleteChosenChange.bind(this);
        this.handleAutocompleteFocusChange = this.handleAutocompleteFocusChange.bind(this);
        this.handleAutocompleteHiddenChange = this.handleAutocompleteHiddenChange.bind(this);
        this.handleLayoutClick = this.handleLayoutClick.bind(this);
        this.handleKey = this.handleKey.bind(this);
        this.handleClick = this.handleClick.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.handlePopState = this.handlePopState.bind(this);
        this.setIsSubmitting = this.setIsSubmitting.bind(this);
        this.stayOnSubmissionsPage = this.stayOnSubmissionsPage.bind(this);
        this.authenticateUser = this.authenticateUser.bind(this);
        this.updateUserInfo = this.updateUserInfo.bind(this);
        this.confirmNavigation = this.confirmNavigation.bind(this);
        this.navigate = this.navigate.bind(this);
        this.handleBeforeUnload = this.handleBeforeUnload.bind(this);
        this.render = this.render.bind(this);

        console.log('APP FILTERS', Filters.hrefToFilters(props.href));

        this.historyEnabled = !!(typeof window != 'undefined' && window.history && window.history.pushState);

        // Todo: Migrate session & user_actions to redux store?
        var session = false;
        var user_actions = [];

        if (props.sessionMayBeSet !== null){ // Only provided from server
            if (props.sessionMayBeSet === false) session = false;
            if (props.sessionMayBeSet === true) session = true;
            else session = false; // Else is null
        } else {
            session = !!(JWT.get('cookie')); // Same cookie sent to server-side to authenticate, so it must match.
        }

        // user_info.details is kept in sync to client-side via browser.js, user_info.user_actions is not.
        // Don't use user_actions unless session is also true.
        // user_actions is only set client-side upon login (it cannot expire unless logout).
        var user_info = JWT.getUserInfo();
        if (user_info && typeof user_info.user_actions !== 'undefined' && Array.isArray(user_info.user_actions)){
            user_actions = user_info.user_actions;
        }

        // Save navigate fxn and other req'd stuffs to GLOBAL navigate obj.
        // So that we may call it from anywhere if necessary without passing through props.
        navigate.setNavigateFunction(this.navigate);

        console.log("App Initial State: ", session, user_actions);

        this.state = {
            'errors': [],
            'dropdownComponent': undefined,
            'content': undefined,
            'session': session,
            'user_actions': user_actions,
            'schemas': this.props.context.schemas || null,
            'isSubmitting': false
        };
    }

    // Once the app component is mounted, bind keydowns to handleKey function
    componentDidMount() {
        globals.bindEvent(window, 'keydown', this.handleKey);

        this.authenticateUser();
        // Load schemas into app.state, access them where needed via props (preferred, safer) or this.context.
        this.loadSchemas();

        // The href prop we have was from serverside. It would not have a hash in it, and might be shortened.
        // Here we grab full-length href from window and update props.href (via Redux), if it is different.
        var query_href;
        // Technically these two statements should be exact same. Props.href is put into <link...> (see render() ). w.e.
        if (document.querySelector('link[rel="canonical"]')){
            query_href = document.querySelector('link[rel="canonical"]').getAttribute('href');
        } else {
            query_href = this.props.href;
        }
        // Grab window.location.href w/ query_href as fallback. Remove hash if need to.
        query_href = globals.maybeRemoveHash(globals.windowHref(query_href));
        if (this.props.href !== query_href){
            store.dispatch({
                type: {'href':query_href}
            });
        }

        // If the window href has a hash, which SHOULD NOT remain (!== globals.maybeRemoveHash()), strip it on mount to match app's props.href.
        var parts = url.parse(query_href);
        if (
            typeof window.location.hash === 'string' &&
            window.location.hash.length > 0 &&
            (!parts.hash || parts.hash === '')
        ){
            window.location.hash = '';
        }


        if (this.historyEnabled) {
            var data = this.props.context;
            try {
                window.history.replaceState(data, '', window.location.href);
            } catch (exc) {
                // Might fail due to too large data
                window.history.replaceState(null, '', window.location.href);
            }
            // Avoid popState on load, see: http://stackoverflow.com/q/6421769/199100
            var register = window.addEventListener.bind(window, 'popstate', this.handlePopState, true);
            if (window._onload_event_fired) {
                register();
            } else {
                window.addEventListener('load', setTimeout.bind(window, register));
            }
        } else {
            window.onhashchange = this.onHashChange;
        }
        window.onbeforeunload = this.handleBeforeUnload;

        // Load up analytics
        analytics.initializeGoogleAnalytics(
            analytics.getTrackingId(this.props.href),
            this.props.context,
            this.props.expSetFilters
        );
    }

    componentDidUpdate(prevProps, prevState) {
        var key;
        if (this.props) {

            if (this.props.href !== prevProps.href){ // We navigated somewhere else.

                // Register google analytics pageview event.
                analytics.registerPageView(this.props.href, this.props.context, this.props.expSetFilters);

                // We need to rebuild tooltips after navigation to a different page.
                ReactTooltip.rebuild();

            }


            for (key in this.props) {
                if (this.props[key] !== prevProps[key]) {
                    console.log('changed props: %s', key);
                }
            }
        }
        if (this.state) {
            if (prevState.session !== this.state.session && ChartDataController.isInitialized()){
                setTimeout(function(){
                    // Delay 100ms.
                    console.log("SYNCING CHART DATA");
                    ChartDataController.sync();
                }, 100);
            }
            for (key in this.state) {
                if (this.state[key] !== prevState[key]) {
                    console.log('changed state: %s', key);
                }
            }
        }
    }

    // functions previously in persona, mixins.js
    fetch(url, options) {
        options = _.extend({credentials: 'same-origin'}, options);
        var http_method = options.method || 'GET';
        var headers = options.headers = _.extend({}, options.headers);
        // Strip url fragment.
        var url_hash = url.indexOf('#');
        if (url_hash > -1) {
            url = url.slice(0, url_hash);
        }
        var data = options.body ? options.body : null;
        var request = ajax.promise(url, http_method, headers, data, options.cache === false ? false : true);
        request.xhr_begin = 1 * new Date();
        request.then(response => {
            request.xhr_end = 1 * new Date();
        });
        return request;
    }

    // Retrieve current React context
    getChildContext() {
        return {
            dropdownComponent: this.state.dropdownComponent, // ID of component with visible dropdown
            listActionsFor: this.listActionsFor,
            currentResource: this.currentResource,
            location_href: this.props.href,
            onDropdownChange: this.handleDropdownChange, // Function to process dropdown state change
            portal: portal,
            hidePublicAudits: true, // True if audits should be hidden on the UI while logged out
            fetch: this.fetch,
            session: this.state.session,
            navigate: this.navigate,
            contentTypeIsJSON: App.contentTypeIsJSON,
            updateUserInfo: this.updateUserInfo,
            schemas : this.state.schemas
        };
    }

    listActionsFor(category) {
        if (category === 'context') {
            var context = this.currentResource();
            var name = this.currentAction();
            var context_actions = [];
            Array.prototype.push.apply(context_actions, context.actions || []);

            if (!name && context.default_page) {
                context = context.default_page;
                var actions = context.actions || [];
                for (var i = 0; i < actions.length; i++) {
                    var action = actions[i];
                    if (action.href[0] == '#') {
                        action.href = context['@id'] + action.href;
                    }
                    context_actions.push(action);
                }
            }
            return context_actions;
        }
        if (category === 'user_section') {
            return portal.user_section;
        }
        if (category === 'user') {
            return this.state.user_actions;
        }
        if (category === 'global_sections') {
            return portal.global_sections;
        }
    }

    currentResource() { return this.props.context; }

    currentAction() {
        var href_url = url.parse(this.props.href);
        var hash = href_url.hash || '';
        var name;
        if (hash.slice(0, 2) === '#!') {
            name = hash.slice(2);
        }
        return name;
    }

    loadSchemas(callback, forceFetch = false){
        if (this.state.schemas !== null && !forceFetch){
            // We've already loaded these successfully (hopefully)
            if (typeof callback === 'function') callback(this.state.schemas);
            return this.state.schemas;
        }
        ajax.promise('/profiles/?format=json').then(data => {
            if (App.contentTypeIsJSON(data)){
                this.setState({
                    schemas: data
                }, () => {
                    // Let Filters have access to schemas for own functions.
                    Filters.getSchemas = () => this.state.schemas;
                    // Rebuild tooltips because they likely use descriptions from schemas
                    ReactTooltip.rebuild();
                    if (typeof callback === 'function') callback(data);
                });
            }
        });
    }

    getStatsComponent(){
        if (!this.refs || !this.refs.navigation) return null;
        if (!this.refs.navigation.refs) return null;
        if (!this.refs.navigation.refs.stats) return null;
        return this.refs.navigation.refs.stats;
    }

    updateStats(currentCounts, totalCounts = null, callback = null){
        var statsComponent = this.getStatsComponent();
        if (statsComponent){
            if (totalCounts === null){
                return statsComponent.updateCurrentCounts(currentCounts, callback);
            } else {
                return statsComponent.updateCurrentAndTotalCounts(currentCounts, totalCounts, callback);
            }
        }
        return null;
    }

    // When current dropdown changes; componentID is _rootNodeID of newly dropped-down component
    handleDropdownChange(componentID) {
        // Use React _rootNodeID to uniquely identify a dropdown menu;
        // It's passed in as componentID
        this.setState({dropdownComponent: componentID});
    }

    handleAutocompleteChosenChange(chosen) { this.setState({autocompleteTermChosen: chosen}); }

    handleAutocompleteFocusChange(focused) { this.setState({autocompleteFocused: focused}); }

    handleAutocompleteHiddenChange(hidden) { this.setState({autocompleteHidden: hidden}); }

    // Handle a click outside a dropdown menu by clearing currently dropped down menu
    handleLayoutClick(e) {
        if (this.state.dropdownComponent !== undefined) {
            this.setState({dropdownComponent: undefined});
        }
    }

    handleClick(event) {
        // https://github.com/facebook/react/issues/1691
        if (event.isDefaultPrevented()) return;

        var target = event.target;
        var nativeEvent = event.nativeEvent;

        // SVG anchor elements have tagName == 'a' while HTML anchor elements have tagName == 'A'
        while (target && (target.tagName.toLowerCase() != 'a' || target.getAttribute('data-href'))) {
            target = target.parentElement;
        }
        if (!target) return;

        if (target.getAttribute('disabled')) {
            event.preventDefault();
            return;
        }

        // Ensure this is a plain click
        if (nativeEvent.which > 1 || nativeEvent.shiftKey || nativeEvent.altKey || nativeEvent.metaKey) return;

        // Skip links with a data-bypass attribute.
        if (target.getAttribute('data-bypass')) return;

        var href = target.getAttribute('href');
        if (href === null) href = target.getAttribute('data-href');
        if (href === null) return;

        // Skip javascript links
        if (href.indexOf('javascript:') === 0) return;

        // Skip external links
        if (!origin.same(href)) return;

        // Skip links with a different target
        if (target.getAttribute('target')) return;

        // Skip @@download links
        if (href.indexOf('/@@download') != -1) return;

        // Don't cache requests to user profile.
        var navOpts = {};
        if (target.getAttribute('data-no-cache')) navOpts.cache = false;

        // With HTML5 history supported, local navigation is passed
        // through the navigate method.
        if (this.historyEnabled) {
            event.preventDefault();
            this.navigate(href, navOpts);
            if (this.refs && this.refs.navigation){
                this.refs.navigation.closeMobileMenu();
            }
            if (target && target.blur) target.blur();
        }
    }

    // Submitted forms are treated the same as links
    handleSubmit(event) {
        var target = event.target;

        // Skip POST forms
        if (target.method != 'get') return;

        // Skip forms with a data-bypass attribute.
        if (target.getAttribute('data-bypass')) return;

        // Skip external forms
        if (!origin.same(target.action)) return;

        var options = {};
        var action_url = url.parse(url.resolve(this.props.href, target.action));
        options.replace = action_url.pathname == url.parse(this.props.href).pathname;
        var search = serialize(target);
        if (target.getAttribute('data-removeempty')) {
            search = search.split('&').filter(function (item) {
                return item.slice(-1) != '=';
            }).join('&');
        }
        var href = action_url.pathname;
        if (search) {
            href += '?' + search;
        }
        options.skipRequest = target.getAttribute('data-skiprequest');

        if (this.historyEnabled) {
            event.preventDefault();
            this.navigate(href, options);
        }
    }

    handlePopState(event) {
        if (this.DISABLE_POPSTATE) return;
        var href = window.location.href;

        if (!this.confirmPopState(href)){
            window.history.pushState(window.state, '', this.props.href);
            return;
        }

        if (!this.confirmNavigation(href)) {
            //window.history.pushState(window.state, '', this.props.href);
            var d = {
                'href': href
            };
            if (event.state){
                d.context = event.state;
            }
            store.dispatch({
                type: d
            });
            return;
        }
        if (!this.historyEnabled) {
            window.location.reload();
            return;
        }
        var request = this.props.contextRequest;
        if (event.state) {
            // Abort inflight xhr before dispatching
            if (request && this.requestCurrent) {
                // Abort the current request, then remember we've aborted it so that we don't render
                // the Network Request Error page.
                if (request && typeof request.abort === 'function'){
                    request.abort();
                    console.warn("Aborted previous request", request);
                }
                this.requestAborted = true;
                this.requestCurrent = false;
            }
            store.dispatch({
                type: {
                    'href': href,
                    'context': event.state
                }
            });

        }
        // Always async update in case of server side changes.
        this.navigate(href, {'replace': true});
    }

    // If ESC pressed while drop-down menu open, close the menu
    handleKey(e) {
        if (e.which === 27) {
            if (this.state.dropdownComponent !== undefined) {
                e.preventDefault();
                this.handleDropdownChange(undefined);
            } else if (!this.state.autocompleteHidden) {
                e.preventDefault();
                this.handleAutocompleteHiddenChange(true);
            }
        } else if (e.which === 13 && this.state.autocompleteFocused && !this.state.autocompleteTermChosen) {
            e.preventDefault();
        }
    }

    authenticateUser(callback = null){
        // check existing user_info in local storage and authenticate
        var idToken = JWT.get();
        if(idToken && (!this.state.session || !this.state.user_actions)){ // if JWT present, and session not yet set (from back-end), try to authenticate

            ajax.promise('/login', 'POST', {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': 'Bearer '+idToken
            }, JSON.stringify({id_token: idToken}))
            .then(response => {
                if (response.code || response.status || response.id_token !== idToken) throw response;
                return response;
            })
            .then(response => {
                JWT.saveUserInfo(response);
                this.updateUserInfo(callback);
            }, error => {
                // error, clear JWT token from cookie & user_info from localStorage (via JWT.remove())
                // and unset state.session & state.user_actions (via this.updateUserInfo())
                JWT.remove();
                this.updateUserInfo(callback);
            });
            return idToken;
        }
        return null;
    }

    updateUserInfo(callback = null){
        // get user actions (a function of log in) from local storage
        var userActions = [];
        var session = false;
        var userInfo = JWT.getUserInfo();
        if (userInfo){
            userActions = userInfo.user_actions;
            var currentToken = JWT.get(); // We definitively use Cookies for JWT. It can be unset by response headers from back-end.
            if (currentToken) session = true;
            else if (this.state.session === true) {
                Alerts.queue(Alerts.LoggedOut);
            }
        }

        var stateChange = {};
        if (!_.isEqual(userActions, this.state.user_actions)) stateChange.user_actions = userActions;
        if (session != this.state.session) stateChange.session = session;

        if (Object.keys(stateChange).length > 0){
            this.setState(stateChange, typeof callback === 'function' ? callback.bind(this, session, userInfo) : null);
        } else {
            if (typeof callback === 'function') callback(session, userInfo);
        }
    }

    // functions previously in navigate, mixins.js
    onHashChange (event) {
        // IE8/9
        store.dispatch({
            type: {'href':document.querySelector('link[rel="canonical"]').getAttribute('href')}
        });
    }

    /** Rules to prevent browser from changing to 'href' via back/forward buttons. */
    confirmPopState(href){
        if (this.stayOnSubmissionsPage()) return false;
        return true;
    }

    /** Only navigate if href changes */
    confirmNavigation(href, options) {

        // check if user is currently on submission page
        // if so, warn them about leaving
        if (this.stayOnSubmissionsPage()){
            return false;
        }

        if(options && options.inPlace && options.inPlace==true){
            return true;
        }

        if(href===this.props.href){
            return false;
        }

        var partsNew = url.parse(href),
            partsOld = url.parse(this.props.href);

        if (partsNew.path === partsOld.path && globals.isHashPartOfHref(null, partsNew)){
            return false;
        }

        return true;
    }

    // check this.state.isSubmitting to prompt user if navigating away
    // from the submissions page
    stayOnSubmissionsPage() {
        // can override state in options
        // override with replace, which occurs on back button navigation
        if(this.state.isSubmitting){
            var msg = 'Leaving will cause all unsubmitted work to be lost. Are you sure you want to proceed?';
            if(confirm(msg)){
                // we are no longer submitting
                this.setIsSubmitting(false);
                return false;
            }else{
                // stay
                return true;
            }
        } else {
            return false;
        }
    }

    navigate(href, options = {}, callback = null, fallbackCallback = null, includeReduxDispatch = {}) {
        // options.skipRequest only used by collection search form<commit_msg>app.stayOnSubmissionsPage -> check new href (page about to arrive at for confirmNavigation, or page just arrived at for confirmPopState) to make sure it's NOT the edit/create submissions page before asking confirmation.<commit_after>'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import url from 'url';
import _ from 'underscore';
import ReactTooltip from 'react-tooltip';
var serialize = require('form-serialize');
var jwt = require('jsonwebtoken');
import jsonScriptEscape from '../libs/jsonScriptEscape';
import * as globals from './globals';
import ErrorPage from './static-pages/ErrorPage';
import Navigation from './navigation';
import SubmissionView from './submission/submission-view';
import Footer from './footer';
import * as store from '../store';
import * as origin from '../libs/origin';
import { Filters, ajax, JWT, console, isServerSide, navigate, analytics } from './util';
import Alerts from './alerts';
import { FacetCharts } from './facetcharts';
import { ChartDataController } from './viz/chart-data-controller';
import ChartDetailCursor from './viz/ChartDetailCursor';
import { getTitleStringFromContext } from './item-pages/item';

/**
 * The top-level component for this application.
 *
 * @module {Component} app
 */

/**
 * Used to temporarily store Redux store values for simultaneous dispatch.
 *
 * @memberof module:app
 */
let dispatch_dict = {};

/**
 * Top bar navigation & link schema definition.
 *
 * @memberof module:app
 */
const portal = {
    portal_title: '4DN Data Portal',
    global_sections: [
        {
            id: 'browse',
            sid:'sBrowse',
            title: 'Browse',
            //url: '/browse/?type=ExperimentSetReplicate&experimentset_type=replicate&limit=all',
            url : function(currentUrlParts){
                if (!currentUrlParts) return '/browse/?type=ExperimentSetReplicate&experimentset_type=replicate&limit=all'; // Default/fallback
                return Filters.filtersToHref(
                    store.getState().expSetFilters,
                    currentUrlParts.protocol + '//' + currentUrlParts.host + '/browse/'
                );
            },
            active : function(currentWindowPath){
                if (currentWindowPath && currentWindowPath.indexOf('/browse/') > -1) return true;
                return false;
            }
        },
        {id: 'help', sid:'sHelp', title: 'Help', children: [
            {id: 'gettingstarted', title: 'Getting started', url: '/help', children : [
                {id: 'metadatastructure', title: 'Metadata structure', url: '/help#metadata-structure'},
                {id: 'datasubmission', title: 'Data submission', url: '/help#data-submission'},
                {id: 'restapi', title: 'REST API', url: '/help#rest-api'},
            ]},
            {id: 'submitting-metadata', title: 'Submitting Metadata', url: '/help/submitting'},
            {id: 'about', title: 'About', url: '/about'}
        ]}
    ],
    user_section: [
            {id: 'login', title: 'Log in', url: '/'},
            {id: 'accountactions', title: 'Register', url: '/help'}
            // Remove context actions for now{id: 'contextactions', title: 'Actions', url: '/'}
    ]
};


// See https://github.com/facebook/react/issues/2323
class Title extends React.Component {

    constructor(props){
        super(props);
        this.componentDidMount = this.componentDidMount.bind(this);
        this.render = this.render.bind(this);
    }

    componentDidMount() {
        var node = document.querySelector('title');
        if (node && !node.getAttribute('data-reactid')) {
            node.setAttribute('data-reactid', this._rootNodeID);
        }
    }

    render() {
        return <title {...this.props}>{this.props.children}</title>;
    }

}


/**
 * Creates a promise which completes after a delay, performing no network request.
 * Used to perform a promise.race to see if this timeout or a network requests completes first, which
 * then allows us to set app.state.slow and render a loading icon until long-running network request completes.
 */
class Timeout {
    constructor(timeout) {
        this.promise = new Promise(resolve => setTimeout(resolve.bind(undefined, this), timeout));
    }
}

/**
 * @alias module:app
 */


export default class App extends React.Component {

    static SLOW_REQUEST_TIME = 750

    static contentTypeIsJSON(content) {
        var isJson = true;
        try{
            var json = JSON.parse(JSON.stringify(content));
        }catch(err){
            isJson = false;
        }
        return isJson;
    }

    static scrollTo() {
        var hash = window.location.hash;
        if (hash && document.getElementById(hash.slice(1))) {
            window.location.replace(hash);
        } else {
            window.scrollTo(0, 0);
        }
    }

    static getRenderedPropValues(document, filter = null){
        var returnObj = {};
        var script_props;
        if (typeof filter === 'string') script_props = document.querySelectorAll('script[data-prop-name="' + filter + '"]');
        else script_props = document.querySelectorAll('script[data-prop-name]');
        for (var i = 0; i < script_props.length; i++) {
            var elem = script_props[i];
            var prop_name = elem.getAttribute('data-prop-name');
            if (filter && Array.isArray(filter)){
                if (filter.indexOf(prop_name) === -1) continue;
            }
            var elem_value = elem.text;
            var elem_type = elem.getAttribute('type') || '';
            if (elem_type == 'application/json' || elem_type.slice(-5) == '+json') {
                elem_value = JSON.parse(elem_value);
            }
            //if (elem.getAttribute('data-prop-name') === 'user_details' && !filter){
                // pass; don't include as is not a redux prop
            //} else {
                returnObj[prop_name] = elem_value;
            //}
        }
        return returnObj;
    }

    static getRenderedProps(document, filters = null) {
        return _.extend(App.getRenderedPropValues(document, filters), {
            'href' : document.querySelector('link[rel="canonical"]').getAttribute('href') // Ensure the initial render is exactly the same
        });
    }

    static propTypes = {
        "sessionMayBeSet" : PropTypes.any,    // Whether Auth0 session exists or not.
    }

    static defaultProps = {
        'sessionMayBeSet' : null
    }

    static childContextTypes = {
        dropdownComponent: React.PropTypes.string,
        listActionsFor: React.PropTypes.func,
        currentResource: React.PropTypes.func,
        location_href: React.PropTypes.string,
        onDropdownChange: React.PropTypes.func,
        portal: React.PropTypes.object,
        hidePublicAudits: React.PropTypes.bool,
        fetch: React.PropTypes.func,
        session: React.PropTypes.bool,
        navigate: React.PropTypes.func,
        contentTypeIsJSON: React.PropTypes.func,
        updateUserInfo: React.PropTypes.func,
        schemas: React.PropTypes.object
    }

    constructor(props){
        super(props);
        this.componentDidMount = this.componentDidMount.bind(this);
        this.componentDidUpdate = this.componentDidUpdate.bind(this);
        this.fetch = this.fetch.bind(this);
        this.getChildContext = this.getChildContext.bind(this);
        this.listActionsFor = this.listActionsFor.bind(this);
        this.currentResource = this.currentResource.bind(this);
        this.currentAction = this.currentAction.bind(this);
        this.loadSchemas = this.loadSchemas.bind(this);
        this.getStatsComponent = this.getStatsComponent.bind(this);
        this.updateStats = this.updateStats.bind(this);

        // Global event handlers. These will catch events unless they are caught and prevented from bubbling up earlier.
        this.handleDropdownChange = this.handleDropdownChange.bind(this);
        this.handleAutocompleteChosenChange = this.handleAutocompleteChosenChange.bind(this);
        this.handleAutocompleteFocusChange = this.handleAutocompleteFocusChange.bind(this);
        this.handleAutocompleteHiddenChange = this.handleAutocompleteHiddenChange.bind(this);
        this.handleLayoutClick = this.handleLayoutClick.bind(this);
        this.handleKey = this.handleKey.bind(this);
        this.handleClick = this.handleClick.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.handlePopState = this.handlePopState.bind(this);
        this.setIsSubmitting = this.setIsSubmitting.bind(this);
        this.stayOnSubmissionsPage = this.stayOnSubmissionsPage.bind(this);
        this.authenticateUser = this.authenticateUser.bind(this);
        this.updateUserInfo = this.updateUserInfo.bind(this);
        this.confirmNavigation = this.confirmNavigation.bind(this);
        this.navigate = this.navigate.bind(this);
        this.handleBeforeUnload = this.handleBeforeUnload.bind(this);
        this.render = this.render.bind(this);

        console.log('APP FILTERS', Filters.hrefToFilters(props.href));

        this.historyEnabled = !!(typeof window != 'undefined' && window.history && window.history.pushState);

        // Todo: Migrate session & user_actions to redux store?
        var session = false;
        var user_actions = [];

        if (props.sessionMayBeSet !== null){ // Only provided from server
            if (props.sessionMayBeSet === false) session = false;
            if (props.sessionMayBeSet === true) session = true;
            else session = false; // Else is null
        } else {
            session = !!(JWT.get('cookie')); // Same cookie sent to server-side to authenticate, so it must match.
        }

        // user_info.details is kept in sync to client-side via browser.js, user_info.user_actions is not.
        // Don't use user_actions unless session is also true.
        // user_actions is only set client-side upon login (it cannot expire unless logout).
        var user_info = JWT.getUserInfo();
        if (user_info && typeof user_info.user_actions !== 'undefined' && Array.isArray(user_info.user_actions)){
            user_actions = user_info.user_actions;
        }

        // Save navigate fxn and other req'd stuffs to GLOBAL navigate obj.
        // So that we may call it from anywhere if necessary without passing through props.
        navigate.setNavigateFunction(this.navigate);

        console.log("App Initial State: ", session, user_actions);

        this.state = {
            'errors': [],
            'dropdownComponent': undefined,
            'content': undefined,
            'session': session,
            'user_actions': user_actions,
            'schemas': this.props.context.schemas || null,
            'isSubmitting': false
        };
    }

    // Once the app component is mounted, bind keydowns to handleKey function
    componentDidMount() {
        globals.bindEvent(window, 'keydown', this.handleKey);

        this.authenticateUser();
        // Load schemas into app.state, access them where needed via props (preferred, safer) or this.context.
        this.loadSchemas();

        // The href prop we have was from serverside. It would not have a hash in it, and might be shortened.
        // Here we grab full-length href from window and update props.href (via Redux), if it is different.
        var query_href;
        // Technically these two statements should be exact same. Props.href is put into <link...> (see render() ). w.e.
        if (document.querySelector('link[rel="canonical"]')){
            query_href = document.querySelector('link[rel="canonical"]').getAttribute('href');
        } else {
            query_href = this.props.href;
        }
        // Grab window.location.href w/ query_href as fallback. Remove hash if need to.
        query_href = globals.maybeRemoveHash(globals.windowHref(query_href));
        if (this.props.href !== query_href){
            store.dispatch({
                type: {'href':query_href}
            });
        }

        // If the window href has a hash, which SHOULD NOT remain (!== globals.maybeRemoveHash()), strip it on mount to match app's props.href.
        var parts = url.parse(query_href);
        if (
            typeof window.location.hash === 'string' &&
            window.location.hash.length > 0 &&
            (!parts.hash || parts.hash === '')
        ){
            window.location.hash = '';
        }


        if (this.historyEnabled) {
            var data = this.props.context;
            try {
                window.history.replaceState(data, '', window.location.href);
            } catch (exc) {
                // Might fail due to too large data
                window.history.replaceState(null, '', window.location.href);
            }
            // Avoid popState on load, see: http://stackoverflow.com/q/6421769/199100
            var register = window.addEventListener.bind(window, 'popstate', this.handlePopState, true);
            if (window._onload_event_fired) {
                register();
            } else {
                window.addEventListener('load', setTimeout.bind(window, register));
            }
        } else {
            window.onhashchange = this.onHashChange;
        }
        window.onbeforeunload = this.handleBeforeUnload;

        // Load up analytics
        analytics.initializeGoogleAnalytics(
            analytics.getTrackingId(this.props.href),
            this.props.context,
            this.props.expSetFilters
        );
    }

    componentDidUpdate(prevProps, prevState) {
        var key;
        if (this.props) {

            if (this.props.href !== prevProps.href){ // We navigated somewhere else.

                // Register google analytics pageview event.
                analytics.registerPageView(this.props.href, this.props.context, this.props.expSetFilters);

                // We need to rebuild tooltips after navigation to a different page.
                ReactTooltip.rebuild();

            }


            for (key in this.props) {
                if (this.props[key] !== prevProps[key]) {
                    console.log('changed props: %s', key);
                }
            }
        }
        if (this.state) {
            if (prevState.session !== this.state.session && ChartDataController.isInitialized()){
                setTimeout(function(){
                    // Delay 100ms.
                    console.log("SYNCING CHART DATA");
                    ChartDataController.sync();
                }, 100);
            }
            for (key in this.state) {
                if (this.state[key] !== prevState[key]) {
                    console.log('changed state: %s', key);
                }
            }
        }
    }

    // functions previously in persona, mixins.js
    fetch(url, options) {
        options = _.extend({credentials: 'same-origin'}, options);
        var http_method = options.method || 'GET';
        var headers = options.headers = _.extend({}, options.headers);
        // Strip url fragment.
        var url_hash = url.indexOf('#');
        if (url_hash > -1) {
            url = url.slice(0, url_hash);
        }
        var data = options.body ? options.body : null;
        var request = ajax.promise(url, http_method, headers, data, options.cache === false ? false : true);
        request.xhr_begin = 1 * new Date();
        request.then(response => {
            request.xhr_end = 1 * new Date();
        });
        return request;
    }

    // Retrieve current React context
    getChildContext() {
        return {
            dropdownComponent: this.state.dropdownComponent, // ID of component with visible dropdown
            listActionsFor: this.listActionsFor,
            currentResource: this.currentResource,
            location_href: this.props.href,
            onDropdownChange: this.handleDropdownChange, // Function to process dropdown state change
            portal: portal,
            hidePublicAudits: true, // True if audits should be hidden on the UI while logged out
            fetch: this.fetch,
            session: this.state.session,
            navigate: this.navigate,
            contentTypeIsJSON: App.contentTypeIsJSON,
            updateUserInfo: this.updateUserInfo,
            schemas : this.state.schemas
        };
    }

    listActionsFor(category) {
        if (category === 'context') {
            var context = this.currentResource();
            var name = this.currentAction();
            var context_actions = [];
            Array.prototype.push.apply(context_actions, context.actions || []);

            if (!name && context.default_page) {
                context = context.default_page;
                var actions = context.actions || [];
                for (var i = 0; i < actions.length; i++) {
                    var action = actions[i];
                    if (action.href[0] == '#') {
                        action.href = context['@id'] + action.href;
                    }
                    context_actions.push(action);
                }
            }
            return context_actions;
        }
        if (category === 'user_section') {
            return portal.user_section;
        }
        if (category === 'user') {
            return this.state.user_actions;
        }
        if (category === 'global_sections') {
            return portal.global_sections;
        }
    }

    currentResource() { return this.props.context; }

    currentAction() {
        var href_url = url.parse(this.props.href);
        var hash = href_url.hash || '';
        var name;
        if (hash.slice(0, 2) === '#!') {
            name = hash.slice(2);
        }
        return name;
    }

    loadSchemas(callback, forceFetch = false){
        if (this.state.schemas !== null && !forceFetch){
            // We've already loaded these successfully (hopefully)
            if (typeof callback === 'function') callback(this.state.schemas);
            return this.state.schemas;
        }
        ajax.promise('/profiles/?format=json').then(data => {
            if (App.contentTypeIsJSON(data)){
                this.setState({
                    schemas: data
                }, () => {
                    // Let Filters have access to schemas for own functions.
                    Filters.getSchemas = () => this.state.schemas;
                    // Rebuild tooltips because they likely use descriptions from schemas
                    ReactTooltip.rebuild();
                    if (typeof callback === 'function') callback(data);
                });
            }
        });
    }

    getStatsComponent(){
        if (!this.refs || !this.refs.navigation) return null;
        if (!this.refs.navigation.refs) return null;
        if (!this.refs.navigation.refs.stats) return null;
        return this.refs.navigation.refs.stats;
    }

    updateStats(currentCounts, totalCounts = null, callback = null){
        var statsComponent = this.getStatsComponent();
        if (statsComponent){
            if (totalCounts === null){
                return statsComponent.updateCurrentCounts(currentCounts, callback);
            } else {
                return statsComponent.updateCurrentAndTotalCounts(currentCounts, totalCounts, callback);
            }
        }
        return null;
    }

    // When current dropdown changes; componentID is _rootNodeID of newly dropped-down component
    handleDropdownChange(componentID) {
        // Use React _rootNodeID to uniquely identify a dropdown menu;
        // It's passed in as componentID
        this.setState({dropdownComponent: componentID});
    }

    handleAutocompleteChosenChange(chosen) { this.setState({autocompleteTermChosen: chosen}); }

    handleAutocompleteFocusChange(focused) { this.setState({autocompleteFocused: focused}); }

    handleAutocompleteHiddenChange(hidden) { this.setState({autocompleteHidden: hidden}); }

    // Handle a click outside a dropdown menu by clearing currently dropped down menu
    handleLayoutClick(e) {
        if (this.state.dropdownComponent !== undefined) {
            this.setState({dropdownComponent: undefined});
        }
    }

    handleClick(event) {
        // https://github.com/facebook/react/issues/1691
        if (event.isDefaultPrevented()) return;

        var target = event.target;
        var nativeEvent = event.nativeEvent;

        // SVG anchor elements have tagName == 'a' while HTML anchor elements have tagName == 'A'
        while (target && (target.tagName.toLowerCase() != 'a' || target.getAttribute('data-href'))) {
            target = target.parentElement;
        }
        if (!target) return;

        if (target.getAttribute('disabled')) {
            event.preventDefault();
            return;
        }

        // Ensure this is a plain click
        if (nativeEvent.which > 1 || nativeEvent.shiftKey || nativeEvent.altKey || nativeEvent.metaKey) return;

        // Skip links with a data-bypass attribute.
        if (target.getAttribute('data-bypass')) return;

        var href = target.getAttribute('href');
        if (href === null) href = target.getAttribute('data-href');
        if (href === null) return;

        // Skip javascript links
        if (href.indexOf('javascript:') === 0) return;

        // Skip external links
        if (!origin.same(href)) return;

        // Skip links with a different target
        if (target.getAttribute('target')) return;

        // Skip @@download links
        if (href.indexOf('/@@download') != -1) return;

        // Don't cache requests to user profile.
        var navOpts = {};
        if (target.getAttribute('data-no-cache')) navOpts.cache = false;

        // With HTML5 history supported, local navigation is passed
        // through the navigate method.
        if (this.historyEnabled) {
            event.preventDefault();
            this.navigate(href, navOpts);
            if (this.refs && this.refs.navigation){
                this.refs.navigation.closeMobileMenu();
            }
            if (target && target.blur) target.blur();
        }
    }

    // Submitted forms are treated the same as links
    handleSubmit(event) {
        var target = event.target;

        // Skip POST forms
        if (target.method != 'get') return;

        // Skip forms with a data-bypass attribute.
        if (target.getAttribute('data-bypass')) return;

        // Skip external forms
        if (!origin.same(target.action)) return;

        var options = {};
        var action_url = url.parse(url.resolve(this.props.href, target.action));
        options.replace = action_url.pathname == url.parse(this.props.href).pathname;
        var search = serialize(target);
        if (target.getAttribute('data-removeempty')) {
            search = search.split('&').filter(function (item) {
                return item.slice(-1) != '=';
            }).join('&');
        }
        var href = action_url.pathname;
        if (search) {
            href += '?' + search;
        }
        options.skipRequest = target.getAttribute('data-skiprequest');

        if (this.historyEnabled) {
            event.preventDefault();
            this.navigate(href, options);
        }
    }

    handlePopState(event) {
        if (this.DISABLE_POPSTATE) return;
        var href = window.location.href; // Href which browser just navigated to, but maybe not yet set to this.props.href

        if (!this.confirmPopState(href)){
            window.history.pushState(window.state, '', this.props.href);
            return;
        }

        if (!this.confirmNavigation(href)) {
            //window.history.pushState(window.state, '', this.props.href);
            var d = {
                'href': href
            };
            if (event.state){
                d.context = event.state;
            }
            store.dispatch({
                type: d
            });
            return;
        }
        if (!this.historyEnabled) {
            window.location.reload();
            return;
        }
        var request = this.props.contextRequest;
        if (event.state) {
            // Abort inflight xhr before dispatching
            if (request && this.requestCurrent) {
                // Abort the current request, then remember we've aborted it so that we don't render
                // the Network Request Error page.
                if (request && typeof request.abort === 'function'){
                    request.abort();
                    console.warn("Aborted previous request", request);
                }
                this.requestAborted = true;
                this.requestCurrent = false;
            }
            store.dispatch({
                type: {
                    'href': href,
                    'context': event.state
                }
            });

        }
        // Always async update in case of server side changes.
        this.navigate(href, {'replace': true});
    }

    // If ESC pressed while drop-down menu open, close the menu
    handleKey(e) {
        if (e.which === 27) {
            if (this.state.dropdownComponent !== undefined) {
                e.preventDefault();
                this.handleDropdownChange(undefined);
            } else if (!this.state.autocompleteHidden) {
                e.preventDefault();
                this.handleAutocompleteHiddenChange(true);
            }
        } else if (e.which === 13 && this.state.autocompleteFocused && !this.state.autocompleteTermChosen) {
            e.preventDefault();
        }
    }

    authenticateUser(callback = null){
        // check existing user_info in local storage and authenticate
        var idToken = JWT.get();
        if(idToken && (!this.state.session || !this.state.user_actions)){ // if JWT present, and session not yet set (from back-end), try to authenticate

            ajax.promise('/login', 'POST', {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': 'Bearer '+idToken
            }, JSON.stringify({id_token: idToken}))
            .then(response => {
                if (response.code || response.status || response.id_token !== idToken) throw response;
                return response;
            })
            .then(response => {
                JWT.saveUserInfo(response);
                this.updateUserInfo(callback);
            }, error => {
                // error, clear JWT token from cookie & user_info from localStorage (via JWT.remove())
                // and unset state.session & state.user_actions (via this.updateUserInfo())
                JWT.remove();
                this.updateUserInfo(callback);
            });
            return idToken;
        }
        return null;
    }

    updateUserInfo(callback = null){
        // get user actions (a function of log in) from local storage
        var userActions = [];
        var session = false;
        var userInfo = JWT.getUserInfo();
        if (userInfo){
            userActions = userInfo.user_actions;
            var currentToken = JWT.get(); // We definitively use Cookies for JWT. It can be unset by response headers from back-end.
            if (currentToken) session = true;
            else if (this.state.session === true) {
                Alerts.queue(Alerts.LoggedOut);
            }
        }

        var stateChange = {};
        if (!_.isEqual(userActions, this.state.user_actions)) stateChange.user_actions = userActions;
        if (session != this.state.session) stateChange.session = session;

        if (Object.keys(stateChange).length > 0){
            this.setState(stateChange, typeof callback === 'function' ? callback.bind(this, session, userInfo) : null);
        } else {
            if (typeof callback === 'function') callback(session, userInfo);
        }
    }

    // functions previously in navigate, mixins.js
    onHashChange (event) {
        // IE8/9
        store.dispatch({
            type: {'href':document.querySelector('link[rel="canonical"]').getAttribute('href')}
        });
    }

    /** Rules to prevent browser from changing to 'href' via back/forward buttons. */
    confirmPopState(href){
        if (this.stayOnSubmissionsPage(href)) return false;
        return true;
    }

    /** Only navigate if href changes */
    confirmNavigation(href, options) {

        // check if user is currently on submission page
        // if so, warn them about leaving
        if (this.stayOnSubmissionsPage(href)){
            return false;
        }

        if(options && options.inPlace && options.inPlace==true){
            return true;
        }

        if(href===this.props.href){
            return false;
        }

        var partsNew = url.parse(href),
            partsOld = url.parse(this.props.href);

        if (partsNew.path === partsOld.path && globals.isHashPartOfHref(null, partsNew)){
            return false;
        }

        return true;
    }

    // check this.state.isSubmitting to prompt user if navigating away
    // from the submissions page
    stayOnSubmissionsPage(href) {
        // can override state in options
        // override with replace, which occurs on back button navigation
        if(this.state.isSubmitting){
            if (typeof href === 'string'){
                if (href.indexOf('#!edit') > -1 || href.indexOf('#!create') > -1){
                    // Cancel out if we are "returning" to edit or create (submissions page) href.
                    return false;
                }
            }
            var msg = 'Leaving will cause all unsubmitted work to be lost. Are you sure you want to proceed?';
            if(confirm(msg)){
                // we are no longer submitting
                this.setIsSubmitting(false);
                return false;
            }else{
                // stay
                return true;
            }
        } else {
            return false;
        }
    }

    navigate(href, options = {}, callback = null, fallbackCallback = null, includeReduxDispatch = {}) {
        // options.skipRequest only used by collection search form
{'fraction_non_alphanumeric': 0.11649234373692578, 'fraction_numerical': 0.0020751401556355115, 'mean_word_length': 2.014782301599314, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 38, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4423710', 'n_tokens_mistral': 15999, 'n_tokens_neox': 15435, 'n_words': 3945}