/** @format */
/**
 * External dependencies
 */
import {
        isUndefined,
        orderBy,
        has,
        map,
        unionBy,
        reject,
        isEqual,
        get,
        zipObject,
        includes,
        isArray,
        values,
        omit,
        startsWith,
        isInteger,
} from 'lodash';

/**
 * Internal dependencies
 */
import {
        COMMENT_COUNTS_UPDATE,
        COMMENTS_CHANGE_STATUS,
        COMMENTS_EDIT,
        COMMENTS_RECEIVE,
        COMMENTS_DELETE,
        COMMENTS_RECEIVE_ERROR,
        COMMENTS_COUNT_INCREMENT,
        COMMENTS_COUNT_RECEIVE,
        COMMENTS_LIKE,
        COMMENTS_UNLIKE,
        COMMENTS_TREE_SITE_ADD,
        COMMENTS_WRITE_ERROR,
        READER_EXPAND_COMMENTS,
        COMMENTS_SET_ACTIVE_REPLY,
} from 'state/action-types';
import { combineReducers, createReducer, keyedReducer } from 'state/utils';
import {
        PLACEHOLDER_STATE,
        NUMBER_OF_COMMENTS_PER_FETCH,
        POST_COMMENT_DISPLAY_TYPES,
} from './constants';
import trees from './trees/reducer';
import { getStateKey, getErrorKey, commentHasLink } from './utils';

const getCommentDate = ( { date } ) => new Date( date );

const isCommentManagementEdit = newProperties =>
        has( newProperties, 'commentContent' ) &&
        has( newProperties, 'authorDisplayName' ) &&
        has( newProperties, 'authorUrl' );

const updateComment = ( commentId, newProperties ) => comment => {
        if ( comment.ID !== commentId ) {
                return comment;
        }
        const updateLikeCount = has( newProperties, 'i_like' ) && isUndefined( newProperties.like_count );

        // Comment Management allows for modifying nested fields, such as `author.name` and `author.url`.
        // Though, there is no direct match between the GET response (which feeds the state) and the POST request.
        // This ternary matches and formats the updated fields sent by Comment Management's Edit form,
        // in order to optimistically update the state without temporary loss of information.
        const newComment = isCommentManagementEdit( newProperties )
                ? {
                                ...comment,
                                author: {
                                        ...comment.author,
                                        name: newProperties.authorDisplayName,
                                        url: newProperties.authorUrl,
                                },
                                content: newProperties.commentContent,
                  }
                : { ...comment, ...newProperties };

        return {
                ...newComment,
                ...( updateLikeCount && {
                        like_count: newProperties.i_like ? comment.like_count + 1 : comment.like_count - 1,
                } ),
        };
};

/***
 * Comments items reducer, stores a comments items Immutable.List per siteId, postId
 * @param {Object} state redux state
 * @param {Object} action redux action
 * @returns {Object} new redux state
 */
export function items( state = {}, action ) {
        const { type, siteId, postId, commentId, like_count } = action;

        // cannot construct stateKey without both
        if ( ! siteId || ! postId ) {
                return state;
        }

        const stateKey = getStateKey( siteId, postId );

        switch ( type ) {
                case COMMENTS_CHANGE_STATUS:
                        const { status } = action;
                        return {
                                ...state,
                                [ stateKey ]: map( state[ stateKey ], updateComment( commentId, { status } ) ),
                        };
                case COMMENTS_EDIT:
                        const { comment } = action;
                        return {
                                ...state,
                                [ stateKey ]: map( state[ stateKey ], updateComment( commentId, comment ) ),
                        };
                case COMMENTS_RECEIVE:
                        const { skipSort } = action;
                        const comments = map( action.comments, _comment => ( {
                                ..._comment,
                                contiguous: ! action.commentById,
                                has_link: commentHasLink( _comment.content, _comment.has_link ),
                        } ) );
                        const allComments = unionBy( state[ stateKey ], comments, 'ID' );
                        return {
                                ...state,
                                [ stateKey ]: ! skipSort ? orderBy( allComments, getCommentDate, [ 'desc' ] ) : allComments,
                        };
                case COMMENTS_DELETE:
                        return {
                                ...state,
                                [ stateKey ]: reject( state[ stateKey ], { ID: commentId } ),
                        };
                case COMMENTS_LIKE:
                        return {
                                ...state,
                                [ stateKey ]: map(
                                        state[ stateKey ],
                                        updateComment( commentId, { i_like: true, like_count } )
                                ),
                        };
                case COMMENTS_UNLIKE:
                        return {
                                ...state,
                                [ stateKey ]: map(
                                        state[ stateKey ],
                                        updateComment( commentId, { i_like: false, like_count } )
                                ),
                        };
                case COMMENTS_RECEIVE_ERROR:
                case COMMENTS_WRITE_ERROR:
                        const { error, errorType } = action;
                        return {
                                ...state,
                                [ stateKey ]: map(
                                        state[ stateKey ],
                                        updateComment( commentId, {
                                                placeholderState: PLACEHOLDER_STATE.ERROR,
                                                placeholderError: error,
                                                placeholderErrorType: errorType,
                                        } )
                                ),
                        };
        }

        return state;
}

export const fetchStatusInitialState = {
        before: true,
        after: true,
        hasReceivedBefore: false,
        hasReceivedAfter: false,
};

const isValidExpansionsAction = action => {
        const { siteId, postId, commentIds, displayType } = action.payload;
        return (
                siteId &&
                postId &&
                isArray( commentIds ) &&
                includes( values( POST_COMMENT_DISPLAY_TYPES ), displayType )
        );
};

const expansionValue = type => {
        const { full, excerpt, singleLine } = POST_COMMENT_DISPLAY_TYPES;
        switch ( type ) {
                case full:
                        return 3;
                case excerpt:
                        return 2;
                case singleLine:
                        return 1;
        }
};

export const expansions = createReducer(
        {},
        {
                [ READER_EXPAND_COMMENTS ]: ( state, action ) => {
                        const { siteId, postId, commentIds, displayType } = action.payload;

                        if ( ! isValidExpansionsAction( action ) ) {
                                return state;
                        }

                        const stateKey = getStateKey( siteId, postId );
                        const currentExpansions = state[ stateKey ] || {};

                        const newDisplayTypes = map( commentIds, id => {
                                if (
                                        ! has( currentExpansions, id ) ||
                                        expansionValue( displayType ) > expansionValue( currentExpansions[ id ] )
                                ) {
                                        return displayType;
                                }
                                return currentExpansions[ id ];
                        } );
                        // generate object of { [ commentId ]: displayType }
                        const newVal = zipObject( commentIds, newDisplayTypes );

                        return {
                                ...state,
                                [ stateKey ]: Object.assign( {}, state[ stateKey ], newVal ),
                        };
                },
        }
);

/***
 * Stores whether or not there are more comments, and in which directions, for a particular post.
 * Also includes whether or not a before/after has ever been queried
 * Example state:
 *  {
 *     [ siteId-postId ]: {
 *       before: bool,
 *       after: bool,
 *       hasReceivedBefore: bool,
 *       hasReceivedAfter: bool,
 *     }
 *  }
 *
 * @param {Object} state redux state
 * @param {Object} action redux action
 * @returns {Object} new redux state
 */
export const fetchStatus = createReducer(
        {},
        {
                [ COMMENTS_RECEIVE ]: ( state, action ) => {
                        const { siteId, postId, direction, commentById } = action;
                        const stateKey = getStateKey( siteId, postId );

                        // we can't deduce anything from a commentById fetch.
                        if ( commentById ) {
                                return state;
                        }

                        const hasReceivedDirection =

--- selectors ---
/** @format */
/***
 * External dependencies
 */
import {
        filter,
        find,
        findLast,
        flatMap,
        get,
        groupBy,
        keyBy,
        map,
        mapValues,
        partition,
        pickBy,
        size,
        sortBy,
} from 'lodash';

/**
 * Internal dependencies
 */
import treeSelect from 'lib/tree-select';
import { fetchStatusInitialState } from './reducer';
import { getStateKey, deconstructStateKey, getErrorKey } from './utils';

/***
 * Gets comment items for post
 * @param {Object} state redux state
 * @param {Number} siteId site identification
 * @param {Number} postId site identification
 * @return {Array} comment items
 */
export const getPostCommentItems = ( state, siteId, postId ) =>
        get( state.comments.items, `${ siteId }-${ postId }` );

export const getDateSortedPostComments = treeSelect(
        ( state, siteId, postId ) => [ getPostCommentItems( state, siteId, postId ) ],
        ( [ comments ] ) => {
                return sortBy( comments, comment => new Date( comment.date ) );
        }
);

export const getCommentById = ( { state, commentId, siteId } ) => {
        const errorKey = getErrorKey( siteId, commentId );
        if ( get( state, 'comments.errors', {} )[ errorKey ] ) {
                return state.comments.errors[ errorKey ];
        }

        const commentsForSite = flatMap(
                filter( get( state, 'comments.items', [] ), ( comment, key ) => {
                        return deconstructStateKey( key ).siteId === siteId;
                } )
        );
        return find( commentsForSite, comment => commentId === comment.ID );
};

export const getCommentErrors = state => {
        return state.comments.errors;
};

/***
 * Get total number of comments on the server for a given post
 * @param {Object} state redux state
 * @param {Number} siteId site identification
 * @param {Number} postId site identification
 * @return {Number} total comments count on the server. if not found, assume infinity
 */
export const getPostTotalCommentsCount = ( state, siteId, postId ) =>
        get( state.comments.totalCommentsCount, `${ siteId }-${ postId }` );
/***
 * Get most recent comment date for a given post
 * @param {Object} state redux state
 * @param {Number} siteId site identification
 * @param {Number} postId site identification
 * @return {Date} most recent comment date
 */
export const getPostNewestCommentDate = treeSelect(
        ( state, siteId, postId ) => [ getPostCommentItems( state, siteId, postId ) ],
        ( [ comments ] ) => {
                const firstContiguousComment = find( comments, 'contiguous' );
                return firstContiguousComment ? new Date( get( firstContiguousComment, 'date' ) ) : undefined;
        }
);

/***
 * Get oldest comment date for a given post
 * @param {Object} state redux state
 * @param {Number} siteId site identification
 * @param {Number} postId site identification
 * @return {Date} earliest comment date
 */
export const getPostOldestCommentDate = treeSelect(
        ( state, siteId, postId ) => [ getPostCommentItems( state, siteId, postId ) ],
        ( [ comments ] ) => {
                const lastContiguousComment = findLast( comments, 'contiguous' );
                return lastContiguousComment ? new Date( get( lastContiguousComment, 'date' ) ) : undefined;
        }
);

/***
 * Gets comment tree for a given post
 * @param {Object} state redux state
 * @param {Number} siteId site identification
 * @param {Number} postId site identification
 * @param {String} status String representing the comment status to show. Defaults to 'approved'.
 * @param {Number} authorId - when specified we only return pending comments that match this id
 * @return {Object} comments tree, and in addition a children array
 */
export const getPostCommentsTree = treeSelect(
        ( state, siteId, postId ) => [ getPostCommentItems( state, siteId, postId ) ],
        ( [ allItems ], siteId, postId, status = 'approved', authorId ) => {
                const items = filter( allItems, item => {
                        //only return pending comments that match the comment author
                        const commentAuthorId = get( item, 'author.ID' );
                        if (
                                authorId &&
                                commentAuthorId &&
                                item.status === 'unapproved' &&
                                commentAuthorId !== authorId
                        ) {
                                return false;
                        }
                        if ( status !== 'all' ) {
                                return item.isPlaceholder || item.status === status;
                        }
                        return true;
                } );

                // separate out root comments from comments that have parents
                const [ roots, children ] = partition( items, item => item.parent === false );

                // group children by their parent ID
                const childrenGroupedByParent = groupBy( children, 'parent.ID' );

                // Generate a new map of parent ID to an arra<response clipped><NOTE>Due to the max output limit, only part of the full response has been shown to you.</NOTE>p-types';
import { connect } from 'react-redux';
import { map, zipObject, fill, size, filter, get, compact, partition, min, noop } from 'lodash';

/***
 * Internal dependencies
 */
import getActiveReplyCommentId from 'state/selectors/get-active-reply-comment-id';
import PostComment from 'blocks/comments/post-comment';
import { POST_COMMENT_DISPLAY_TYPES } from 'state/comments/constants';
import {
        commentsFetchingStatus,
        getDateSortedPostComments,
        getExpansionsForPost,
        getHiddenCommentsForPost,
        getPostCommentsTree,
        getCommentErrors,
} from 'state/comments/selectors';
import ConversationCaterpillar from 'blocks/conversation-caterpillar';
import { recordAction, recordGaEvent, recordTrack } from 'reader/stats';
import PostCommentFormRoot from 'blocks/comments/form-root';
import { requestPostComments, requestComment, setActiveReply } from 'state/comments/actions';
import { getErrorKey } from 'state/comments/utils';
import { getCurrentUserId } from 'state/current-user/selectors';

/**
 * ConversationsCommentList is the component that represents all of the comments for a conversations-stream
 * Some of it is boilerplate stolen from PostCommentList (all the activeXCommentId bits) but the special
 * convos parts are related to:
 *  1. caterpillars
 *  2. commentsToShow
 *
 * As of the time of this writing, commentsToShow is constructing by merging two objects:
 *  1. expansion state in the reducer for the specific post
 *  2. commentIds handed from the api as seeds to start with as open. high watermark will replace this logic.
 *
 * So when a post is loaded, the api gives us 3 comments.  This component creates an object that looks like:
 *   { [commentId1]: 'is-excerpt', [commentId2]: 'is-excerpt', [commentId3]: 'is-excerpt' } and then
 *   hands that down to all of the PostComments so they will know how to render.
 *
 * This component will also display a caterpillar if it has any children comments that are hidden.
 * It can determine hidden state by seeing that the number of commentsToShow < totalCommentsForPost.
 */

const FETCH_NEW_COMMENTS_THRESHOLD = 20;
export class ConversationCommentList extends React.Component {
        static propTypes = {
                post: PropTypes.object.isRequired, // required by PostComment
                commentIds: PropTypes.array.isRequired,
                shouldRequestComments: PropTypes.bool,
                setActiveReply: PropTypes.func,
        };

        static defaultProps = {
                enableCaterpillar: true,
                shouldRequestComments: true,
                setActiveReply: noop,
        };

        state = {
                activeEditCommentId: null,
        };

        onEditCommentClick = commentId => this.setState( { activeEditCommentId: commentId } );
        onEditCommentCancel = () => this.setState( { activeEditCommentId: null } );
        onUpdateCommentText = commentText => this.setState( { commentText: commentText } );

        onReplyClick = commentId => {
                this.setActiveReplyComment( commentId );
                recordAction( 'comment_reply_click' );
                recordGaEvent( 'Clicked Reply to Comment' );
                recordTrack( 'calypso_reader_comment_reply_click', {
                        blog_id: this.props.post.site_ID,
                        comment_id: commentId,
                } );
        };

        onReplyCancel = () => {
                this.setState( { commentText: null } );
                recordAction( 'comment_reply_cancel_click' );
                recordGaEvent( 'Clicked Cancel Reply to Comment' );
                recordTrack( 'calypso_reader_comment_reply_cancel_click', {
                        blog_id: this.props.post.site_ID,
                        comment_id: this.props.activeReplyCommentId,
                } );
                this.resetActiveReplyComment();
        };

        reqMoreComments = ( props = this.props ) => {
                const { siteId, postId, enableCaterpillar, shouldRequestComments } = props;

                if ( ! shouldRequestComments || ! props.commentsFetchingStatus ) {
                        return;
                }

                const { haveEarlierCommentsToFetch, haveLaterCommentsToFetch } = props.commentsFetchingStatus;

                if ( enableCaterpillar && ( haveEarlierCommentsToFetch || haveLaterCommentsToFetch ) ) {
                        const direction = haveEarlierCommentsToFetch ? 'before' : 'after';
                        props.requestPostComments( { siteId, postId, direction } );
                }
        };

        componentDidMount() {
                this.resetActiveReplyComment();
                this.reqMoreComments();
        }

        componentWillReceiveProps( nextProps ) {
                const { hiddenComments, commentsTree, siteId, commentErrors } = nextProps;

                // if we are running low on comments to expand then fetch more
                if ( size( hiddenComments ) < FETCH_NEW_COMMENTS_THRESHOLD ) {
                        this.reqMoreComments();
                }

                // if we are missing any comments in the hierarchy towards a comment that should be shown,
                // then load them one at a time. This is not the most efficient method, ideally we could
                // load a subtree
                const inaccessible = this.getInaccessibleParentsIds(
                        commentsTree,
                        Object.keys( this.getCommentsToShow() )
                );
                inaccessible
                        .filter( commentId => ! commentErrors[ getErrorKey( siteId, commentId ) ] )
                        .forEach( commentId => {
                                nextProps.requestComment( {
                                        commentId,
                                        siteId,
                                } );
                        } );
        }

        getParentId = ( commentsTree, childId ) =>
                get( commentsTree, [ childId, 'data', 'parent', 'ID' ] );
        commentHasParent = ( commentsTree, childId ) => !! this.getParentId( commentsTree, childId );
        commentIsLoaded = ( commentsTree, commentId ) => !! get( commentsTree, commentId );

        getInaccessibleParentsIds = ( commentsTree, commentIds ) => {
                // base case
                if ( size( commentIds ) === 0 ) {
                        return [];
                }

                const withParents = filter( commentIds, id => this.commentHasParent( commentsTree, id ) );
                const parentIds = map( withParents, id => this.getParentId( commentsTree, id ) );

                const [ accessible, inaccessible ] = partition( parentIds, id =>
                        this.commentIsLoaded( commentsTree, id )
                );

                return inaccessible.concat( this.getInaccessibleParentsIds( commentsTree, accessible ) );
        };

        // @todo: move all expanded comment set per commentId logic to memoized selectors
        getCommentsToShow = () => {
                const { commentIds, expansions, commentsTree, sortedComments } = this.props;

                const minId = min( commentIds );
                const startingCommentIds = ( sortedComments || [] )
                        .filter( comment => {
                                return comment.ID >= minId || comment.isPlaceholder;
                        } )
                        .map( comment => comment.ID );

                const parentIds = compact(
                        map( startingCommentIds, id => this.getParentId( commentsTree, id ) )
                );
                const commentExpansions = fill(
                        Array( startingCommentIds.length ),
                        POST_COMMENT_DISPLAY_TYPES.excerpt
                );
                const parentExpansions = fill( Array( parentIds.length ), POST_COMMENT_DISPLAY_TYPES.excerpt );

                const startingExpanded = zipObject(
                        startingCommentIds.concat( parentIds ),
                        commentExpansions.concat( parentExpansions )
                );

                return { ...startingExpanded, ...expansions };
        };

        setActiveReplyComment = commentId => {
                const siteId = get( this.props, 'post.site_ID' );
                const postId = get( this.props, 'post.ID' );

                if ( ! siteId || ! postId ) {
                        return;
                }

                this.props.setActiveReply( {
                        siteId,
                        postId,
                        commentId,
                } );
        };

        resetActiveReplyComment = () => {
                this.setActiveReplyComment( null );
        };

        render() {
                const { commentsTree, post, enableCaterpillar } = this.props;

                if ( ! post ) {
                        return null;
                }

                const commentsToShow = this.getCommentsToShow();
                const isDoneLoadingComments =
                        ! this.props.commentsFetchingStatus.haveEarlierCommentsToFetch &&
                        ! this.props.commentsFetchingStatus.haveLaterCommentsToFetch;

                // if you have finished loading comments, then lets use the comments we have as the final comment count
                // if we are still loading comments, then assume what the server initially told us is right
                const commentCount = isDoneLoadingComments
                        ? filter( commentsTree, comment => get( comment, 'data.type' ) === 'comment' ).length // filter out pingbacks/trackbacks
                        : post.discussion.comment_count;

                const showCaterpillar = enableCaterpillar && size( commentsToShow ) < commentCount;

                return (
                        <div className="conversations__comment-list">
                                <ul className="conversations__comment-list-ul">
                                        { showCaterpillar && (
                                                <ConversationCaterpillar
                                                        blogId={ post.site_ID }
                                                        postId={ post.ID }
                                                        commentCount={ commentCount }
                                                        commentsToShow={ commentsToShow }
                                                />
                                        ) }
                                        { map( commentsTree.children, commentId => {
                                                return (
                                                        <PostComment
                                                                showNestingReplyArrow
                                                                hidePingbacksAndTrackbacks
                                                                enableCaterpillar={ enableCaterpillar }
                                                                post={ post }
                                                                commentsTree={ commentsTree }
                                                                key={ commentId }
                                                                commentId={ commentId }
                                                                maxDepth={ 2 }
                                                                commentsToShow={ commentsToShow }
                                                                onReplyClick={ this.onReplyClick }
                                                                onReplyCancel={ this.onReplyCancel }
                                                                activeReplyCommentId={ this.props.activeReplyCommentId }
                                                                onEditCommentClick={ this.onEditCommentClick }
                                                                onEditCommentCancel={ this.onEditCommentCancel }
                                                                activeEditCommentId={ this.state.activeEditCommentId }
                                                                onUpdateCommentText={ this.onUpdateCommentText }
                                                                onCommentSubmit={ this.resetActiveReplyComment }
                                                                commentText={ this.state.commentText }
                                                                showReadMoreInActions={ true }
                                                                displayType={ POST_COMMENT_DISPLAY_TYPES.excerpt }
                                                        />
                                                );
                                        } ) }
                                        <PostCommentFormRoot
                                                post={ this.props.post }
                                                commentsTree={ this.props.commentsTree }
                                                commentText={ this.state.commentText }
                                                onUpdateCommentText={ this.onUpdateCommentText }
                                                activeReplyCommentId={ this.props.activeReplyCommentId }
                                        />
                                </ul>
                        </div>
                );
        }
}

const ConnectedConversationCommentList = connect(
        ( state, ownProps ) => {
                const { site_ID: siteId, ID: postId, discussion } = ownProps.post;
                const authorId = getCurrentUserId( state );
                return {
                        siteId,
                        postId,
                        sortedComments: getDateSortedPostComments( state, siteId, postId ),
                        commentsTree: getPostCommentsTree( state, siteId, postId, 'all', authorId ),
                        commentsFetchingStatus:
                                commentsFetchingStatus( state, siteId, postId, discussion.comment_count ) || {},
                        expansions: getExpansionsForPost( state, siteId, postId ),
                        hiddenComments: getHiddenCommentsForPost( state, siteId, postId ),
                        activeReplyCommentId: getActiveReplyCommentId( {
                                state,
                                siteId,
                                postId,
                        } ),
                        commentErrors: getCommentErrors( state ),
                };
        },
        { requestPostComments, requestComment, setActiveReply }
)( ConversationCommentList );

export default ConnectedConversationCommentList;
[The command completed with exit code 0.]
[Current working directory: /workspace/wp-calypso]
[Python interpreter: /usr/bin/python]
[Command finished with exit code 0]